Featuring Design System

Data Grid

TanStack Table 위에 올린 헤드리스 데이터 그리드 — 상태와 라우팅만 소유하고 UI는 위임합니다.

개요

@featuring-corp/data-gridTanStack Table v8 위에 올린 헤드리스(headless) 데이터 그리드입니다. 스프레드시트급 상호작용(셀 선택·클립보드·인라인 편집·자동 채우기·셀 병합)을 제공하되, 셀의 모양은 소유하지 않습니다.

  • 얇은 인프라(thin-infra) — 그리드는 상태와 키/포인터 라우팅만 책임집니다. 셀·헤더·에디터의 렌더는 전적으로 소비자 코드입니다.
  • 명시 합성(explicit composition)useDataGrid + 기능 훅(useDataGridSelection/Editing/FillHandle)을 만들어 <DataGrid.Root>주입합니다. 매직 없는 의존성 주입.
  • TanStack 그대로 — row model·정렬·필터·페이지네이션은 TanStack의 표준 API를 그대로 씁니다. 그리드는 그 위에 셀 선택·편집·클립보드를 얹을 뿐입니다.
  • 디자인 시스템 정합 — 크기(sm/md/lg)·토큰·@layer ft-* 캐스케이드·$css가 컴포넌트 패키지와 동일하게 동작합니다.
  • 접근성 우선role grid/table/treegrid, aria-rowindex/aria-colindex 절대 좌표, aria-sort/aria-selected를 사실대로 노출합니다.

