toggle menu

[AngularJS] 순수한 AngularJS 기반 트리뷰(TreeView)

2013. 8. 2. 13:51 AngularJS
이전에 Directive에 대해 포스팅했을 때, Directive의 재귀호출을 응용해서 만든 간단한 트리 메뉴 소스를 공개했었다.

이때의 소스는 Directive를 활용하기는 했지만, 불필요한 부분이 많았고, jqLite에 대한 의존성이 높았기 때문에 다소 아쉬운 부분이 있었다. 하지만 이번에는 AngularJS의 특성을 최대한 활용하였고, AngularJS 어플리케이션에서 쉽게 활용할 수 있도록 모듈화하여 GitHub을 통해 공개했다. (내친김에 http://ngmodules.org/에도 살짝 추가했다^^)

기존에는 다이렉티브 두 개를 활용해서 구현했었는데, 이번에는 하나의 다이렉티브로 처리했고 소스코드도 90줄에 불과할 정도로 간결하다. 용량도 closure compiler로 minified된 버전의 경우 1.36kb 정도이고, 개발용 소스도 2.5kb 정도로 작다.

Angular Treeview - github.com
jsFiddle

구현 초기 버전의 경우, 간단히 소스를 분석해보면 AngularJS 입문자들에게 상당한 도움이 될거라 생각되어 이번에 개발한 Angular Treeview 의 로직에 대해 설명해 보고자 한다.



실제 사용예

먼저 jsFiddle을 통해 간단하게 실제 동작을 살펴보자.




기본적인 트리뷰의 기능을 살펴볼 수 있다. 테스트용 JSON 데이터는 BBC 방송에서 제공하는 메뉴 구조를 가져왔다.
다른 일반적인 트리뷰와 마찬가지로 아이콘 부분을 클릭하면 트리가 펼쳐지거나 접히고, 글씨부분을 클릭하면 선택된다. 선택된 노드에 대한 정보는 상단의 Selected Node 란에 표시되도록 예제를 만들어보았다.




적용 방법

<div
  data-angular-treeview
  data-tree-model="treedata"
  data-node-id="id"
  data-node-label="label"
  data-node-children="children" >
</div>


사용 방법은 DIV 등의 태그에 속성명으로 data-angular-treeview 라고 써줌으로써 인식한다. tree-model 에는 scope 상에 트리 변수명을 지정해준다. 여기에서는 $scope.treedata 변수에 트리 구조가 들어있다고 지정한 것이다. id는 해당 노드의 id인데, 현재까지는 사실상 크게 필요가 없는 속성이다. label은 실제로 트리뷰에 표시되는 내용이고, children은 자식 배열이 들이 있는 속성명이다.

실제 $scope.treedata에는 아래와 같은 형태로 데이터가 들어가 있을 것이다.

$scope.treedata = 
[
    { label : "User", id : "role1", children : [
        { label : "subUser1", id : "role11", children : [] },
        { label : "subUser2", id : "role12", children : [
            { label : "subUser2-1", id : "role121", children : [
                { label : "subUser2-1-1", id : "role1211", children : [] },
                { label : "subUser2-1-2", id : "role1212", children : [] }
            ]}
        ]}
    ]},
    { label : "Admin", id : "role2", children : [] },
    { label : "Guest", id : "role3", children : [] }
];  


트리뷰를 사용하려면 현재의 Angular Module에 아래와 같이 인젝션을 해주어야 한다. 물론 자바스크립트 파일과 스타일시트 파일을 HTML 페이지 상에서 포함시켜주는 것은 기본이다.

angular.module('myApp', ['angularTreeview']);

트리 메뉴의 아이템을 클릭했을 때는 선택된 노드가 $scope.currentNode 에 저장된다. 따라서 클릭한 시점을 컨트롤러에서 인지하기 위해서는 $watch 메서드를 사용할 수도 있을 것이고, 아예 트리뷰 소스를 커스터마이즈해서 원하는 구현을 넣어줄 수도 있을 것이다. 아래는 $watch 메서드를 활용해서 컨트롤러에서 클릭한 시점에 선택된 노드의 정보를 콘솔 로그로 뿌려주는 로직이다.

$scope.$watch( 'currentNode', function( newObj, oldObj ) {
    if( $scope.currentNode && angular.isObject($scope.currentNode) ) {
        console.log( 'Node Selected!!' );
        console.log( $scope.currentNode );
    }
}, false);




로직 분석

공개된 소스를 한번 살펴보자. 사용된 주요 개념은 AngularJS의 module과 directive, 재귀호출로 나눌 수 있다. module은 다른 AngularJS Application에 주입을 쉽게하기 위해 감쌌고, 대부분의 로직은 AngularJS 자체적인 Directive와 Custom Directive 등으로 짜여져 있다.


(function ( angular ) {
	'use strict';

	//AngularJS 모듈
	angular.module( 'angularTreeview', [] ).directive( 'treeModel', function( $compile ) {
		return {
			restrict: 'A', //다이렉티브는 속성으로 인지한다.
			link: function ( scope, element, attrs ) {
				//트리 모델
				var treeModel = attrs.treeModel;

				//노드 아이디
				var nodeId = attrs.nodeId || 'id';

				//노드 라벨
				var nodeLabel = attrs.nodeLabel || 'label';

				//자식 노드들
				var nodeChildren = attrs.nodeChildren || 'children';

				//트리 템플릿. 이 템플릿 내에 data-tree-model 다이렉티브가 다시 존재하는 형태로 재귀호출이 구현된다.
				var template = 
					'<ul>' + 
						'<li data-ng-repeat="node in ' + treeModel + '">' + 
							'<i class="collapsed" data-ng-show="node.' + nodeChildren + '.length && node.collapsed" data-ng-click="selectNodeHead(node)"></i>' + 
							'<i class="expanded" data-ng-show="node.' + nodeChildren + '.length && !node.collapsed" data-ng-click="selectNodeHead(node)"></i>' + 
							'<i class="normal" data-ng-hide="node.' + nodeChildren + '.length"></i> ' + 
							'<span data-ng-class="node.selected" data-ng-click="selectNodeLabel(node)">{{node.' + nodeLabel + '}}</span>' + 
							'<div data-ng-hide="node.collapsed" data-tree-model="node.' + nodeChildren + '" data-node-id=' + nodeId + ' data-node-label=' + nodeLabel + ' data-node-children=' + nodeChildren + '></div>' + 
						'</li>' + 
					'</ul>'; 


				//트리 모델명을 HTML 태그의 속성으로 입력했는지 체크
				if( treeModel && treeModel.length ) {
					
					//data-angular-treeview 속성이 있으면 루트 노드이고,
					//루트 노드일 때에만 클릭 이벤트 처리 메서드를 선언한다.
					if( attrs.angularTreeview ) {

						//노드의 아이콘 부분(노드 헤드)을 클릭했을 때,
						scope.selectNodeHead = scope.selectNodeHead || function( selectedNode ){

							//collapsed값을 토글해 준다.
							selectedNode.collapsed = !selectedNode.collapsed;
						};

						//노드의 이름 부분(노드 라벨)을 클릭했을 때
						scope.selectNodeLabel = scope.selectNodeLabel || function( selectedNode ){

							//기존에 선택되어 있던 노드의 하이라이팅 클래스를 제거해주고,
							if( scope.currentNode && scope.currentNode.selected ) {
								scope.currentNode.selected = undefined;
							}

							//현재 선택한 노드에 하이라이팅 클래스를 설정해 준다.
							selectedNode.selected = 'selected'

							//현재 선택한 노드를 $scope.currentNode 에 넣어준다.
							scope.currentNode = selectedNode;
						};
					}

					//템플릿을 렌더링해서 뿌려준다.
					element.html(null).append( $compile( template )( scope ) );
				}
			}
		};
	});
})( angular );


재귀호출의 핵심은 의외로 template 변수에 있다. 이 변수에는 화면에 뿌려줄 HTML 템플릿이 담겨져 있는데, 주석에도 설명한 것과 같이 이 템플릿 안에 다시 directive를 사용해서 재귀호출을 구현하고 있다. 물론 모든 자식 요소가 다 표현되고 나면 더이상 재귀호출이 일어나지 않는다.

클릭 이벤트에 대해 처리하는 메서드 등은 반복해서 선언할 필요가 없기 때문에 루트 노드를 생성할 때만 한번 선언된다. 트리가 펼쳐지고 사라지는 것 모두 AngularJS에서 자체적으로 제공해주는 directive인 ng-show 나 ng-hide를 활용했고 클릭 이벤트 역시 ng-click 을 사용했다. 또 트리 내용이 표시되는 것은 ng-repeat을 활용해 처리했다.




결론

전체적으로 AngularJS의 특성들만을 활용해서 구현했기 때문에 분석해본다면, AngularJS에 대한 이해가 한층 더 깊어질 수 있을 것이다. 앞으로 트리뷰에 필요한 기능들을 조금씩 추가해서 보다 완성도 있는 AngularJS 라이브러리가 되는 것을 기대해본다. 








AngularJS 관련 포스팅 더보기