Appearance
Tree
@jennifersoft/apm-components의 Tree 컴포넌트 문서입니다. 이 문서는 @jennifersoft/apm-components@1.4.1 기준으로 정리했습니다.
계층 데이터를 가상 스크롤로 렌더하는 저수준 트리입니다. 검색·전체선택 등 부가 기능이 필요하면 TreeSelector를 사용하세요.
특징
@vueuse/components의UseVirtualList기반 가상 스크롤 (행 높이 24px 고정)- 제네릭
<T>로 노드에 임의 데이터를 실을 수 있음 - 접힘·선택 상태를 소유하지 않음 — 이벤트만 emit하고 갱신은 호출자가 함
인터랙티브 데모
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
width | number | — (필수) | 폭(px). 0이면 100% |
height | number | — (필수) | 높이(px). 가상 스크롤 계산에 사용 |
data | TreeNode<T>[] | — (필수) | 트리 데이터 |
checkedKeys | string[] | [] | 외부에서 지정하는 선택 키 목록 |
typescript
interface TreeNode<T> {
key: string;
label: string;
children: TreeNode<T>[];
fold?: boolean; // true면 접힘
check?: boolean; // 노드 자체의 선택 상태
disable?: boolean;
icon?: IconTypes; // @jennifersoft/vue-components-v2의 ICON_TYPE
data?: T; // 임의 페이로드
}children은 필수 필드입니다. 리프 노드도 빈 배열을 넣어야 합니다.
이벤트
typescript
emit('change-node-fold', key: string)
emit('click-node-label', args: [key: string, checked: boolean])click-node-label의 페이로드가 두 인자가 아니라 길이 2의 튜플 하나임에 주의하세요.
typescript
function onLabelClick([key, checked]: [string, boolean]) { /* ... */ }선택 상태 결정 규칙
checkedKeys와 노드의 check 필드가 함께 작동하며, 우선순위가 있습니다.
checkedKeys | 선택 여부 |
|---|---|
비어 있음 ([]) | 노드의 check 값만 사용 |
| 하나 이상 | checkedKeys에 포함되거나 또는 check가 true |
NOTE
checkedKeys가 비어 있지 않을 때는 OR 조건입니다. checkedKeys로 선택을 완전히 통제하려면 데이터의 check를 모두 false로 두세요.
슬롯
| 슬롯 | 설명 |
|---|---|
icon | 노드 아이콘 영역을 대체. 내부 Node의 slot props를 그대로 전달 |
사용 예제
vue
<script setup lang="ts">
import { ref } from 'vue';
import { Tree } from '@jennifersoft/apm-components';
import type { TreeNode } from '@jennifersoft/apm-components';
interface Payload {
domainId: number;
}
const data = ref<TreeNode<Payload>[]>([
{
key: 'domain-1',
label: 'OrderService',
fold: false,
children: [
{ key: 'inst-1', label: 'order-01', children: [], data: { domainId: 1 } },
{ key: 'inst-2', label: 'order-02', children: [], data: { domainId: 1 } },
],
},
]);
const checkedKeys = ref<string[]>([]);
// 접힘 상태는 호출자가 갱신한다
function onFoldChange(key: string) {
toggleFold(data.value, key);
}
function onLabelClick([key, checked]: [string, boolean]) {
checkedKeys.value = checked
? [...checkedKeys.value, key]
: checkedKeys.value.filter((item) => item !== key);
}
</script>
<template>
<Tree
:width="0"
:height="320"
:data="data"
:checked-keys="checkedKeys"
@change-node-fold="onFoldChange"
@click-node-label="onLabelClick"
>
<template #icon="slotProps">
<CustomIcon v-bind="slotProps" />
</template>
</Tree>
</template>관련 타입
typescript
/** 평탄화 후 렌더링에 사용하는 형태 */
interface TreeRenderingData<T = unknown>
extends Omit<TreeNode<T>, 'children' | 'data'> {
depth: number;
leaf: boolean;
}
interface TreeCacheMap<T = unknown> {
[key: string]: {
parent: string | null;
data: TreeRenderingData<T>;
};
}내부적으로 convertRenderingData()가 트리를 평탄화하면서 depth와 leaf를 계산합니다. 접힌 노드의 자식은 목록에서 제외되므로 가상 스크롤 길이가 자동으로 줄어듭니다.
주의사항
- 행 높이는 24px 고정입니다. CSS로 바꾸면 가상 스크롤 위치 계산이 어긋납니다.
height를 주지 않거나 0으로 두면 아무것도 보이지 않습니다. 컨테이너 높이를 계산해 넘기세요.- 접힘·선택은 컴포넌트가 관리하지 않습니다. emit을 받아
data또는checkedKeys를 갱신해야 화면이 바뀝니다. - 검색, 전체 선택, 부분 선택(indeterminate)이 필요하면 TreeSelector를 사용하세요.