Skip to content

데이터 테이블 (Data Table)

DataTable 컴포넌트는 Headless UI 라이브러리 TanStack을 기반으로 구축된 테이블 컴포넌트입니다.

@tanstack/vue-table, @tanstack/vue-virtual을 사용하여 구현되었습니다.

display: gridgrid-template-columns를 사용하여 컬럼 리사이징이 가능하도록 구현되었습니다.

이름
이메일
권한
부서
상태
마지막 접속

주요 기능 (Features)

  • 행 선택 (Row Selection): 단일 및 다중 행 선택을 지원하며, 외부에서 selection 값이 변경되면 선택 행으로 부드럽게 자동 스크롤됩니다.
  • 정렬 (Sorting): UI 인디케이터를 포함한 단일 컬럼 정렬 (오름차순/내림차순)을 지원합니다.
  • 컬럼 리사이징 (Column Resizing): 테이블 헤더에서만 컬럼 리사이징을 지원하며, 드래그 중에는 바디 영역에도 세로 가이드 라인이 표시됩니다.
  • 컬럼 표시 제어 (Column Visibility): 프로그래밍 방식으로 컬럼의 표시 여부를 제어할 수 있습니다.
  • 커스텀 셀 (Custom Cells): Vue 컴포넌트 또는 렌더 함수(Render functions)를 사용하여 셀 렌더링을 완전히 커스터마이징할 수 있습니다.
  • 로딩 및 빈 상태 (Loading & Empty States): 로딩 중이거나 데이터가 없을 때를 처리하기 위한 전용 Slot과 Prop을 제공합니다.
  • 툴팁 지원 (Tooltip Support): 내용이 길어 잘리는 경우(Ellipsis), 툴팁을 통해 전체 내용을 확인할 수 있습니다.
  • 다양한 크기 (Sizes): mini, small, normal (기본), large 네 가지 크기를 지원합니다.

테이블 변형 (Table Variant)

variant prop을 사용하여 테이블의 시각적 스타일을 inset (기본값) 또는 attached 모드로 설정할 수 있습니다.

Inset (Default)

이름
이메일
권한
부서
상태
마지막 접속

Attached

이름
이메일
권한
부서
상태
마지막 접속

테이블 크기 (Table Size)

size prop을 사용하여 테이블 행의 높이와 폰트 크기를 조절할 수 있습니다.

Mini

이름
이메일
권한
부서
상태
마지막 접속

Small

이름
이메일
권한
부서
상태
마지막 접속

Normal (Default)

이름
이메일
권한
부서
상태
마지막 접속

Large

이름
이메일
권한
부서
상태
마지막 접속

Row Selection (행 선택)

selection-mode prop으로 선택 모드를 설정할 수 있습니다.

  • 'none': 선택 불가 (기본값)
  • 'single': 단일 행 선택
  • 'checkbox': 체크박스를 통한 다중 행 선택

selection prop 또는 v-model:selection으로 선택 행을 외부에서 제어할 수 있습니다. 외부 값이 변경되어 선택 행이 바뀌면, 테이블은 해당 행이 보이도록 부드럽게 자동 스크롤합니다. 체크박스 다중 선택에서는 전달된 배열에서 처음 매칭된 행을 기준으로 스크롤합니다.

Single Selection (단일 선택)

기본적인 단일 선택 모드입니다. 행을 클릭하여 선택합니다.

이름
이메일
권한
부서
상태
마지막 접속
vue
<template>
    <DataTable
        :data="data"
        :columns="columns"
        selection-mode="single"
        @update:selection="handleRowSelection"
    />
</template>

Checkbox Selection (체크박스 선택)

selection-mode="checkbox"를 설정하면 첫 번째 컬럼에 체크박스가 자동으로 추가됩니다.

이름
이메일
권한
부서
상태
마지막 접속
vue
<template>
    <DataTable
        :data="data"
        :columns="columns"
        selection-mode="checkbox"
        @update:selection="handleRowSelection"
    />
</template>

Expand Row (행 확장)

단일 선택 모드(selection-mode="single")일 때, 이미 선택된 행을 한 번 더 클릭하면 행이 확장되면서 추가 정보를 보여주는 기능을 지원합니다. expanded-row 슬롯을 사용하여 확장 영역의 내용을 정의할 수 있습니다.

이름
이메일
권한
부서
상태
마지막 접속
vue
<script setup lang="ts">
import { DataTable } from '@jennifersoft/vue-components-v2';

