Appearance
데이터 테이블 (Data Table)
DataTable 컴포넌트는 Headless UI 라이브러리 TanStack을 기반으로 구축된 테이블 컴포넌트입니다.
@tanstack/vue-table, @tanstack/vue-virtual을 사용하여 구현되었습니다.
display: grid와 grid-template-columns를 사용하여 컬럼 리사이징이 가능하도록 구현되었습니다.
이름
이메일
권한
부서
상태
마지막 접속
주요 기능 (Features)
- 행 선택 (Row Selection): 단일 및 다중 행 선택을 지원합니다.
- 정렬 (Sorting): UI 인디케이터를 포함한 단일 컬럼 정렬 (오름차순/내림차순)을 지원합니다.
- 컬럼 리사이징 (Column Resizing): 전체 높이 시각적 가이드(Full-height visual guide)를 제공하는 인터랙티브 컬럼 리사이징을 지원합니다.
- 컬럼 표시 제어 (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': 체크박스를 통한 다중 행 선택
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-table의 ColumnDef 타입을 사용하여 정의합니다.
타입 안전한 컬럼 정의 (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} 형식의 슬롯 이름을 사용합니다.
이름
이메일
권한
부서
상태
마지막 접속
vue
<script setup lang="ts">
import { DataTable, Badge } from '@jennifersoft/vue-components-v2';
// ...
const getBadgeColor = (status: string) => {
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)" :text="value" />
</template>
<!-- name 컬럼 커스터마이징 -->
<template #cell-name="{ value, row }">
<strong>{{ value }}</strong> (ID: {{ row.id }})
</template>
</DataTable>
</template>컬럼 표시 제어 (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-columnsCSS 변수를 통해 컬럼 너비를 효율적으로 관리합니다. - 클래스 명명 (Class naming): BEM 컨벤션을 따릅니다 (예:
js-data-table,js-data-table__header). - 리사이징 (Resizing): 리사이즈 핸들은 테이블 전체 높이(헤더 + 바디)에 걸쳐 표시되도록
absolute positioning을 사용하여 구현되었습니다.
컬럼 리사이징 동작 (Column Resizing Behavior)
이 테이블은 전체 너비가 고정된 상태에서 컬럼의 비율을 조정하는 방식을 사용합니다. 사용자가 컬럼의 리사이즈 핸들을 드래그하면 다음과 같이 동작합니다:
- 그룹 분할: 리사이즈 핸들을 기준으로 좌측 그룹(핸들이 속한 컬럼 포함)과 우측 그룹으로 나뉩니다.
- 비율 유지: 핸들을 이동하여 변경된 너비만큼, 각 그룹 내의 컬럼들이 현재 비율을 유지하며 동시에 늘어나거나 줄어듭니다.
- 전체 너비 고정: 테이블 전체의 너비는 변하지 않으십니다.
예를 들어, 3개의 컬럼 A | B | C가 있을 때 B의 오른쪽 핸들을 우측으로 드래그하면:
- 좌측 그룹 (A, B): 너비가 증가하며, A와 B가 기존 비율대로 커집니다.
- 우측 그룹 (C): 너비가 감소하며, C가 작아집니다.
이 방식은 반응형 레이아웃에서 컬럼 간의 상대적인 크기 균형을 유지하는 데 유리합니다.
가상 스크롤 (Virtual Scrolling)
대량의 데이터를 효율적으로 렌더링하기 위해 가상 스크롤(Virtual Scrolling)을 지원합니다. @tanstack/vue-virtual을 내부적으로 사용하여 뷰포트에 표시되는 행만 렌더링하므로, 수만 개의 행이 있어도 높은 성능을 유지합니다.
가상 스크롤을 사용하려면 테이블의 부모 컨테이너나 테이블 자체에 높이(height) 가 지정되어 있어야 합니다.
이름
이메일
권한
부서
상태
마지막 접속
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
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | Required | 표시할 데이터 객체들의 배열입니다. |
columns | ColumnDef<T>[] | Required | 컬럼 정의 설정입니다. |
selection | T | T[] | undefined | 선택된 행 데이터(v-model 지원) |
selectionMode | 'none' | 'single' | 'checkbox' | 'none' | 행 선택 모드를 설정합니다. |
columnVisibility | VisibilityState | {} | 컬럼의 표시 상태를 정의하는 객체입니다. |
hideHeader | boolean | false | true일 때 테이블의 컬럼 헤더를 숨깁니다. |
skeleton | boolean | false | true일 때 로딩 스켈레톤(Skeleton) 상태를 표시합니다. |
emptyText | string | 'No Data' | 데이터 배열이 비어있을 때 표시할 텍스트입니다. |
size | 'mini' | 'small' | 'normal' | 'large' | 'normal' | 테이블의 크기(행 높이 및 폰트)를 설정합니다. |
showColumnLines | boolean | false | true일 때 데이터 셀의 세로 구분선을 표시합니다. |
showRowLines | boolean | true | true일 때 데이터 행의 가로 구분선을 표시합니다. |
stripedRows | boolean | false | true일 때 홀수 행에 배경색을 적용합니다. |
variant | 'inset' | 'attached' | 'inset' | 테이블의 시각적 스타일을 설정합니다. |
Events
| Event | Payload | Description |
|---|---|---|
update:selection | T | T[] | 행 선택 상태가 변경될 때 발생합니다. |
update:columnVisibility | VisibilityState | 컬럼 표시 상태가 변경될 때 발생합니다. |
Slots
| Slot Name | Description |
|---|---|
loading-state | 로딩 오버레이 영역에 표시할 커스텀 콘텐츠입니다. |
empty-state | 데이터가 없을 때 표시할 커스텀 콘텐츠입니다. |
cell-{id} | 특정 컬럼의 셀 내용을 커스터마이징합니다. |
expanded-row | 선택된 행 다시 클릭시 보여지는 확장 콘텐츠입니다. |