DataGrid - 대규모 데이터
5,000행 이상 시나리오에서 row identity, value recomputation, viewport buffer를 관리하는 패턴입니다.
reopt designUpdated
1. 라이브 예제
stable row identity, cached value access, pixel-buffer tuning을 함께 쓰는 대량 데이터 preset입니다.
Scale preset: getRowId + valueCache + rowBufferPx
Last commit: -
2. 권장 튜닝 순서
- `getRowId`로 row identity를 먼저 고정합니다.
- `valueCache`를 켜서 interaction-only update의 `getValue` 재계산을 줄입니다.
- sort/reorder가 잦다면 `valueCacheStrategy="row-id"`를 기본으로 둡니다.
- dynamic row height에서는 `rowBufferPx`로 render buffer를 잡습니다.
- `maxRenderedRows`로 예외적인 render window 확장을 제한합니다.
- `onVisibleRegionChanged`는 외부 로딩/동기화가 필요할 때만 연결합니다.
3. 각 설정이 제어하는 비용
getRowId
row reorder와 sort 이후에도 key churn, cache churn, subtree churn을 줄입니다.
valueCache
active move, selection, search refine 같은 interaction 경로에서 반복 `getValue` 비용을 줄입니다.
rowBufferPx
dynamic row height에서 viewport buffer를 pixel 단위로 안정적으로 제어합니다.
maxRenderedRows
예외적인 scroll/window 계산이 발생해도 렌더 범위가 과도하게 커지는 것을 막습니다.
rendererRefreshMode
mount 비용이 큰 셀에서 remount 대신 renderer reuse 정책을 선택할 수 있습니다.
4. 권장 시작 설정
큰 데이터셋에서는 먼저 stable row identity와 cached value access를 고정하는 편이 좋습니다. 기본 시작점은 `rowBufferPx` 중심의 pixel buffer 조합으로 맞춥니다.
import { DataGrid } from "@reopt-ai/opt-datagrid";
<DataGrid
rows={rows}
columns={columns}
getRowId={(row) => String(row.id)}
valueCache
valueCacheStrategy="row-id"
rowBufferPx={480}
maxRenderedRows={600}
height={440}
ariaLabel="Scale grid"
/>;5. 뷰포트 기반 지연 로딩
`useDataGridRemoteDataSource`는 viewport window fetch와 batch save를 하나의 headless 계약으로 묶어줍니다. 이때도 `getRowId`, `valueCache`, `rowBufferPx`를 같이 두면 remote sync 비용과 render cost를 분리해서 다루기 쉽습니다. 백엔드 payload shape과 이벤트 계약은 원격 연동 계약 문서에 정리돼 있습니다.
import * as React from "react";
import {
DataGrid,
type DataGridColumn,
useDataGridRemoteDataSource,
} from "@reopt-ai/opt-datagrid";
interface Row {
id: number;
name: string;
}
const columns: DataGridColumn<Row>[] = [
{
id: "id",
title: "ID",
width: 100,
align: "right",
getValue: (row) => String(row.id),
},
{ id: "name", title: "Name", width: 220, getValue: (row) => row.name },
];
function PaginatedScaleGrid() {
const remote = useDataGridRemoteDataSource<Row>({
rowCount: 100000,
pageSize: 200,
preloadPages: 1,
getVisibleColumnIds: (region) =>
columns.slice(region.startCol, region.endCol + 1).map((column) => column.id),
openView: async ({ signal }) => {
const response = await fetch("/api/grid/views/open", {
method: "POST",
signal,
});
const payload = await response.json();
return {
viewId: payload.viewId,
rowCount: payload.totalRowCount,
snapshotVersion: payload.snapshotVersion,
};
},
subscribeToInvalidations: ({ viewId, onInvalidate }) => {
const events = new EventSource(`/api/grid/views/${viewId}/events`);
events.addEventListener("invalidate", (event) => {
const payload = JSON.parse((event as MessageEvent<string>).data);
onInvalidate({
rows: payload.rows,
rowCount: payload.totalRowCount,
snapshotVersion: payload.snapshotVersion,
movedRowIds: payload.movedRowIds,
invalidateRanges: payload.invalidateRanges,
});
});
return () => events.close();
},
makePlaceholderRow: (rowIndex) => ({
id: rowIndex + 1,
name: "Loading...",
}),
loadRows: async ({ start, end, viewId, visibleColumnIds, signal }) => {
const params = new URLSearchParams({
start: String(start),
end: String(end),
});
if (visibleColumnIds?.length) {
params.set("columns", visibleColumnIds.join(","));
}
const response = await fetch(
`/api/grid/views/${viewId}/window?${params.toString()}`,
{ signal },
);
const payload = await response.json();
return {
rows: payload.rows,
rowCount: payload.totalRowCount,
snapshotVersion: payload.snapshotVersion,
};
},
saveEdits: async ({ edits, viewId, snapshotVersion, signal }) => {
const response = await fetch(`/api/grid/views/${viewId}/edits`, {
method: "POST",
signal,
body: JSON.stringify({ edits, snapshotVersion }),
});
const payload = await response.json();
return {
rows: payload.rows,
snapshotVersion: payload.snapshotVersion,
movedRowIds: payload.movedRowIds,
invalidateRanges: payload.invalidateRanges,
rejectedEdits: payload.rejectedEdits,
};
},
revalidateAfterSave: "affected-pages",
});
React.useEffect(() => {
if (remote.rejectedEdits.length > 0) {
console.warn(remote.rejectedEdits);
}
}, [remote.rejectedEdits]);
return (
<DataGrid
rows={remote.rows}
columns={columns}
height={460}
rowHeight={34}
getRowId={(row) => String(row.id)}
valueCache
valueCacheStrategy="row-id"
rowBufferPx={320}
maxRenderedRows={500}
onVisibleRegionChanged={remote.onVisibleRegionChanged}
onCellsEdited={remote.onCellsEdited}
/>
);
}6. 측정 기반 검증
성능은 추측보다 baseline과 diff로 보는 편이 정확합니다. 아래 명령으로 현재 상태를 저장하고, 이후 변경과 compare/history로 회귀를 추적할 수 있습니다.
bun run --filter @reopt-ai/opt-datagrid benchmark
bun run --filter @reopt-ai/opt-datagrid benchmark:save-baseline
bun run --filter @reopt-ai/opt-datagrid benchmark:compare
bun run --filter @reopt-ai/opt-datagrid benchmark:history- `duration`: 시나리오 전체 wall-clock 비용
- `getValue`: 값 계산 hot path 압력
- `renderCell`: rerender fan-out을 간접적으로 보는 지표
7. 전체 코드 예제
import * as React from "react";
import { DataGrid, type DataGridColumn } from "@reopt-ai/opt-datagrid";
interface EventRow {
id: number;
name: string;
role: string;
region: string;
}
const columns: DataGridColumn<EventRow>[] = [
{ id: "id", title: "ID", width: 100, align: "right", editable: false, getValue: (row) => String(row.id) },
{ id: "name", title: "Name", width: 220, editable: true, getValue: (row) => row.name, setValue: (row, value) => ({ ...row, name: value }) },
{ id: "role", title: "Role", width: 140, editable: true, getValue: (row) => row.role, setValue: (row, value) => ({ ...row, role: value }) },
{ id: "region", title: "Region", width: 140, editable: true, getValue: (row) => row.region, setValue: (row, value) => ({ ...row, region: value }) },
];
export function ScaleGridExample() {
const rows = React.useMemo(
() =>
Array.from({ length: 5000 }, (_, index) => ({
id: index + 1,
name: `Member-${index + 1}`,
role: index % 3 === 0 ? "Owner" : index % 3 === 1 ? "Editor" : "Viewer",
region: ["Seoul", "Tokyo", "Singapore", "Berlin"][index % 4]!,
})),
[],
);
return (
<DataGrid
rows={rows}
columns={columns}
getRowId={(row) => String(row.id)}
valueCache
valueCacheStrategy="row-id"
rowBufferPx={480}
maxRenderedRows={600}
height={440}
ariaLabel="Scale grid"
/>
);
}