DataGrid - 컬럼 설계 플레이북
컬럼 정의를 재사용 가능한 설계 단위로 관리하면서 파생 값 invalidation과 renderer 비용을 제어하는 패턴입니다.
reopt designUpdated
1. 컬럼 설계 원칙
- 표시용 변환은 `getValue`에, 데이터 반영은 `setValue`에 분리합니다.
- 재사용 가능한 커스텀 편집기는 column-level `editor.component` 로 선언합니다.
- 포맷(통화/상태 배지)은 `renderCell`로 표현 레이어를 고정합니다.
- `getValue`는 deterministic하고 가능한 한 cheap하게 유지합니다.
- 계산 비용이 큰 파생 컬럼은 `dependsOnColumnIds`로 invalidation 범위를 좁힙니다.
- mount 비용이 큰 셀은 `refreshCellRenderer`로 renderer reuse 조건을 명시합니다.
- 화면별로 공통 컬럼 묶음을 export해서 재사용 단위를 만듭니다.
2. 파생 컬럼과 invalidation
파생 컬럼은 표시 자체보다 invalidation 범위를 먼저 설계하는 편이 중요합니다. `dependsOnColumnIds`를 지정하면 편집된 필드와 실제로 연결된 컬럼만 refresh 대상으로 좁힐 수 있습니다.
import { type DataGridColumn } from "@reopt-ai/opt-datagrid";
interface InvoiceRow {
id: number;
customer: string;
owner: string;
amount: number;
status: "paid" | "pending";
}
export const columns: DataGridColumn<InvoiceRow>[] = [
{
id: "customer",
title: "Customer",
width: 240,
editable: true,
getValue: (row) => row.customer,
setValue: (row, nextValue) => ({ ...row, customer: nextValue }),
},
{
id: "owner",
title: "Owner",
width: 180,
editable: true,
getValue: (row) => row.owner,
setValue: (row, nextValue) => ({ ...row, owner: nextValue }),
},
{
id: "summary",
title: "Summary",
width: 260,
editable: false,
dependsOnColumnIds: ["customer", "owner"],
getValue: (row) => `${row.customer} · ${row.owner}`,
},
{
id: "amount",
title: "Amount",
width: 140,
align: "right",
editable: false,
getValue: (row) => row.amount.toLocaleString(),
},
{
id: "status",
title: "Status",
width: 140,
editable: false,
getValue: (row) => row.status,
renderCell: ({ value }) =>
value === "paid" ? (
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-xs text-emerald-700">Paid</span>
) : (
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs text-amber-700">Pending</span>
),
refreshCellRenderer: (prev, next) => prev.value === next.value,
},
];3. renderer refresh 전략
- badge/action/summary 셀처럼 mount 비용이 있는 경우 `refreshCellRenderer`로 reuse 조건을 명시합니다.
- renderer 전체 정책은 `rendererRefreshMode`로 조정하고, column 단위 조건은 `refreshCellRenderer`에서 다룹니다.
- 값이 바뀌지 않은 sibling cell까지 다시 mount할 필요가 없을 때 특히 유용합니다.
4. 컬럼 인터랙션 설계
컬럼 리사이즈/재정렬/메뉴 액션은 `onColumnsChange`, `onColumnResize`, `onColumnMoved`, `onHeaderMenuAction`으로 분리해 저장합니다.
import * as React from "react";
import { DataGrid, type DataGridColumn } from "@reopt-ai/opt-datagrid";
interface Row {
id: number;
name: string;
status: "active" | "blocked";
}
function ColumnManagedGrid() {
const rows = React.useMemo<Row[]>(
() => [
{ id: 1, name: "Alpha", status: "active" },
{ id: 2, name: "Beta", status: "blocked" },
],
[],
);
const [columns, setColumns] = React.useState<readonly DataGridColumn<Row>[]>(
[
{ id: "name", title: "Name", width: 220, editable: true, resizable: true, reorderable: true, menu: true, getValue: (row) => row.name, setValue: (row, v) => ({ ...row, name: v }) },
{ id: "status", title: "Status", width: 140, editable: false, resizable: true, reorderable: true, menu: true, getValue: (row) => row.status },
],
);
const saveWidth = React.useCallback((columnId: string, size: number) => {
console.log("save width", columnId, size);
}, []);
const saveOrder = React.useCallback((from: number, to: number) => {
console.log("save order", from, to);
}, []);
const handleSortAction = React.useCallback((colIndex: number, action: string) => {
console.log("sort action", colIndex, action);
}, []);
return (
<DataGrid
rows={rows}
columns={columns}
onColumnsChange={setColumns}
onColumnResize={(column, size) => saveWidth(column.id, size)}
onColumnMoved={(from, to) => saveOrder(from, to)}
onHeaderMenuAction={(colIndex, action) => handleSortAction(colIndex, action)}
/>
);
}