자바 스크립트(JavaScript)를 이용하여

간단한 TreeGrid를 제작해 본다.

TreeGrid는 다음 그림(EasyUI TreeGrid)처럼

데이터를 트리와 그리드(List)의 결합 형태로 출력하는

라이브러리를 의미한다.


여기서는 실제로 사용할 수 있는 TreeGrid를 만드는 것이 아니라

간단한 TreeGrid를 단계별로 구현해 보면서

TreeGrid원리와

자바스크립트, Json 사용법 등을 설명하려고 한다.


실제 사용은 TreeGrid jQuery, EasyTree

무료(MIT) 라이브러리 등이 많이 있으니

이것을 그대로 사용하는 것이 좋고,

여기서는 간단하게 TreeGrid를 만들어 보면서

실력을 향상 시키는 것이 목적이다.


예제도 이 라이브러리에서 사용된 일부 코드를 가져와서 구성했으며

본 내용을 다루는 기본을 이해하고

차후 이 라이브러리를 분석해 보면 실력 향상에 도움이 될 것이다.


기본적으로 자바 스크립트, JQuery, JSON의 기본적인 사용법은 알고 있어야 하고,

모르는 경우 찾아보길 바란다.


구성 단계는 다음과 같이 6단계로 세분화하여 정리하였다.

  1. 트리용 데이터(JSON)을 화면에 출력
  2. 데이터(JSON)를 트리형(계층형)으로 보여 주기 위해 들여쓰기
  3. 라이브러리화
  4. 클릭시 자식 노드 숨기기
  5. 클릭시 자식 노드 숨김 / 보이기
  6. 버그 수정

이 단계를 거쳐도 실제적인 TreeGrid를 만들지는 못한다.

TreeGrid를 제작하는 원리를 익히며

자바스크립트(JavaScript)와 JQuery, Json 사용법 등을 익히는 것이 목적이다.


트리 그리드(TreeGrid)를 만들기 위해

다음 그림과 같이 데이터를 화면에 출력하는 방법부터 시작한다.


그림과 같이 3개의 데이터를 화면에 출력하기 위해서는

다음의 html처럼

테이블(table) 태그를 사용해서 웹 페이지를 작성한다.

이렇게 웹 페이지에 테이블 태그를 작성해 두고 사용하는 것은

고정된 문서(구성)으로 사용하는 것으로,

트리 형태로

데이터를 들여쓰기 하고

하위 노드를 숨겼다 보여주는 동적인 기능을 구현하기 어렵다.

따라서, 다음 코드와 같이

자바스크립트(JavaScript, JS)를 이용하여

테이블 태그를 생성하고 조절하여

동적 웹 페이지를 제작하게 된다.
 

sample1.html


먼저, 11 라인과 16 라인에서

option과 data라는 배열 변수를 선언하고 초기값을 주었다.

option변수는 트리 그리드의 각 열(column)을 필드로 보고

각 필드의 특성을 지정하는 변수로 사용한다.

즉, 배열 크기가 3 개 이므로 3 개의 필드를 사용한다.

각 배열은 Json으로 구성되었고,

필드의 구체적인 정보를 담고 있다 (예제라 width 하나 뿐임).

필드명은 작업명(task), 시작일자(startdate), 종료일자(enddate)이고

각각의 크기는 200px, 100px, 100px로 지정하였다.

이 필드들은 테이블 태그의 열(td)로 생성되게 된다.


배열 data 변수는 그리드에 출력한 데이터를 담은 변수로

하나의 인자가 하나의 데이터, 즉 행(row, line)이 되고

예제에서 배열 크기가 3 이니 데이터 개수는 3 이 된다.

필드 구성은 앞서 구성한 option 변수에서 선언한 필드에

값을 주어 구성한다.

option 변수에서 선언하지 않은 필드를 사용해도 되며,

이 경우는 숨겨진 필드로

보이진 않지만 프로그램 처리상 필요한 역할을 할 수 있다.

option과 data 변수는

필드명(task, startdate, enddate)으로 서로 찾게 되고

data 변수의 값들을 출력하면서,

option의 속성(width)을 가져와서 화면을 구성하게 된다 (31, 32 라인).


다음으로 코드의 23~25 라인에 사용된 JQuery는

동적으로 테이블(Table) 태그를 생성하고,

