Row Expansion
getExpandedRowModel로 행 아래 임의 detail 패널을 펼치는 헤드리스 확장 모델.
개요
행 확장(Row Expansion)은 한 행이 자기 자신의 상세 패널을 아래로 펼치는 기능입니다. TanStack의
getExpandedRowModel을 useDataGrid에 넘기면 확장 상태가 활성화되고, 실제 토글 UI와 detail 패널은
소비자 코드가 책임집니다.
Tree Data·Grouping과 다릅니다. Tree Data는
getSubRows로 부모 행이 자식 행들을 펼치는 계층 트리(role="treegrid")이고, Grouping은 집계 행을 생성하는 집계 모델입니다. 세 기능 모두expanded상태를 공유하지만 의미가 다릅니다.
패키지가 소유하는 것과 소비자가 소유하는 것의 구분:
| 패키지 제공 | 소비자 책임 |
|---|---|
getExpandedRowModel 팩토리 | 토글 버튼 UI |
row.getIsExpanded() / getToggleExpandedHandler() / toggleExpanded() | detail 패널 레이아웃·콘텐츠 |
data-expanded 속성 (DataGrid.Row) | <DataGrid.Body> render-prop 안의 조건부 렌더 |
DataGridRowState.isExpanded / depth state callback | ExpandedState / onExpandedChange |
기본 사용법
useDataGrid에 getExpandedRowModel과 getRowCanExpand를 주고, <DataGrid.Body> render-prop 안에서
row.getIsExpanded()가 true일 때 detail 패널을 직접 그립니다.
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 }, ]; // row model은 자동 주입되지 않으므로 expanded 모델을 명시 추가. const expandedRowModel = getExpandedRowModel(); // 확장 토글 컬럼 — columnRole: 'control'로 표시해 셀 선택·병합 범위에서 chrome으로 취급. function makeExpandColumn() { return { id: '_expand', size: 44, enableSorting: false, meta: { columnRole: 'control' }, header: ({ header }) => ( <DataGrid.HeaderCell header={header} aria-label="Expand column"> {null} </DataGrid.HeaderCell> ), cell: (info) => { const row = info.row; const expanded = row.getIsExpanded(); return ( <DataGrid.Cell cell={info.cell}> <IconButton size="sm" variant="tertiary" onClick={row.getToggleExpandedHandler()} aria-expanded={expanded} aria-label={expanded ? 'Collapse row' : 'Expand row'} > <IconChevronRightOutline size={16} style={{ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.15s ease', }} /> </IconButton> </DataGrid.Cell> ); }, }; } // detail 패널 — 행이 아니라 일반 컨테이너라 [data-row] 개수에 영향을 주지 않습니다. function DetailPanel({ row }) { return ( <Box $css={{ padding: '$spacing-400', backgroundColor: '$background-2', borderBottomWidth: '1px', borderBottomStyle: 'solid', borderBottomColor: '$border-default', }} > {/* 임의 콘텐츠 — 소비자가 row.original로 원본 데이터를 자유롭게 렌더합니다 */} <Typo variant="$body-2">{row.original.name} · 상세 정보</Typo> </Box> ); } const columns = [ makeExpandColumn(), { accessorKey: 'name', header: ({ header }) => <DataGrid.HeaderCell header={header}>채널</DataGrid.HeaderCell>, cell: (info) => <DataGrid.Cell cell={info.cell}>{String(info.getValue() ?? '')}</DataGrid.Cell>, }, { accessorKey: 'followers', header: ({ header }) => <DataGrid.HeaderCell header={header}>팔로워</DataGrid.HeaderCell>, cell: (info) => <DataGrid.Cell cell={info.cell}>{Number(info.getValue() ?? 0).toLocaleString()}</DataGrid.Cell>, }, ]; function ExpansionGrid() { const [expanded, setExpanded] = useState({}); const table = useDataGrid({ data, columns, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: expandedRowModel, // 확장 모델 명시 주입 getRowId: (r) => r.id, getRowCanExpand: () => true, // 모든 행 확장 가능 state: { expanded }, onExpandedChange: setExpanded, }); return ( <DataGrid.Root table={table} size="md"> <DataGrid.Header /> {/* render-prop Body — Fragment로 행과 detail 패널을 함께 묶습니다 */} <DataGrid.Body> {(row, rowIndex) => ( <React.Fragment key={row.id}> <DataGrid.Row row={row} rowIndex={rowIndex}> {row.getVisibleCells().map((cell) => ( <React.Fragment key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </React.Fragment> ))} </DataGrid.Row> {row.getIsExpanded() && <DetailPanel row={row} />} </React.Fragment> )} </DataGrid.Body> </DataGrid.Root> ); } render(<ExpansionGrid />);
핵심 포인트 세 가지입니다.
Fragment래핑 —<DataGrid.Row>와 detail 패널을<Fragment key={row.id}>로 묶어야 Body가 행 단위로 key를 추적할 수 있습니다.- detail 패널은 행이 아닙니다 —
role="row"가 없는 일반 컨테이너라[data-row]개수는 그대로 유지됩니다. 스크롤 위치·행 선택·가상화 rowIndex가 틀어지지 않습니다. getExpandedRowModel은 소비자 명시 책임 — row model은 자동 주입되지 않으므로useDataGrid에 직접 넘겨야 합니다. 넘기지 않으면row.getIsExpanded()가 항상 false입니다.
전체 펼침/접기
table.toggleAllRowsExpanded(force?) / table.getIsAllRowsExpanded() / table.getIsSomeRowsExpanded()로
툴바를 만들 수 있습니다.
const allExpanded = table.getIsAllRowsExpanded();
<Button.Root size="sm" variant="secondary" onClick={() => table.toggleAllRowsExpanded(!allExpanded)}>
<Button.Text>{allExpanded ? '전체 접기' : '전체 펼치기'}</Button.Text>
</Button.Root>행 전체 클릭으로 토글
토글을 control 컬럼 버튼이 아니라 행 전체에 배선할 수도 있습니다. <DataGrid.Row onClick>에
row.toggleExpanded()를 연결합니다.
<DataGrid.Row
row={row}
rowIndex={rowIndex}
onClick={() => row.toggleExpanded()}
style={{ cursor: 'pointer' }}
>
{row.getVisibleCells().map(/* ... */)}
</DataGrid.Row>조건부 펼침
getRowCanExpand로 일부 행만 확장 가능하게 만듭니다. row.getCanExpand()가 false인 행은 토글 버튼을
노출하지 않아야 합니다(거짓 affordance 방지).
const table = useDataGrid<Row>({
// ...
getExpandedRowModel: expandedRowModel,
// 특정 조건의 행만 확장 가능 — 여기선 'inactive'가 아닌 행만.
getRowCanExpand: (row) => row.original.status !== 'inactive',
});컬럼 셀에서 row.getCanExpand()를 확인해 토글을 조건부 렌더합니다.
cell: (info) => {
const row = info.row;
// 확장 불가 행 — 빈 셀로 자리만 유지합니다.
if (!row.getCanExpand()) return <DataGrid.Cell cell={info.cell}>{null}</DataGrid.Cell>;
const isExpanded = row.getIsExpanded();
return (
<DataGrid.Cell cell={info.cell}>
<IconButton
size="sm"
variant="tertiary"
onClick={row.getToggleExpandedHandler()}
aria-expanded={isExpanded}
aria-label={isExpanded ? 'Collapse row' : 'Expand row'}
>
<IconChevronRightOutline
size={16}
style={{
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
transition: 'transform 0.15s ease',
}}
/>
</IconButton>
</DataGrid.Cell>
);
},상태 스타일링
확장 상태는 두 경로로 노출됩니다.
data-expanded 속성
<DataGrid.Row>는 행이 펼쳐졌을 때 data-expanded="" 속성을 부여합니다. CSS 셀렉터로
잡을 수 있습니다.
[data-row][data-expanded] {
background-color: var(--global-colors-primary-10);
}DataGridRowState state callback
<DataGrid.Row>의 style / className은 DataGridRowState를 인자로 받는 상태 콜백을 허용합니다.
isExpanded와 depth를 읽어 확장된 행을 강조할 수 있습니다.
<DataGrid.Row
row={row}
rowIndex={rowIndex}
style={(state) =>
state.isExpanded
? {
backgroundColor: 'var(--global-colors-primary-10)',
boxShadow: 'inset 3px 0 0 0 var(--global-colors-primary-60)',
}
: {}
}
className={(state) => (state.isExpanded ? 'is-expanded-row' : '')}
>
{/* ... */}
</DataGrid.Row>상태 기반 스타일에는 $css(정적 atomic) 대신 style 콜백을 써야 합니다. 토큰값은 $spacing-400 형식이
아니라 CSS 변수 var(--...) 형식으로 적습니다. 콜백은 항상 CSSProperties를 반환해야 하고, 비활성
분기는 {}를 반환합니다.
Prop
Type
다음 단계
- Tree Data —
getSubRows로 부모 행이 자식 행을 펼치는 계층 트리(role="treegrid"). aria-level / aria-posinset / aria-setsize를 자동 부여합니다. - Grouping —
getGroupedRowModel로 집계 행을 생성하는 집계/그룹화 모델. - Composition & DI — render-prop Body의 동작 원리와 자동 렌더 vs render-prop 차이.
- 라이브 스토리 — 기본 확장, 조건부 확장, 행 클릭 토글, state 콜백 스타일링을 실제로 확인하세요.