Architecture
DataGridInstance, runtime registry, context 전파, compound 트리 — 그리드가 내부적으로 어떻게 짜였는가.
DataGridInstance — TanStack Table + 얇은 확장
useDataGrid(options)는 TanStack의 Table<TData>를 만든 뒤 그 위에 그리드가 필요로 하는 필드 몇 개만 얹어
DataGridInstance<TData, TError>를 돌려줍니다.
interface DataGridInstance<TData, TError = unknown> extends Table<TData> {
loading: boolean; // 외부 fetch 결과 (undefined → false)
error: TError | null; // (undefined → null)
getSkeletonRowCount: () => number; // skeletonRows ?? pagination.pageSize ?? 0
debug?: DataGridDebug; // 영역별 콘솔 로그
virtualization?: DataGridVirtualization; // 행/열 윈도잉
runtime: DataGridRuntime; // 내부 registry (아래)
}핵심은 TanStack 인스턴스를 대체하지 않고 확장한다는 점입니다. 정렬·필터·페이지네이션·visibility·pinning·grouping·expanding은 모두 TanStack의 표준 API와 상태로 동작합니다. 그리드가 더하는 것은 뷰 상태(loading/error)와 상호작용 인프라(selection/editing/fill)뿐입니다.
loading/error는Root의 prop이 아니라useDataGrid의 옵션입니다. 뷰 상태는 데이터 인스턴스에 속한다는 결정 — fetch 결과를 그대로 흘려 넣습니다. (View state 참고)
runtime registry — 렌더 중 mutation을 제거한 안정 객체
가상화에서 스크롤-투-액티브는 "지금 화면 밖 행으로 스크롤"하기 위해 virtualizer 인스턴스를 알아야 합니다. virtualizer는 Body가
렌더 중 생성하는데, 이를 TanStack 인스턴스 필드에 직접 박으면(table.x = …) 렌더 순서 해저드가 생깁니다.
그래서 useDataGrid가 인스턴스 생성 시 항상 존재하는 안정 객체 runtime을 1회 부여합니다.
interface DataGridRuntime {
gridId?: string; // Root가 기록 — 한 페이지 다중 그리드 scroll-to-active scope
rowVirtualizer: Virtualizer<HTMLDivElement, Element> | null; // Body가 기록
columnVirtualizer: Virtualizer<HTMLDivElement, Element> | null; // Body가 기록
}Body가 렌더 중 virtualizer를 여기에 기록(ref 시맨틱 — setState 아님, 1프레임 지연 없음)하고, useDataGridScrollToActive가
읽습니다. identity가 불변이라 "임의 시점 부착"이 "항상 존재하는 객체에 대한 ref 쓰기"로 바뀌어 순서 해저드가 사라집니다.
context 전파 — Root가 DI 허브
<DataGrid.Root>는 table/selection/editing/fillHandle/size/role을 받아 두 개의 context로 전파합니다.
DataGridContext— table, size, selection, editing, fillHandle, gridId, columnIndexMap 등 그리드 전역.DataGridCellContext— 현재 셀(cell).CellDisplay/CellEditor/useDataGridCellEdit이 읽습니다.
Header/Body/Row/Cell은 이 context에서 인스턴스를 읽어 동작합니다. 그래서 소비자는 selection/editing을 셀까지 prop으로
내려보낼 필요가 없습니다 — Root에 한 번 주입하면 됩니다.
<DataGrid.Root table selection editing fillHandle size role> ─┐ DataGridContext
<DataGrid.Header> │ (table·size·selection·editing·…)
<DataGrid.HeaderRow> <DataGrid.HeaderCell header|column/> … │
<DataGrid.Body> │
<DataGrid.Row row rowIndex> │
<DataGrid.Cell cell|column> ──┘ DataGridCellContext (cell)
<DataGrid.CellDisplay/> ← edit 아닐 때
<DataGrid.CellEditor> ← edit 시 mount
<DataGrid.TextEditor/> 또는 커스텀(useDataGridCellEdit)
<DataGrid.Empty/> <DataGrid.Loading/> <DataGrid.Error/> ← 뷰 상태 슬롯compound 트리 — 무엇이 무엇을 책임지나
| 컴포넌트 | 책임 | 모드 |
|---|---|---|
Root | DI 허브·키/포인터 라우팅·role·size | — |
ScrollContainer | 스크롤 영역([data-scroll-container]) — pinned/sticky 기준 | — |
Header | 헤더 그룹 렌더 | 자동 / (headerGroup) => … |
HeaderRow / HeaderCell | 헤더 행·셀(header 또는 column 유니온) | — |
Body | 행 렌더·로딩 시 skeleton·가상화 윈도잉 | 자동 / (row, rowIndex) => … |
Row / Cell | 행·셀(cell 또는 column 유니온)·data-* 상태 노출 | — |
CellDisplay / CellEditor | edit 아님 / edit 시 mount 분기 | — |
TextEditor | 패키지 기본 인라인 텍스트 에디터(IME·키·blur 내장) | opt-in |
Empty / Loading / Error | 뷰 상태 슬롯 | — |
SortArrow / ResizeHandle | 정렬 인디케이터 / 리사이즈 핸들(명시 부착) | — |
Header/Body는 children이 없으면 자동 렌더하고, 함수 children이면 render-prop으로 전환되어 행·셀을 소비자가 직접 구성합니다.
Cell/HeaderCell의 cell/column 유니온은 일반 모드(cell)와 skeleton/동적 모드(column)를 구분합니다. (Composition & DI)
data-* 상태 표면 — 스타일링/테스트 계약
셀·헤더·행은 상태를 data-* 속성과 state-callback으로 노출합니다. CSS는 data-* 셀렉터로,
프로그램은 state callback(className/style)으로, 테스트는 data-* 셀렉터로 잡습니다.
| 속성 | 의미 |
|---|---|
[data-cell] / [data-row] | 셀 / 행 루트 (+ data-row-id/data-column-id) |
[data-cell-active] / [data-cell-in-range] | 활성 셀 / 선택 범위 포함 |
[data-cell-editing] / [data-cell-readonly] | 편집 중 / 읽기 전용 |
[data-pinned] / [data-pinned-last] | 고정(left/right) / 고정 그룹의 마지막 |
[data-sortable] / [data-sorted] | 정렬 가능(배선됨) / 정렬됨(asc·desc) |
[data-header-cell] / [data-header-row] | 헤더 셀 / 헤더 행 |
상태 기반 스타일은
$css가 아니라className/stylestate callback으로 줍니다.$css(rainbow-sprinkles)는 atomic이라 콜백·상태·중첩 셀렉터를 받지 않습니다.
자기완결(self-contained) 패키지
data-grid는 컴포넌트 패키지에 런타임 의존하지 않도록 src/system/에 자체 렌더 인프라(useRenderComponent, rainbow-sprinkles
설정, theme 계약, typo)를 복제 소유합니다. 빌드는 Rollup으로 ESM/CJS dual 출력, CSS는 @layer ft-components로 래핑되고,
브랜드 프리셋(dist/preset/featuring.css·dataEffect.css)이 레이어 순서·리셋·토큰을 묶습니다. 덕분에 미래에 별도 시스템 패키지로
승격하기 쉽습니다.