가변 행 높이 (Variable Row Height)
getRowHeight로 데이터에 따라 행 높이를 지정하면서 가상 스크롤 성능을 유지하는 방법을 알아봅니다.
LIVE DEMO가변 행 높이 (Variable Row Height) 인터랙티브 예제
인터랙티브 데모를 불러오는 중입니다…
LIVE DEMO가변 행 높이 (Variable Row Height) 인터랙티브 예제
인터랙티브 데모를 불러오는 중입니다…
import * as React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import { t } from './i18n';
type ActivityKind = 'milestone' | 'update' | 'note';
interface ProjectActivity {
id: number;
kind: ActivityKind;
title: string;
owner: string;
status: string;
rowHeight: number;
}
const kindLabels: Record<ActivityKind, string> = {
milestone: t('마일스톤', 'Milestone'),
update: t('업데이트', 'Update'),
note: t('메모', 'Note'),
};
const activities: BGridDataItem<ProjectActivity>[] = Array.from({ length: 200 }, (_, index) => {
const kind: ActivityKind = index % 12 === 0 ? 'milestone' : index % 4 === 0 ? 'update' : 'note';
const rowHeight = kind === 'milestone' ? 52 : kind === 'update' ? 40 : 29;
return {
values: {
id: index + 1,
kind,
title:
kind === 'milestone'
? t(`스프린트 ${Math.floor(index / 12) + 1} 완료`, `Sprint ${Math.floor(index / 12) + 1} completed`)
: t(`프로젝트 활동 ${index + 1}`, `Project activity ${index + 1}`),
owner: ['Mina', 'Daniel', 'Sora', 'Alex'][index % 4],
status:
kind === 'milestone'
? t('완료', 'Done')
: index % 3 === 0
? t('검토 중', 'In review')
: t('진행 중', 'In progress'),
rowHeight,
},
};
});
const columns: BGridColumn<ProjectActivity>[] = [
{ id: 'id', key: 'id', label: '#', width: 70, align: 'center' },
{
id: 'kind',
key: 'kind',
label: t('유형', 'Type'),
width: 110,
itemRender: ({ values }) => <strong>{kindLabels[values.kind]}</strong>,
},
{ id: 'title', key: 'title', label: t('활동', 'Activity'), width: 260 },
{ id: 'owner', key: 'owner', label: t('담당자', 'Owner'), width: 100 },
{ id: 'status', key: 'status', label: t('상태', 'Status'), width: 110 },
{
id: 'rowHeight',
key: 'rowHeight',
label: t('행 높이', 'Row height'),
width: 100,
align: 'right',
itemRender: ({ value }) => `${value}px`,
},
];
export default function VariableRowHeightExample() {
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const getRowHeight = React.useCallback((row: ProjectActivity) => row.rowHeight, []);
return (
<DataGridContainer ref={containerRef} className='variable-row-height-example'>
<BGrid<ProjectActivity>
width={width}
height={height}
columns={columns}
data={activities}
rowKey='id'
showLineNumber
frozenRowCount={1}
getRowHeight={getRowHeight}
/>
</DataGridContainer>
);
}import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* BGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .bgrid-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}데이터에 따라 행 높이 지정하기
getRowHeight(row, index)는 현재 표시 순서의 행 데이터와 인덱스를 받아 전체 행 높이를 픽셀로 반환합니다. 위 예제에서는 마일스톤은 52px, 업데이트는 40px, 일반 메모는 29px로 표시합니다.
const getRowHeight = React.useCallback((row: ProjectActivity) => {
if (row.kind === 'milestone') return 52;
if (row.kind === 'update') return 40;
return 29;
}, []);
<BGrid
data={activities}
columns={columns}
getRowHeight={getRowHeight}
/>
콜백은 BGridDataItem 래퍼가 아니라 item.values를 받습니다. 정렬이나 필터링이 적용되면 index는 변경된 표시 순서를 기준으로 합니다.
성능 특성
BeautifulGrid는 표시 데이터나 getRowHeight 참조가 바뀔 때 높이와 누적 오프셋을 한 번 계산합니다. 스크롤 중에는 캐시된 값을 사용하고 이진 탐색으로 시작 행을 찾으므로 콜백이 반복 실행되지 않습니다. 불필요한 캐시 재생성을 피하려면 예제처럼 useCallback으로 함수 참조를 안정적으로 유지하세요.
콘텐츠를 DOM에서 측정하는 자동 높이는 지원하지 않습니다. getRowHeight를 생략하거나 0, 음수, NaN, Infinity를 반환하면 itemHeight + itemPadding * 2가 fallback으로 사용됩니다.