조용진의 개발 블로그
← 글 목록

느려진 React 테이블을 가볍게: 1만 건 검증과 프로덕션 적용

React가상화성능 최적화TanStack Virtual

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.5ms16.3ms
스크롤 프레임약 30fps약 60fps

Commit 시간은 기록상 약 53% 감소했다. 이 수치는 테스트 데이터와 당시 장비에서 얻은 측정값이며 셀 복잡도, 브라우저, 화면 높이에 따라 달라질 수 있다. 공용 테이블에는 가상화 경로를 추가하고, 데이터가 많은 운영 목록에서 선택적으로 활성화했다.

대용량 목록 최적화에서 가장 먼저 볼 것은 메모이제이션 개수가 아니다. 사용자에게 보이지 않는 행까지 DOM에 만들고 있는지를 확인하는 것이 더 직접적인 출발점이었다.