컴포넌트/그리드

TreeGrid (트리 그리드)

VanillaFront 2026. 9. 29. 23:06

Va.TreeGrid — 계층 데이터 그리드 컴포넌트

부모-자식 관계를 가진 계층 데이터를 표 형태로 표시·편집·조회하는 트리 그리드 컴포넌트입니다. Va.Grid를 상속받아 가상 스크롤, 정렬, 필터링, 고정 컬럼, 셀 인라인 편집(FlashTag), 셀 내 상시 컴포넌트(Tag), 다단 헤더, 요약 행, 다중 선택 등 그리드의 모든 기능을 계승하면서, 노드 확장/축소, 트리 렌더링, 트리를 지키는 정렬·필터, 폴더 모드까지 트리 전용 기능을 제공합니다.


클래스 정보

  • Va.TreeGrid extends Va.Grid
  • tagName: 'treeGrid'
  • isContainer: false
  • 파일: va_treegrid.js

기본 사용법

{
    ref: 'refTreeGrid',
    tagName: 'treeGrid',
    style: { flex: 1, width: '100%' },
    columns: [
        { type: 'tree', key: 'display', title: '구분', width: 200, align: 'left' },
        { key: 'age',    title: '나이', width: 100 },
        { key: 'degree', title: '등급', width: 100 }
    ],
    onRowClick: 'onRowClick'
}

데이터 세팅 (mounted 후):

this.getRef('refTreeGrid').setData([
    { key: '1',   parentKey: null, display: '부서A', age: 40, degree: 5 },
    { key: '1-1', parentKey: '1',  display: '홍길동', age: 32, degree: 3 },
    { key: '1-2', parentKey: '1',  display: '김철수', age: 28, degree: 2 },
    { key: '2',   parentKey: null, display: '부서B', age: 45, degree: 6 }
]);

핵심: 계층 관계는 데이터의 parentKey (또는 vaDataParentId) 필드로 지정합니다. 컬럼 중 하나에 type: 'tree'를 지정하면 그 컬럼에 확장/축소 화살표와 들여쓰기가 자동 표시됩니다.


핸들러는 반드시 문자열 참조 (JSON 원칙)

VanillaFront는 화면 정의를 JSON으로 유지하는 철학입니다. 모든 이벤트/렌더 핸들러는 문자열로 View 메서드 이름을 지정합니다.

{ onRowClick: 'onRowClick', onEdit: 'onEditCell', onSummaryRender: 'onSummaryRender' }

인라인 function 사용 금지 — JSON 직렬화/에디터/저장 불가.


컬럼 정의 (columns)

Va.Grid의 모든 컬럼 속성을 그대로 사용하며, 트리 전용으로 아래 항목이 중요합니다.

속성타입설명

type String 'tree' 지정 시 확장/축소 UI와 들여쓰기 자동 표시. 컬럼 중 하나는 필수
key String 데이터 필드명 (필수)
title String 헤더 텍스트 (필수)
width Number 고정 폭 (px)
fillRatio Number 가변 폭 비율
align String 'left', 'center', 'right'
sortable Boolean 헤더 클릭 정렬 (같은 부모 노드 안에서만 정렬)
fixed Boolean 좌측 고정 컬럼 (가로 스크롤에도 유지)
summaryType String 요약 계산 ('sum', 'avg', 'count', 'max' 등)
onSummaryRender String 커스텀 요약 셀 렌더 메서드 이름
flashTag Object 셀 인라인 편집 컴포넌트
tag Object 셀 안 상시 표시 컴포넌트
onRender String 커스텀 셀 렌더 메서드 이름

주요 TreeGrid 속성

속성타입기본값설명

columns Array — 컬럼 정의 (필수, 하나는 type: 'tree')
data Array [] 초기 데이터 (parentKey로 부모 지정)
folderMode Boolean false 폴더 아이콘 스타일 표시
size String 'medium' xsmall, small, medium, large, xlarge
clickToSelect Boolean true 클릭 시 자동 선택
selectType String 'singleSelect' 'singleSelect', 'multiSelect'
sortable Boolean — 정렬 활성 (같은 부모 안에서만 동작)
stripe Boolean — 홀짝 행 배경 구분
autoCommit Boolean false 편집 즉시 커밋
enableCopy Boolean false Ctrl+C 셀 복사
summaryColumns Boolean — 요약 행 활성
summaryBottom Boolean — 요약 행을 하단에 표시

금지 옵션 — TreeGrid에서는 아래를 사용하면 에러 알림이 표시됩니다.

  • groupColumns — 트리 구조와 충돌
  • groupSummary — 트리 구조와 충돌

트리 구조 자체가 그룹핑을 대체합니다.


주요 이벤트

이벤트콜백발생 시점

onRowClick 'onRowClick' 행 클릭
onRowDblClick 'onRowDblClick' 행 더블클릭
onEdit 'onEditCell' 셀 편집 완료
onExpand 'onExpand' 노드 확장
onCollapse 'onCollapse' 노드 축소

주요 메서드

