검색 결과를 표시하는 테이블이 5초 이상 느리게 렌더링되는 문제가 발생하였습니다.
측정 결과를 보고자 하는 날짜를 검색하여 측정 결과를 테이블로 표시하는 페이지입니다.
약 1,000개의 데이터를 표시해야 하고 요소마다 색상 표시, 버튼, 시간 계산 등을 표시해야 합니다.
네트워크 탭에서 확인해 보니 네트워크 상태가 좋지 않은 경우 최초 요청은 약 4초가 발생하고, 이후 요청에는 약 1~2초의 시간이 발생했습니다. 하지만, 서버 측에서는 현재 상황에서 페이징이 힘들기 때문에 클라이언트에서 최적화해야 하는 상황입니다.
💡 개발자 도구의 성능 탭에서 네트워크 지연 시간, 자바스크립트 처리 소요 시간 등 다양한 측정 결과를 확인할 수 있습니다.
캐시 사용 중지에 체크하고 날짜를 검색하여 데이터가 테이블에 표시되는 데까지 성능 탭에서 측정한 결과입니다.

어디선가 "React에서 DOM 1,000개 정도는 가볍게 렌더링할 수 있다"고 들었던 기억이 있지만, 표시해야 하는 데이터가 많아서 그런지 화면에 리스트를 렌더링하는 데에 약 2,000ms 이상의 시간이 사용되는 것을 성능 탭에서 확인할 수 있었습니다.
✨ 이 글에서는 이러한 문제를 해결하기 위해 React 가상화 기법을 적용하여 해결하는 방법을 공유하려 합니다.
가상화(virtual)는 실제로 화면에 보이는 요소만 렌더링 하고 보이지 않는 요소는 렌더링하지 않는 기법입니다. 예를 들어 10,000개의 항목이 있는 리스트가 있다고 가정할 때, 사용자가 한 번에 볼 수 있는 항목은 10~20개 정도뿐입니다.
가상화는 이런 상황에서 실제로 보이는 항목만 DOM에 렌더링하고, 나머지는 '가상화'하여 메모리와 성능을 최적화합니다.
가상화를 통해 얻을 수 있는 이점은 다음과 같습니다.
DOM 요소 수 감소: 화면에 보이는 항목만 생성하므로 메모리 사용량이 크게 줄어듭니다.
렌더링 성능 향상: 실제로 보이는 요소만 렌더링하기 때문에 초기 로딩 시간이 크게 줄어듭니다.
이 글에서는 Tanstack Virtual을 통해 최적화했습니다.
Tanstack Virtual은 긴 요소 목록을 가상화하는 헤드리스 UI 유틸리티입니다. 컴포넌트가 아니므로 마크업이나 스타일을 자동으로 제공하거나 렌더링하지 않습니다. 스타일, 디자인 및 구현에 대한 100% 제어권이 사용자에게 있습니다.
Tanstack Virtual Introduction.
이처럼 Tanstack Virtual은 가상화 기능만 제공하고 스타일과 구현은 사용자에게 제어권이 있기 때문에 MUI를 사용하는 현재 상황에 잘 맞는 패키지라고 생각하여 선택했습니다.
검색 결과를 테이블에 표시하는 컴포넌트를 간략하게 작성한 코드입니다.
export default function App({ positionList }) {
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>...</TableCell>
...
</TableRow>
</TableHead>
<TableBody>
{positionList.map((row) => (
<TableRow key={row.id}>
<TableCell>...</TableCell>
...
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
흔하게 사용되는 패턴으로 여기에 Tanstack Virtual을 통해 가상화를 적용해 보겠습니다.
import { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
export default function App({ positionList }) {
const parentRef = useRef(null)
const rowVirtualizer = useVirtualizer({
count: positionList.length, // 가상화할 항목 총 개수
getScrollElement: () => parentRef.current, // 스크롤이 발생하는 부모 요소
estimateSize: () => 73, // 각 항목의 대략적인 높이
overscan: 3, // 보이는 항목 외에 추가로 렌더링할 항목 수
})
return (
<TableContainer sx={{ height: 500 }} ref={parentRef}>
<Table sx={{ tableLayout: 'fixed' }}>
<TableHead>
<TableRow>
<TableCell>...</TableCell>
...
</TableRow>
</TableHead>
<TableBody
style={{
position: 'relative',
height: `${rowVirtualizer.getTotalSize()}px`, // 리스트 전체 높이
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
const row = positionList[virtualItem.index]
return (
<TableRow key={row.id}>
<TableCell>...</TableCell>
...
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
)
}
Tanstack Vritual 공식 문서에 친절한 설명을 통해 쉽게 적용할 수 있었습니다.
가상 스크롤이 적용됐는지 요소 탭에서 간단하게 확인할 수 있습니다.

가상화 기법을 통해 가상 스크롤 적용 후 개선된 성능과 메모리를 비교해 보겠습니다.

가상 스크롤 적용 후 성능 측정 결과로 네트워크 요청 이후 화면에 DOM을 렌더링하는 데까지 약 50ms로 측정되는 것을 확인할 수 있습니다.
2,000ms에서 50ms로 단축되면서 렌더링 시간이 약 40배가량 빨라졌다고 볼 수 있습니다.
이제 메모리를 비교해 보겠습니다.
💡 성능 탭과 마찬가지로 메모리 탭에서 힙 스냅샷 찍기로 할당된 메모리를 측정할 수 있습니다.

최적화 이전에는 힙 메모리가 전체가 약 190MB로 JS 객체와 기타 DOM 요소에 많은 메모리가 할당된 것을 확인할 수 있습니다.

최적화 이후에 힙 메모리는 전체가 약 100MB로 이전과 달리 90MB가 줄어들었습니다. 이전과 비교했을 때 JS 객체와 DOM 요소에 할당되었던 부분이 상당히 줄어든 것을 확인할 수 있습니다.
이렇게 React 가상화 기법을 통해 가상 스크롤을 구현하여 렌더링 성능과 메모리를 개선할 수 있었습니다.
문제 상황
약 1,000개 데이터를 테이블로 표시하는 페이지에서 5초 이상 렌더링 지연 발생
네트워크 요청은 1~2초 소요되지만, DOM 렌더링에 약 2,000ms 추가 소요
서버 측 페이징이 어려워 클라이언트에서 최적화 필요
해결 방법
Tanstack Virtual 라이브러리 사용
실제 화면에 보이는 요소만 렌더링하고 나머지는 가상화
성능 개선 결과
렌더링 시간: 2,000ms -> 50ms (약 40배 성능 향상)
메모리 사용량: 190MB -> 100MB (약 90MB 절약)