const subColumns = [
    { accessorKey: 'date', header: '접속 일자', size: 120 },
    { accessorKey: 'ip', header: 'IP 주소', size: 150 },
    { accessorKey: 'status', header: '상태', size: 100 },
];

const subData = [
    { date: '2023-10-01', ip: '192.168.1.1', status: 'Success' },
    { date: '2023-10-05', ip: '192.168.1.10', status: 'Failed' },
    { date: '2023-10-12', ip: '10.0.0.5', status: 'Success' },
];
</script>

<template>
    <DataTable :data="data" :columns="columns" selection-mode="single">
        <template #expanded-row="{ row }">
            <div class="expanded-content">
                <p style="margin-bottom: 8px;">
                    <strong>상세 정보:</strong> {{ row.name }}님의 접속 이력
                </p>
                <DataTable :data="subData" :columns="subColumns" size="mini" />
            </div>
        </template>
    </DataTable>
</template>

<style scoped>
.expanded-content {
    padding: 16px;
    background-color: var(--gray-50);
    border-bottom: 1px solid var(--gray-200);
}
</style>

컬럼 라인 (Column Lines)

show-column-lines prop을 사용하여 데이터 셀의 세로 구분선을 표시할 수 있습니다.

이름
이메일
권한
부서
상태
마지막 접속

행 라인 (Row Lines)

show-row-lines: false prop을 사용하여 데이터 행의 가로 구분선을 표시하지 않을 수 있습니다.

이름
이메일
권한
부서
상태
마지막 접속

스트라이프 행 (Striped Rows)

striped-rows prop을 사용하여 홀수 행에 배경색을 적용하여 가독성을 높일 수 있습니다.

이름
이메일
권한
부서
상태
마지막 접속

헤더 숨기기 (Hide Header)

hide-header prop을 사용하여 테이블의 컬럼 헤더를 숨길 수 있습니다.

사용법 (Usage)

vue
<script setup lang="ts">
import { ref } from 'vue';
import { DataTable } from '@jennifersoft/vue-components-v2';
import type { ColumnDef } from '@tanstack/vue-table';

interface User {
    id: number;
    name: string;
    email: string;
    role: string;
    department: string;
    status: string;
    lastLogin: string;
}

const data = ref<User[]>([
    {
        id: 1,
        name: '김철수',
        email: 'chulsu.kim@example.com',
        role: '관리자',
        department: '개발팀',
        status: '활성',
        lastLogin: '2024-01-15',
    },
    {
        id: 2,
        name: '이영희',
        email: 'younghee.lee@example.com',
        role: '사용자',
        department: '디자인팀',
        status: '휴식',
        lastLogin: '2024-01-20',
    },
    // ... 더 많은 데이터
]);

const columns: ColumnDef<User>[] = [
    { accessorKey: 'name', header: '이름', size: 100 },
    { accessorKey: 'email', header: '이메일', size: 100 },
    { accessorKey: 'role', header: '권한', size: 100 },
    { accessorKey: 'department', header: '부서', size: 100 },
    { accessorKey: 'status', header: '상태', size: 100 },
    { accessorKey: 'lastLogin', header: '마지막 접속', size: 100 },
];
</script>

<template>
    <DataTable :data="data" :columns="columns" />
</template>

컬럼 정의 (Column Definitions)

컬럼은 @tanstack/vue-tableColumnDef 타입을 사용하여 정의합니다.

타입 안전한 컬럼 정의 (Type-Safe Column Definition)

createColumnHelper를 사용하면 위에서 커스텀한 meta 속성(align)의 타입 추론 및 자동 완성 지원을 받을 수 있습니다.

typescript
import { createColumnHelper } from '@tanstack/vue-table';

const columnHelper = createColumnHelper<User>();

const columns = [
    columnHelper.accessor('name', {
        header: '이름',
        meta: {
            align: 'center', // 'left' | 'center' | 'right' 자동 완성 지원
        },
    }),
];

기본 컬럼 (Basic Column)

typescript
{
  accessorKey: 'status',
  header: '상태',
  size: 120, // 픽셀 단위 초기 크기
  minSize: 80, // 최소 크기
  maxSize: 200, // 최대 크기
  meta: {
      align: 'center' // 'left' | 'center' | 'right' (기본값: 'left')
  }
}

정렬 커스터마이징 (Sorting Customization)