확장/축소 (TreeGrid 전용)

  • expand(data) — 특정 노드 확장
  • collapse(data) — 특정 노드 축소
  • expandAll(depth?) — 전체 확장 (depth 지정 시 해당 깊이까지)
  • collapseAll(depth?) — 전체 축소 (depth 지정 시 해당 깊이까지)

데이터 조작

  • setData(list) — 전체 교체
  • getData() — 현재 데이터
  • appendData(parent, row) — 특정 부모의 자식으로 추가 (트리 전용)
  • insertData(row, target) — target 위치에 삽입
  • modifyData(row) — 행 갱신
  • removeData(row) — 행 삭제 (자식 노드 포함 삭제)

변경 추적 (autoCommit: false 사용 시)

  • commit() — 변경사항 커밋
  • rollback() — 변경사항 되돌리기
  • getAddedData() — 추가된 행 목록
  • getModifiedData() — 수정된 행 목록
  • getRemovedData() — 삭제된 행 목록

선택

  • getSelectedData() — 선택 데이터 (single: 객체, multi: 배열)
  • getSelectedIndex() — 선택 인덱스

확장/축소 제어

onExpand() {
    let selectedData = this.getRef('refTreeGrid').getSelectedData();
    this.getRef('refTreeGrid').expand(selectedData);
}
onCollapse() {
    let selectedData = this.getRef('refTreeGrid').getSelectedData();
    this.getRef('refTreeGrid').collapse(selectedData);
}
onExpandAll() {
    this.getRef('refTreeGrid').expandAll();
}
onCollapseAllDepth1() {
    this.getRef('refTreeGrid').collapseAll(1);  // 2단계까지만 유지
}

버튼 배치 예:

{ tagName: 'button', text: '선택 확장',   onClick: 'onExpand' },
{ tagName: 'button', text: '선택 축소',   onClick: 'onCollapse' },
{ tagName: 'button', text: '전체 확장',   onClick: 'onExpandAll' },
{ tagName: 'button', text: '전체 축소',   onClick: 'onCollapseAll' }

자식 노드 추가 — appendData

TreeGrid는 일반 그리드와 달리 부모 노드 아래로 자식을 추가하는 개념이 필요합니다. appendData(parent, row)가 그 역할을 합니다.

onAppendData() {
    let selectedData = this.getRef('refTreeGrid').getSelectedData();
    this.getRef('refTreeGrid').appendData(selectedData, {
        key: '888', display: '추가그룹', age: 7, degree: 8
    });
}

선택된 노드가 부모가 되어 그 아래에 자식으로 붙습니다. parent가 null이면 루트에 추가됩니다.


트리 구조를 지키는 정렬·필터

일반 그리드의 정렬을 트리에 적용하면 계층이 무너지지만, TreeGrid는 같은 부모(vaDataParentId) 안에서만 정렬합니다. 필터도 마찬가지로 조건에 맞는 자식이 있으면 부모 노드는 자동 유지되어 트리 구조가 보존됩니다.

{
    tagName: 'treeGrid',
    sortable: true,
    columns: [
        { type: 'tree', key: 'display', title: '구분', width: 200 },
        { key: 'age',    title: '나이', width: 100, sortable: true },
        { key: 'degree', title: '등급', width: 100, sortable: true }
    ]
}

요약 행 — summaryColumns

컬럼별 합계·평균·개수 등을 자동 계산해 하단 요약 행에 표시합니다.

{
    tagName: 'treeGrid',
    summaryColumns: true,
    summaryBottom: true,
    columns: [
        { type: 'tree', key: 'display', title: '구분', width: 300,
          onSummaryRender: 'onSummaryRender' },
        { key: 'age',    title: '나이', width: 150, align: 'right',
          summaryType: 'sum' },
        { key: 'degree', title: '등급', width: 150, align: 'right',
          summaryType: 'max' }
    ]
}

커스텀 요약 렌더:

onSummaryRender(grid, key, values, data, column, rowIndex, colIndex, row, col, cell) {
    if (key == 'display') {
        cell.innerHTML = '합계';
        col.style.justifyContent = 'right';
    } else if (key == 'age') {
        let sum = 0;
        for (let i=0; i < values.length; i++) sum += values[i];
        cell.innerHTML = '개수: ' + values.length + '개 / 합계: ' + sum;
    }
}

변경 추적 + Commit/Rollback

autoCommit: false로 두면 사용자의 추가/수정/삭제가 임시 상태로 유지되고, 서버 저장 시점에 한꺼번에 처리할 수 있습니다.

{
    ref: 'refTreeGrid',
    tagName: 'treeGrid',
    autoCommit: false,
    columns: [
        { key: 'status', type: 'status', title: '상태', width: 60 },  // 변경 상태 표시
        { key: 'key',    title: 'Key',   width: 100 },
        { type: 'tree', key: 'display', title: '구분', width: 300 },
        { key: 'age',    title: '나이', width: 100, align: 'right' },
        { key: 'degree', title: '등급', width: 100, align: 'center' }
    ]
}

핵심 컬럼: type: 'status'를 넣으면 각 행의 변경 상태(추가/수정/삭제)가 시각적으로 표시됩니다.

핸들러:

