컴포넌트/그리드

Grid (그리드)

VanillaFront 2026. 9. 23. 12:58

Va.Grid — 데이터 그리드 컴포넌트

대량 데이터를 테이블 형태로 표시·편집·조회하는 핵심 그리드 컴포넌트입니다. 가상 스크롤, 정렬, 필터링, 그룹핑, 고정 컬럼, 셀 인라인 편집(FlashTag), 셀 내 상시 컴포넌트(Tag), 엑셀/CSV 다운로드/업로드, 다단 헤더, 요약 행, 다중 선택 등 실무에서 필요한 그리드 기능을 총망라합니다.

클래스 정보

Va.Grid extends Va.Component
tagName: 'grid'
isContainer: false
파일: va_grid.js

기본 사용법

{
    ref: 'refGrid',
    tagName: 'grid',
    style: { flex: 1, minHeight: '400px', width: '100%' },
    columns: [
        { key: 'itemId',   title: '품목ID', width: 100, align: 'center' },
        { key: 'itemName', title: '품목명', fillRatio: 1 },
        { key: 'price',    title: '가격',   width: 120, align: 'right' }
    ],
    onRowClick: 'onRowClick'
}

데이터 세팅 (mounted 후):

this.getRef('refGrid').setData([
    { itemId: '001', itemName: '노트북', price: 1500000 },
    { itemId: '002', itemName: '마우스', price: 25000 }
]);

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

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

{ onRowClick: 'onRowClick', onEdit: 'onEditCell', onRender: 'onColorRender' }

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

컬럼 정의 (columns)

각 컬럼 객체의 속성:

속성타입설명

key String 데이터 필드명 (필수)
title String 헤더 텍스트 (필수)
width Number 고정 폭 (px)
fillRatio Number 가변 폭 비율 — 남는 공간 배분
minWidth Number 최소 폭 (fillRatio와 조합)
align String 'left', 'center', 'right'
type String 'date', 'number', 'checkbox', 'rowNumber'
dateFormat String 예: 'mdy' (type: date일 때)
dateSeperator String 예: '/', '-'
sortable Boolean 헤더 클릭 정렬
hidden Boolean 숨김
fixed Boolean 좌측 고정 컬럼 (가로 스크롤에도 유지)
summaryType String 요약 계산 ('sum', 'avg', 'count' 등)
flashTag Object 셀 인라인 편집 컴포넌트 설정
tag Object 셀 안 상시 표시 컴포넌트 (버튼 등)
onRender String 커스텀 셀 렌더 메서드 이름
onEdit String 셀 편집 종료 콜백

주요 그리드 속성

속성타입기본값설명

columns Array 컬럼 정의 (필수)
data Array [] 초기 데이터
size String 'medium' xsmall, small, medium, large, xlarge
rowHeight Number 35 행 높이 (size로 자동 조정)
headerHeight Number 34 헤더 높이
clickToSelect Boolean true 클릭 시 자동 선택
selectType String 'singleSelect' 'singleSelect', 'multiSelect'
sortable Boolean 전체 컬럼 정렬 활성
stripe Boolean 홀짝 행 배경 구분
columnBorder Boolean true 셀 구분선
autoCommit Boolean false 편집 즉시 커밋
enableCopy Boolean false Ctrl+C 셀 복사
groupColumns Array 그룹핑 설정
groupSummary Boolean 그룹별 요약 행 자동
excelColumns Array 엑셀 다운로드 컬럼 정의
csvColumns Array CSV 다운로드 컬럼 정의

주요 이벤트

이벤트콜백발생 시점

onRowClick 'onRowClick' 행 클릭
onRowDblClick 'onRowDblClick' 행 더블클릭
onEdit 'onEditCell' 셀 편집 완료

주요 메서드

데이터 조작:

  • setData(list) — 전체 교체
  • getData() — 현재 데이터
  • addData(row) — 마지막에 추가
  • insertData(row, target) — target 위치에 삽입 (target: 선택 데이터 or 인덱스)
  • modifyData(row) — 행 갱신
  • removeData(row) — 행 삭제

선택:

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

다운로드/업로드:

  • downloadExcel(filename) — 엑셀 파일
  • downloadCsv() — CSV 파일
  • uploadCsv(content) — CSV 문자열 로드

상태:

  • isEditing (속성) — 편집 중이면 true

커스텀 셀 렌더 — onRender

핸들러는 View 메서드 이름 문자열로 지정.

검증된 시그니처 (DemoGridColumnType.js 기준):

onColorRender(grid, key, value, groupData, column, rowIndex, colIndex, row, col, cell, component)

인자의미

