jQuery 기반의 강력한 트리 UI 플러그인 zTree의 설치부터 고급 활용까지 한 번에 정리합니다.
zTree는 jQuery 기반의 오픈소스 트리 UI 플러그인입니다. 2010년대 초반부터 국내 공공기관, 기업 인트라넷, 관리자 페이지에서 폭넓게 사용되어 왔습니다.
| 특징 | 설명 |
|---|---|
| 경량 | 압축 기준 약 60KB 수준 |
| 체크박스/라디오 | 부모-자식 연동 자동 처리 |
| Ajax 지연 로딩 | 클릭 시 자식 노드를 서버에서 동적으로 가져옴 |
| 드래그 앤 드롭 | 노드 이동, 순서 변경 지원 |
| 인라인 편집 | 노드 추가·수정·삭제를 UI 상에서 직접 처리 |
| 대용량 처리 | 수천 개 노드에서도 안정적인 성능 |
<!-- jQuery (필수 의존성) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- zTree CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ztree@3.5.48/css/zTreeStyle/zTreeStyle.min.css">
<!-- zTree JS (all = core + excheck + exedit) -->
<script src="https://cdn.jsdelivr.net/npm/ztree@3.5.48/js/jquery.ztree.all.min.js"></script>
기능별로 분리 로딩도 가능합니다.
jquery.ztree.core.min.js— 기본 트리jquery.ztree.excheck.min.js— 체크박스/라디오 (core 필요)jquery.ztree.exedit.min.js— 편집/드래그 (core 필요)
npm install ztree
import 'ztree/css/zTreeStyle/zTreeStyle.css';
import 'ztree';
<!-- 트리 컨테이너: ul 태그에 ztree 클래스 필수 -->
<ul id="myTree" class="ztree"></ul>
<script>
const setting = {};
const zNodes = [
{ name: "루트", open: true, children: [
{ name: "자식 1" },
{ name: "자식 2" }
]}
];
// 초기화: $.fn.zTree.init(컨테이너, 설정, 데이터)
$.fn.zTree.init($("#myTree"), setting, zNodes);
</script>
zTree는 두 가지 데이터 구조를 지원합니다.
JSON 구조 그대로 children 배열로 계층을 표현합니다.
const zNodes = [
{
name: "회사",
open: true,
children: [
{
name: "개발팀",
children: [
{ name: "프론트엔드" },
{ name: "백엔드" }
]
},
{ name: "디자인팀" }
]
}
];
DB에서 조회한 평면 배열을 그대로 사용할 수 있어 실무에서 더 많이 쓰입니다.
const setting = {
data: {
simpleData: {
enable: true, // 단순 데이터 모드 활성화
idKey: "id", // 기본값 "id"
pIdKey: "pId", // 부모 참조 키, 기본값 "pId"
rootPId: 0 // 루트 노드의 pId 값, 기본값 null
}
}
};
const zNodes = [
{ id: 1, pId: 0, name: "회사", open: true },
{ id: 2, pId: 1, name: "개발팀", open: true },
{ id: 3, pId: 1, name: "디자인팀" },
{ id: 4, pId: 2, name: "프론트엔드" },
{ id: 5, pId: 2, name: "백엔드" }
];
| 속성 | 타입 | 설명 |
|---|---|---|
name | string | 표시될 노드 이름 |
id | any | 노드 고유 ID (단순 모드) |
pId | any | 부모 노드 ID (단순 모드) |
open | boolean | 펼침 여부 (기본 false) |
isParent | boolean | 자식 없어도 폴더처럼 표시 |
checked | boolean | 체크 초기값 |
icon | string | 커스텀 아이콘 경로 |
iconOpen | string | 펼쳐진 상태 아이콘 |
iconClose | string | 닫힌 상태 아이콘 |
url | string | 클릭 시 이동할 URL |
target | string | URL 열기 대상 (_blank 등) |
nocheck | boolean | 이 노드만 체크박스 숨김 |
chkDisabled | boolean | 체크박스 비활성화 |
setting 객체는 view, data, async, check, edit, callback 6개 영역으로 나뉩니다.
const setting = {
view: {
showIcon: true, // 노드 아이콘 표시 여부 (기본 true)
showLine: true, // 연결선 표시 여부 (기본 true)
showTitle: true, // 마우스 오버 title 속성 표시
selectedMulti: false, // 다중 선택 허용 (기본 false)
expandSpeed: "fast", // 펼침 애니메이션 속도 ("", "slow", "normal", "fast")
dblClickExpand: true, // 더블클릭으로 펼치기 (기본 true)
nameIsHTML: false, // name을 HTML로 렌더링할지 여부
fontCss: function(treeId, node) {
// 노드별 인라인 스타일 반환
return node.level === 0
? { "font-weight": "bold" }
: {};
},
addDiyDom: function(treeId, node) {
// 노드 DOM에 커스텀 요소 추가
const id = node.tId + "_span";
if ($("#" + id).children("span.badge").length === 0) {
$("#" + id).after(`<span class="badge">${node.count || 0}</span>`);
}
}
}
};
const setting = {
data: {
simpleData: {
enable: true,
idKey: "id",
pIdKey: "pId",
rootPId: 0
},
key: {
name: "label", // name 키 이름을 "label"로 변경
title: "tip", // title 속성 키 이름 변경
children: "nodes", // children 키 이름을 "nodes"로 변경
url: "link" // url 키 이름 변경
}
}
};
data.key는 서버 API 응답 필드명이 다를 때 유용합니다.
const setting = {
callback: {
// 노드 클릭
onClick: function(event, treeId, treeNode) {
console.log("클릭:", treeNode.name, treeNode.id);
},
// 노드 더블클릭
onDblClick: function(event, treeId, treeNode) {
console.log("더블클릭:", treeNode.name);
},
// 체크박스 상태 변경
onCheck: function(event, treeId, treeNode) {
console.log("체크:", treeNode.name, treeNode.checked);
},
// 노드 펼치기 전
beforeExpand: function(treeId, treeNode) {
return true; // false 반환 시 펼치기 중단
},
// 노드 펼친 후
onExpand: function(event, treeId, treeNode) {
console.log("펼침:", treeNode.name);
},
// 노드 접은 후
onCollapse: function(event, treeId, treeNode) {
console.log("접힘:", treeNode.name);
},
// Ajax 로딩 성공 후
onAsyncSuccess: function(event, treeId, treeNode, msg) {
console.log("로딩 완료:", treeNode.name);
},
// Ajax 로딩 실패
onAsyncError: function(event, treeId, treeNode, XMLHttpRequest, textStatus, errorThrown) {
console.error("로딩 실패:", textStatus);
},
// 노드 추가 후
onNodeCreated: function(event, treeId, treeNode) {
console.log("노드 생성:", treeNode.name);
}
}
};
const setting = {
check: {
enable: true, // 체크박스 활성화
chkStyle: "checkbox", // "checkbox" 또는 "radio"
chkboxType: {
"Y": "ps", // 체크 시: p(부모 연동), s(자식 연동)
"N": "ps" // 해제 시: p(부모 연동), s(자식 연동)
}
// 연동 옵션: "p" 부모만, "s" 자식만, "ps" 둘 다, "" 연동 없음
}
};
| chkboxType 값 | 동작 |
|---|---|
{ "Y": "ps", "N": "ps" } | 체크/해제 시 부모·자식 모두 연동 (기본) |
{ "Y": "s", "N": "s" } | 체크 시 자식만 연동, 부모 영향 없음 |
{ "Y": "", "N": "" } | 완전 독립 (연동 없음) |
{ "Y": "p", "N": "p" } | 체크 시 부모만 연동 |
const treeObj = $.fn.zTree.getZTreeObj("myTree");
// 체크된 노드만
const checkedNodes = treeObj.getCheckedNodes(true);
// 체크 안 된 노드만
const uncheckedNodes = treeObj.getCheckedNodes(false);
// ID 배열로 변환
const checkedIds = checkedNodes.map(node => node.id);
console.log(checkedIds); // [1, 3, 5]
const treeObj = $.fn.zTree.getZTreeObj("myTree");
// 특정 노드 체크
const node = treeObj.getNodeByParam("id", 3);
treeObj.checkNode(node, true, true); // (노드, 체크여부, 자식연동)
// 전체 체크 / 해제
treeObj.checkAllNodes(true);
treeObj.checkAllNodes(false);
const setting = {
check: {
enable: true,
chkStyle: "radio",
radioType: "all" // "level": 같은 레벨끼리만, "all": 전체에서 하나만
}
};
노드 클릭 시 서버에서 자식 데이터를 받아오는 방식입니다. 초기 로드 시간을 줄이고 대용량 트리에 유리합니다.
const setting = {
async: {
enable: true,
url: "/api/tree/children", // 서버 엔드포인트
autoParam: ["id", "name=label"], // 요청에 포함할 노드 속성
// "name=label"은 name 값을 label 파라미터명으로 전송
otherParam: { // 추가 고정 파라미터
token: "my-secret-token",
type: "menu"
},
type: "post", // HTTP 메서드 (기본 "post")
contentType: "application/x-www-form-urlencoded",
dataFilter: function(treeId, parentNode, responseData) {
// 서버 응답을 zTree 형식으로 가공
return responseData.result || [];
}
}
};
// isParent: true 인 노드는 자식 없이 폴더처럼 표시되다가
// 클릭 시 Ajax 요청 발생
const zNodes = [
{ id: 1, pId: 0, name: "전체 메뉴", isParent: true, open: false }
];
@GetMapping("/api/tree/children")
public List<TreeNode> getChildren(@RequestParam Long id) {
return treeService.findByParentId(id);
}
[
{ "id": 10, "pId": 1, "name": "서브메뉴 1", "isParent": false },
{ "id": 11, "pId": 1, "name": "서브메뉴 2", "isParent": true }
]
const setting = {
async: {
enable: true,
url: function(treeId, node) {
// 노드 타입별로 다른 API 호출
return node.type === "folder"
? "/api/tree/folder"
: "/api/tree/file";
}
}
};
const setting = {
edit: {
enable: true, // 편집 기능 전체 활성화
showRemoveBtn: true, // 삭제 버튼 표시
showRenameBtn: true, // 이름 변경 버튼 표시
removeTitle: "삭제",
renameTitle: "이름 변경",
drag: {
enable: true, // 드래그 앤 드롭 활성화
autoExpandTrigger: true, // 드래그 중 자동 펼침
prev: true, // 노드 앞에 드롭 허용
next: true, // 노드 뒤에 드롭 허용
inner: true // 노드 안으로 드롭 허용 (자식으로)
}
},
callback: {
// 삭제 전 확인
beforeRemove: function(treeId, treeNode) {
return confirm(`"${treeNode.name}"을 삭제하시겠습니까?`);
},
// 이름 변경 완료 후
onRename: function(event, treeId, treeNode, isCancel) {
if (!isCancel) {
// 서버에 변경 사항 저장
$.post("/api/tree/rename", { id: treeNode.id, name: treeNode.name });
}
},
// 드롭 전 유효성 검사
beforeDrop: function(treeId, treeNodes, targetNode, moveType) {
// 루트 노드는 이동 불가
if (targetNode && targetNode.level === 0 && moveType === "inner") {
return false;
}
return true;
},
// 드롭 완료 후
onDrop: function(event, treeId, treeNodes, targetNode, moveType, isCopy) {
console.log("이동된 노드:", treeNodes[0].name);
console.log("이동 위치:", moveType); // "prev", "next", "inner"
}
}
};
// 트리 인스턴스 가져오기
const treeObj = $.fn.zTree.getZTreeObj("myTree");
// 루트 노드 목록
const roots = treeObj.getNodes();
// 조건으로 노드 검색 (단일)
const node = treeObj.getNodeByParam("id", 5);
// 조건으로 노드 검색 (복수)
const nodes = treeObj.getNodesByParam("type", "folder");
// 체크된 노드
const checked = treeObj.getCheckedNodes(true);
// 선택(하이라이트)된 노드
const selected = treeObj.getSelectedNodes();
// 부모 노드
const parent = treeObj.getNodeByTId(node.parentTId);
// 특정 부모 아래에 추가
const parent = treeObj.getNodeByParam("id", 1);
treeObj.addNodes(parent, [
{ id: 100, name: "새 노드 1" },
{ id: 101, name: "새 노드 2" }
]);
// 루트에 추가 (parent = null)
treeObj.addNodes(null, [{ id: 200, name: "루트 노드" }]);
// 특정 위치에 삽입 (index)
treeObj.addNodes(parent, 0, [{ id: 102, name: "맨 앞에 추가" }]);
// 이름 변경
const node = treeObj.getNodeByParam("id", 5);
node.name = "변경된 이름";
treeObj.updateNode(node);
// 삭제
treeObj.removeNode(node);
// 모든 자식 삭제
treeObj.removeChildNodes(parent);
// 전체 펼치기 / 접기
treeObj.expandAll(true); // 전체 펼치기
treeObj.expandAll(false); // 전체 접기
// 특정 노드
treeObj.expandNode(node, true, true, true);
// 인자: (노드, 펼침여부, 자식포함, 애니메이션)
// 선택 처리
treeObj.selectNode(node);
treeObj.cancelSelectedNode(node);
// 체크
treeObj.checkNode(node, true, true); // 체크
treeObj.checkNode(node, false, true); // 해제
treeObj.checkAllNodes(true); // 전체 체크
// 데이터 새로고침
$.fn.zTree.init($("#myTree"), setting, newData);
// 트리 인스턴스 제거
$.fn.zTree.destroy("myTree");
const zNodes = [
{
id: 1, pId: 0, name: "서버",
icon: "/icons/server.png", // 기본 아이콘
iconOpen: "/icons/server-on.png", // 펼쳐진 상태
iconClose: "/icons/server-off.png" // 닫힌 상태
}
];
const setting = {
view: {
addDiyDom: function(treeId, treeNode) {
const aObj = $("#" + treeNode.tId + "_a");
// 기존 아이콘 숨기고 Font Awesome 등 적용
aObj.find(".button").css("display", "none");
if (treeNode.isParent) {
aObj.prepend('<i class="fa fa-folder" style="margin-right:4px"></i>');
} else {
aObj.prepend('<i class="fa fa-file" style="margin-right:4px"></i>');
}
}
}
};
const setting = {
view: {
fontCss: function(treeId, treeNode) {
if (treeNode.disabled) {
return { color: "#aaa", "text-decoration": "line-through" };
}
if (treeNode.level === 0) {
return { "font-weight": "bold", color: "#333" };
}
return {};
}
}
};
/* 선택된 노드 배경색 변경 */
.ztree li a.curSelectedNode {
background-color: #e8f4ff;
border: 1px solid #b8d9f8;
color: #1a73e8;
}
/* 호버 스타일 */
.ztree li a:hover {
background-color: #f5f5f5;
}
/* 연결선 색상 변경 */
.ztree li span.button.switch {
background-color: transparent;
}
체크박스 + Ajax 동적 로딩 + 노드 클릭 이벤트를 조합한 파일 탐색기 예제입니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>파일 탐색기</title>
<link rel="stylesheet" href="css/zTreeStyle/zTreeStyle.css">
<style>
body { font-family: sans-serif; display: flex; height: 100vh; margin: 0; }
#sidebar { width: 280px; border-right: 1px solid #ddd; padding: 16px; overflow-y: auto; }
#content { flex: 1; padding: 16px; }
#selectedPath { color: #555; font-size: 14px; margin-bottom: 12px; }
.ztree li a.curSelectedNode { background: #e8f4ff; border-color: #b8d9f8; }
</style>
</head>
<body>
<div id="sidebar">
<h3 style="margin-top:0">파일 탐색기</h3>
<ul id="fileTree" class="ztree"></ul>
</div>
<div id="content">
<div id="selectedPath">← 파일을 선택하세요</div>
<div id="fileInfo"></div>
</div>
<script src="js/jquery.min.js"></script>
<script src="js/jquery.ztree.all.min.js"></script>
<script>
$(function () {
const setting = {
view: {
showIcon: true,
selectedMulti: false
},
data: {
simpleData: { enable: true }
},
check: {
enable: true,
chkboxType: { "Y": "ps", "N": "ps" }
},
async: {
enable: true,
url: "/api/files/children",
autoParam: ["id"],
dataFilter: function (treeId, parentNode, res) {
return res.data || [];
}
},
callback: {
onClick: function (event, treeId, node) {
if (!node.isParent) {
$("#selectedPath").text("선택: " + getFullPath(node));
loadFileInfo(node.id);
}
},
onCheck: function (event, treeId, node) {
const checked = $.fn.zTree.getZTreeObj("fileTree").getCheckedNodes(true);
console.log("체크된 파일 수:", checked.length);
}
}
};
const initNodes = [
{ id: 1, pId: 0, name: "루트", isParent: true, open: true,
icon: "img/folder.png" },
{ id: 2, pId: 1, name: "문서", isParent: true },
{ id: 3, pId: 1, name: "사진", isParent: true },
{ id: 4, pId: 2, name: "보고서.docx", isParent: false }
];
$.fn.zTree.init($("#fileTree"), setting, initNodes);
// 노드의 전체 경로 계산
function getFullPath(node) {
const parts = [node.name];
let current = node;
const treeObj = $.fn.zTree.getZTreeObj("fileTree");
while (current.parentTId) {
current = treeObj.getNodeByTId(current.parentTId);
parts.unshift(current.name);
}
return parts.join(" / ");
}
// 파일 정보 로드
function loadFileInfo(id) {
$.get("/api/files/" + id, function (data) {
$("#fileInfo").html(`
<p><b>이름:</b> ${data.name}</p>
<p><b>크기:</b> ${data.size} KB</p>
<p><b>수정일:</b> ${data.updatedAt}</p>
`);
});
}
});
</script>
</body>
</html>
zTree는 jQuery 생태계에서 매우 성숙한 라이브러리지만, 현대적인 프레임워크 환경에서는 아래 대안도 고려하세요.
| 상황 | 추천 라이브러리 |
|---|---|
| React | rc-tree (Ant Design), react-arborist, @mui/x-tree-view |
| Vue 3 | el-tree (Element Plus), vue-treeselect |
| 순수 JS | jsTree, Treant.js |
| 대용량 + 가상화 | react-arborist (가상 스크롤 내장) |