Sorting
클라이언트/서버 정렬, SortArrow 배선, 다중 정렬, 외부 컨트롤, aria-sort.
개요
정렬 엔진은 TanStack Table의 getSortedRowModel이 담당합니다. 그리드 패키지는 두 가지만 더합니다.
DataGrid.SortArrow— 정렬 방향(asc/desc)과 다음 방향 미리보기(ghost)를 표시하는 인디케이터 primitive.- 정직한
aria-sort— 실제 정렬 상태는 배선 여부와 무관하게 항상 노출됩니다. 외부 컨트롤로 프로그램적으로 정렬해도 보조기술(AT)에 사실이 전달됩니다.
그리드는 헤더 클릭에 정렬을 자동 배선하지 않습니다. 소비자가 onClick={header.column.getToggleSortingHandler()}와 <DataGrid.SortArrow>를 명시적으로 부착해야 합니다. 덕분에 같은 헤더에 필터 팝업·컬럼 메뉴 등 다른 인터랙션도 자유롭게 결합할 수 있습니다.
기본 사용법
클라이언트 단일 정렬의 최소 구성입니다. getSortedRowModel을 useDataGrid에 전달하고, 헤더에 onClick + <DataGrid.SortArrow>를 부착합니다.
'use no memo'; // React Compiler와 TanStack Table mutable 인스턴스 충돌 방지
import { useState } from 'react';
import {
getCoreRowModel,
getSortedRowModel,
useDataGrid,
DataGrid,
} from '@featuring-corp/data-grid';
import type { ColumnDef, SortingState } from '@featuring-corp/data-grid';
type Row = { id: string; name: string; followers: number };
const columns: ColumnDef<Row>[] = [
{
accessorKey: 'name',
header: ({ header }) => (
<DataGrid.HeaderCell
header={header}
onClick={header.column.getToggleSortingHandler()}
>
채널
<DataGrid.SortArrow
direction={header.column.getIsSorted()}
nextDirection={header.column.getNextSortingOrder()}
/>
</DataGrid.HeaderCell>
),
cell: (info) => (
<DataGrid.Cell cell={info.cell}>{info.getValue() as string}</DataGrid.Cell>
),
},
{
accessorKey: 'followers',
// enableSorting: false — 정렬 불가 컬럼. affordance(커서/tabIndex/aria-sort) 미부착.
enableSorting: false,
header: ({ header }) => (
<DataGrid.HeaderCell header={header}>팔로워</DataGrid.HeaderCell>
),
cell: (info) => (
<DataGrid.Cell cell={info.cell}>{info.getValue() as number}</DataGrid.Cell>
),
},
];
function SortableGrid({ data }: { data: Row[] }) {
const [sorting, setSorting] = useState<SortingState>([]);
const table = useDataGrid<Row>({
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
data,
columns,
getRowId: (r) => r.id,
state: { sorting },
onSortingChange: setSorting,
});
return (
<DataGrid.Root table={table}>
<DataGrid.Header />
<DataGrid.Body />
</DataGrid.Root>
);
}아래 그리드에서 채널 헤더를 클릭하면 정렬이 바뀝니다(asc → desc → 해제). 팔로워는 enableSorting: false라 affordance가 붙지 않습니다.
const data = [ { id: '1', name: '김민준', followers: 128000 }, { id: '2', name: '이서연', followers: 84500 }, { id: '3', name: '박도윤', followers: 233000 }, { id: '4', name: '최하은', followers: 45200 }, ]; const columns = [ { accessorKey: 'name', header: ({ header }) => ( <DataGrid.HeaderCell header={header} onClick={header.column.getToggleSortingHandler()}> 채널 <DataGrid.SortArrow direction={header.column.getIsSorted()} nextDirection={header.column.getNextSortingOrder()} /> </DataGrid.HeaderCell> ), cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue()}</DataGrid.Cell>, }, { accessorKey: 'followers', enableSorting: false, header: ({ header }) => <DataGrid.HeaderCell header={header}>팔로워</DataGrid.HeaderCell>, cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue().toLocaleString()}</DataGrid.Cell>, }, ]; function SortableGrid() { const [sorting, setSorting] = useState([]); const table = useDataGrid({ data, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getRowId: (row) => row.id, state: { sorting }, onSortingChange: setSorting, }); return ( <DataGrid.Root table={table} size="md"> <DataGrid.Header /> <DataGrid.Body /> </DataGrid.Root> ); } render(<SortableGrid />);
정렬 affordance 게이팅: data-sortable(pointer 커서·tabIndex=0·Enter/Space 활성·aria-sort="none")은 capability(column.getCanSort()) + 실제 배선(onClick)이 모두 있을 때만 부착됩니다. enableSorting: false인 컬럼이나 onClick을 붙이지 않은 헤더에는 affordance가 생기지 않습니다. hover 미리보기(ghost)는 이 게이트와 별개로, SortArrow에 nextDirection을 줄 때만 표시됩니다(아래 hover 미리보기 제어 참고).
공통 패턴 — SortHeader 래퍼
동일한 sort 배선을 여러 컬럼에 적용할 때는 작은 래퍼 함수로 정리하면 편합니다.
function SortHeader<T>({
header,
children,
}: {
header: Header<T, unknown>;
children: ReactNode;
}) {
return (
<DataGrid.HeaderCell
header={header}
onClick={header.column.getToggleSortingHandler()}
>
{children}
<DataGrid.SortArrow
direction={header.column.getIsSorted()}
nextDirection={header.column.getNextSortingOrder()}
/>
</DataGrid.HeaderCell>
);
}다중 정렬
enableMultiSort: true를 주면 여러 컬럼을 동시에 정렬할 수 있습니다. 기본 동작은 Shift+클릭으로 컬럼을 누적하며, isMultiSortEvent: () => true를 주면 Shift 없이도 누적됩니다.
const table = useDataGrid<Row>({
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
data,
columns,
getRowId: (r) => r.id,
state: { sorting },
onSortingChange: setSorting,
enableMultiSort: true,
isMultiSortEvent: () => true, // Shift 없이 클릭만으로 컬럼 누적
});정렬 순서는 SortingState 배열로 드러납니다. 배열의 앞쪽 항목이 우선순위가 높습니다.
// 예: 카테고리 오름차순 → 단가 내림차순
const [sorting, setSorting] = useState<SortingState>([
{ id: 'category', desc: false },
{ id: 'costPerPost', desc: true },
]);서버 정렬 (manual)
서버가 정렬을 수행하는 경우 manualSorting: true를 전달합니다. 그리드는 클라이언트 측 재정렬을 건너뛰고, onSortingChange로 받은 SortingState를 fetch 파라미터로 서버에 넘기는 방식으로 동작합니다.
function ServerSortGrid() {
const [sorting, setSorting] = useState<SortingState>([
{ id: 'costPerPost', desc: true },
]);
const { data, isLoading, error } = useFetchRows({
sorting, // SortingState를 그대로 fetch 파라미터로
});
const table = useDataGrid<Row>({
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), // manualSorting:true면 실제로는 호출 안 됨
data: data?.items ?? [],
columns,
rowCount: data?.total,
getRowId: (r) => r.id,
manualSorting: true,
state: { sorting },
onSortingChange: setSorting,
loading: isLoading,
error,
skeletonRows: 10,
});
return (
<DataGrid.Root table={table}>
<DataGrid.Header />
<DataGrid.Body />
</DataGrid.Root>
);
}
getSortedRowModel은manualSorting: true일 때 실질적으로 동작하지 않습니다. 그러나 나중에 클라이언트 정렬로 전환하거나 혼용 시나리오를 위해 넣어두는 것이 일반적입니다.
외부 컨트롤
정렬 트리거를 헤더 밖(툴바·버튼)에 두는 경우입니다. 헤더에 onClick을 붙이지 않으므로 affordance(커서/tabIndex)가 없고, nextDirection을 주지 않아 hover 미리보기(ghost)도 뜨지 않습니다. 하지만 실제 정렬 상태는 정직하게 aria-sort/data-sorted + SortArrow 방향으로 노출됩니다.
const data = [ { id: '1', name: '김민준', followers: 128000 }, { id: '2', name: '이서연', followers: 84500 }, { id: '3', name: '박도윤', followers: 233000 }, { id: '4', name: '최하은', followers: 45200 }, { id: '5', name: '정시우', followers: 67800 }, ]; const columns = [ { accessorKey: 'name', header: ({ header }) => <DataGrid.HeaderCell header={header}>채널</DataGrid.HeaderCell>, cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue()}</DataGrid.Cell>, }, { accessorKey: 'followers', // onClick 없음 — 헤더는 표시 전용. SortArrow는 현재 방향만 표시. header: ({ header }) => ( <DataGrid.HeaderCell header={header}> 팔로워 <DataGrid.SortArrow direction={header.column.getIsSorted()} /> </DataGrid.HeaderCell> ), cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue().toLocaleString()}</DataGrid.Cell>, }, ]; function ExternalSortGrid() { const [sorting, setSorting] = useState([]); const table = useDataGrid({ data, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getRowId: (row) => row.id, state: { sorting }, onSortingChange: setSorting, }); return ( <VStack $css={{ gap: '$spacing-300' }}> {/* 외부 정렬 트리거 */} <HStack $css={{ gap: '$spacing-200' }}> <Button.Root variant="secondary" size="sm" onClick={() => setSorting([{ id: 'followers', desc: false }])}> 팔로워 ↑ </Button.Root> <Button.Root variant="secondary" size="sm" onClick={() => setSorting([{ id: 'followers', desc: true }])}> 팔로워 ↓ </Button.Root> <Button.Root variant="tertiary" size="sm" onClick={() => setSorting([])}> 정렬 해제 </Button.Root> </HStack> <DataGrid.Root table={table}> <DataGrid.Header /> <DataGrid.Body /> </DataGrid.Root> </VStack> ); } render(<ExternalSortGrid />);
접근성
| 상황 | aria-sort 값 | 부착 조건 |
|---|---|---|
| 정렬됨 (오름차순) | "ascending" | 배선 여부 무관 — 항상 노출 |
| 정렬됨 (내림차순) | "descending" | 배선 여부 무관 — 항상 노출 |
| 정렬 가능·미정렬 | "none" | 배선 있을 때만 (isSortInteractive) |
| 정렬 불가 | 없음 | enableSorting: false 또는 배선 없음 |
aria-sort="none"은 "이 컬럼은 정렬 가능하지만 현재 정렬되어 있지 않다"는 안내입니다. 배선(onClick)이 없는 헤더에 붙이면 거짓 안내가 되므로 그리드가 자동으로 차단합니다.
외부 컨트롤로 프로그램적으로 정렬한 경우에도 aria-sort가 정직하게 갱신되어 스크린 리더 사용자에게 현재 정렬 상태가 전달됩니다.
접근성 전반은 접근성 페이지를 참고하세요.
DataGrid.SortArrow props
Prop
Type
hover 미리보기 제어 — nextDirection
hover 미리보기(ghost)는 nextDirection을 줄 때만 표시됩니다. 정렬 트리거가 헤더 클릭이 아니라 컬럼 메뉴 등 별도 위치에 있어 미리보기가 "클릭하면 정렬될 것 같다"는 오해를 줄 수 있는 경우, nextDirection을 생략하면 ghost 없이 현재 정렬 방향만 표시됩니다. 헤더의 onClick 배선 유무와는 무관합니다.
// 미리보기 ON — 다음 클릭 방향을 hover로 예고
<DataGrid.SortArrow
direction={header.column.getIsSorted()}
nextDirection={header.column.getNextSortingOrder()}
/>
// 미리보기 OFF — nextDirection 생략. 실제 정렬 방향만 표시.
<DataGrid.SortArrow direction={header.column.getIsSorted()} />다음 단계
- Columns — 컬럼 정의,
enableSorting,sortDescFirst등 컬럼별 정렬 옵션. - Filtering — 정렬과 함께 쓰는 클라이언트/서버 필터링.
- Pagination — 서버 정렬과 페이지네이션을 함께 배선하는 방법.
- Accessibility —
aria-sort전략과 키보드 내비게이션 전반. - 라이브 스토리 — 클라이언트/서버/다중/외부 컨트롤 정렬을 모두 확인할 수 있습니다.