- 발행일
jsTree 기본 사용법 — 플러그인·API 정리와 실무에서 걸린 지점
jsTree 기본 사용법
jsTree는 트리 구조를 웹에서 표현하는 jQuery 기반 플러그인입니다. 폴더 구조, 조직도, 카테고리 계층을 보여줄 때 씁니다. 표준데이터 관리 시스템의 트리 화면(메타 클래스·OID·인스턴스 트리)에 붙이면서 정리한 레퍼런스이고, 뒤에 실무에서 걸린 지점을 붙였습니다.
플러그인 한눈에 보기
| 플러그인 | 설명 |
|---|---|
checkbox | 노드에 체크박스 표시 |
contextmenu | 우클릭 메뉴 |
dnd | 드래그 앤 드롭 |
search | 노드 검색 |
sort | 자동 정렬 |
state | 열림/선택 상태 기억 (localStorage) |
types | 노드 타입별 아이콘 지정 |
unique | 형제 간 중복 이름 방지 |
wholerow | 행 전체 클릭·강조 |
기본 초기화와 플러그인 예제
// checkbox — 체크된 항목 가져오기
$('#tree').jstree({
plugins: ['checkbox'],
core: {
data: [
{ text: '문서', children: [{ text: '계약서.docx' }, { text: '견적서.xlsx' }] },
{ text: '사진', children: [{ text: '여행사진.jpg' }] },
],
},
});
const checked = $('#tree').jstree('get_checked', true);
// search — 입력과 연결
$('#tree-search').on('keyup', function () {
$('#tree').jstree('search', $(this).val());
});
// contextmenu — 항목을 함수로 구성
$('#tree').jstree({
plugins: ['contextmenu'],
contextmenu: {
items: (node) => ({
create: { label: '노드 추가', action: () => {/* ... */} },
delete: { label: '노드 삭제', action: () => $('#tree').jstree('delete_node', node) },
}),
},
});
// dnd — check_callback 없이는 이동이 전부 거부된다 (아래 실무 절 참고)
$('#tree').jstree({
plugins: ['dnd'],
core: { check_callback: true, data: [/* ... */] },
});
// types — 노드 타입별 아이콘
$('#tree').jstree({
plugins: ['types'],
types: {
default: { icon: 'fa fa-folder' },
file: { icon: 'fa fa-file' },
},
core: { data: [{ text: '문서', type: 'default', children: [{ text: '이력서.docx', type: 'file' }] }] },
});
// AJAX — 노드를 펼칠 때마다 서버에서 자식 로드
$('#tree').jstree({
core: {
data: {
url: '/get/nodes/',
data: (node) => ({ id: node.id }), // 루트는 id가 '#' — 서버가 이 값을 알아야 한다
},
},
});
주요 API
const tree = $('#myTree').jstree(true); // 인스턴스 얻기
| 함수 | 설명 |
|---|---|
open_node(node) / close_node(node) | 노드 열기/닫기 |
create_node(parent, data) | 노드 생성 |
rename_node(node, name) | 이름 변경 |
delete_node(node) | 삭제 |
get_selected() / get_node(id) | 선택 목록 / 노드 객체 |
select_node(node) / deselect_all() | 선택 / 전체 해제 |
주요 이벤트
| 이벤트 | 시점 |
|---|---|
ready.jstree | 초기화 완료 |
select_node.jstree / changed.jstree | 선택 / 선택 집합 변경 |
open_node.jstree / close_node.jstree | 펼침 / 접힘 |
rename_node.jstree / move_node.jstree | 이름 변경 / 이동(dnd 포함) |
| 실무 요구사항 | 조합 |
|---|---|
| 트리 보여주기 + 아이콘 | 기본 + types |
| 선택·검색·우클릭 | checkbox / search / contextmenu |
| 순서 변경·상태 기억·중복 방지 | dnd / state / unique |
실무에서 걸린 지점
1. check_callback의 진짜 의미 — 예제마다 true로 적혀 있는 이유
dnd 예제마다 core.check_callback: true가 들어 있는데, 처음엔 왜 필요한지 몰랐습니다. jsTree는 트리를 바꾸는 모든 조작(create/rename/delete/move)을 기본적으로 거부합니다. true는 "전부 허용"이라는 뜻이고요. 문제는 실무에서 true로 두면 어떤 노드든 어디로든 이동된다는 것 — 루트를 리프 밑으로 끌어넣는 것도 허용됩니다. 실전에서는 함수를 줘야 합니다:
check_callback: (operation, node, parent) => {
if (operation === 'move_node') return parent.id !== '#'; // 루트 승격 금지 등 규칙
return true;
},
예제의 true는 "데모가 되게 하는 값"이지 "실무 기본값"이 아니었습니다.
2. 루트의 id는 '#'이다
AJAX 로딩에서 data: (node) => ({ id: node.id })로 노드 id를 서버에 보내는데, 최초 로드 때 오는 id는 '#' 입니다. 서버가 이걸 모르면 루트 요청이 "id가 #인 노드"를 찾다 실패해요. 서버 쪽에 id == '#' → 최상위 목록 반환 분기가 반드시 필요합니다. 문서를 안 읽고 붙이면 첫 요청부터 걸리는 지점입니다.
3. 기본 contextmenu는 버리고 모달로 갔다
우클릭 메뉴로 노드 추가/수정을 붙여 보면, 기본 contextmenu의 인라인 rename(더블클릭 편집)만으로는 필드가 여러 개인 폼(이름 + 설명 + 타입 등)을 받을 수 없습니다. 결국 contextmenu는 "메뉴를 여는 트리거"로만 쓰고, 실제 입력은 커스텀 모달로 뺐어요. 그 과정은 별도 글로 정리했습니다.
4. state 플러그인은 데이터가 바뀌면 거짓말을 한다
열림/선택 상태를 localStorage에 저장해 주는 건 편한데, 트리 데이터가 서버에서 바뀌면 저장된 상태가 이미 없는 노드를 가리킬 수 있습니다. 관리 화면처럼 노드가 추가·삭제되는 트리에서는 상태 복원이 어긋난 것처럼 보이는 원인이 됐고, 데이터 갱신 시점에 state를 초기화하는 처리가 같이 필요했습니다.
5. 이 글의 search 예제는 그대로 쓰면 안 된다
위 예제는 keyup마다 search()를 호출합니다. 노드 수백 개까지는 티가 안 나는데, 검색은 전체 노드를 순회하므로 트리가 커지면 타이핑마다 전체 탐색이 됩니다. 디바운스를 끼우는 게 맞고, 매칭 노드만 보이게 하려면 search.show_only_matches: true 옵션도 함께 켜야 기대한 화면이 나옵니다. 예제를 정리할 때는 몰랐고, 실무 트리에 붙이면서 배웠습니다.
한계
jQuery 의존은 그대로 남습니다 — 이미 jQuery를 쓰는 서버 렌더링 프로젝트라 문제가 없었지만, React/Vue 프로젝트라면 시작부터 다른 선택지를 보는 게 맞습니다. 수천 노드 이상에서의 렌더 성능은 검증하지 않았습니다(실무 화면은 수백 노드 수준). 라이브러리 선택 배경과 1년 운영 후의 결말은 JSTree & VTree 사전 조사에 있습니다.