이 문서는 코어 모델과 설계 사상을 다룹니다. 실제 사용 예제는 StorybookDataGrid/* 스토리에서 살아 있는 형태로 확인하세요. 여기서는 "왜 이렇게 설계했는가 → 무엇인가 → 어떻게 엮이는가"를 설명합니다.

설치

pnpm add @featuring-corp/data-grid @tanstack/react-table

peer dependency로 다음을 요구합니다.

  • @tanstack/react-table ^8.21.3
  • react ^18 || ^19, react-dom ^18 || ^19

브랜드 토큰·리셋·@layer 순서를 담은 프리셋 CSS를 한 번 import 합니다(컴포넌트 패키지 프리셋을 이미 쓰고 있다면 생략 가능).

import '@featuring-corp/data-grid/preset/featuring';
// 또는
import '@featuring-corp/data-grid/preset/dataEffect';

빠른 시작

가장 단순한 그리드입니다. row model 팩토리는 기본으로 깔리지 않으므로 getCoreRowModel()을 직접 넘겨야 합니다(정렬·필터·페이지네이션도 필요한 것만 명시).

import { useDataGrid, DataGrid, getCoreRowModel, type ColumnDef } from '@featuring-corp/data-grid';

type Row = { id: string; name: string; followers: number };

const columns: ColumnDef<Row>[] = [
	{
		accessorKey: 'name',
		header: ({ header }) => <DataGrid.HeaderCell header={header}>채널</DataGrid.HeaderCell>,
		cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue() as string}</DataGrid.Cell>,
	},
	{
		accessorKey: 'followers',
		header: ({ header }) => <DataGrid.HeaderCell header={header}>팔로워</DataGrid.HeaderCell>,
		cell: (info) => <DataGrid.Cell cell={info.cell}>{(info.getValue() as number).toLocaleString()}</DataGrid.Cell>,
	},
];

function Grid({ data }: { data: Row[] }) {
	const table = useDataGrid<Row>({
		data,
		columns,
		getCoreRowModel: getCoreRowModel(),
		getRowId: (r) => r.id,
	});

	return (
		<DataGrid.Root table={table}>
			<DataGrid.Header />
			<DataGrid.Body />
		</DataGrid.Root>
	);
}

아래는 살아 있는 예제입니다 — 코드를 직접 고치면 즉시 반영됩니다. (data·columns는 컴포넌트 에서 한 번만 만들어 안정적인 참조를 유지합니다 — 렌더마다 새로 만들면 controlled 상태와 만나 무한 렌더가 됩니다.)

const data = [
{ id: '1', name: '김민준', followers: 128000 },
{ id: '2', name: '이서연', followers: 84500 },
{ id: '3', name: '박도윤', followers: 233000 },
{ id: '4', name: '최하은', followers: 45200 },
];

const columns = [
{
  accessorKey: 'name',
  header: ({ header }) => <DataGrid.HeaderCell header={header}>채널</DataGrid.HeaderCell>,
  cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue()}</DataGrid.Cell>,
},
{
  accessorKey: 'followers',
  header: ({ header }) => <DataGrid.HeaderCell header={header}>팔로워</DataGrid.HeaderCell>,
  cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue().toLocaleString()}</DataGrid.Cell>,
},
];

function HelloGrid() {
const table = useDataGrid({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getRowId: (row) => row.id,
});
return (
  <DataGrid.Root table={table} size="md">
    <DataGrid.Header />
    <DataGrid.Body />
  </DataGrid.Root>
);
}

render(<HelloGrid />);

<DataGrid.Header /><DataGrid.Body />는 children이 없으면 자동 렌더합니다(모든 헤더 그룹·행을 순회). 행/셀을 직접 구성하려면 render-prop 모드로 전환합니다 — Composition & DI를 참고하세요.

멘탈 모델 — 세 조각의 합성

그리드는 세 종류의 객체를 명시적으로 합성합니다. 훅이 인스턴스를 만들고, Root가 그것을 context로 전파합니다.

useDataGrid(options) ─────────────▶ table   (TanStack Table + loading/error/virtualization 확장)
useDataGridSelection(table) ──────▶ selection
useDataGridEditing(table, opts) ──▶ editing
useDataGridFillHandle(table, opts)▶ fillHandle

<DataGrid.Root table selection editing fillHandle>   ← DI 허브
  <DataGrid.Header /> <DataGrid.Body />               ← context에서 위 인스턴스를 읽어 동작
</DataGrid.Root>
  • useDataGrid — TanStack Table을 만들고 loading/error/getSkeletonRowCount/virtualization/debug를 얹은 DataGridInstance를 돌려줍니다.
  • 기능 훅 — 셀 선택·편집·자동 채우기는 각각 독립 훅입니다. 필요한 기능만 만들면 됩니다(편집 없는 읽기 전용 그리드는 useDataGridEditing을 호출하지 않습니다).
  • Root는 DI 허브 — 훅 반환값을 prop으로 받아 context에 전파합니다. table에 부착하지 않고 prop으로 주입하므로 훅 호출 순서에 의존하지 않고, 타입이 합성을 강제합니다. 주입하지 않은 기능은 조용히 비활성(개발 모드에서 1회 경고)됩니다.

자세한 구조는 ArchitectureComposition & DI에서 다룹니다.

무엇을 소유하고, 무엇을 위임하나

이 그리드의 핵심 결정은 책임 경계입니다.

그리드가 소유소비자가 소유
셀 선택 상태(anchor/range)·키보드 내비게이션셀·헤더의 렌더(column.cell/header)
편집 라이프사이클(진입/commit/cancel)·undo/redo에디터 UI(TextInput·Select·DatePicker…)
클립보드 직렬화(TSV)·붙여넣기 타일링·잘라내기 이동데이터 state 갱신(onCellEdit)
자동 채우기 기하(드래그/더블클릭)채우기 값 계산 정책(meta.fill/getFillValue)
좌표 공간·병합(colSpan/rowSpan) dense 시맨틱빈/로딩/에러 레이아웃(minHeight·정렬)
ARIA 역할·좌표·정렬/선택 상태 노출aria-label로케일 문자열

그래서 그리드는 "어떤 에디터를 쓸지", "셀이 어떻게 생겼는지", "에러 화면이 어떤 모양인지"에 어떤 의견도 갖지 않습니다. 이 무의견(unopinionated) 성질이 디자인 시스템 안에서 자유로운 조립을 가능하게 합니다.

다른 그리드와의 차이

  • AG Grid / MUI X DataGrid 대비 — 그것들은 셀 렌더러·테마·에디터를 프레임워크가 소유합니다. 이 그리드는 그 반대로, 렌더를 전부 소비자에게 돌려주고 상호작용 인프라만 제공합니다.
  • TanStack Table 단독 대비 — TanStack은 row model·정렬·필터의 "두뇌"지만 셀 선택·클립보드·인라인 편집·셀 병합은 없습니다. 이 패키지는 그 위에 그 스프레드시트 레이어를 얹고, 디자인 시스템 토큰·접근성을 묶습니다.

다음 단계

설계 사상·코어 모델 → 기능별 사용법 → 레퍼런스 순으로 이어집니다. 사이드바도 같은 순서입니다.

코어 모델

  • ArchitectureDataGridInstance·runtime registry·context 전파·compound 트리.
  • Composition & DI — 명시 DI 모델, 자동 렌더 vs render-prop, Cell/HeaderCell 유니온.

컬럼

  • Columns — accessor·display 컬럼, meta, 가시성·리사이즈·순서.
  • Grouped headers — 중첩 columnDef로 만드는 다단계 헤더.
  • SortinggetSortedRowModel·SortArrow·다중/서버 정렬.
  • Filtering — 전역·컬럼·패싯 필터·서버 위임.
  • Pagination — 클라이언트/서버 페이지네이션·aria-rowindex 절대 좌표.

선택 · 편집

  • Row selection — 체크박스 행 선택·전체 선택·controlled.
  • Selection model — 셀 선택(anchor/range 다중 사각형)·정책 seam.
  • Editing model — 얇은 인프라 에디터·record-on-commit/confirm·undo/redo.
  • Validationmeta.parseValue 단일 진실·동기/비동기 검증.
  • Clipboard & Fill — TSV·잘라내기 이동·채우기 우선순위.

행 그룹 · 계층

  • GroupinggetGroupedRowModel·집계·treegrid.
  • Tree datagetSubRows 계층·aria-level/posinset/setsize.
  • Expansion — 행 아래 상세 패널 펼치기.

레이아웃 · 스케일

  • Cell mergingcolSpan/rowSpan dense 좌표.
  • Pinning — sticky 좌/우 고정·경계 마커·병합 clamp.
  • Reordering — @dnd-kit 레시피·columnOrder·onCellPointerDown veto.
  • Virtualization — 행/열 윈도잉·estimateRowHeight·scroll-to-active.
  • View state — loading/error/empty·skeleton.
  • Server-side data — manual sort/filter/pagination·서버 CRUD.
  • Touch & Pointer — 통합 포인터 모델·탭/더블탭·선택 핸들.

레퍼런스