12 KiB
Data Table (Pattern)
2026-10-10 추가 수정 요청 기준: 기본 검색 가운데 / 추가 조건 검색 왼쪽, 조건→검색어→조회→초기화·8px. Header는 Card 밖, 검색·목록 도구·결과는 한 Card. 페이지 크기 label useId로 인스턴스 분리. 과거 검증 기록은 당시 결과이며 현재 검증은 추가 수정 보고서를 따릅니다.
현재 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이다. Pattern 자체는 조회 버튼으로 조건을 확정하는 별도 공개 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): 선택 행 면 = --hm-sem-accent 20%(hover·selected+hover와 같은 Soft Surface, Table에만 적용하는 20% 정책; Select 강조는 기존 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
2026-10-11 v2 추가 요청: 대표 Story의 목록 검색과 ‘열’ 버튼은 공간이 충분할 때 같은 행에 둔다. 검색 form의 내용 폭을 기준으로 공간이 부족할 때만 목록 도구가 다음 행으로 줄바꿈된다. 기본 검색은 form 안에서 가운데, 추가 조건은 왼쪽, ‘열’ 버튼은 우측 정렬한다. 간격은 기존 --hm-filter-bar-gap(8px)을 재사용하고 모바일 검색·버튼 행 정렬을 유지한다. src/list-screen.css의 hm-data-table-* 조합에만 적용하며 공통 FilterBar·DataTable Component API/CSS는 변경하지 않는다.
Table primitive의 container가 가로 스크롤한다. Pattern은 [data-slot="data-table"] { min-width: 0 }로 grid / flex 부모 안에서 페이지를 밀지 않게 한다. 390px에서 페이지 가로 overflow 없음(표만 520px 안에서 스크롤), footer는 요약 / 페이지 크기 / 페이지 이동이 줄바꿈된다. 카드형 변환은 없다.
11.1 식별자 줄바꿈과 열 폭
Table의 overflow-wrap: anywhere는 긴 일반 콘텐츠를 보호하지만 좁은 ID 열에서 JOB-001을 여러 줄로 분절할 수 있다. ID·날짜 등 의미상 한 줄이어야 하는 값은 해당 cell의 콘텐츠만 보호한다. 제목 등 장문은 기존 줄바꿈을 유지한다.
const columns: DataTableColumnDef<Job>[] = [
{ accessorKey: 'id', header: 'ID', enableSorting: false,
cell: ({ row }) => <span className="whitespace-nowrap">{row.original.id}</span> },
// 제목·그 외 열은 요구사항에 맞는 기존 cell composition을 유지한다.
]
현재 DataTable은 ColumnDef.size/minSize를 DOM의 th/td width 또는 min-width로 연결하지 않는다. 해당 값만 지정하고 폭이 확보됐다고 보고하지 않는다. nowrap 콘텐츠의 실제 폭·cell padding·표 내부 스크롤을 측정한다. 긴 식별자의 별도 최소 폭이 필요하면 소비 콘텐츠의 min-width composition과 실제 DOM 폭을 확인한다. Typography·Color·공통 Table CSS는 변경하지 않는다. Ellipsis를 쓰는 요구사항이라면 키보드/터치로 전체 값을 확인할 경로를 함께 제공하며 ID를 잘라서 식별 가능하다고 판단하지 않는다.
네 폭에서 ID 전체 텍스트·Range 줄 수·cell 내용 폭·table/container scrollWidth/clientWidth와 문서 가로 넘침을 기록한다. 긴 제목 줄바꿈, 표 내부 가로 스크롤과 마지막 열 접근을 함께 확인한다. min-width: 0만으로 ID 가독성을 보장하지 않는다.
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.