Grouping
TanStack 행 그룹핑·집계 — 그룹/집계/placeholder 행 렌더와 treegrid ARIA.
개요
행 그룹핑은 TanStack Table이 전담합니다. 패키지에 그룹 정책은 없습니다 — 그룹 행의 생김새·토글·집계 표기가
전부 소비자 cell 렌더 함수 안에 있습니다.
세 가지 행 타입이 생깁니다.
| 타입 | 판별 | 의미 |
|---|---|---|
| 그룹 행 | cell.getIsGrouped() | 그룹 헤더 — 묶인 값과 자식 수 표시 |
| 집계 행 | cell.getIsAggregated() | 집계 결과(sum/mean/…) 표시 |
| placeholder | cell.getIsPlaceholder() | 그룹 컬럼이 leaf 행에 만드는 빈 자리 |
연관 페이지:
- Tree data —
getSubRows로 원본 데이터 계층을 선언하는 방식. 그룹핑과 달리 부모·자식 관계가 데이터에 미리 있습니다. - Expansion — 행 아래 상세 패널을 펼치는 패턴. 그룹 펼침/접힘과 메커니즘이 다릅니다.
기본 사용법
useDataGrid에 getGroupedRowModel() + getExpandedRowModel()을 같이 넘기고, state.grouping으로
묶을 컬럼 키 배열을 선언합니다. 컬럼의 cell에서 세 플래그로 분기해 각 행 타입을 직접 그립니다.
const data = [ { id: '1', name: '김민준', category: '뷰티', followers: 128000 }, { id: '2', name: '이서연', category: '뷰티', followers: 84500 }, { id: '3', name: '박도윤', category: '게임', followers: 233000 }, { id: '4', name: '최하은', category: '게임', followers: 45200 }, { id: '5', name: '정시우', category: '푸드', followers: 67800 }, { id: '6', name: '강지우', category: '푸드', followers: 152000 }, { id: '7', name: '윤서준', category: '뷰티', followers: 39100 }, { id: '8', name: '임하준', category: '게임', followers: 98700 }, ]; const columns = [ { accessorKey: 'category', header: ({ header }) => <DataGrid.HeaderCell header={header}>카테고리</DataGrid.HeaderCell>, cell: (info) => { const { cell } = info; return ( <DataGrid.Cell cell={cell}> {cell.getIsGrouped() ? ( // 그룹 행: 토글 버튼 + 그룹 값 + 멤버 수 <button onClick={info.row.getToggleExpandedHandler()}> {info.row.getIsExpanded() ? '▾' : '▸'} {String(info.getValue())} ({info.row.subRows.length}) </button> ) : cell.getIsPlaceholder() ? null : ( // leaf의 그룹 컬럼 자리는 비움 String(info.getValue() ?? '') )} </DataGrid.Cell> ); }, }, { accessorKey: 'name', header: ({ header }) => <DataGrid.HeaderCell header={header}>채널</DataGrid.HeaderCell>, cell: (info) => ( <DataGrid.Cell cell={info.cell}> {/* 집계·placeholder 행에는 채널명이 없으므로 비움 */} {info.cell.getIsAggregated() || info.cell.getIsPlaceholder() ? null : String(info.getValue() ?? '')} </DataGrid.Cell> ), }, { accessorKey: 'followers', aggregationFn: 'sum', header: ({ header }) => <DataGrid.HeaderCell header={header}>팔로워</DataGrid.HeaderCell>, cell: (info) => { const { cell } = info; return ( <DataGrid.Cell cell={cell} $css={cell.getIsAggregated() ? { fontWeight: '$fontWeight-bold' } : undefined}> {cell.getIsPlaceholder() ? null : Number(info.getValue() ?? 0).toLocaleString()} </DataGrid.Cell> ); }, }, ]; function GroupedGrid() { const [grouping, setGrouping] = useState(['category']); const [expanded, setExpanded] = useState(true); // 처음엔 전체 펼침 const table = useDataGrid({ data, columns, getCoreRowModel: getCoreRowModel(), getGroupedRowModel: getGroupedRowModel(), getExpandedRowModel: getExpandedRowModel(), getRowId: (row) => row.id, state: { grouping, expanded }, onGroupingChange: setGrouping, onExpandedChange: setExpanded, }); const selection = useDataGridSelection(table); return ( <DataGrid.Root table={table} selection={selection} role="treegrid" size="md"> <DataGrid.Header /> <DataGrid.Body /> </DataGrid.Root> ); } render(<GroupedGrid />);
role="treegrid"를 명시하는 이유: 그룹 행이 생기면 행 간 계층 관계가 생깁니다. treegrid를 선언해야
Row가 aria-level·aria-expanded를 부착합니다. 자동으로 전환되지 않으므로 반드시 명시합니다.
cell 분기 패턴 요약
cell: (info) => {
const { cell } = info;
if (cell.getIsGrouped()) {
// 그룹 행 — 토글·그룹 값·멤버 수
return <DataGrid.Cell cell={cell}>…</DataGrid.Cell>;
}
if (cell.getIsAggregated()) {
// 집계 행 — aggregationFn이 계산한 값
return <DataGrid.Cell cell={cell}>…</DataGrid.Cell>;
}
if (cell.getIsPlaceholder()) {
// 그룹 컬럼이 leaf 행에서 차지하는 빈 자리 — 보통 null 반환
return <DataGrid.Cell cell={cell}>{null}</DataGrid.Cell>;
}
// 일반 leaf 행
return <DataGrid.Cell cell={cell}>{String(info.getValue() ?? '')}</DataGrid.Cell>;
},집계 — aggregationFn
컬럼에 aggregationFn을 선언하면 그룹 행의 집계 셀이 채워집니다. 집계는 별도 row model 없이
getGroupedRowModel 내부에서 계산됩니다.
built-in 집계 함수
TanStack이 제공하는 문자열 식별자입니다.
| 값 | 의미 |
|---|---|
'sum' | 합계 |
'mean' | 평균 |
'count' | 개수 |
'min' | 최솟값 |
'max' | 최댓값 |
'extent' | [min, max] 배열 |
'median' | 중앙값 |
'unique' | 유일값 배열 |
'uniqueCount' | 유일값 개수 |
{
accessorKey: 'costPerPost',
aggregationFn: 'sum', // 그룹 합계
cell: (info) => {
const { cell } = info;
return (
<DataGrid.Cell
cell={cell}
$css={cell.getIsAggregated() ? { fontWeight: '$fontWeight-bold' } : undefined}
>
{cell.getIsPlaceholder() ? null : krwFormatter.format(Number(info.getValue() ?? 0))}
</DataGrid.Cell>
);
},
}커스텀 집계 함수
built-in으로 표현할 수 없는 도메인 규칙은 함수를 직접 넘깁니다.
시그니처는 (columnId: string, leafRows: Row[], childRows: Row[]) => unknown입니다.
leafRows는 그룹의 최하위 행, childRows는 직접 자식 행입니다.
// 팔로워 가중 참여율 = Σ(rate × followers) / Σ(followers)
const weightedEngagement = (
_columnId: string,
leafRows: TanstackRow<MyRow>[],
): number => {
let weightSum = 0;
let weighted = 0;
leafRows.forEach((row) => {
const rate = row.getValue<number>('engagementRate');
const followers = row.original.followers; // 표시 컬럼이 아닌 값은 row.original에서 읽음
weighted += rate * followers;
weightSum += followers;
});
return weightSum ? weighted / weightSum : 0;
};
{
accessorKey: 'engagementRate',
aggregationFn: weightedEngagement, // 함수 직접 전달
cell: (info) => { … }
}TanstackRow는 @featuring-corp/data-grid에서 임포트합니다.
import type { TanstackRow } from '@featuring-corp/data-grid';그룹 펼치기/접기
getExpandedRowModel()을 추가하고 state.expanded + onExpandedChange를 배선하면 그룹 행을 접고
펼칠 수 있습니다. 개별 행 토글과 전체 토글 두 패턴이 있습니다.
개별 행 토글
row.getToggleExpandedHandler()를 토글 버튼의 onClick에 연결합니다.
<button
onClick={row.getToggleExpandedHandler()}
aria-expanded={row.getIsExpanded()}
aria-label={row.getIsExpanded() ? 'Collapse group' : 'Expand group'}
>
{row.getIsExpanded() ? '▾' : '▸'}
</button>onPointerDown에서 e.stopPropagation()을 호출해 그리드의 셀 선택 시작 동작과 충돌을 막습니다.
<IconButton
onClick={row.getToggleExpandedHandler()}
onPointerDown={(e) => e.stopPropagation()}
aria-expanded={row.getIsExpanded()}
>
…
</IconButton>전체 펼치기/접기
table.toggleAllRowsExpanded(true/false)로 모든 그룹을 한 번에 제어합니다.
<button onClick={() => grid.toggleAllRowsExpanded(true)}>전체 펼치기</button>
<button onClick={() => grid.toggleAllRowsExpanded(false)}>전체 접기</button>초기 상태
ExpandedState를 true로 초기화하면 모든 그룹이 처음부터 펼쳐집니다.
const [expanded, setExpanded] = useState<ExpandedState>(true);특정 그룹만 열려면 행 id를 키로 하는 객체를 사용합니다.
const [expanded, setExpanded] = useState<ExpandedState>({ 'group-beauty': true });다중 그룹핑 키
state.grouping에 여러 키를 넘으면 중첩 그룹(nested group)이 만들어집니다.
배열 순서가 그룹핑 순서입니다.
const [grouping, setGrouping] = useState<GroupingState>(['category', 'status']);
// → 카테고리로 묶은 뒤, 그 안에서 상태로 한 번 더 묶음각 depth에서 집계가 독립적으로 다시 계산됩니다. treegrid를 선언하면
바깥 그룹은 aria-level=1, 안쪽 그룹은 aria-level=2, leaf는 aria-level=3으로 노출됩니다.
접근성 — treegrid
그룹 행이 생기면 반드시 Root에 role="treegrid"를 명시합니다.
이 역할이 선언될 때만 Row가 계층 ARIA를 부착합니다.
<DataGrid.Root table={grid} selection={selection} role="treegrid">Row가 부착하는 속성은 다음과 같습니다.
| 속성 | 값 | 부착 조건 |
|---|---|---|
aria-level | row.depth + 1 (1-based) | treegrid의 모든 행 |
aria-expanded | true / false | 확장 가능 행만 (row.getCanExpand()) — leaf는 미부착 |
aria-posinset | row.index + 1 (1-based) | 중첩 행만 (부모가 있는 행) |
aria-setsize | 부모 subRows.length | 중첩 행만 |
그룹 행도 일반 행처럼 visible row model에 포함되므로 셀 선택·키보드 내비·클립보드 복사에 동등하게 참여합니다.
접근성 전반(role 기본값 규칙, 절대 좌표, 로케일 중립 정책)은 Accessibility에서 다룹니다.
API
Prop
Type
다음 단계
- Tree data — 원본 데이터에 부모·자식 관계가 있을 때
getSubRows로 계층을 선언하는 방식. - Expansion — 행 아래 상세 패널(row detail)을 펼치는 컴포지션 패턴.
- Accessibility —
role선택 규칙, treegrid 계층 ARIA, 로케일 중립 정책 전체. - 라이브 예제: Storybook — DataGrid/Grouping