기본적으로 테이블은 데이터의 타입에 따라 정렬을 수행하지만, cell 포맷팅으로 인해 표시되는 값과 실제 정렬 기준값이 다른 경우(예: 1,000 ms 문자열로 표시되지만 숫자로 정렬해야 하는 경우) sortingFn을 사용하여 정렬 방식을 지정할 수 있습니다.

  • basic: 표준 JavaScript 비교 연산자를 사용합니다. (숫자, 문자열 등)
  • datetime: 날짜 객체를 정렬합니다.
  • alphanumeric: 문자와 숫자가 섞인 문자열을 정렬합니다.
  • Custom Function: 직접 정렬 로직을 작성할 수 있습니다.
typescript
{
  accessorKey: 'responseTime',
  header: '응답 시간',
  cell: (info) => `${info.getValue().toLocaleString()} ms`, // 포맷팅된 문자열 표시
  sortingFn: 'basic', // 원본 데이터(숫자) 기준으로 정렬
}

텍스트 정렬 (Text Alignment)

컬럼 정의의 meta.align 속성을 사용하여 헤더와 셀의 텍스트 정렬을 제어할 수 있습니다.

  • left: 왼쪽 정렬 (기본값)
  • center: 가운데 정렬
  • right: 오른쪽 정렬
이름 (Left)
권한 (Center)
금액 (Right)
vue
<script setup lang="ts">
import { DataTable } from '@jennifersoft/vue-components-v2';
import { ref } from 'vue';

const columnsWithAlignment = [
    {
        accessorKey: 'name',
        header: '이름 (Left)',
        size: 100,
        meta: { align: 'left' },
    },
    {
        accessorKey: 'role',
        header: '권한 (Center)',
        size: 100,
        meta: { align: 'center' },
    },
    {
        accessorKey: 'price',
        header: '금액 (Right)',
        size: 100,
        meta: { align: 'right' },
    },
];

const dataWithAlignment = ref([
    { id: 1, name: 'Items A', role: 'Admin', price: '1,000,000' },
    { id: 2, name: 'Items B', role: 'User', price: '500,000' },
    { id: 3, name: 'Items C', role: 'Guest', price: '10,000' },
]);
</script>

<template>
    <DataTable
        :data="dataWithAlignment"
        :columns="columnsWithAlignment"
        :enable-row-selection="true"
    />
</template>

셀 슬롯 (Cell Slots / Templates)

prop을 통한 렌더 함수 정의 외에도, Vue의 슬롯 기능을 사용하여 셀 내용을 커스터마이징할 수 있습니다. 컬럼의 id 또는 accessorKey를 기반으로 cell-{columnId} 형식의 슬롯 이름을 사용합니다. datacolumns를 같은 제네릭 타입으로 선언하면 cell-* 슬롯과 expanded-row 슬롯의 row가 해당 타입으로 추론됩니다.

이름
이메일
권한
부서
상태
마지막 접속
vue
<script setup lang="ts">
import { DataTable, Badge } from '@jennifersoft/vue-components-v2';
import type { ColumnDef } from '@jennifersoft/vue-components-v2';

interface User {
    id: number;
    name: string;
    status: '활성' | '휴식' | '차단';
}

const data: User[] = [
    { id: 1, name: '김철수', status: '활성' },
    { id: 2, name: '이영희', status: '휴식' },
];

const columns: ColumnDef<User>[] = [
    { accessorKey: 'name', header: '이름', size: 100 },
    { accessorKey: 'status', header: '상태', size: 100 },
];

const getBadgeColor = (status: User['status']) => {
    switch (status) {
        case '활성':
            return 'green-light';
        case '휴식':
            return 'orange-light';
        default:
            return 'red-light';
    }
};
</script>

<template>
    <DataTable :data="data" :columns="columns">
        <!-- status 컬럼 커스터마이징 -->
        <template #cell-status="{ value }">
            <Badge :color="getBadgeColor(value as User['status'])" :text="value" />
        </template>

        <!-- row는 User 타입으로 추론됩니다. -->
        <template #cell-name="{ value, row }">
            <strong>{{ value }}</strong> (ID: {{ row.id }})
        </template>
    </DataTable>
</template>

슬롯 타입 (Slot Types)

DataTable은 동적 셀 슬롯과 확장 행 슬롯의 타입을 제공합니다. 직접 사용하는 경우에는 대부분 자동 추론만으로 충분합니다. DataTable을 감싸는 래퍼 컴포넌트를 만들거나 슬롯을 다시 전달해야 하는 경우에는 패키지에서 export하는 슬롯 타입을 사용할 수 있습니다.

typescript
import type {
    DataTableCellSlotProps,
    DataTableExpandedRowSlotProps,
    DataTableSlots,
} from '@jennifersoft/vue-components-v2';

