DataTable
먼저 보고, 그다음 계약을 읽습니다
정렬·페이지 슬라이싱·행 클릭을 갖춘 제네릭 데이터 표.
Component intent의도와 경계 읽기
columns 정의(key·header·accessor·render·sortable·align·width)와 rows 배열로 구동되는 SSOT 표. 정렬은 controlled(sortBy+sortDir+onSort)와 uncontrolled(defaultSortBy) 두 모드를 지원하고, accessor 가 있으면 그 값으로, 없으면 row[key] 로 비교한다(숫자·Date·한국어 로케일 자동 판별). page+pageSize 를 주면 자체적으로 slice 만 하며, 페이지 이동 UI 는 Pagination 컴포넌트가 별도로 담당한다. loading 시 skeleton 행, 빈 rows 시 emptyState 를 렌더한다.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { DataTable } from "@axe/ui";import type { DataTableColumn } from "@axe/ui";type Deal = { name: string; stage: string; amount: number };const columns: DataTableColumn<Deal>[] = [ { key: "name", header: "딜", sortable: true }, { key: "stage", header: "단계" }, { key: "amount", header: "투자액", sortable: true, align: "right", render: (r) => `${r.amount.toLocaleString()}원`, },];<DataTable columns={columns} rows={deals} getRowKey={(r) => r.name} defaultSortBy="amount" defaultSortDir="desc" onRowClick={(r) => openDeal(r)}/>import { DataTable, Pagination } from "@axe/ui";const [sortBy, setSortBy] = useState("amount");const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");const [page, setPage] = useState(1);const pageSize = 10;<> <DataTable columns={columns} rows={rows} sortBy={sortBy} sortDir={sortDir} onSort={(key, dir) => { setSortBy(key); setSortDir(dir); }} page={page} pageSize={pageSize} stickyHeader loading={isLoading} /> <Pagination page={page} pageSize={pageSize} total={rows.length} onPageChange={setPage} /></><div class="axe-data-table"> <div class="axe-data-table__scroll"> <table class="axe-data-table__table"> <thead class="axe-data-table__thead"> <tr> <th class="axe-data-table__th" scope="col">딜</th> <th class="axe-data-table__th axe-data-table__th--right" scope="col">투자액</th> </tr> </thead> <tbody> <tr class="axe-data-table__tr"> <td class="axe-data-table__td">AXE 포트A</td> <td class="axe-data-table__td axe-data-table__td--right">3,000,000원</td> </tr> </tbody> </table> </div></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
columns | DataTableColumn<Row>[] | 필수 | — | 열 정의 배열. 각 항목: key(정렬/aria 식별자) · header(ReactNode) · accessor?(정렬값 추출, 미지정 시 row[key]) · render?(셀 커스텀, 미지정 시 row[key]) · sortable? · align?("left"|"right"|"center") · width?(CSS 폭). |
rows | Row[] | 필수 | — | 표시할 데이터 행. Row 는 제네릭 — 어떤 객체든 가능. |
getRowKey | (row: Row, index: number) => string | — | — | React key 로만 사용(onRowClick 은 row 객체를 직접 받으므로 무관). 미지정 시 배열 index 사용. |
sortBy | string | — | — | controlled 정렬 — 현재 정렬 열의 key. onSort 와 함께 줘야 controlled 로 인식. |
sortDir | "asc" | "desc" | — | — | controlled 정렬 방향. controlled 인데 미지정이면 "asc" 로 취급. |
onSort | (key: string, dir: SortDir) => void | — | — | 정렬 헤더 클릭 콜백. sortBy 와 함께 주면 controlled. sortBy 만 주고 이걸 안 주면 controlled 로 인식되지 않아 dev 경고와 함께 sortBy 값이 무시되고, 헤더 클릭 정렬은 내부 state 로 폴백해 계속 동작한다. |
defaultSortBy | string | — | — | uncontrolled 초기 정렬 열. 이후 정렬 state 는 컴포넌트가 보유. |
defaultSortDir | "asc" | "desc" | — | "asc" | uncontrolled 초기 정렬 방향. |
page | number | — | — | 1-based 현재 페이지. pageSize 와 함께 있어야 slice 발생. |
pageSize | number | — | — | 페이지당 행 수. page 와 함께 주면 (page-1)*pageSize 부터 slice. loading skeleton 행 수의 기본값이기도 함(미지정 시 5). |
onPageChange | (page: number) => void | — | — | 시그니처 대칭을 위해 노출되나 컴포넌트가 내부에서 호출하지 않는다 — 페이지 전환은 짝을 이루는 Pagination 이 발생시키고, 그 값을 부모가 page 로 되돌려 준다. |
onRowClick | (row: Row) => void | — | — | 행 클릭 콜백. 지정 시 행에 `--clickable` 클래스와 클릭 핸들러가 붙는다. |
rowClickable | boolean | — | false | onRowClick 이 있을 때 행 전체를 role=button+tabIndex 위젯으로 만들지. 기본 false(table 시맨틱 보존) — 셀 안에 `<a>`/`<button>` 등 interactive 가 있으면 nested interactive 충돌을 피하려 false 로 두고, 그 경우 마우스 클릭은 되지만 키보드/role 위임은 셀 내부 컨트롤이 책임진다. |
loading | boolean | — | — | true 면 tbody 자리에 skeleton 행(pageSize 또는 5개)을 렌더. |
emptyState | React.ReactNode | — | — | rows 가 비었을 때 placeholder. 미지정 시 muted "데이터 없음". |
stickyHeader | boolean | — | — | thead 를 sticky 로 고정. `.axe-data-table__scroll` 에 max-height(480px)가 걸려 세로 스크롤이 생길 때 유효. |
...rest | Omit<React.HTMLAttributes<HTMLDivElement>, "onClick"> | — | — | className 병합 후 나머지는 루트 div 로 전파. onClick 은 행 단위 onRowClick 과 혼동을 막으려 제외됨. forwardRef 는 루트 div 를 가리킨다. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-data-table | stable | 루트 div — elevated 배경·border·radius-lg·overflow hidden(radius clip). |
.axe-data-table--sticky | stable | stickyHeader 모디파이어 — scroll 영역 max-height + thead sticky 활성. |
.axe-data-table__scroll | stable | 가로(및 sticky 시 세로) 스크롤 컨테이너. |
.axe-data-table__table | stable | `<table>` — width 100%·min-width 600px·border-collapse. |
.axe-data-table__thead | stable | thead — subtle 배경. |
.axe-data-table__th | stable | 헤더 셀 — xs·uppercase·secondary. `--right/--center` 로 정렬. |
.axe-data-table__th--sortable | stable | 정렬 가능 헤더 — padding 을 0 으로 두고 내부 sort-btn 이 padding 을 갖는다. |
.axe-data-table__sort-btn | stable | 정렬 헤더의 실제 `<button>` — 전폭 flex·hover/focus 시 강조. |
.axe-data-table__td | stable | 본문 셀 — tabular-nums·middle 정렬. `--right/--center` 로 정렬. |
.axe-data-table__tr | stable | 본문 행 — 하단 border·hover 시 accent 4% 배경. |
.axe-data-table__tr--clickable | stable | onRowClick 이 있을 때 — cursor pointer·focus-visible outline. |
.axe-data-table__empty | stable | 빈 상태 셀 — 중앙 muted 텍스트(colSpan 전폭). |
.axe-data-table__th--active | internal | 현재 정렬 중인 헤더 — 컴포넌트 정렬 state 로만 토글되는 내부 훅. |
.axe-data-table__sort-arrow | internal | 정렬 방향 화살표(∧/∨/↕) — aria-hidden 장식. |
.axe-data-table__tr--empty | internal | 빈 상태 행 마커 — hover 무효화. |
.axe-data-table__tr--skeleton | internal | loading skeleton 행. |
.axe-data-table__skel | internal | shimmer 플레이스홀더 바(indicators.css 의 axe-shimmer 공유). |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- 정렬 헤더는 네이티브 `<button>` — Tab 포커스 후 Enter/Space 로 정렬 토글(asc↔desc)
- rowClickable=true 인 행은 Enter/Space 로 onRowClick 실행(preventDefault 로 스크롤 방지)
정렬 가능 열의 `<th>` 는 aria-sort=ascending|descending|none, scope="col". 정렬 화살표는 aria-hidden. rowClickable=true 인 행만 role="button"+tabIndex=0 를 받고, 기본(false)은 table 시맨틱을 보존한다.
dev 모드에서 sortBy 만 주고 onSort 를 빠뜨리면 console.warn 으로 controlled 오설정을 경고한다. 셀 내부에 링크/버튼이 있으면 rowClickable 을 false 로 둬 nested interactive 충돌을 피한다.