id가 tree(#tree)인 div (39 라인)에 추가하는 코드이다.

이렇게 해서 지정된 위치(사용자가 원하는 위치)에 TreeGrid가 생성된다.

마지막으로 27 ~34 라인이 데이터를 화면에 출력하는 코드이다.

데이터를 화면에 출력하는 방법은

데이터의 개수만큼 반복하면서(for, each)

한 데이터가 가진 필드들(for, each)의 값을 출력하면 된다.

출력 방식은 테이블 테그를 사용하기 때문에

행은 TR,

열은 TD를 동적으로 생성하여 구현하게 된다.


즉, 데이터(data) 변수의 크기 만큼 반복하면서 $.each( data, function( index, row)

TR 태그를 생성해서 행을 출력하고 var tr = $("<tr>").appendTo(table)

필드 개수만큼 반복해서 $.each( option, function( i, fieldInfo)

TD 태그를 생성한다. var td = $("<td>").appendTo(tr)

앞서 option 과 data 변수는

필드명(task, startdate, enddate)으로 서로 찾는 다고 설명했는 데,

생성한 TD에 필드 값을 출력하기 위해

option의 필드명(field)으로

현재 data의 출력하려고 하는 행(row)에서 값을 찾아와 row[fieldInfo.field]

TD에 넣어준다(html).


참고: 두개의 each문이 사용되었다.

$.each( data, function( index, row)
$.each( option, function( i, fieldInfo)


배열 변수인 data, option의 개수 만큼 반복해서 실행하는 JQuery 문법으로

콜백 함수의 첫 번째 파라메터 index와 i는 배열의 몇 번째 데이터인지

row와 fieldInfo는 배열 중 각 인자의 값이 넘겨 진다.

data는 각각의 행(노드 값)이 넘어오고 하나의 행은 Json으로 구성되어 있다.

option는 각 필드 정보의 행이 Json으로 넘겨진다.

따라서 each문 안에서

option은 fieldInfo.field, fieldInfo.width로 사용되고

data는 row.task, row.startdate, row.enddate로 사용된다.

또, Json 데이터는 fieldInfo["field"], row["task"] 같이

배열처럼 사용할 수 있다.

31 라인에서는 이 코드를 혼용해서 사용했다. td.html( row[fieldInfo.field]);


이상의 코드를 실행하면,

앞서 HTML의 테이블 태그로만 작성된 코드가 생성된다.

즉, 두 개의 코드를 실행한 결과가 같다는 것으로

하나는 HTML만으로,

하나는 JS를 이용해서 생성한 것이다.

웹 브라우저의 DOM 탐색기(F12)를 실행해서

JS로 생성된 HTML코드를 확인해 보자



JQuery 기초 문법 몇 가지

$("#tree")처럼 문자열에 #을 사용하면

HTML 태그 ID가

지정된 문자열(tree)과 일치하는 태그를 찾아서 반환한다.

반환된 클래스는 HTML 태그의 기능 외에

JQuery에서 제공하는 부가적인 기능도 추가되어 있다.

즉, JQuery 문법으로 클래스를 사용하게 된다.


$("<table>")처럼 꺽쇠(<>)를 사용하면

해당 HTML 태그를 생성해서 JQuery 클래스가 반환된다.


여기에는 사용되지 않았지만(다음 예제들에 나옴)

$(".treegrid9-1")처럼 점(.)을 사용하면

해당 문자열을 CSS의 클래스 명(Classname)으로 사용하는

HTML 태그를 찾아서 JQuery 클래스로 반환된다.

#과 유사하게 사용되는데

#은 ID를 찾는 것이고

점(.)은 클래스 명에서 찾는 것이다.


주의: 자바 스크립트의 클래스와 CSS의 클래스를 헷갈린다면 TreeGrid 제작 관련 내용이 도움이 안 될 수도 있다.

JQuery에 대한 간단한 설명은 검색을 해보거나 여기를 참조하면 된다.


데이터(JSON)를 트리형(계층형)으로 보여 주기 위해

다음 그림과 같이 들여쓰기를 구현해 본다.

Job2는 Job1의 자식으로 처리하고

그림과 같이 부모인 Job1보다 뒤로 들여쓰기 하여

자식이라는 것을 표현하였다.

트리 구조가 표현되게 되는 것이다.

이러한 표현을 하기 위해서는

가장 먼저 데이터를 구성할 때

부모 / 자식 관계를 제대로 표현해 줘야 하는데

자식 노드(TR)에서 부모 노드(TR)를 지정하는 방식

부모 노드(TR)가 자식 노드(TR) 리스트를 보유하는 방식

2 가지 방식으로 저장 할 수 있다.


먼저, 자식 노드에서 부모 노드를 지정하는 방법을 알아본다.

개념을 정리해 보면

자식에서 부모를 지정하려면

부모의 고유 식별자(ID)가 있어야 한다.

그래야 특정 노드가 내 부모 임을 표현할 수 있기 때문이다.

따라서, 각 노드(데이터 - data)는

자신을 나타내는 고유 식별 값인 ID와

자신의 부모가 누구 인지 나타내기 위해

부모의 고유 식별 값 ID를 나태내는 값(Parent)이 있어야 한다.


이 값을 가지고

화면 출력을 위해 노드(TR)를 생성할 때

부모가 없으면 노드만 생성하면 된다.

부모가 있으면 최상위 부모(최초 조상-단군)를 찾아서

최상위 부모로부터 몇 대손(depth)인지 파악하고

그 깊이(depth) 만큼 부모로 부터 들여쓰기 해주면 된다.


sample2.html


코드 21 라인에 있는 data 변수의 값을 확인해 보면

앞서의 sample1과 다르게

id와 parent 필드가 추가된 것을 알 수 있다.

앞서 정리한 개념을 처리한 것으로

부모 / 자식 관계를 표현하기 위해 추가하였다.

id 필드의 값은 데이터 내에서 고유한 값이어야 하고,

parent 필드의 값은 나의 부모의 id 값이 저장 된다.


다음은 이렇게 저장한 데이터를 적절하게 화면에 출력하는 것이다.

먼저, 데이터 간의 부모 / 자식 관계를

화면에 표현해야 하는데

데이터 하나가 테이블(TABLE) 태그의 TR로 표현되기 때문에

TR 태그 간의 관계로 구성하게 된다.

다음 단계에서 구현할 접는 기능을 위해서도 필요하다.

즉, TR 태그에 부모 / 자식 관계가 표현되어야 하는데

CSS 클래스명(class속성)으로 처리한다.

HTML 태그의 ID 속성으로 처리하기도 하는데

ID 속성은 이런 저런 데이터를 같이 저장하는 것이 어려워서

대부분 ID 속성 보다는 CSS 클래스명을 이용한다.

TR 태그에도 자기 ID(treegrid9-)와

부모 ID(treegrid9-parent-)가 같이 저장되어야 하기 때문이다.

(이해가 안될 경우 이 글 마지막 그림 참조)


참고: 데이터 값을 식별하는 ID와 HTML 태그의 ID 속성을 혼동할 수 있는데

다른 것이니 잘 구별해서 사용해야 한다.

데이터 값을 식별하는 ID는 고유 값을 가진다는 걸

표현하기 위해 자주 사용하는 변수 이름으로 다른 명칭을 사용해되 된다.


34 라인에 JQuery로 TR을 생성하면서 $("<tr>")

데이터의 id(row.id)와 문자열("treegrid9-")을 합쳐서

클래스를 추가(addClass)하여 TR마다 고유 이름(id)를 부여한다.

데이터의 ID만 사용해도 되지만,

개발한 TreeGrid가 라이브러리 형태로 제공되면

다양한 다른 페이지들과 같이 사용되고

그럴 때 다른 사람이 작성한 클래스명과 같은 경우가 생겨,

문제가 발생할 수 있기 때문에

식별 문자열(treegrid9-)을 추가해서 작성하였다.


참고: data변수에 사용된 id와 parent는 option 변수에 없다.

즉, id와 parent는 화면에 출력되지 않고 숨겨진 필드처럼 사용 된다.


36 라인에서 부모가 있으면(row.parent)

부모의 다음 행에 추가하고(tr.insertAfter)

부모가 없으면 테이블에 추가한다 tr.appendTo(table).

부모를 찾을 때 클래스 명으로 찾는데

JQuery를 이용해서 찾는다.

클래스 명은 점(.)을 이용한다. $(".treegrid9-" + row.parent)


부모 다음에 출력하고 나면

클래스 명에 자신의 부모가 누구라고 표시해야 한다. tr.addClass("treegrid9-parent-" + row.parent);

다만 부모 ID(treegrid9-?)를 그대로 사용하면

명명 규칙이 같아서

어느 것이 자신의 ID이고

어느 것이 부모를 의미하는지 구분할 수 없어

식별 문자열을

자신의 ID는 treegrid9- 으로

부모는 treegrid9-parent- 으로 사용한다.


39 라인의 getDepth 함수에 대한 구체적인 설명은 생략하니 분석해 보길 바란다.

getDepth 함수는

현재 자신이 root로부터 몇 번째 자식인지 계산하는 함수이다 (depth).

원리는 현재 자신(TR)의 클래스 명에 부모가 있는지 확인해 보고

있으면 부모의 ID만 추출하고(parentPattern)

추출된 부모의 ID를 식별문자열(treegrid9-)과 결합해 부모를 찾는다.

다시 그 부모의 클래스 명에서

부모가 있는지 확인해 보고

있으면 부모의 ID만 추출하여 부모를 찾아가면서

부모가 없을 때까지 반복해서 while (parent)

개수를 확인하여 반환한다.


부모의 ID만 추출할때 정규식을 사용하였다.

해당 정규식은 treegrid9-parent-([A-Za-z0-9_-]+) 으로

부모를 나타내기 위해 사용한 식별문자열을 의미한다.


깊이(depth)를 알아야

필드 값을 출력할 때 들여쓰기를 할 수 있다.

깊이가 1 이면(사람으로 치면 단군 왕검의 1 대손이라는 의미)

부모보다 뒤로 몇 글자 들여쓰기 해주면 된다.

공백(&nbsp;)를 사용해도 되지만

여기서는 너비가 16px인 SPAN 태그를 이용해서 구현했다.

깊이가 1 이면 SPAN 태그 하나,

깊이가 2 이면 SPAN 태그 둘 ….

을 생성해서 들여쓰기를 구현하게 된다.


45 라인의 IF문은 if (i===0 && depth>0)

첫 번째 필드(task)만 들여쓰기 하기 위한 것이다.

앞서 43라 인에서 필드의 개수만큼 반복하도록(each) 작성했기 때문에

모든 필드에 대해서 해당 코드가 실행된다.


실행 결과를 DOM 탐색기로 확인하면

다음과 같이 클래스명(class)에

각 행(TR)의 ID와 부모 ID가 표시 되어 있는 것을 볼 수 있다.


정리하면

트리 구조로 보여 주기 위해서는

부모 / 자식 관계 설정을 해야 하고

관계 설정을 위해서는

자신을 식별하는 ID와

부모를 표시하는 Parent가 있어야 한다.

이 두 가지를 데이터에 표현하고

화면에 테이블로 구현해주면 되는 것이다.





부모 노드가 자식 리스트를 보유하는 방식은

데이터에 내 자식 리스트(배열)을 표시하면 된다.

앞서 구현한 자식 노드에서 부모 노드를 지정하는 방식은

부모가 하나이기 때문에 parent 변수 하나만 추가하면 됐지만

한 부모의 자식은 여럿이 될 수 있기 때문에

배열(children)을 사용해야 한다.

그리고, 자식의 자식의 자식의 ... 로 작성될 수 있기 때문에

배열의 배열의 배열...

과 같이 무한하게 표시될 수 있다.

이렇게 무한하게 표현되는 데이터를 처리하기 위해서는

데이터가 있는 만큼 무한하게

반복해서 처리할 수 있는 재귀 함수가 필요하다.

데이터(행)를 처리하고

해당 데이터에 자식이 있으면

다시 데이터를 처리하고

해당 데이터에 자식이 있으면 ....

이렇게 반복되는 것을 하나만 함수로 작성하고

데이터가 없을 때까지 반복해서 호출하는 것을 재귀함수 라고 한다.



sample2_1.html


먼저, 20 라인의 데이터를 보면

앞서 구현한 자식 노드에서 부모 노드를 지정하는 방식과 비슷하게

자신을 나타내는 ID가 추가되었다.

다만, 부모 대신에 자식들(children)이 배열로 구현되어 있다.

부모는 하나지만

자식은 여러 명일 수 있기 때문에 복수형으로 사용한다.

자식 필드로 children이 선언되고

부모와 동일한 구조로 데이터가 저장된다.

여기서는 처리하지 않았지만

자식의 자식일 경우

Children을 계속 넣어주면 된다.


앞서의 방식보다 코드가 깔끔해 보이는데

재귀 호출을 사용하였다.

재귀 호출, 재귀 함수라고도 하는데

자신이 자신을 부르는 것을 의미한다.

자식이 어느 정도(몇 대손)까지 있을 지 모르기 때문에

자식에 대한 처리를 위해 재귀 호출을 사용했다.


재귀 함수인 setData는

파라미터로 데이터와 부모 태그, 깊이(몇 대손)를 주게 된다.

데이터는 자식 리스트(children)를 의미하고,

부모 태그는 부모 TR을 의미하는 데

첫 데이터(단군)은 테이블(table) 태그이다.

깊이는 처음에 0 일 것이고

재귀호출 할 때 마다 1 씩 더해 준다.


주어진 데이터의 개수 만큼 반복해서 $.each( data, function( index, row)

TR을 생성하고

CSS 클래스 명으로 이름을 부여한다.

다만, 위치는

부모가 없으면 테이블에 추가하고(appendTo)

부모가 있으면 부모 다음에 넣어준다 (insertAfter).

다만 조금 더 정확하게 구현하려면 부모 다음이 아니고

해당 부모의 마지막 자식 다음이 되어야 한다.

구현 방법이 복잡해서 여기서는 부모 다음에 추가했다.

부모가 있는지 없는지는

파라미터로 넘어온 부모의 태그 타입(nodeName)이

테이블이면 부모가 없는 것으로 처리했다.


들여쓰기 하는 방식은

자식 노드에서 부모 노드를 지정하는 방법과 같이

깊이의 개수만큼 SPAN 태그를 생성해 준다.


마지막으로 자식이 있으면 if (row.children)

다시 자신(setData)을 호출하고

파라미터로 자식 리스트를 넣어주면 된다.


자식 노드에서 부모 노드를 지정하는 방식(sample2.html)과

부모 노드가 자식 리스트를 보유하는 방식(sample2_1.html)을 구현하였다.

일반적으로 대부분의 라이브러리들이 (TreeGrid jQuery, EasyTree)

부모 노드가 자식 리스트를 보유하는 방식을 사용하고 있다.


하지만, 개인적으로

자식 노드에서 부모 노드를 지정하는 방식이 더 좋다고 생각한다.

데이터 베이스에 데이터가 저장되는 방식이

자식 노드에서 부모 노드를 지정하는 방식과 같기 때문이다.

(본 블로그의 무한 댓글 참조)

즉, 데이터 베이스에 ID와 Parent 필드로 저장하고 있다.

부모 노드가 자식 리스트를 보유하는 방식을 사용하게 되면

ID와 Parent 필드로 구성된 데이터를 정리해서

자식 리스트를 작성해야 하기 때문이다.

즉, Java나 PHP등의 서버 언어에서의 처리가 있어야 한다.


따라서 JS 코드만 보면

자식 노드에서 부모 노드를 지정하는 방식이 더 복잡하지만

데이터 베이스의 데이터를 그대로 사용할 수 있기 때문에

이후의 기능 개선도

자식 노드에서 부모 노드를 지정하는 방식으로 구현할 예정이다.



라이브러리화 한다는 것은

HTML 파일에 같이 작성된 자바 스크립트(JS) 코드를

별도의 JS 파일에 함수로 구현하고

이 함수만 호출하면

TreeGrid를 사용할 수 있도록 하겠다는 것이다.

또, 사용자(개발자)가 자신의 상황에 맞추어

간단한 설정만 하면 TreeGrid를 사용할 수 있도록 하는 것이다.

즉, TreeGrid 라이브러리 파일(sample3_treegrid9.js)과

이 라이브러리를 사용하는 HTML 파일(sample3.html)로 분할 해서 구현한다.


라이브러리화한 후,

HTML 파일에 남아있는 코드를 보면

복잡한 코드는 사라지고 option과 data만 남은 것을 볼 수 있다.

option과 data는

TreeGrid 사용자의 상황에 따라 바꿔서 지정하고

사용자는 30 라인에 있는 것처럼

TreeGrid9을 실행만 시키면 된다.


sample3.html


기존의 코드는 다음과 같이 sample3_treegrid9.js파일에 작성하고

HTML(sample3.html)에서는

8 라인에서 작성한 것처럼 <script src="sample3_treegrid9.js"></script>

JS 파일을 가져오기 하면 된다.

sample3_treegrid9.js


이상의 코드와

이전 예제(sample2.html)와 차이는

1 라인의 TreeGrid9 함수뿐이다.

외부 파일로 빼면서 함수로 구성한 것이다.

모든 코드는 동일하다.

다만, HTML에서 호출할 때 함수로 호출하지 않고

New를 사용해서 클래스로 실행했다.

간단하게는 함수화해서 함수를 호출해도 되지만

앞으로 여러 가지 기능을 추가해 나갈 것이기 때문에

클래스 방식으로 사용하였다.

JS에서는 함수와 클래스가 유사하게 사용된다.

자세한 설명은 [JS 클래스]로 검색하면 찾을 수 있다.



본 예제는 모든 데이터를 화면에 출력하고

자식 노드는 부모 노드 보다 뒤로 들여쓰기 하여 구현했다.

이번 단계에서는 부모 노드를 클릭하면

자식 노드를 감추고 보여주는 기능을 구현할 예정인데

쉽게 코드를 설명하기 위해

숨기는 기능부터 먼저 구현해 본다.


기능 구현을 위해 고려해야 할 대상은 다음과 같다.

  • 클릭 대상 선택
  • 자식이 있는 부모와 없는 부모, 자식을 보여주고 있는지 숨기고 있는지 구분하는 방법


둘 다 같은 결과를 필요로 하는데,

하나의 노드(행)를 클릭해서 자식을 숨기게 되면

현재 어떤 노드가 선택되었는지 보여 주는 기능을 구현하기 어렵다.

(행 선택 기능으로 여기서는 구현하지 않는다.)

그리고 두 번째 기능을 구현하기 위해

대부분 노드 앞에 특정 기호를 사용하게 된다.

즉, 특정 기호로 자식이 있는지 없는지 표시하고

특정 기호를 클릭하면 자식을 보이거나 숨기게 구현다.


윈도우 탐색기를 예로 하면

하위 폴더가 있는 경우 + 기호가 나오고

부모 폴더의 + 기호를 클릭하면

하위 폴더가 나타나며 – 기호로 바뀌게 된다.

여기서는 이 기호를 Expander라고 하고

Expander를 클릭하면 자식을 숨기거나

보이도록 구현한다.


다음으로 처리해야 할 것은

Expander를 클릭하면

내 자식 노드들을 찾아서 숨기는 것이다.

나의 자식은 나를 부모로 지정한 노드들을 의미하고

나를 부모로 지정한 노드들은

클래스명에 "treegrid9-parent-"를 이용하여

나(나의 ID)를 지정한 노드들을 의미한다.


이상의 개념을

다음과 같이 코드로 구현하면 되는데

코드를 보기 전에

위 개념을 토대로 직접 작성 해보는 것이 실력향상에 도움이 될 수 있다.


sample4.html

HTML 파일에서 11 라인에 CSS 클래스 하나를 추가 했다.

자식 노드를 숨기고 보여주기 위한 expander와 관련된 것으로

treegrid9-indent와 같은 기능을 하지만

expander 기호에 마우스를 올리면

클릭할 수 있는 기호로 바꾸는 스타일이 추가된 것이다.


sample4_treegrid9.js

먼저 30 라인을 보면 If문 조건이 바뀌었다.

이전에는 첫 번째 필드(i===0)이고

깊이가 0 이상 (depth>0)이면 들여쓰기를 하도록 했는데,

현재는 깊이 조건이 빠져서

모든 노드에서 들여쓰기 span이 생성된다.

자식이 있는 부모에 expander 기호를 붙이게 되면

다음과 같이 자식이 없는 부모와 줄 맞춤이 맞지 않게 된다.

           + Job1
           Job3

따라서 모든 노드에 span을 생성하고

자식이 있는 부모 노드만 span에 expander 기호와

CSS 스타일(treegrid9-expander)를 넣어서 보기 좋게 만들었다.


다음으로 기존 코드에 추가된

13 라인부터 22 라인을 이해하면 된다.

자식이 부모를 지정하는 방식이기 때문에

자식을 생성하다가

부모 ID(row.parent)로 부모를 찾아서 $(".treegrid9-" + row.parent)

부모의 클래스 명에 treegrid9-expanded라는

이름이 있으면 부모 표시가 된 것이니 넘어가고

없으면 부모 표시가 되지 않은 것이니 !parent.hasClass("treegrid9-expanded")

부모 표시와 처리를 진행하게 된다.

클래스명 treegrid9-expanded는 식별자로 사용되는 이름이고

별도의 CSS 지정을 하지 않았다.

이렇게도 많이 사용하기 때문에 기억해두면 좋을 것이다.


부모 처리가 되지 않았으며

먼저, expander로 사용할 span 태그를 찾는다.

여기서는 부모(parent[0] 즉 TR)의

첫 번째 셀(TD) 중에서 첫 번째 자식(span)을

expander 기호를 넣을 태그로 보고 .addClass("treegrid9-expander")

– 기호를 넣어준다. expander.html("-")


expander(기호)를 클릭하면

현재 노드의 자식 노드를 찾아서 숨겨주는 처리를 해야 한다.

클릭한 개체(expander 즉 this)에

가장 가까운(closest) TR을 찾아서 $(this).closest("tr")

현재 노드를 파악하고,

현재 노드의 ID을 추출한 뒤 getNodeId(tr)

해당 ID을 부모로 지정한 "tr.treegrid9-parent-" + id

모든 자식(TR)을 찾아서 find

보이지 않게 (hide) 처리하면 된다.

JQuery의 closest, find, hide를 활용한 코드로

자세한 설명은 찾아 보길 바란다.


마지막으로 현재 노드의 ID 값을 찾는 getNodeId 함수는

getDepth 함수와 같이 정규식을 사용해서

ID 값(숫자)만 남겨서 반환하는 함수이다.


sample4.html 파일을 실행한 후

expander(-)를 클릭해서

다음과 같이 Job2가 숨겨지는지 확인해 보자




앞서의 예제에서는 expander를 클릭하면

자식 노드가 숨겨지게 코드를 작성하였다.

여기서는 expander를 다시 클릭하면

감춰진 자식 노드가 보이는 기능을 구현한다.

즉, expander를 클릭하면 숨겨지고,

다시 클릭하면 나타나고

다시 클릭하면 숨겨지도록 구현한다.


개념부터 정리하면 모든 노드는 펼쳐져 있다.

이때 부모 노드의 expander 기호는 모두 – 이다.

부모 노드의 expander를 클릭하면

기호는 –에서 열수 있다는 의미인 + 기호로 바꾸고

자식 노드들을 보이지 않게(Hide) 처리한다.

다시 expander를 클릭하면

열렸다는 의미로 기호는 – 로 바꾸어 주고

자식 노드들을 보이게(show) 처리하면 된다.


간단한 개념이기 때문에

직접 구현해 본 후 다음 코드를 보는 것이 좋다.


기존 코드에 다음과 같이 몇 라인만 바꾸어 주면 된다.

            if (!parent.hasClass("treegrid9-expanded")){
                parent.addClass("treegrid9-expanded");
                var expander = $(parent[0].cells[0].firstChild).addClass("treegrid9-expander");
                expander.html("-");
                expander.click(function() {
                    var isexpanded = $(this).html() === "-";
                    if (isexpanded) {
                        $(this).html("+");
                    } else {
                        $(this).html("-");
                    }
                    var tr = $(this).closest("tr");
                    var id = getNodeId(tr);
                    table.find("tr.treegrid9-parent-" + id).each(function(i, e){
                        if (isexpanded)
                            $(this).hide();
                        else {
                            $(this).show();
                        }
                    });
                });
            }

sample5_treegrid9.js

expander를 클릭하면

expander의 기호가 무엇인지 판단해서 $(this).html() === "-"

자식 노드를 보일 것인지(show) 숨길 것인지(hide)를 처리한다.


자식노드를 찾는 것도 앞서의 코드와 동일하다.


실행해 보면 트리 라이브러리처럼

자식이 보였다 안보였다 하는 것을 확인 할 수 있다.


이제 데이터를 바꿔서 실행해 보자.

var data = [
                {id:"1", "task": "job1", "startdate": "2016-10-11", "enddate": "2016-11-11"},
                {id:"2", "task": "job2", "startdate": "2016-10-11", "enddate": "2016-10-30", parent: "1"},
                {id:"3", "task": "job3", "startdate": "2016-11-12", "enddate": "2016-12-11", parent: "2"}
            ];

sample5.html

이전 예제에서는 계층 구조(depth)가 1 단계였는데

Job3을 Job2의 자식으로 변경하여 실행해 보자.

다음 그림과 같이 실행된다.

몇 가지 문제가 발생하는 것을 알 수 있다.

먼저, 계층이 많아지니 expander 기호의 위치가

모두 행의 맨 앞에 있어서 적절하지 않아 보인다.

그리고, 마우스로 클릭해보면

바로 아래의 자식은 숨겨지는데

자식의 자식은 숨겨지지 않는다.


어떻게 수정해야 하는지 생각해 보고

다음 예제에서 이 문제를 해결하는 방법을 정리한다.





다음과 같이 데이터를 구성하고 실행하면

var data = [
                {id:"1", "task": "job1", "startdate": "2016-10-11", "enddate": "2016-12-11"},
                {id:"2", "task": "job2", "startdate": "2016-10-11", "enddate": "2016-10-30", parent: "1"},
                {id:"3", "task": "job3", "startdate": "2016-11-12", "enddate": "2016-12-11", parent: "2"},
                {id:"4", "task": "job4", "startdate": "2016-12-12", "enddate": "2016-12-31"}
            ];

sample6.html

다음과 같이 2 가지 문제가 있는 것을 찾았다.

1. expander 기호의 위치가 행의 맨 앞에 있어서 적절하지 않아 보인다.

2. job1의 expander 기호를 클릭하면 job2만 숨겨진다.

  즉, 손자(job3)은 숨겨지지 않는다.


이 버그도 쉽게 해결할 수 있으니

먼저 해보고 다음 설명을 읽는 것이 좋을 것 이다.


            if (!parent.hasClass("treegrid9-expanded")){
                parent.addClass("treegrid9-expanded");
                var expander = $(parent[0].cells[0]).find("span").last().addClass("treegrid9-expander");
                expander.html("-");
                expander.click(function() {
                    var isexpanded = $(this).html() === "-";
                    if (isexpanded) {
                        $(this).html("+");
                    } else {
                        $(this).html("-");
                    }
                    var tr = $(this).closest("tr");
                    getChildren(getNodeId(tr), isexpanded);
                    function getChildren(id, isexpanded) {
                            table.find("tr.treegrid9-parent-" + id).each(function(i, e){
                                 if (isexpanded)
                                    $(this).hide();
                                 else {
                                     $(this).show();
                                 }
                                id = getNodeId($(this));
                            if (table.find("tr.treegrid9-parent-" + id).length>0){
                                getChildren(id, isexpanded);
                            } ;
                        });
                    }
                });
            }

sample6_treegrid9.js


먼저 expander 기호의 위치 문제는

노드의 맨 앞에 있는 기호를

뒤로 보내서 task 필드의 값 바로 앞에 두면 된다.

들여쓰기와 expander 기호 표시를 위해

task 필드 앞에 span을 생성하였는데,

기존에는 expander 기호를 맨 앞 span에 넣었다.

이것을 맨뒤 span에 넣으면 된다.


따라서 다음과 같이 코드를 수정한다.

기존    var expander = $(parent[0].cells[0].firstChild).addClass("treegrid9-expander");

변경    var expander = $(parent[0].cells[0]).find("span").last().addClass("treegrid9-expander");

기존에는 첫 번째 TD(task 필드)의

첫번째 자식(span)에 기호를 넣었고,

수정된 코드는 첫 번째 TD(task 필드)의자식 중

span을 찾아서 마지막(last) 자식에 기호를 넣게 작성하였다.

그러면 다음 그림과 같이 task 필드의 값 바로 앞에

expander 기호가 위치하게 된다.


두번째 문제는

job1의 expander 기호를 클릭하면 job2만 숨겨지고

손자(job3)은 숨겨지지 않는 것이다.


이것은 자식 찾기를 한번만 했기 때문에 발생한다.

앞서 코드 구성에서

바로 위 부모만(직계 존손) 자신의

자신의 노드 클래스명에 표시하도록 했고(treegrid9-parent-)

부모는 자신을 부모로 표기한 이 값만을 찾도록 하고 있다. table.find("tr.treegrid9-parent-" + id).each(function(i, e)

즉, 모든 자식을 찾아야 하는데

모든 자식을 찾으려면

나(클릭한 노드)를 중심으로

나를 부모로 하는 자식 노드를 찾아서 숨기거나 보이도록 처리한다.

그리고, 각 자식 노드를

부모로 하는 노드가 있는 지 확인해서 if (table.find("tr.treegrid9-parent-" + id).length>0)

있으면 다시 자식 노드를 찾아서 숨기거나 보이도록 처리한다.

그리고, 다시 각 자식 노드를

부모로 하는 노드가 있는 지 확인해서 if (table.find("tr.treegrid9-parent-" + id).length>0)

이렇게 무한 반복으로

자식 노드를 찾아서 처리해야 한다.

복잡해 보이지만

이것은 재귀 함수를 사용하면 간단하게 처리된다.

기존 코드를 getChildren 함수에 넣고

이 함수를 자식이 있으면 호출하도록 작성하였다.


또, getChildren를 함수내 내장 함수로 작성했다.

개인적으로 JS의 재미있는 기능이라고 생각하며

자세한 내용은 찾아보길 바란다.


지금까지 간단한 TreeGrid를 제작하는 예제를 통해서

자바스크립트, JQuery, JSon 등의 사용법을 익혀보았다.

노드 선택, Ajax 처리 등의 기능을 추가하면

제법 그럴듯한 TreeGrid를 만들 수 있을 것이다.

누군가 제대로 된 오픈 소스 라이브러리를 개발하길 기대하며……


+ Recent posts