interface User {
    id: number;
    name: string;
    status: string;
}

type UserCellSlotProps = DataTableCellSlotProps<User>;
type UserExpandedRowSlotProps = DataTableExpandedRowSlotProps<User>;
type UserTableSlots = DataTableSlots<User>;

슬롯별 props는 다음과 같습니다.

SlotProps
cell-{id}{ row: T; cell: Cell<T, any>; value: any; index: number }
expanded-row{ row: T; tableRow: Row<T> }
loading-state없음
empty-state없음

컬럼 표시 제어 (Column Visibility)

column-visibility prop을 사용하여 특정 컬럼만 표시할 수 있습니다. VisibilityState 객체 ({ [columnId]: boolean })를 전달해야 합니다.

이름
이메일
권한
부서
상태
마지막 접속
vue
<script setup lang="ts">
import { ref } from 'vue';
import { DataTable } from '@jennifersoft/vue-components-v2';
import type { VisibilityState } from '@tanstack/vue-table';

const allColumnIds = ['select', 'name', 'email', 'role', 'status'];
const visibleCols = ref<VisibilityState>({});

const data = ref([
    /* ... */
]);
const columns = [
    /* ... */
];
</script>

<template>
    <div style="display: flex; gap: 8px;">
        <label v-for="col in allColumnIds" :key="col">
            <input
                type="checkbox"
                :checked="visibleCols[col] !== false"
                @change="
                    (e) =>
                        (visibleCols = {
                            ...visibleCols,
                            [col]: (e.target as HTMLInputElement).checked,
                        })
                "
            />
            {{ col }}
        </label>
    </div>

    <DataTable
        :data="data"
        :columns="columns"
        :column-visibility="visibleCols"
    />
</template>

스켈레톤 로딩 (Skeleton Loading)

skeleton prop을 사용하여 데이터 로딩 중임을 나타내는 스켈레톤 UI를 표시할 수 있습니다.

Toggle Skeleton
이름
이메일
권한
부서
상태
마지막 접속
vue
<script setup lang="ts">
import { ref } from 'vue';
import { DataTable, ToggleSwitch } from '@jennifersoft/vue-components-v2';

const showSkeleton = ref(true);

const allColumnIds = ['select', 'name', 'email', 'role', 'status'];
const visibleCols = ref(['select', 'name', 'email', 'role', 'status']);

const data = ref([
    /* ... */
]);
const columns = [
    /* ... */
];
</script>

<template>
    <div style="display: flex; align-items: center; gap: 8px;">
        <span>Toggle Skeleton:</span>
        <ToggleSwitch v-model="showSkeleton" />
    </div>

    <DataTable :data="data" :columns="columns" :skeleton="showSkeleton" />
</template>

스타일링 (Styling)

이 컴포넌트는 유연성을 확보하고 리사이즈 핸들의 정확한 정렬을 보장하기 위해 native <table> 요소 대신 div 기반의 구조와 CSS Grid 레이아웃을 사용합니다.

  • CSS Grid & Variables: 성능 최적화를 위해 CSS Grid를 사용하며, --grid-template-columns CSS 변수를 통해 컬럼 너비를 효율적으로 관리합니다.
  • 클래스 명명 (Class naming): BEM 컨벤션을 따릅니다 (예: js-data-table, js-data-table__header).
  • 리사이징 (Resizing): 리사이즈 핸들은 헤더 셀 경계에서만 표시되며, 클릭 가능한 핸들 영역은 8px입니다. 드래그 중에는 바디 영역에 2px 세로 가이드 라인이 표시됩니다.

컬럼 리사이징 동작 (Column Resizing Behavior)

이 테이블은 전체 너비가 고정된 상태에서 컬럼의 비율을 조정하는 방식을 사용합니다. 사용자는 테이블 헤더의 컬럼 경계에 있는 리사이즈 핸들을 드래그하여 컬럼 너비를 변경할 수 있습니다. 바디 영역에서는 리사이즈 조작이 불가능하며, 드래그 중인 경계를 확인할 수 있도록 바디 영역에 세로 가이드 라인만 표시됩니다.

사용자가 컬럼의 리사이즈 핸들을 드래그하면 다음과 같이 동작합니다:

  1. 그룹 분할: 리사이즈 핸들을 기준으로 좌측 그룹(핸들이 속한 컬럼 포함)과 우측 그룹으로 나뉩니다.
  2. 비율 유지: 핸들을 이동하여 변경된 너비만큼, 각 그룹 내의 컬럼들이 현재 비율을 유지하며 동시에 늘어나거나 줄어듭니다.
  3. 전체 너비 고정: 테이블 전체의 너비는 변하지 않습니다.

