9.1 KiB
Data Table (Pattern)
현재 HM 구현의 기준 문서. 공통 원칙은 docs/guidelines/프로젝트-지침.md(§6, §7, Pattern 절)를 따른다. 이 문서는 Data Table Pattern 고유의 API·상태·결정만 기록한다.
1. 상태
| 항목 | 내용 |
|---|---|
| 구현 상태 | 구현 완료 (현재 HM 규격) |
| 기준일 | 2026-10-03 |
| 기반 | @tanstack/react-table 9.2.4 + HM Table primitive |
| 대표 Story | Patterns/Data/Data Table → Data Table (With Filter Bar는 숨김 verification) |
| 보류 | §12 |
2. 목적 / 사용 범위
업무 목록 화면의 표. 기존 Table(presentation primitive)에 정렬·행 선택·페이지·열 표시·로딩/빈 상태·행 액션을 조합한다. Table에는 어떤 로직도 넣지 않았다. 서버 데이터 호출·서버 사이드 페이지/정렬·가상 스크롤은 범위 밖이다.
3. 구성 요소
DataTable, useDataTable, DataTableColumnHeader, DataTableViewOptions, DataTablePagination, dataTableSelectColumn. (DataTable이 DataTablePagination을 기본으로 포함한다.)
4. 구조
Table primitive (정적 표시, 변경 없음)
↑ 조합
DataTable pattern ── useDataTable (TanStack useTable + 기능 등록)
├ DataTableColumnHeader 정렬 버튼
├ dataTableSelectColumn HM Checkbox 선택 열
├ DataTableViewOptions HM Dropdown Menu + Button
└ DataTablePagination HM Pagination + Select(페이지 크기) + 선택 수 요약
5. Public API
const table = useDataTable({ columns, data, getRowId, initialState }) // TanStack TableOptions에서 features만 뺀 것
<DataTableViewOptions table={table} labels={{ name: "이름" }} />
<DataTable table={table} loading emptyMessage="..." pagination pageSizeOptions={[10, 20, 50]} />
columns:DataTableColumnDef<TData>[](TanStackColumnDef, 등록된 기능 포함). 정렬 header는({ column }) => <DataTableColumnHeader column={column} title="이름" />.DataTable:table,loading,emptyMessage,pagination,pageSizeOptions,className. 그 외 TanStack 옵션은useDataTable에 그대로 전달한다(복제 prop 없음).data와columns는 참조가 안정적이어야 한다(모듈 상수,useMemo, state).- 타입:
DataTableColumnDef,DataTableInstance,DataTableOptions,DataTableProps.
6. 상태 관리
TanStack Table v9의 state model 그대로. v9는 기능을 명시 등록하는 방식(tableFeatures)이라 v8 예제(useReactTable, getCoreRowModel)와 다르다. 등록한 기능: column filtering, global filtering, row sorting, row selection, row pagination, column visibility와 각 row model(filteredRowModel, sortedRowModel, paginatedRowModel), includesString·equalsString filter, alphanumeric·basic·datetime sort.
- 상태는 TanStack 내부가 소유한다(필요하면
useDataTable에state/atoms로 제어). 검색은table.setGlobalFilter, 열 필터는table.getColumn(id)?.setFilterValue, 초기화는resetGlobalFilter(true)/resetColumnFilters(true)로 한다. 필터 변경 시 TanStack이 page index를 0으로 되돌린다. - error는 Data Table이 소유하지 않는다(소비자가 Alert / Toast로 처리).
조회 버튼을 사용하는 목록은 FilterBar의 draft / applied 소비 예제를 따른다. 입력 중에는 draft만 바꾸고 조회 시 table 필터 또는 useMemo로 만든 data를 갱신한다. 초기화는 입력값과 적용값, 사용 중인 table 필터와 페이지를 소비자가 함께 처리한다. 대표 Data Table Story는 표 기능의 운영 Reference이며, WithFilterBarStory(patterns-data-data-table--with-filter-bar-story)는 입력 즉시 적용을 확인하는 숨김 verification이다. 조회 버튼으로 조건을 확정하는 공개 API나 서버 호출 기능은 제공하지 않는다.
7. 조합 컴포넌트
Table, Checkbox(header mixed), Pagination, Select(페이지 크기), Dropdown Menu(열 표시), Button(열 표시 trigger), Skeleton(loading). 행 액션은 소비자가 열 정의에서 Dropdown Menu(또는 Context Menu)로 구성한다. Story는 행 액션 결과를 Sonner toast로 보이지만 Pattern은 Sonner에 의존하지 않는다.
8. Styling / Token
Header·cell·row·selected 면은 packages/ui/src/theme/table.css(--hm-table-*)가 소유하고 Pattern은 다시 정의하지 않는다. 컬러 역할(2026-10-06): 선택 행 면 = --accent 45%(hover·selected+hover와 같은 Soft Surface, Select Highlight / Selected와 동일한 Accent 45% 정책), 선택 Checkbox = --hm-sem-state-active(Light 2). 면은 hover와 같으므로 선택 여부는 Checkbox와 선택 수 요약으로 구분한다. Pattern 전용: packages/ui/src/theme/data-table.css의 --hm-data-table-*(footer gap·padding, summary 색·크기, 정렬 버튼 간격·아이콘 크기·활성 색, empty padding·색, skeleton 높이). 신규 Primitive·Semantic 없음.
- Loading: header 유지, body는 Skeleton 행(최대 5행),
aria-busy. - Sort Direction Accent:
--hm-data-table-sort-active-color는 현재--hm-sem-palette-green-light-3(Green Light 3 palette)를 참조한다. 별도 State Semantic으로 승격하지 않았으며 Primary/Active/Accent 역할에 자동 연결되지 않는다. Token 이름의active는 Control Active Semantic 계약을 뜻하지 않는다. - Empty: 한 행을 열 수만큼
colSpan, 문구는emptyMessage(원본 0건과 필터 결과 0건 동일 UI).
9. Storybook
Patterns/Data/Data Table→Data Table(대표: 선택·정렬·열 표시·행 액션·페이지 크기·페이지),With Filter Bar(숨김 Filter Bar 연동 검증). verification:WithFilterBar,Loading,Empty.- 샘플 데이터는
src/patterns/sample-users.ts(47행, 네트워크 없음).
10. Accessibility
semantic <table> 유지. 정렬 열 <th aria-sort>(ascending / descending), 정렬 버튼 aria-label("이름 정렬, 오름차순"), 버튼이라 Enter / Space 동작. 선택 Checkbox는 aria-label("현재 페이지 전체 선택", "행 선택"), header는 aria-checked="mixed". 선택 수 요약은 role="status". Pagination은 기존 nav + aria-current / aria-disabled.
11. Responsive
Table primitive의 container가 가로 스크롤한다. Pattern은 [data-slot="data-table"] { min-width: 0 }로 grid / flex 부모 안에서 페이지를 밀지 않게 한다. 390px에서 페이지 가로 overflow 없음(표만 520px 안에서 스크롤), footer는 요약 / 페이지 크기 / 페이지 이동이 줄바꿈된다. 카드형 변환은 없다.
12. 보류 / 비범위
- column resizing·pinning, grouping, tree, virtualization, infinite scroll, drag reorder, 서버 사이드 페이지/정렬, 편집 가능한 표.
- 선택 action toolbar(선택 수 요약만 제공), 행 클릭 이동, 다중 정렬 UI.
- 정렬 header와 열 표시 메뉴의 열 이름: header가 컴포넌트일 수 있어 메뉴 문구는
labels로 받는다. - 확인하지 못한 것: 스크린 리더 낭독, 키보드 Tab 순서 전체, 페이지 크기 Select 조작, 행 선택 + 페이지 이동 후 선택 유지.
With Filter Barverification Story의 DatePicker / Combobox는controlled={true}를 사용한다. 초기화는 부모의 검색·상태·날짜·부서 state와table.resetGlobalFilter(true)/table.resetColumnFilters(true)를 함께 비운다. 날짜·부서의 undefined 초기화는 선택값을 비우며, 해당 목적의keyremount를 제거했다. 외부 값 변경은 자식 콜백을 호출하지 않으므로 테이블 필터는 소비자가 명시적으로 초기화해야 한다. 팝업 닫기·검색어·달력 월 초기화는 별도 계약이며, 이 Story는 일반 운영 Reference로 승격하지 않는다.- Legacy 호환 범위:
controlled를 생략한 DatePicker / Combobox는 기존value !== undefined판정을 유지한다. 외부 undefined 초기화 시 내부 값이 다시 표시될 수 있으므로, 과거keyremount 우회 설명은 이 Legacy 호출에 한정한다. 신규 연동은 명시적 Controlled 모드를 사용한다.
13. 관련 파일 / 검증
- 2026-10-08:
With Filter Barverification Story 소스를 격리 Headless Edge 환경에서 실행했다. 부서(검색/global filter) + 상태/등록일(column filter)로 1행 조회 후 초기화하면 전체 47개와 기존 표 표시를 복원하고 날짜·부서 Trigger DOM을 유지했다. 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
packages/ui/src/patterns/data-table/{data-table.tsx,index.ts}
packages/ui/src/theme/data-table.css
packages/ui/src/index.ts, theme.css, tailwind.css(@source patterns)
src/patterns/data-table.stories.tsx, sample-users.ts
npm run typecheck,npm run build통과(2026-10-03).- Browser 확인: 정렬 none → asc → desc → none과
aria-sort, header mixed / 전체 / 해제와 선택 수 요약·data-state="selected", 페이지 이동, 열 숨김(메뉴 유지), 행 액션이 행 데이터를 받음(toast), Loading(skeleton 30개,aria-busy), Empty(colSpan), 390px overflow 없음. 연동은filter-bar.md§13.