grid 그리드 컴포넌트
key 컬럼 key
value 셀 값
groupData 행 데이터 (그룹 컨텍스트 포함)
column 컬럼 정의 객체
rowIndex, colIndex 인덱스
row 행 DOM
col 셀 DOM (스타일 직접 수정)
cell 셀 wrapper DOM
component 관련 컴포넌트

config:

columns: [
    { key: 'color', title: '색상', width: 100, onRender: 'onColorRender' },
    { key: 'amt',   title: '금액', width: 100, align: 'right', onRender: 'onAmtRender' }
]

View 클래스:

// 색상 값을 셀 배경색으로
onColorRender(grid, key, value, groupData, column, rowIndex, colIndex, row, col, cell, component) {
    col.style.backgroundColor = value;
}

// 금액 값에 따라 행 색상 변경
onAmtRender(grid, key, value, groupData, column, rowIndex, colIndex, row, col, cell, component) {
    if (value > 500) {
        row.style.backgroundColor = 'lightgreen';
        row.style.color = 'red';
    } else if (value < 0) {
        row.style.backgroundColor = 'pink';
    }
}

// 셀 padding 조정
onRenderItem(grid, key, value, groupData, column, rowIndex, colIndex, row, col, cell, component) {
    cell.style.paddingLeft = '30px';
}

셀 인라인 편집 — flashTag

FlashTag는 셀 클릭 시 셀 위에 뜨는 편집 컴포넌트. 편집 후 다른 곳 클릭 시 값이 자동 반영.

지원 컴포넌트 (DemoGridFlashTag.js 기준):

  • input — 텍스트 입력
  • combobox — 드롭다운 선택
  • datePicker — 날짜
  • checkbox — 체크박스 (valueType으로 저장 형식 지정)
  • slider — 슬라이더

기본 사용

columns: [
    // 텍스트 입력
    {
        key: 'itemName', title: '품목명', width: 200,
        flashTag: {
            tagName: 'input',
            style: { width: '100%' }
        }
    },
    // 콤보박스 (View의 DataManager 참조)
    {
        key: 'itemId', title: '품목코드', width: 200,
        flashTag: {
            tagName: 'combobox',
            key: 'itemId',
            display: 'itemName',
            mountDataManager: 'itemDataManager',
            style: { width: '100%' }
        },
        onEdit: 'onEditItemId'
    },
    // 슬라이더
    {
        key: 'price', title: '가격', width: 200, align: 'center',
        flashTag: {
            tagName: 'slider',
            min: 0, max: 100, step: 1,
            style: { width: '100%' }
        }
    },
    // 체크박스 (Y/N 값)
    {
        key: 'useYn', title: '사용여부', width: 100, align: 'center',
        flashTag: {
            tagName: 'checkbox',
            valueType: 'YN'
        }
    },
    // 날짜 선택
    {
        key: 'createDate', title: '등록일', width: 120,
        flashTag: {
            tagName: 'datePicker',
            style: { width: '100%' }
        }
    }
]

FlashTag onEdit 콜백 확장

콤보 선택 시 다른 컬럼 값도 함께 세팅:

onEditItemId(grid, key, value, data, column, rowIndex, colIndex, row, col, cell, component) {
    // component는 flashTag의 combobox 인스턴스
    data['itemName'] = component.getDisplay();
    this.getRef('refGrid').modifyData(data);
}

FlashTag에서 DataManager 연동

mounted() {
    // View에 DataManager 준비
    this.itemDataManager = new Va.DataManager();
    this.itemDataManager.set([
        { itemId: '001', itemName: '품목1' },
        { itemId: '002', itemName: '품목2' }
    ]);
}

// columns 안에서 mountDataManager로 참조
{
    key: 'itemId',
    flashTag: {
        tagName: 'combobox',
        key: 'itemId',
        display: 'itemName',
        mountDataManager: 'itemDataManager'
    }
}

셀 내 상시 컴포넌트 — tag

셀 안에 항상 표시되는 컴포넌트 (버튼 등):

{
    key: 'reg', title: '등록', width: 100, align: 'center',
    tag: {
        tagName: 'button',
        text: '버튼',
        appearance: 'primary',
        onClick: 'onButtonClick',
        style: { width: '100%' }
    }
}

핸들러:

onButtonClick(btn) {
    // btn.getGrid()로 그리드 접근
    let grid = btn.getGrid();
    console.log(grid);
}

그룹핑 — groupColumns

특정 컬럼 값 기준으로 행 그룹화 (DemoGridGrouping.js 기준):

{
    tagName: 'grid',
    groupColumns: [
        { key: 'itemName', summary: false },
        { key: 'color',    summary: false }
    ],
    columns: [
        { key: 'itemName', title: '품명', width: 100 },
        { type: 'rowNumber', key: 'rowNumber', title: '#', width: 80, align: 'center' },
        { key: 'color',      title: '색상', width: 100, align: 'center' },
        { key: 'amt',        title: '금액', width: 200, align: 'right' },
        { key: 'createDate', title: '등록일', type: 'date', width: 140, align: 'center' }
    ],
    onRowClick: 'onRowClick'
}