예를 들어, 3개의 컬럼 A | B | C가 있을 때 B의 오른쪽 핸들을 우측으로 드래그하면:

  • 좌측 그룹 (A, B): 너비가 증가하며, A와 B가 기존 비율대로 커집니다.
  • 우측 그룹 (C): 너비가 감소하며, C가 작아집니다.

이 방식은 반응형 레이아웃에서 컬럼 간의 상대적인 크기 균형을 유지하는 데 유리합니다.

가상 스크롤 (Virtual Scrolling)

대량의 데이터를 효율적으로 렌더링하기 위해 가상 스크롤(Virtual Scrolling)을 지원합니다. @tanstack/vue-virtual을 내부적으로 사용하여 뷰포트에 표시되는 행만 렌더링하므로, 수만 개의 행이 있어도 높은 성능을 유지합니다.

가상 스크롤을 사용하려면 테이블의 부모 컨테이너나 테이블 자체에 높이(height) 가 지정되어 있어야 합니다.

selection prop이 외부에서 변경되면 가상 스크롤 상태에서도 선택된 행의 현재 위치를 찾아 부드럽게 자동 스크롤합니다.

이름
이메일
권한
부서
상태
마지막 접속
vue
<script setup lang="ts">
import { ref } from 'vue';
import { DataTable } from '@jennifersoft/vue-components-v2';

const virtualData = ref(
    Array.from({ length: 10000 }).map((_, i) => ({
        id: i,
        name: `User ${i}`,
        status: i % 2 === 0 ? 'Active' : 'Inactive',
        lastLogin: new Date().toISOString().split('T')[0],
    }))
);
</script>

<template>
    <DataTable
        :data="virtualData"
        :columns="columns"
        virtual-scroll
        :style="{ height: '400px' }"
        selection-mode="single"
    >
        <template #expanded-row="{ row }">
            <div class="expanded-content">
                <p>
                    <strong>상세 정보:</strong> 가상 스크롤 데이터
                    {{ row.name }}님의 상세 내용입니다.
                </p>
            </div>
        </template>
    </DataTable>
</template>

<style scoped>
.expanded-content {
    padding: 16px;
    background-color: var(--gray-50);
    border-bottom: 1px solid var(--gray-200);
}
</style>

Props

PropTypeDefaultDescription
dataT[]Required표시할 데이터 객체들의 배열입니다.
columnsColumnDef<T>[]Required컬럼 정의 설정입니다.
selectionT | T[]undefined선택된 행 데이터(v-model 지원). 외부에서 값이 변경되면 선택 행으로 자동 스크롤합니다.
selectionMode'none' | 'single' | 'checkbox''none'행 선택 모드를 설정합니다.
columnVisibilityVisibilityState{}컬럼의 표시 상태를 정의하는 객체입니다.
hideHeaderbooleanfalsetrue일 때 테이블의 컬럼 헤더를 숨깁니다.
skeletonbooleanfalsetrue일 때 로딩 스켈레톤(Skeleton) 상태를 표시합니다.
emptyTextstring'No Data'데이터 배열이 비어있을 때 표시할 텍스트입니다.
size'mini' | 'small' | 'normal' | 'large''normal'테이블의 크기(행 높이 및 폰트)를 설정합니다.
showColumnLinesbooleanfalsetrue일 때 데이터 셀의 세로 구분선을 표시합니다.
showRowLinesbooleantruetrue일 때 데이터 행의 가로 구분선을 표시합니다.
stripedRowsbooleanfalsetrue일 때 홀수 행에 배경색을 적용합니다.
variant'inset' | 'attached''inset'테이블의 시각적 스타일을 설정합니다.

Events

EventPayloadDescription
update:selectionT | T[]행 선택 상태가 변경될 때 발생합니다.
update:columnVisibilityVisibilityState컬럼 표시 상태가 변경될 때 발생합니다.

Slots

Slot NamePropsDescription
loading-state없음로딩 오버레이 영역에 표시할 커스텀 콘텐츠입니다.
empty-state없음데이터가 없을 때 표시할 커스텀 콘텐츠입니다.
cell-{id}{ row: T; cell: Cell<T, any>; value: any; index: number }특정 컬럼의 셀 내용을 커스터마이징합니다.
expanded-row{ row: T; tableRow: Row<T> }선택된 행 다시 클릭시 보여지는 확장 콘텐츠입니다.