Tree 트리
플랫 데이터를 트리로 변환하거나 선택을 전파하는 작업은 모두 core 순수 함수에서 처리하며, 컴포넌트는 렌더링과 키보드 조작만 담당합니다.
기본 사용법
<script setup>
import { ref } from 'vue'
const treeData = [
{ key: 'a', label: '프론트엔드', children: [
{ key: 'a-1', label: 'Vue' },
{ key: 'a-2', label: 'React' },
] },
{ key: 'b', label: '백엔드', children: [{ key: 'b-1', label: 'Node' }] },
{ key: 'c', label: '데브옵스', disabled: true },
]
</script>
<template>
<!-- 기본 사용법 -->
<LhTree :data="treeData" :default-expanded-keys="['a']" />
</template><script>
export default {
data() {
return {
treeData: [
{ key: 'a', label: '프론트엔드', children: [
{ key: 'a-1', label: 'Vue' },
{ key: 'a-2', label: 'React' },
] },
{ key: 'b', label: '백엔드', children: [{ key: 'b-1', label: 'Node' }] },
{ key: 'c', label: '데브옵스', disabled: true },
],
}
},
}
</script>
<template>
<!-- 기본 사용법 -->
<LhTree :data="treeData" :default-expanded-keys="['a']" />
</template>체크 가능 (계단식)
상위 노드를 체크하면 모든 하위 노드로 계단식 전파됩니다. 하위 노드를 모두 선택하면 상위 노드도 자동으로 선택되며, 일부만 선택된 경우 중간 선택 상태 (indeterminate)로 표시됩니다.
<script setup lang="ts">
const treeData = [
{ key: 'a', label: '프론트엔드', children: [
{ key: 'a-1', label: 'Vue' },
{ key: 'a-2', label: 'React' },
] },
{ key: 'b', label: '백엔드', children: [{ key: 'b-1', label: 'Node' }] },
{ key: 'c', label: '데브옵스', disabled: true },
]
</script>
<template>
<!-- 체크 계단식 전파 -->
<LhTree :data="treeData" show-checkbox :default-expanded-keys="['a', 'b']" />
</template><script>
export default {
data() {
return {
treeData: [
{ key: 'a', label: '프론트엔드', children: [
{ key: 'a-1', label: 'Vue' },
{ key: 'a-2', label: 'React' },
] },
{ key: 'b', label: '백엔드', children: [{ key: 'b-1', label: 'Node' }] },
{ key: 'c', label: '데브옵스', disabled: true },
],
}
},
}
</script>
<template>
<!-- 체크 계단식 전파 -->
<LhTree :data="treeData" show-checkbox :default-expanded-keys="['a', 'b']" />
</template>지연 로딩
lazy를 켜면 로드되지 않은 노드를 펼칠 때 load 콜백이 호출됩니다 (resolve로 자식 노드를 전달하며 펼치고, reject로 취소합니다). 로딩 중에는 화살표가 회전하는 애니메이션으로 표시됩니다. 로드 가능한 노드는 isLeaf: false를 명시적으로 선언해야 합니다 (그렇지 않으면 children이 없는 노드가 리프로 추론되어 전개 화살표가 표시되지 않습니다). 로드된 자식 노드가 여전히 로드 대상 브랜치인 경우에도 마찬가지로 isLeaf: false를 선언하고, 실제 리프는 isLeaf: true로 선언합니다.
<script setup>
import { ref } from 'vue'
const lazyData = ref([
{ key: 'lazy', label: '클릭하여 로드', isLeaf: false },
])
function loadNodes(node, resolve, reject) {
// 비동기 요청 시뮬레이션
setTimeout(() => {
if (node.key === 'lazy') {
resolve([
{ key: 'lazy-1', label: '자식 노드 1', isLeaf: true },
{ key: 'lazy-2', label: '자식 노드 2', isLeaf: true },
])
} else {
reject(new Error('nothing to load'))
}
}, 600)
}
</script>
<template>
<!-- 지연 로딩 -->
<LhTree :data="lazyData" lazy :load="loadNodes" />
</template><script>
export default {
data() {
return {
lazyData: [{ key: 'lazy', label: '클릭하여 로드', isLeaf: false }],
}
},
methods: {
// 비동기 요청 시뮬레이션
loadNodes(node, resolve, reject) {
setTimeout(() => {
if (node.key === 'lazy') {
resolve([
{ key: 'lazy-1', label: '자식 노드 1', isLeaf: true },
{ key: 'lazy-2', label: '자식 노드 2', isLeaf: true },
])
} else {
reject(new Error('nothing to load'))
}
}, 600)
},
},
}
</script>
<template>
<!-- 지연 로딩 -->
<LhTree :data="lazyData" lazy :load="loadNodes" />
</template>부모-자식 연동 해제 (checkStrictly)
checkStrictly를 켜면 체크가 노드 자신에게만 적용됩니다. 하위 노드로 계단식 전파되지 않고, 상위 노드로 거슬러 올라가지 않으며, 중간 선택 상태도 없습니다.
<script setup lang="ts">
const treeData = [
{ key: 'a', label: '프론트엔드', children: [
{ key: 'a-1', label: 'Vue' },
{ key: 'a-2', label: 'React' },
] },
{ key: 'b', label: '백엔드', children: [{ key: 'b-1', label: 'Node' }] },
{ key: 'c', label: '데브옵스', disabled: true },
]
</script>
<template>
<!-- 부모-자식 연동 해제 -->
<LhTree :data="treeData" show-checkbox check-strictly :default-expanded-keys="['a', 'b']" />
</template><script>
export default {
data() {
return {
treeData: [
{ key: 'a', label: '프론트엔드', children: [
{ key: 'a-1', label: 'Vue' },
{ key: 'a-2', label: 'React' },
] },
{ key: 'b', label: '백엔드', children: [{ key: 'b-1', label: 'Node' }] },
{ key: 'c', label: '데브옵스', disabled: true },
],
}
},
}
</script>
<template>
<!-- 부모-자식 연동 해제 -->
<LhTree :data="treeData" show-checkbox check-strictly :default-expanded-keys="['a', 'b']" />
</template>선택과 제어형 키 값
multiple은 다중 선택을 허용합니다. expand-on-click-node는 노드 클릭 시 펼치거나 접습니다. default-selected-keys / v-model:selected-keys로 선택을 제어하고, default-checked-keys / v-model:checked-keys로 체크를 제어하며, default-expanded-keys / v-model:expanded-keys로 전개를 제어합니다. disabled는 트리 전체를 비활성화합니다.
선택: [ "x" ] | 체크: [ "x-1" ]
<script setup>
import { ref } from 'vue'
const selKeys = ref(['x'])
const chkKeys = ref(['x-1'])
const ctrlData = [
{ key: 'x', label: '디자인', children: [
{ key: 'x-1', label: '색채' },
{ key: 'x-2', label: '서체' },
] },
{ key: 'y', label: '엔지니어링' },
]
</script>
<template>
<!-- 제어형 선택과 체크 -->
<LhTree
v-model:selected-keys="selKeys"
v-model:checked-keys="chkKeys"
:data="ctrlData"
multiple
expand-on-click-node
:default-expanded-keys="['x']"
:default-selected-keys="['x']"
:default-checked-keys="['x-1']"
/>
<p class="demo-tree-log">선택: {{ selKeys }} | 체크: {{ chkKeys }}</p>
<!-- 트리 전체 비활성화 -->
<LhTree :data="ctrlData" disabled />
</template><script>
export default {
data() {
return {
selKeys: ['x'],
chkKeys: ['x-1'],
ctrlData: [
{ key: 'x', label: '디자인', children: [
{ key: 'x-1', label: '색채' },
{ key: 'x-2', label: '서체' },
] },
{ key: 'y', label: '엔지니어링' },
],
}
},
}
</script>
<template>
<!-- 제어형 선택과 체크 -->
<LhTree
v-model:selected-keys="selKeys"
v-model:checked-keys="chkKeys"
:data="ctrlData"
multiple
expand-on-click-node
:default-expanded-keys="['x']"
:default-selected-keys="['x']"
:default-checked-keys="['x-1']"
/>
<p class="demo-tree-log">선택: {{ selKeys }} | 체크: {{ chkKeys }}</p>
<!-- 트리 전체 비활성화 -->
<LhTree :data="ctrlData" disabled />
</template>API
LhTree Props
| 속성명 | 설명 | 타입 | 기본값 |
|---|---|---|---|
| data | 트리 데이터 | [LhTreeNode](#type-LhTreeNode)[] | — |
| showCheckbox | 체크박스 표시 | boolean | false |
| multiple | 다중 선택 (노드 클릭 시 교체 대신 선택을 전환) | boolean | false |
| expandOnClickNode | 리프가 아닌 노드 클릭 시 펼치기/접기 | boolean | true |
| defaultExpandedKeys | 기본 펼침 키 (비제어형) | string[] | () => [] |
| expandedKeys | 제어형 펼침 키 | string[] | — |
| defaultSelectedKeys | 기본 선택 키 (비제어형) | string[] | () => [] |
| selectedKeys | 제어형 선택 키 | string[] | — |
| defaultCheckedKeys | 기본 체크 키 (비제어형) | string[] | () => [] |
| checkedKeys | 제어형 체크 키 | string[] | — |
| checkStrictly | 부모-자식 체크 연동 해제 (체크가 자신에게만 적용되며, 계단식 전파와 중간 선택 없음) | boolean | false |
| lazy | 지연 로딩 모드 (로드되지 않은 노드 펼침 시 load 호출) | boolean | false |
| load | 지연 로딩 콜백 (resolve로 자식 노드를 전달하며 펼치고, reject로 취소) | (node: LhTreeNode, resolve: (children: LhTreeNode[]) => void, reject: (reason?: unknown) => void) => void | — |
| disabled | 전체 비활성화 | boolean | false |
| sx | sx 빠른 레이아웃 속성 | LhSxProps | — |
Events
| 이벤트명 | 설명 | 콜백 매개변수 |
|---|---|---|
| update:expandedKeys | 펼침 키 목록이 변경될 때 트리거되며, v-model:expanded-keys와 함께 사용 | (keys: string[]) |
| update:selectedKeys | 선택 키 목록이 변경될 때 트리거되며, v-model:selected-keys와 함께 사용 | (keys: string[]) |
| update:checkedKeys | 체크 키 목록이 변경될 때 트리거되며, v-model:checked-keys와 함께 사용 | (keys: string[]) |
| select | 노드가 클릭되어 선택되거나 선택이 전환된 후 트리거 | (key: string, node: LhTreeNode) |
| check | 체크가 변경된 후 트리거 | (keys: string[], node: LhTreeNode) |
| expand | 노드가 펼쳐지거나 접힌 후 트리거 | (key: string, expanded: boolean) |
| nodeLoad | 지연 로딩 자식 노드가 반환된 후 트리거 | (node: LhTreeNode, children: LhTreeNode[]) |
슬롯: node (스코프 { node, level, expanded, selected }), empty.
LhTreeNode
| 필드 | 설명 | 타입 |
|---|---|---|
| key | 고유 키 | string |
| label | 표시 텍스트 | string |
| children | 자식 노드 | LhTreeNode[] |
| disabled | 비활성화 | boolean |
| isLeaf | 리프 여부 (전달하지 않으면 children으로부터 추론) | boolean |
접근성
role="tree"+role="treeitem"이며,aria-level/aria-expanded/aria-selected/aria-disabled가 모두 노출됩니다- 체크박스는
role="checkbox"+aria-checked입니다 (중간 선택은mixed) - 키보드 (APG tree 패턴): ↑/↓로 항목 간 이동 (비활성화 항목은 건너뛰고 순환), →로 펼치거나 첫 번째 자식으로 진입, ←로 접거나 상위 노드로 복귀, Home/End로 처음과 끝, Enter/Space로 활성화. roving tabindex를 사용합니다
타입 정의
이 컴포넌트가 외부로 내보내는 TypeScript 타입 구조는 다음과 같습니다 (packages/components/tree/src/types.ts와 @lh-ui/share/core/* 소스에서 자동 추출되었습니다. 각 항목을 클릭하여 펼쳐 보세요. 속성 설명은 위쪽 Props 표를 참고하세요):
LhTreeProps
export interface LhTreeProps {
/** 트리 데이터 */
data: LhTreeNode[]
/** 체크박스 표시 */
showCheckbox?: boolean
/** 다중 선택 (노드 클릭 시 교체 대신 선택을 전환) */
multiple?: boolean
/** 리프가 아닌 노드 클릭 시 펼치기/접기 */
expandOnClickNode?: boolean
/** 기본 펼침 키 (비제어형) */
defaultExpandedKeys?: string[]
/** 제어형 펼침 키 */
expandedKeys?: string[]
/** 기본 선택 키 (비제어형) */
defaultSelectedKeys?: string[]
/** 제어형 선택 키 */
selectedKeys?: string[]
/** 기본 체크 키 (비제어형) */
defaultCheckedKeys?: string[]
/** 제어형 체크 키 */
checkedKeys?: string[]
/** 부모-자식 체크 연동 해제 (체크가 노드 자신에게만 적용되며, 계단식 전파와 중간 선택 없음) */
checkStrictly?: boolean
/** 지연 로딩 모드: 로드되지 않은 리프가 아닌 노드 펼침 시 load 호출 */
lazy?: boolean
/** 지연 로딩 콜백: resolve로 자식 노드를 전달하며 펼치고, reject (또는 에러 발생)로 취소 */
load?: (
node: LhTreeNode,
resolve: (children: LhTreeNode[]) => void,
reject: (reason?: unknown) => void,
) => void
/** 전체 비활성화 */
disabled?: boolean
/** sx 빠른 레이아웃 속성 */
sx?: LhSxProps
}LhFlatTreeNode (@lh-ui/share/core/tree에서 제공)
/** 평탄화된 노드 (렌더링용) */
export interface LhFlatTreeNode {
node: LhTreeNode
key: string
level: number
parentKey: string | null
/** 조상 체인 (자신은 제외) */
ancestorKeys: string[]
/** 리프 여부 */
isLeaf: boolean
}LhTreeNode (@lh-ui/share/core/tree에서 제공)
/** 트리 노드 데이터 */
export interface LhTreeNode {
/** 고유 키 */
key: string
/** 표시 텍스트 */
label: string
/** 자식 노드 */
children?: LhTreeNode[]
/** 비활성화 */
disabled?: boolean
/** 리프 여부 (전달하지 않으면 children으로부터 추론) */
isLeaf?: boolean
}LhTreeCheckState (@lh-ui/share/core/tree에서 제공)
/** 체크 상태 */
export type LhTreeCheckState = 'checked' | 'indeterminate' | 'unchecked'