동일 itemName끼리 묶이고, 그 안에서 다시 color별로 묶임.

고정 컬럼 — fixed

columns: [
    { key: 'itemId',    title: '품목ID',  width: 100, fixed: true },
    { key: 'itemName',  title: '품목명',  width: 100, fixed: true },
    { key: 'price',     title: '가격',    width: 200 },   // 스크롤 대상
    { key: 'stock',     title: '재고',    width: 200 }
]

앞쪽 두 컬럼이 가로 스크롤해도 좌측 고정.

정렬 — Sorting

columns: [
    { key: 'itemId', title: 'ID', width: 100, sortable: true },
    { key: 'price',  title: '가격', width: 120, sortable: true }
]
// 또는 전체 활성
{ tagName: 'grid', sortable: true, columns: [...] }

데이터 CRUD — 추가/삽입/변경/삭제

DemoGridFlashTag.js 검증 예제:

// 마지막에 추가
onAdd() {
    this.getRef('refGrid').addData({
        itemGroupId: '999', itemGroupName: '추가그룹',
        itemId: '999', itemName: '추가품목'
    });
}

// 선택 위치에 삽입
onInsert() {
    let selectedData = this.getRef('refGrid').getSelectedData();
    if (selectedData == null) {
        new Va.Alert({ title: '확인', message: '먼저 행을 선택하십시오' }).show(this.getView());
        return;
    }
    this.getRef('refGrid').insertData({
        itemGroupId: '999', itemGroupName: '추가그룹',
        itemId: '999', itemName: '추가품목'
    }, selectedData);
}

// 선택 행 변경
onModify() {
    let selectedData = this.getRef('refGrid').getSelectedData();
    if (selectedData == null) {
        alert('데이터를 선택해 주십시오.');
        return;
    }
    selectedData.itemGroupName = '변경함';
    this.getRef('refGrid').modifyData(selectedData);
}

// 선택 행 삭제
onRemove() {
    let selectedData = this.getRef('refGrid').getSelectedData();
    if (selectedData == null) {
        alert('데이터를 선택해 주십시오.');
        return;
    }
    this.getRef('refGrid').removeData(selectedData);
}

// 전체 데이터 조회 (편집 중 확인)
onGetAllData() {
    if (this.getRef('refGrid').isEditing == true) {
        new Va.Alert({
            title: '확인',
            message: '아직 편집중입니다. 편집을 종료하고 다시 시도하십시오'
        }).show(this);
        return;
    }
    console.log(this.getRef('refGrid').getData());
}

// 선택 데이터 조회
onGetSelectedData() {
    console.log(this.getRef('refGrid').getSelectedData());
}

엑셀/CSV 다운로드·업로드

DemoGridExcelDownload.js 검증 예제:

{
    tagName: 'grid',
    excelColumns: [
        { name: 'itemId',   text: '품목ID',   type: 'text',   width: '100px', align: 'center' },
        { name: 'itemName', text: '품목명',   type: 'text',   width: '150px' },
        { name: 'price',    text: '가격',     type: 'number', width: '100px', align: 'right' }
    ],
    csvColumns: [
        { name: 'itemId',   text: '품목ID' },
        { name: 'itemName', text: '품목명' },
        { name: 'price',    text: '가격' }
    ]
}

핸들러:

// 엑셀 다운로드
onExcelDownload() {
    this.getRef('refGrid').downloadExcel('엑셀 다운로드 테스트');
}

// CSV 다운로드
onCsvDownload() {
    this.getRef('refGrid').downloadCsv();
}

// CSV 업로드 (파일 컴포넌트에서)
onCsvUpload(comp) {
    let file = comp.fieldEl.files[0];
    let reader = new FileReader();
    reader.view = this;
    reader.onload = function(e) {
        this.view.getRef('refGrid').uploadCsv(e.target.result);
    };
    reader.readAsText(file);
}

셀 복사 — Ctrl+C

{ tagName: 'grid', enableCopy: true }

활성 시 선택 셀에서 Ctrl+C로 복사 (탭 구분자, 엑셀에 붙여넣기 가능).

다중 선택 + Shift

{
    tagName: 'grid',
    selectType: 'multiSelect',
    columns: [
        { key: 'chk', title: '', width: 40, type: 'checkbox' },
        // ...
    ]
}

조작:

  • 일반 클릭: 해당 행만 선택
  • Ctrl + 클릭: 개별 토글 (추가/해제)
  • Shift + 클릭: 마지막 선택 위치부터 클릭 위치까지 범위 선택
  • 드래그: 드래그 범위 선택

