이 글은 누구를 위한 것인가
- 관리자 화면의 데이터 테이블이 복잡해서 사용자가 못 찾는 팀
- 10만 행 데이터를 테이블에 표시하려고 고민하는 개발자
- 모바일에서 테이블을 어떻게 보여줄지 모르는 팀
들어가며
데이터 테이블은 B2B·관리자 화면의 핵심 컴포넌트다. 정렬·필터·선택·일괄 처리가 자연스럽게 동작해야 한다. 10만 행 이상은 가상화(Virtualization) 없이는 브라우저가 멈춘다.
이 글은 bluefoxdev.kr의 데이터 테이블 UX 가이드 를 참고하여 작성했습니다.
1. 데이터 테이블 UX 패턴
[테이블 기능 우선순위]
필수:
정렬 (클릭으로 오름/내림차순 토글)
페이지네이션 (행 수 선택 가능)
컬럼 헤더 고정 (세로 스크롤 시)
반응형 (모바일 대응)
권장:
필터 (컬럼별 또는 전체 검색)
행 선택 + 일괄 처리
컬럼 너비 조정
컬럼 순서 변경
내보내기 (CSV, Excel)
선택:
인라인 편집 (셀 직접 수정)
컬럼 숨김/표시 토글
고정 컬럼 (수평 스크롤 시)
그룹화 (Row Grouping)
피벗
[모바일 테이블 전략]
방법 1: 수평 스크롤 (명확한 스크롤 힌트)
방법 2: 카드 뷰로 변환 (좁은 화면에서)
방법 3: 중요 컬럼만 표시 + "더 보기" 확장
권장: 넓이에 따라 테이블 ↔ 카드 자동 전환
[컬럼 정렬 시각 언어]
정렬 안됨: 비활성 화살표 (↕)
오름차순: 위 화살표 (↑) 강조
내림차순: 아래 화살표 (↓) 강조
정렬 기준 컬럼: 헤더 배경색 살짝 변경
2. 고성능 데이터 테이블
import { useVirtual } from "@tanstack/react-virtual";
import { useRef } from "react";
// TanStack Table + 가상 스크롤
function VirtualDataTable<T>({
data,
columns,
rowHeight = 48,
}: {
data: T[];
columns: ColumnDef<T>[];
rowHeight?: number;
}) {
const parentRef = useRef<HTMLDivElement>(null);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
const { rows } = table.getRowModel();
// 가상 스크롤 (10만 행도 부드럽게)
const rowVirtualizer = useVirtual({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => rowHeight,
overscan: 10,
});
return (
<div ref={parentRef} className="overflow-auto" style={{ height: "600px" }}>
<table className="w-full text-sm">
{/* 고정 헤더 */}
<thead className="sticky top-0 bg-white z-10 border-b">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-4 py-3 text-left font-medium text-gray-600 whitespace-nowrap cursor-pointer select-none"
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center gap-1">
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === "asc" ? " ↑" :
header.column.getIsSorted() === "desc" ? " ↓" : " ↕"}
</div>
</th>
))}
</tr>
))}
</thead>
{/* 가상 스크롤 바디 */}
<tbody
style={{ height: `${rowVirtualizer.totalSize}px`, position: "relative" }}
>
{rowVirtualizer.virtualItems.map((virtualRow) => {
const row = rows[virtualRow.index];
return (
<tr
key={row.id}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualRow.start}px)`,
width: "100%",
}}
className="border-b hover:bg-gray-50"
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-4 py-3 whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
);
}
마무리
데이터 테이블에서 성능이 문제가 된다면 먼저 서버 사이드 페이지네이션을 시도하라. 페이지당 100행으로 제한하면 가상화 없이도 부드럽게 동작한다. 가상 스크롤은 "전체 데이터를 한 화면에서 스크롤해야 하는" 경우에만 필요하다.