onCommit()   { this.getRef('refTreeGrid').commit(); }
onRollback() { this.getRef('refTreeGrid').rollback(); }

onGetAddedData()    { console.log(this.getRef('refTreeGrid').getAddedData());    }
onGetModifiedData() { console.log(this.getRef('refTreeGrid').getModifiedData()); }
onGetRemovedData()  { console.log(this.getRef('refTreeGrid').getRemovedData());  }

서버 저장 흐름:

onSave() {
    let added    = this.getRef('refTreeGrid').getAddedData();
    let modified = this.getRef('refTreeGrid').getModifiedData();
    let removed  = this.getRef('refTreeGrid').getRemovedData();
    
    TreeService.save(this, { added, modified, removed }, (view, ok) => {
        if (ok) view.getRef('refTreeGrid').commit();
    });
}

종합 예시 — 확장/축소 + CRUD + 변경 추적

config() {
    return {
        tagName: 'page',
        tags: [{
            tagName: 'panel',
            style: { flex: 1 },
            tags: [{
                tagName: 'actionBar',
                layout: 'ds-flex fd-row ai-center gap-s',
                tags: [
                    { tagName: 'button', text: '선택 확장',  onClick: 'onExpand' },
                    { tagName: 'button', text: '선택 축소',  onClick: 'onCollapse' },
                    { tagName: 'button', text: '전체 확장',  onClick: 'onExpandAll' },
                    { tagName: 'button', text: '전체 축소',  onClick: 'onCollapseAll' },
                    { tagName: 'div', style: { flex: 1 } },
                    { tagName: 'button', text: '추가',       onClick: 'onAppend' },
                    { tagName: 'button', text: '변경',       onClick: 'onModify' },
                    { tagName: 'button', text: '삭제',       appearance: 'danger', onClick: 'onRemove' },
                    { tagName: 'button', text: 'Commit',     onClick: 'onCommit' },
                    { tagName: 'button', text: 'Rollback',   onClick: 'onRollback' }
                ]
            }, {
                ref: 'refTreeGrid',
                tagName: 'treeGrid',
                style: { flex: 1, width: '100%' },
                autoCommit: false,
                selectType: 'singleSelect',
                sortable: true,
                stripe: true,
                columns: [
                    { key: 'status', type: 'status', title: '상태', width: 60 },
                    { type: 'tree', key: 'display', title: '구분', width: 300 },
                    { key: 'age',    title: '나이', width: 100, align: 'right',
                      flashTag: { tagName: 'input', style: { width: '100%' } } },
                    { key: 'degree', title: '등급', width: 100, align: 'center',
                      flashTag: { tagName: 'slider', min: 0, max: 10, step: 1 } },
                    { key: 'date',   title: '날짜', width: 120, align: 'center' }
                ],
                onRowClick: 'onRowClick',
                onEdit: 'onEditCell'
            }]
        }]
    };
}

onAppend() {
    let selected = this.getRef('refTreeGrid').getSelectedData();
    this.getRef('refTreeGrid').appendData(selected, {
        key: '999', display: '신규노드', age: 0, degree: 1
    });
}
onModify() {
    let selected = this.getRef('refTreeGrid').getSelectedData();
    if (selected == null) { alert('데이터를 선택해 주십시오.'); return; }
    selected.age = 111;
    this.getRef('refTreeGrid').modifyData(selected);
}
onRemove() {
    let selected = this.getRef('refTreeGrid').getSelectedData();
    if (selected == null) { alert('데이터를 선택해 주십시오.'); return; }
    this.getRef('refTreeGrid').removeData(selected);
}

주의사항

  • 컬럼 중 하나는 반드시 type: 'tree' — 없으면 확장/축소 UI가 표시되지 않음
  • parentKey(또는 vaDataParentId) 필드 필수 — 부모-자식 관계를 이 필드로 판단
  • groupColumns, groupSummary 사용 금지 — 알림창이 뜨고 초기화 실패
  • 정렬은 같은 부모 안에서만 동작 — 트리 구조 보존을 위해 의도된 동작
  • removeData는 자식 노드까지 함께 삭제 — 부모 선택 후 삭제 시 서브트리 전체 제거
  • appendData(parent, row) 시그니처 주의 — 첫 인자가 부모, 둘째가 신규 행
  • autoCommit: false + type: 'status' 컬럼 조합 권장 — 변경 상태를 시각적으로 표시
  • 핸들러는 문자열 참조만 — 인라인 function은 JSON 원칙 위반
  • 최상위에 명시적 크기 필요 — style: { flex: 1, minHeight: '...' } 없으면 안 그려짐

대안 비교

상황추천

계층 데이터 (부서, 카테고리, 폴더) Va.TreeGrid
일반 데이터 목록/편집 Va.Grid
동적 컬럼/피벗 Va.DynamicGrid / Va.PivotGrid
트리만 (표 아님) Va.Tree
정적 격자 레이아웃 Va.GridDiv

참고

'컴포넌트 > 그리드' 카테고리의 다른 글

PivotGrid (피벗 그리드)  (0) 2026.09.29
Grid (그리드)  (0) 2026.09.23