선택 데이터 조회:

onClickDelete() {
    let selected = this.getRef('refGrid').getSelectedData();  // 배열
    if (!selected || selected.length === 0) {
        alert('선택된 항목이 없습니다.');
        return;
    }
    selected.forEach(item => this.getRef('refGrid').removeData(item));
}

종합 예시 — CRUD + FlashTag + 다운로드

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: 'onAdd' },
                    { tagName: 'button', text: '삽입', onClick: 'onInsert' },
                    { tagName: 'button', text: '변경', onClick: 'onModify' },
                    { tagName: 'button', text: '삭제', appearance: 'danger', onClick: 'onRemove' },
                    { tagName: 'div', style: { flex: 1 } },
                    { tagName: 'button', text: '엑셀', icon: 'ico_download', onClick: 'onExcelDownload' },
                    { tagName: 'button', text: 'CSV', onClick: 'onCsvDownload' }
                ]
            }, {
                ref: 'refGrid',
                tagName: 'grid',
                style: { flex: 1, minHeight: '400px', width: '100%' },
                autoCommit: false,
                enableCopy: true,
                selectType: 'multiSelect',
                sortable: true,
                stripe: true,
                excelColumns: [
                    { name: 'itemId',   text: '품목ID',   width: '100px' },
                    { name: 'itemName', text: '품목명',   width: '150px' },
                    { name: 'price',    text: '가격',     type: 'number', width: '100px' }
                ],
                columns: [
                    { key: 'chk',      title: '',       width: 40,  type: 'checkbox', fixed: true },
                    { key: 'itemId',   title: '품목ID', width: 100, fixed: true, sortable: true },
                    {
                        key: 'itemName', title: '품목명', width: 200, sortable: true,
                        flashTag: { tagName: 'input', style: { width: '100%' } }
                    },
                    {
                        key: 'itemGroupId', title: '그룹', width: 150,
                        flashTag: {
                            tagName: 'combobox',
                            key: 'itemGroupId',
                            display: 'itemGroupName',
                            mountDataManager: 'itemGroupDataManager'
                        }
                    },
                    {
                        key: 'price', title: '가격', width: 120, align: 'right',
                        flashTag: {
                            tagName: 'slider',
                            min: 0, max: 1000000, step: 1000,
                            style: { width: '100%' }
                        }
                    },
                    {
                        key: 'useYn', title: '사용', width: 80, align: 'center',
                        flashTag: { tagName: 'checkbox', valueType: 'YN' }
                    },
                    {
                        key: 'color', title: '색상', width: 100,
                        onRender: 'onColorRender'
                    },
                    {
                        key: 'action', title: '액션', width: 100, align: 'center',
                        tag: {
                            tagName: 'button',
                            text: '상세',
                            appearance: 'primary',
                            onClick: 'onClickDetail'
                        }
                    }
                ],
                onRowClick: 'onRowClick',
                onEdit: 'onEditCell'
            }, {
                ref: 'pagination',
                tagName: 'pagination',
                refGrid: 'refGrid',
                service: 'getItemList'
            }]
        }]
    };
}

// 색상 셀 배경 렌더
onColorRender(grid, key, value, groupData, column, rowIndex, colIndex, row, col, cell, component) {
    col.style.backgroundColor = value;
}

주의사항

  • 최상위에 명시적 크기 필요  style: { flex: 1, minHeight: '...' } 없으면 안 그려짐
  • columns 배열은 필수 — 없으면 초기화 실패
  • 핸들러는 문자열 참조만 — 인라인 function은 JSON 원칙 위반
  • onRender 시그니처 11개 인자 — DemoGridColumnType 기준
  • 가상 스크롤 — 화면에 보이는 행만 DOM 렌더, 대량 데이터도 성능 유지
  • fillRatio 사용 시 — 하나라도 있으면 flexGrid 모드
  • autoCommit: false 기본  commit() 명시 or autoCommit: true로 즉시 반영
  • isEditing 확인 — getData 등 조회 전 편집 중인지 반드시 확인
  • FlashTag의 mountDataManager는 View에 미리 준비 — mounted에서 DataManager 세팅
  • selectType 'multiSelect' 시 getSelectedData는 배열, 'singleSelect'는 객체
  • 엑셀/CSV 컬럼 정의는 별도 — 그리드 columns와 다른 구성 가능

대안 비교

상황추천

데이터 목록/편집 그리드 Va.Grid
트리 구조 그리드 Va.TreeGrid
동적 컬럼/피벗 Va.DynamicGrid / Va.PivotGrid
정적 격자 레이아웃 Va.GridDiv
단순 리스트 Va.Div + 반복

참고