Dojo v1.4 TreeGrid는 웹 페이지에서 계층 구조 데이터를 표시하려 할 때 유용한 위젯이다. 그러나 큰 데이터 세트가
있는 경우 TreeGrid의 성능이 극히 떨어진다. 본 기사에서는 TreeGrid와
QueryStore를 사용자 정의하여 이 문제를 해결할 수 있는 방법을 배워보자. 본 기사에서는 두 개의 위젯을 사용할 때 발생할 수 있는
문제와 오류 발생 이유를 설명한 다음, 솔루션 작성에 도움이 되는 정보를 제공한다. 이 기사에서 사용되는 샘플 코드를 다운로드할 수도
있다.
물론, 그리드를 사용하여 비교적 작은 데이터 세트를 브라우저에서 효과적으로 표시할 수 있다. 이를 통해 정렬, 열 크기 조정 등을 정확하게 수행할 수 있다. 하지만, 특정 시점에서 다룰 수 있는 레코드 수에는 실질적 제한이 있고, 결국 이로 인해 페이지 표시 결과 문제로 이어진다.
이젠, 페이지 표시에 대해서는 잊어도 된다. 이미 해결된 문제이기 때문이다. 그리드가 스크롤할 때 Dojo의 그리드는 느린 DOM(Document Object Model) 렌더링을 사용한다. 수백 개 미만의 레코드를 포함한 비교적 작은 데이터 세트의 경우, 스크롤할 때 느린 DOM 렌더링으로 인해 미리 로드된 데이터 세트를 위한 DOM이 빌드된다. 예를 들어, 100개의 레코드가 있지만 한 번에 20개만 볼 수 있는 경우, 그 특정 섹션으로 스크롤할 때까지 21번째부터 100번째까지의 레코드에 대해서는 노드를 빌드할 필요가 없을 것이다. 열과 관련 태스크를 기준으로 한 정렬은 작은 데이터 세트에 대해 메모리에서 예상대로 작동하고, JavaScript에서 효과적으로 작업을 완료할 수 있다.
매우 큰 데이터 세트의 경우, 열을 기준으로 한 정렬과 같은 태스크를 위해 JavaScript를 사용하는 것은 금방 비현실적으로 되거나, 심지어는
불가능하게 된다. 데이터 세트가 거대한 경우, 모든 데이터를 브라우저로 로드하는 ItemFileReadStore를 이용해 브라우저에서 데이터
세트를 유지보수하려는 것은 현실적이지 못하다.
큰 데이터 세트를 다루기 위한 Dojo의 접근 방법은 간단하고 우수하며, 다음 두 가지로 요약된다.
- 임의 페이지 크기의 서버에서 데이터를 요청할 수 있는 dojo.data 구현. 이 기사에서는
QueryReadStore를 사용한다. - 요청 시 특정 페이지를 요청하고 로드하도록 스크롤할 수 있는 그리드.
본 기사의 나머지 부분에서는 QueryReadStore를 이용해 느리게 로드되는 트리 그리드를 빌드하는 방법을 설명한다.
QueryReadStore, TreeGrid 및 TreeModel
이 섹션에서는 QueryReadStore, TreeGrid 및 TreeModel에 대해
매우 기본적인 내용을 소개한다.
dojocampus.org 문서(참고자료 참조)에 설명되어 있는 것처럼, QueryReadStore는
ItemReadStore와 매우 유사하다. 이 두 가지는 모두 JSON을 교환 형식으로 사용한다. 둘 사이에는 데이터 쿼리 방법에 차이가 있다.
ItemReadStore는 서버에서 페치 1개를 만들고 클라이언트에서 모든 정렬 및 필터링을 처리한다. 수백 개 정도의
레코드라면 괜찮다. 그러나 수십만 개의 레코드가 있거나 인터넷 연결 속도가 느릴 때는 ItemReadStore가 최적의 방법이 아니다.
QueryReadStore는 각각의 정렬 또는 쿼리를 위해 서버에 대한 요청을 만들므로, dojox.grid.DataGrid에서와
같이 데이터 범위가 작은 큰 데이터 세트에 이상적이다. 목록 1은 QueryReadStore 작성 방법을 나타낸 것이다.
목록 1. QueryReadStore 작성
var url = "./servlet/QueryStoreServlet";
var store = new dojox.data.QueryReadStore({
url: url,
requestMethod: "get"
});
|
Tree 및 TreeGrid와 같은 트리 위젯은 계층 구조 데이터 뷰를 표시한다. TreeGrid
위젯 자체는 단지 데이터 뷰일 뿐이다. 진정한 힘은 Tree 위젯이 표시할 실제 계층 구조 데이터를 표시하는 TreeModel에서
나온다.
일반적으로, 데이터는 결국 데이터 저장소에서 오지만, Tree 위젯은 dijit.tree.Model(트리에서
필요로 하는 메소드의 특정 API와 일치하는 오브젝트)과 상호 작용한다. 따라서 Tree 위젯은 항목이 상위 항목을 참조하는 데이터
저장소에서와 같이 다양한 형식의 데이터에 액세스할 수 있다.
TreeModel은 데이터 소스로의 연결, 레이지 로딩 및 Tree 위젯에서 항목과 항목의
계층 구조에 대한 쿼리와 같은 특정 태스크를 담당한다. 예를 들어, 어떤 태스크에서 Tree 위젯에 대한 항목의 하위 항목을 얻을 수도
있다. 또한, TreeModel은 Tree에 데이터 변경 내용을 알리는 역할도 담당한다.
시작하기 위해서는 TreeGrid에 대한 여러 개의 최상위 레벨 항목을 리턴하기 위한
ForestStoreModel(TreeModel의 구현)의 쿼리를 정의할 필요가 있다.
트리가 저장소에 액세스하도록 하기 위한 모델 어댑터도 작성할 것이다. TreeModel을 생성하려면 목록 2의 매개변수가
필요하다.
목록 2. TreeModel을 작성하기 위한 코드
var query = {
type: "PARENT"
};
var treeModel = new dijit.tree.ForestStoreModel({
store: store, // the data store that this model connects to
query: query, // filter multiple top level items
rootId: "$root$",
rootLabel: "ROOT",
childrenAttrs: ["children"], // children attributes used in data store.
/*
For efficiency reasons, Tree doesn't want to query for the children
of an item until it needs to display them. It doesn't want to query
for children just to see if it should draw an expando (+) icon or not.
So we set "deferItemLoadingUntilExpand" to true.
*/
deferItemLoadingUntilExpand: true
});
|
QueryReadStore, TreeGrid 및 TreeModel 사이의 관계
TreeGrid의 사용 방법을 이해하려면 서로 피드하는 다음 트리 컴포넌트를 명심해두자.
-
QueryReadStore는 서버에서 데이터를 페치하는 역할을 담당한다. -
ForestTreeModel은QueryReadStore에서 항목의 계층 구조를 쿼리하여TreeGrid에 업데이트할 것을 알리는 역할을 담당한다. -
TreeGrid는 데이터를 표시하고 사용자 이벤트만 처리하는 역할을 담당한다.
그림 1은 이 셋 사이의 관계를 나타낸 것이다. 목록 3은 트리 작성 방법을 나타낸 것이다.
그림 1. QueryReadStore, TreeGrid 및 TreeModel 사이의 관계
목록 3. 트리 작성
// define the column layout for the tree grid.
var layout = [
{ name: "Name", field: "name", width: "20%" },
{ name: "Age", field: "age", width: "auto" },
{ name: "Position", field: "position", width: "auto" },
{ name: "Telephone", field: "telephone", width: "auto" }
];
// tree grid
var treegrid = new dojox.grid.TreeGrid({
treeModel: treeModel,
structure: layout, // define columns layout
/*
A 0-based index of the cell in which to place the actual expando (+)
icon. Here we define the "Name" column as the expando column.
*/
expandoCell: 0,
defaultOpen: false,
columnReordering: true,
rowsPerPage: 20,
sortChildItems: true,
canSort: function(sortInfo) {
return true;
}
}, "treegrid");
treegrid.startup();
|
QueryReadStore가 TreeGrid에서 작동하는 방식
이 섹션에서는 TreeGrid에서 데이터를 렌더링하고, QueryReadStore로 정렬하고, 상위 노드를
확장하는 방법을 설명한다.
QueryReadStore는 최상위 레벨 노드를 위해 서버로 요청을 보낸다. 저장소에서 이루어질 서버에 대한 첫 번째 요청은
최상위 레벨 노드(루트의 하위)에 대한 요청이 될 것이다. 이 dojo.data 요청은 특정 JSON 형식을 따른다.
트리 그리드는 모델에 제공되는 쿼리를 사용하여 최초 요청을 수행하고, 저장소에서는 그 요청을 대상과 결합한다. 목록 4는 요청 형식, REST URL 패턴 및 요청에 대한 서버 응답을 나타낸 것이다. 예제에서는 REST 패턴으로 요청이 이루어진다.
목록 4. 쿼리 요청 및 서버 응답
// query
{
query: {type: "PARENT"},
start: 0,
count: 20 //rowsPerPage attribute of the tree grid
}
// request sample
url?type=PARENT&start=0&count=20
// server response
{
"identifier": "id",
"label": "name",
"items": [
{ "id": "id_0",
"name": "Edith Barney",
"age": 39,
"position": "Marketing Manager",
"telephone": 69000044
"children": true,
"type": "PARENT"
},
{ "id": "id_1",
"name": "Herbert Jeames",
"age": 43,
"position": "Brand Executive Manager",
"telephone": 69000077,
"type": "Child"
},
...
],
"numRows": 10000 // total records in the grid
}
|
"Edith Barney" 항목은 "PARENT" 유형이고, "Herbert
Jeames" 항목 유형은 "Child"이다. 트리 모델에서의 쿼리는 {type: "PARENT"}이므로,
트리 그리드는 처음에는 "Edith Barney" 레코드만 표시한다.
실제로 하위를 포함할 필요는 없고, children 특성이 있다는 것은 노드가 확장 가능하고 확장 아이콘이 포함될
Tree를 가리킨다. "Edith Barney" 항목의 children 속성은 false 값이
아니므로, 트리 모델에서는 그것을 하위를 포함한 노드로 취급한다. 트리 그리드의 Edith Barney 행에 더하기(+) 기호가 있을 것이다. 그림 2는 그 결과를 나타낸 것이다.
그림 2. 작성된 TreeGrid의 렌더링 결과
TreeGrid에서 데이터를 렌더링했으므로, 상위 노드 정렬 및 확장과 같은 TreeGrid 기능이
QueryReadStore에서 올바로 작동하는지 확인하고 싶을 것이다.
정렬 기능을 실행하려면 Name 열의 헤더를 클릭하고 이름을 오름차순으로 정렬한다. 오름차순이나 내림차순으로 다른 열을 정렬하는 경우도 동일하다.
목록 5. 정렬
// the tree model will pass a request to the query store like this:
{
query: {type: "PARENT"},
start: 0,
count: 20,
sort: {
attribute: "name",
descending: false
}
}
// The query store will request server like this:
url?type=PARENT&start=0&count=20&sort=name
|
그림 3은 정렬된 결과를 나타낸 것이다.
그림 3. 정렬된 결과
큰 트리 데이터 세트의 경우, 아마도 트리 중에서 보이는 노드에 대한 필수 데이터만 로드하고 싶을 것이다. 사용자가 노드를 펼치면 그 노드의 하위 노드가 로드되기 시작한다. 최적의 성능을 위해 확장할 때마다 하나의 HTTP 요청만 하는 것이 이상적이다.
사용자가 예제에 있는 노드 중 하나를 클릭하면 트리에서 저장소에 항목을 로드할 것을 요구하고 저장소에서는 해당 자원을 요청할 것이다. 그러나 더하기 아이콘이 사라지고 하위 행이 표시되지 않는 경우와 같이 이상한 일이 일어날 수도 있다.
그림 4. 확장이 올바로 작동하지 않음
그렇다면, 왜 올바로 작동하지 않는 것일까?
목록 6에서 TreeGrid 및 TreeModel의 소스 코드를 살펴보고 하위 행 확장이 어떤 식으로
작동하는지 확인해보자.
목록 6. TreeGrid 및 TreeModel이 하위 행을 확장하는 방식
//TreeGrid.setOpen
setOpen: function(open){
...
treeModel.getChildren(itm, function(items){
d._loadedChildren = true;
d._setOpen(open);
});
...
}
//TreeModel.getChilren
getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete,
/*function*/ onError){
// summary:
// Calls onComplete() with array of child items of given parent item, all loaded.
var store = this.store;
if(!store.isItemLoaded(parentItem)){
// The parent is not loaded yet, we must be in deferItemLoadingUntilExpand mode,
// so we will load it and just return the children (without loading each child item)
var getChildren = dojo.hitch(this, arguments.callee);
store.loadItem({
item: parentItem,
onItem: function(parentItem){
getChildren(parentItem, onComplete, onError);
},
onError: onError
});
return;
}
// get children of specified item
var childItems = [];
for(var i=0; i<this.childrenAttrs.length; i++){
var vals = store.getValues(parentItem, this.childrenAttrs[i]);
childItems = childItems.concat(vals);
}
}
|
트리 모델에서는 우선 저장소에 상위 항목을 로드할 것을 요구한다. 상위 항목은 children 속성이 이 항목의 모든
하위 항목을 나열하는 배열이 아니기 때문에 완전히 로드되지 않는다.
하지만, 아무튼 이 경우는 store.isItemLoaded(parentItem) 검사 논리를 통과하고 childItems=[true]를
가지게 된다. True가 유효한 항목이 아니기 때문에, 트리 그리드는 올바르지 않은 이 하위 항목을 건너뛰어 결국 아무런 일도 일어나지 않는다. 목록 7에서
store.isItemLoaded 메소드에 대한 코드를 검토하여 무슨 일이 일어나고 있는지 살펴보자.
목록 7. store.isItemLoaded
isItemLoaded: function(/* anything */ something){
// Currently we don't have any state that tells if an item is loaded or not
// if the item exists it's also loaded.
// This might change when we start working with refs inside items ...
return this.isItem(something);
}
|
이 메소드의 주석에 설명이 나와 있다. QueryReadStore는 현재로서는 부분적으로 항목 로드를 지원하지 않는다.
부분적 로딩을 지원하기 위한 QueryReadStore 사용자 정의
이 섹션에서는 QueryReadStore와 TreeGrid를 사용자 정의하여 부분적 로딩을 구현하고
회귀 버그를 수정할 것이다.
사용자 정의된 코드를 전부 다운로드할 수 있다. 이 코드는 Eclipse에서 실행되는 웹 프로젝트로서, 손쉽게 구성된다.
트리 그리드의 확장 함수가 QueryReadStore에서 작동하도록 하려면, 이 함수가 부분적으로 로드된 항목에서 작동하도록
QueryReadStore를 사용자 정의해야 한다. 우선, 클래스를 확장하여 없는 메소드를 추가해야 한다. 목록 8에 표시된 것처럼,
다만 QueryReadStore를 CustomQueryStore로 서브클래스화한다.
목록 8. QueryReadStore의 서브클래스화
/* treegrid-demo\WebContent\script\dojo-1.4.3\demo\data\CustomQueryStore.js */
dojo.provide("demo.data.CustomQueryStore");
dojo.require("dojox.data.QueryReadStore");
dojo.declare("demo.data.CustomQueryStore", dojox.data.QueryReadStore, {
/* @Override */
isItemLoaded: function(/* anything */ something) {
// TODO
}
});
|
목록 9에 표시된 것처럼, 다음 단계는 어떤 항목이 로드되었는지 확인하기 위한 규칙을 변경하는 것이다.
목록 9. isItemLoaded 메소드
/* treegrid-demo\WebContent\script\dojo-1.4.3\demo\grid\CustomTreeGrid.js */
/* @Override isItemLoaded method */
isItemLoaded: function(/* anything */ something) {
// Currently we have item["children"] as a state that tells if an item is
// loaded or not.
// if item["children"] === true, means the item is not loaded.
var isLoaded = false;
if (this.isItem(something)) {
var children = this.getValue(something, "children");
if (children === true) {
// need to lazy loading children
isLoaded = false;
} else {
isLoaded = true;
}
}
return isLoaded;
}
|
QueryReadStore에는 loadItem 메소드가 없으므로, 이 메소드를 작성한다. 이 메소드는
해당 항목의 모든 하위 항목을 나열하는 배열을 서버에 요청할 것이다.
목록 10. loadItem, getValues 및 setValues 오버라이드
/* treegrid-demo\WebContent\script\dojo-1.4.3\demo\grid\CustomTreeGrid.js */
/* @Override loadItem method */
loadItem: function(/* object */ args) {
if (this.isItemLoaded(args.item)) {
return;
}
var item = args.item;
var scope = args.scope || dojo.global;
var sort = args.sort || null;
var onItem = args.onItem;
var onError = args.onError;
if (dojo.isArray(item)) {
item = item[0];
}
// load children
var children = this.getValue(item, "children");
// load children
if (children === true) {
var serverQuery = {};
// "parent" param
var itemId = this.getValue(item, "id");
serverQuery["parent"] = itemId;
// "sort" param
if (sort) {
var attribute = sort.attribute;
var descending = sort.descending;
serverQuery["sort"] = (descending ? "-" : "") + attribute;
}
// ajax request
var _self = this;
var xhrData = {
url: this.url,
handleAs: "json",
content: serverQuery
};
var xhrFunc = (this.requestMethod.toLowerCase() === "post") ?
dojo.xhrPost : dojo.xhrGet;
var deferred = xhrFunc(xhrData);
// onError callback
deferred.addErrback(function(error) {
if (args.onError) {
args.onError.call(scope, error);
}
});
// onLoad callback
deferred.addCallback(function(data) {
if (!data) {
return;
}
if (dojo.isArray(data)) {
var children = data;
var parentItemId = itemId;
var childItems = [];
dojo.forEach(children, function(childData) {
// build child item
var childItem = {};
childItem.i = childData;
childItem.r = this;
childItems.push(childItem);
}, _self);
_self.setValue(item, "children", childItems);
}
if (args.onItem) {
args.onItem.call(scope, item);
}
});
}
}
/* @Override geValues method */
getValues: function(item, attribute) {
// summary:
// See dojo.data.api.Read.getValues()
this._assertIsItem(item);
if (this.hasAttribute(item, attribute)) {
return item.i[attribute] || [];
}
return []; // Array
}
/* @Override seValue method */
setValue: function(/* item */ item, /* attribute-name-string */ attribute,
/* almost anything */ value) {
// summary: See dojo.data.api.Write.set()
// Check for valid arguments
this._assertIsItem(item);
this._assert(dojo.isString(attribute));
this._assert(typeof value !== "undefined");
var success = false;
var _item = item.i;
_item[attribute] = value;
success = true;
return success; // boolean
}
|
현재 예제에는 새 CustomQueryStore가 있다.
목록 11에 표시된 것처럼, JavaScript에서 QueryReadStore를 바꾼다.
목록 11. CustomQueryStore를 사용하여 요청 보내기
var url = "./servlet/QueryStoreServlet";
var store = new demo.data.CustomReadStore({
url: url,
requestMethod: "get"
});
|
이 코드는 그림 5에 표시된 결과를 리턴한다.
그림 5. customQueryStore에 의해 리턴된 결과
다른 기능에 대한 검사를 수행하여 CustomQueryStore를 적용한 후 회귀 버그가 없는지 확인할 시점이다.
우선 한 행을 펼친 다음, Name 헤더를 클릭하여 정렬을 수행한다. 그림 6에 표시된 것과 같은 오류가 발생한다.
그림 6. CustomQueryStore를 정렬할 때 오류 발생
조사를 해보면 트리 그리드가 항상 expando 함수의 상태를 유지한다는 점을 알 수 있다. 정렬 작업 전, 네 번째 행이
펼쳐지고 트리 그리드가 그것을 기억했다. 정렬 작업 후, 항목들이 서로 달랐지만 트리 그리드는 여전히 네 번째 행의 expando 함수를
열려고 할 것이다. 이제 네 번째 행에 대한 항목에 하위 항목이 없기 때문에 오류가 발생했다. 트리 그리드가 정렬 쿼리를 수행할 때 expando
상태를 지울 필요가 있다.
QueryReadStore를 사용자 정의한 것과 같이, TreeGrid를 사용자 정의할 필요가 있다. 그런
다음, CustomTreeGrid를 사용하여 TreeGrid를 바꾸면 정렬 결함이 수정되어야 한다.
목록 12. TreeGrid 사용자 정의
dojo.provide("demo.grid.CustomTreeGrid");
dojo.require("dojox.grid.TreeGrid");
dojo.declare("demo.grid.CustomTreeGrid", dojox.grid.TreeGrid, {
/* @Override */
sort: function() {
this.closeExpando();
this.inherited(arguments);
},
closeExpando: function(identities) {
if (identities) {
if (dojo.isArray(identities)) {
// close multiple expando
dojo.forEach(identities, function(identity) {
this._closeExpando(identity);
}, this);
} else {
// close single expando
var identity = identities;
this._closeExpando(identity);
}
} else {
// close all expando
var expandoCell = this.getCell(this.expandoCell);
for (var identity in expandoCell.openStates) {
this._closeExpando(identity);
}
}
},
_closeExpando: function(identity) {
var expandoCell = this.getCell(this.expandoCell);
if (expandoCell.openStates.hasOwnProperty(identity) === true) {
var open = expandoCell.openStates[identity] || false;
if (open === true) {
// clean up expando cache
this._cleanupExpandoCache(null, identity, null);
}
}
}
});
|
이와 아울러, TreeGrid와 QueryReadStore는 느린 데이터 로드를 해결하기 위한 강력한
조합이다. 미리 대량의 데이터를 전송하지 않고 크고 광범위한 계층 구조 데이터를 표시할 수 있다.
QueryReadStore의 부분적 로딩 지원을 활용하여 확장당 단 한 번의 요청으로 레이지 로딩을 수행할 수 있다.
이 기사를 위해, 우리는 ItemFileReadStore와 QueryReadStore를 사용하여
TreeGrid 성능을 비교했다.
그림 7과 표 1은 사용자 정의된 QueryStore 및 그리드가 ItemFileReadStore 및
기본 TreeGrid에서 사용하는 시간의 약 1/30만 사용한다는 점을 나타낸다.
그림 7. ItemFileReadStore와 CustomQueryStore 간의 성능 비교
표 1. 성능 비교
| 사용되는 데이터 저장소 | 서버 데이터 로드 시간(s) | 그리드 렌더링 시간(s) | 총계 |
|---|---|---|---|
| ItemFileReadStore | 1.45 | 12.65 | 14.1 |
| CustomQueryStore | 0.14 | 0.37 | 0.51 |
이 기사에서는 Dojo TreeGrid가 큰 데이터를 로드하는 성능을 향상하기 위해 사용자 정의된 솔루션을 작성하는 방법을
배워보았다. QueryReadStore, TreeModel 및 TreeGrid가 함께 작동하여
데이터를 페치하고 렌더링하는 방법을 살펴보았다. QueryStore와 TreeGrid를 사용자 정의하면 현재
큰 데이터를 그다지 잘 지원하지 않는 Dojo의 문제점을 해결할 수 있다. 본 기사의 결과에 따르면, 솔루션의 성능은 ItemFileReadStore와
TreeGrid가 쓰는 시간의 약 1/30에 불과한 것으로 나타났다.
| 설명 | 이름 | 크기 | 다운로드 방식 |
|---|---|---|---|
| Sample code for this article | treegrid-demo.zip | 31KB | HTTP |
교육
- Dojo hierarchical data and access through
dojo.data에 대해 읽어보자.
- Dojo 문서에서 다음에 대한 내용을 알아보자.
- dojocampus.org 웹 사이트에서
QueryReadStore에 대한 자세한 내용을 읽어보자.
제품 및 기술 얻기
- Dojo 코드를 구할 수 있다.
- IBM 제품 평가판을 다운로드하거나 IBM
SOA Sandbox의 온라인 시험판을 살펴보고 DB2®, Lotus®,
Rational®, Tivoli® 및 WebSphere® 애플리케이션 개발 도구와 미들웨어 제품을 사용해 볼 수 있다.
토론
- 지금 My developerWorks 프로파일을 작성하고 Dojo에 대한 관심 목록을 설정해 보자. My developerWorks에서 최신 정보를 자주 확인하자.
- 웹 개발에 관심이 있는 다른 developerWorks 멤버를 찾아보자.
- 자신의 지식을 공유하자. 웹 주제를 다루는 developerWorks 그룹에 참여하자.
- developerWorks의 멤버들이 공유하고
있는 웹 주제에 대한 책갈피를 살펴보자.

Sheng Hai Xu is a software engineer with the Globalization Development group at the IBM China Development Lab in Shanghai. He has experience developing web-based tooling with Dojo and Web 2.0 technologies.
