zTree 정리 — jQuery 트리 플러그인

이민재·2026년 4월 9일

zTree 정리 — jQuery 트리 플러그인

jQuery 기반의 강력한 트리 UI 플러그인 zTree의 설치부터 고급 활용까지 한 번에 정리합니다.


목차

  1. zTree란?
  2. 설치 및 기본 세팅
  3. 데이터 구조
  4. setting 옵션 완전 정복
  5. 콜백(Callback) 이벤트
  6. 체크박스 / 라디오 모드
  7. Ajax 동적(지연) 로딩
  8. 노드 편집 및 드래그 앤 드롭
  9. API 메서드 총정리
  10. 커스텀 아이콘 & 스타일
  11. 실전 예제 — 파일 탐색기 UI
  12. 마치며 — 대안 라이브러리

1. zTree란?

zTree는 jQuery 기반의 오픈소스 트리 UI 플러그인입니다. 2010년대 초반부터 국내 공공기관, 기업 인트라넷, 관리자 페이지에서 폭넓게 사용되어 왔습니다.

주요 특징

특징설명
경량압축 기준 약 60KB 수준
체크박스/라디오부모-자식 연동 자동 처리
Ajax 지연 로딩클릭 시 자식 노드를 서버에서 동적으로 가져옴
드래그 앤 드롭노드 이동, 순서 변경 지원
인라인 편집노드 추가·수정·삭제를 UI 상에서 직접 처리
대용량 처리수천 개 노드에서도 안정적인 성능

언제 사용하면 좋을까?

  • 레거시 jQuery 기반 프로젝트
  • 공공기관/기업 관리자 페이지
  • 파일 탐색기, 조직도, 메뉴 구조, 카테고리 관리

2. 설치 및 기본 세팅

CDN 방식

<!-- 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 방식

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>

3. 데이터 구조

zTree는 두 가지 데이터 구조를 지원합니다.

방식 A — 중첩(표준) 데이터

JSON 구조 그대로 children 배열로 계층을 표현합니다.

const zNodes = [
  {
    name: "회사",
    open: true,
    children: [
      {
        name: "개발팀",
        children: [
          { name: "프론트엔드" },
          { name: "백엔드" }
        ]
      },
      { name: "디자인팀" }
    ]
  }
];

방식 B — 단순(평면) 데이터 ⭐ 실무 추천

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: "백엔드" }
];

노드 주요 속성

속성타입설명
namestring표시될 노드 이름
idany노드 고유 ID (단순 모드)
pIdany부모 노드 ID (단순 모드)
openboolean펼침 여부 (기본 false)
isParentboolean자식 없어도 폴더처럼 표시
checkedboolean체크 초기값
iconstring커스텀 아이콘 경로
iconOpenstring펼쳐진 상태 아이콘
iconClosestring닫힌 상태 아이콘
urlstring클릭 시 이동할 URL
targetstringURL 열기 대상 (_blank 등)
nocheckboolean이 노드만 체크박스 숨김
chkDisabledboolean체크박스 비활성화

4. setting 옵션 완전 정복

setting 객체는 view, data, async, check, edit, callback 6개 영역으로 나뉩니다.

4-1. view — 외관 설정

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>`);
      }
    }
  }
};

4-2. data — 데이터 매핑

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 응답 필드명이 다를 때 유용합니다.


5. 콜백(Callback) 이벤트

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);
    }
  }
};

6. 체크박스 / 라디오 모드

체크박스 기본 설정

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": 전체에서 하나만
  }
};

7. Ajax 동적(지연) 로딩

노드 클릭 시 서버에서 자식 데이터를 받아오는 방식입니다. 초기 로드 시간을 줄이고 대용량 트리에 유리합니다.

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 }
];

서버 응답 예시 (Spring Boot)

@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 }
]

특정 노드만 Ajax 로딩 (조건부)

const setting = {
  async: {
    enable: true,
    url: function(treeId, node) {
      // 노드 타입별로 다른 API 호출
      return node.type === "folder"
        ? "/api/tree/folder"
        : "/api/tree/file";
    }
  }
};

8. 노드 편집 및 드래그 앤 드롭

편집 모드 활성화

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"
    }
  }
};

9. API 메서드 총정리

// 트리 인스턴스 가져오기
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");

10. 커스텀 아이콘 & 스타일

이미지 아이콘 지정

const zNodes = [
  {
    id: 1, pId: 0, name: "서버",
    icon: "/icons/server.png",        // 기본 아이콘
    iconOpen: "/icons/server-on.png", // 펼쳐진 상태
    iconClose: "/icons/server-off.png" // 닫힌 상태
  }
];

CSS 클래스로 아이콘 변경

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>');
      }
    }
  }
};

노드별 CSS 스타일 동적 적용

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 {};
    }
  }
};

CSS 커스터마이징 예시

/* 선택된 노드 배경색 변경 */
.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;
}

11. 실전 예제 — 파일 탐색기 UI

체크박스 + 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>

12. 마치며 — 대안 라이브러리

zTree는 jQuery 생태계에서 매우 성숙한 라이브러리지만, 현대적인 프레임워크 환경에서는 아래 대안도 고려하세요.

상황추천 라이브러리
Reactrc-tree (Ant Design), react-arborist, @mui/x-tree-view
Vue 3el-tree (Element Plus), vue-treeselect
순수 JSjsTree, Treant.js
대용량 + 가상화react-arborist (가상 스크롤 내장)

정리

  • 레거시 jQuery 프로젝트 → zTree가 여전히 최선
  • 신규 React/Vue 프로젝트 → 각 프레임워크 생태계의 트리 컴포넌트 사용
  • 공공기관 SI → zTree 요구사항이 많으므로 숙지해두면 유리

profile
초보 개발자

0개의 댓글