Featuring Design System
Getting Started

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/errorRoot의 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 트리 — 무엇이 무엇을 책임지나

컴포넌트책임모드
RootDI 허브·키/포인터 라우팅·role·size
ScrollContainer스크롤 영역([data-scroll-container]) — pinned/sticky 기준
Header헤더 그룹 렌더자동 / (headerGroup) => …
HeaderRow / HeaderCell헤더 행·셀(header 또는 column 유니온)
Body행 렌더·로딩 시 skeleton·가상화 윈도잉자동 / (row, rowIndex) => …
Row / Cell행·셀(cell 또는 column 유니온)·data-* 상태 노출
CellDisplay / CellEditoredit 아님 / edit 시 mount 분기
TextEditor패키지 기본 인라인 텍스트 에디터(IME·키·blur 내장)opt-in
Empty / Loading / Error뷰 상태 슬롯
SortArrow / ResizeHandle정렬 인디케이터 / 리사이즈 핸들(명시 부착)

Header/Body는 children이 없으면 자동 렌더하고, 함수 children이면 render-prop으로 전환되어 행·셀을 소비자가 직접 구성합니다. Cell/HeaderCellcell/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/style state 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)이 레이어 순서·리셋·토큰을 묶습니다. 덕분에 미래에 별도 시스템 패키지로 승격하기 쉽습니다.