느려진 React 테이블을 가볍게: 1만 건 검증과 프로덕션 적용
1만 건 데이터에서 React 테이블의 전체 DOM 렌더링 병목을 확인하고 가상화를 적용해 프로덕션에 반영한 과정을 정리합니다.
데이터가 많은 운영 화면에서 테이블 스크롤이 끊기기 시작했다. 당시 측정 기록에는 약 1,000건 이상에서 초기 표시가 1초를 넘고 스크롤이 약 30fps까지 떨어지는 구간이 남아 있었다. React Profiler의 한 차례 비교에서는 Table Commit 시간이 34.5ms였다. 60fps 목표의 한 프레임 예산인 16.6ms를 이미 넘긴 상태였다.
병목은 데이터 배열보다 DOM에 만들어진 행 수였다
기존 공용 테이블은 현재 페이지의 데이터를 map으로 모두 렌더링했다. 화면 밖의 행도 React 엘리먼트와 DOM 노드로 만들어졌기 때문에 데이터가 늘면 비교·레이아웃·페인트 대상도 함께 늘었다.
개선 방향은 모든 행을 조금 더 빠르게 그리는 것이 아니라 실제로 그리는 행 수를 제한하는 것이었다. 공용 테이블에 enableVirtualRows, estimatedRowHeight, overscan을 선택 속성으로 추가해 기존 화면은 그대로 유지하고 대량 목록에서만 가상화를 켤 수 있게 했다.
공용 테이블에 가상화를 선택적으로 적용했다
실제 구현의 핵심은 다음과 같다.
아래 코드 예시는 설명을 위해 식별자와 운영 설정값을 일반화했습니다.
const virtualizer = useVirtualizer({
count: enableVirtualRows ? visibleRows.length : 0,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () => estimatedRowHeight,
overscan,
enabled: enableVirtualRows,
});
기본 행 높이는 40px, 기본 overscan은 5개다. enableVirtualRows가 꺼져 있으면 기존 렌더링 경로를 사용하고, 켜져 있으면 getVirtualItems()가 돌려준 행만 만든다. 총 스크롤 높이는 getTotalSize()로 유지하고 각 행은 translateY(visibleRow.start)로 원래 위치에 배치했다.
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((visibleRow) => (
<div
key={visibleRow.key}
ref={virtualizer.measureElement}
style={{
position: "absolute",
width: "100%",
transform: `translateY(${visibleRow.start}px)`,
}}
>
{renderItem(visibleRows[visibleRow.index])}
</div>
))}
</div>
가상화는 메시지 발송 대상과 결과처럼 행이 많아질 수 있는 목록에서 명시적으로 활성화했다. 공용 컴포넌트 전체의 동작을 한 번에 바꾸지 않고 필요한 화면부터 적용해 정렬·선택·조건부 스타일 같은 기존 기능의 회귀 범위를 줄였다.
CSS의 content-visibility만으로 충분하지 않았던 이유
content-visibility: auto는 화면 밖 요소의 레이아웃과 페인트를 미룰 수 있어 유용하다. 하지만 그리지 않는 것과 DOM에 만들지 않는 것은 다르다. CSS 방식은 데이터 수만큼 생성된 노드와 React의 비교 비용을 그대로 남긴다.
수천 개 행과 복잡한 셀이 있는 화면에서는 저사양 장비의 메모리 부담까지 고려해야 했다. 가상화는 뷰포트와 overscan 범위의 행만 DOM에 유지하므로 데이터 개수와 DOM 노드 수의 직접적인 비례를 끊을 수 있다. 반대로 브라우저 페이지 검색, 접근성 탐색, 동적 행 높이가 더 중요한 작은 목록이라면 CSS 방식이나 일반 렌더링이 더 단순할 수 있다.
1만 건 검증 후 운영 화면에 반영했다
10,000건 테스트 데이터로 같은 프로파일링 조건을 비교한 당시 기록은 다음과 같다.
| 지표 | 적용 전 | 가상화 적용 후 |
|---|---|---|
| React Commit 시간 | 34.5ms | 16.3ms |
| 스크롤 프레임 | 약 30fps | 약 60fps |
Commit 시간은 기록상 약 53% 감소했다. 이 수치는 테스트 데이터와 당시 장비에서 얻은 측정값이며 셀 복잡도, 브라우저, 화면 높이에 따라 달라질 수 있다. 공용 테이블에는 가상화 경로를 추가하고, 데이터가 많은 운영 목록에서 선택적으로 활성화했다.
대용량 목록 최적화에서 가장 먼저 볼 것은 메모이제이션 개수가 아니다. 사용자에게 보이지 않는 행까지 DOM에 만들고 있는지를 확인하는 것이 더 직접적인 출발점이었다.