Featuring Design System
Grouping & Hierarchy

Row Expansion

getExpandedRowModel로 행 아래 임의 detail 패널을 펼치는 헤드리스 확장 모델.

개요

행 확장(Row Expansion)은 한 행이 자기 자신의 상세 패널을 아래로 펼치는 기능입니다. TanStack의 getExpandedRowModeluseDataGrid에 넘기면 확장 상태가 활성화되고, 실제 토글 UI와 detail 패널은 소비자 코드가 책임집니다.

Tree Data·Grouping과 다릅니다. Tree DatagetSubRows로 부모 행이 자식 행들을 펼치는 계층 트리(role="treegrid")이고, Grouping은 집계 행을 생성하는 집계 모델입니다. 세 기능 모두 expanded 상태를 공유하지만 의미가 다릅니다.

패키지가 소유하는 것과 소비자가 소유하는 것의 구분:

패키지 제공소비자 책임
getExpandedRowModel 팩토리토글 버튼 UI
row.getIsExpanded() / getToggleExpandedHandler() / toggleExpanded()detail 패널 레이아웃·콘텐츠
data-expanded 속성 (DataGrid.Row)<DataGrid.Body> render-prop 안의 조건부 렌더
DataGridRowState.isExpanded / depth state callbackExpandedState / onExpandedChange

기본 사용법

useDataGridgetExpandedRowModelgetRowCanExpand를 주고, <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 / classNameDataGridRowState를 인자로 받는 상태 콜백을 허용합니다. isExpandeddepth를 읽어 확장된 행을 강조할 수 있습니다.

<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 DatagetSubRows로 부모 행이 자식 행을 펼치는 계층 트리(role="treegrid"). aria-level / aria-posinset / aria-setsize를 자동 부여합니다.
  • GroupinggetGroupedRowModel로 집계 행을 생성하는 집계/그룹화 모델.
  • Composition & DI — render-prop Body의 동작 원리와 자동 렌더 vs render-prop 차이.
  • 라이브 스토리 — 기본 확장, 조건부 확장, 행 클릭 토글, state 콜백 스타일링을 실제로 확인하세요.