티스토리 뷰
회사 내부 모니터링 대시보드에서 수천 건 이상의 카드(Card) 컴포넌트를 한 화면에 표시해야 하는 요구사항이 있었습니다.
처음에는 단순히 Array.map을 통해 모든 데이터를 렌더링했으나, 아래와 같은 문제가 발생했습니다.
- 렌더링 지연 : 한 번에 모든 컴포넌트를 그리다 보니, 브라우저 메인 스레드가 과부하되어 화면이 뜨기까지 20~30초 걸림
- 스크롤 성능 저하 : DOM 요소가 늘어나면서 스크롤 시 FPS가 크게 떨어지는 문제 발생
- 사용자 경험(UX) 악화 : API는 이미 빠르게 응답했는데, 렌더링이 끝나지 않아 로딩 스피너가 바로 사라지고도 빈 화면이 한참 동안 노출
문제를 해결하기 위한 방법으로 리스트 가상화를 진행해보기로 했습니다.
리스트 가상화란 ?
화면에 보이는 아이템만 실제로 렌더링하고, 나머지는 미리 렌더링하지 않는 기법
ex) 데이터가 1000개 있을 때, 화면에 보이는 20개만 실제 DOM에 그리고 나머지는 가상으로 관리하다가 스크롤 위치에 따라 교체한다.
- 초기 렌더링 비용 절감 : 렌더링해야 할 요소 개수를 화면에 보이는 범위로 제한하며 초기 렌더링 시간을 줄여준다.
- 메모리 사용량 감소 : DOM 노드가 많아질수록 브라우저 메모리 소비가 커지는데, 실제 DOM에 존재하는 노드를 최소화하여 메모리 사용량을 줄인다.
- 스크롤 성능 개선 : 수천 개의 DOM 요소가 이미 렌더링 되었을 때 스크롤을 하면, 브라우저는 모든 요소의 위치를 다시 계산해야 하므로 FPS가 떨어집니다. 가상화로 현재 스크롤 영역에 있는 DOM만 유지하므로 스크롤이 부드러워진다.
대표 라이브러리
- react-window
- 가볍고 사용이 간단하며, 고정 크기(FixedSizeList) 또는 가변 크기(VariableSizeList) 리스트, 그리드 등을 지원합니다.
- 많은 기능이 필요 없는 “단순 리스트”에 가장 알맞습니다. - react-virtualized
기능이 풍부하고 다양한 레이아웃(그리드, 테이블, 정렬된 리스트, 무한 스크롤 등)을 지원하지만, 설정이 복잡할 수 있습니다.
대규모 프로젝트나 여러 가지 가상화 시나리오가 필요한 경우에 적합합니다. - react-virtuoso
동적 높이, 반응형 그리드, 무한 스크롤, 위로 스크롤시 로딩 등을 편리하게 지원합니다.
내부에서 사이즈 측정을 자동으로 처리해 주어, 특별한 설정 없이도 동적인 아이템 높이를 잘 다룰 수 있습니다.
저는 대시보드의 그리드 형태를 유지하고, 동적 높이 및 반응형 레이아웃 처리가 편리하다는 react-virtuoso를 선택했습니다.
VirtuosoGrid 사용 주의점 & 팁
- 부모 컨테이너 높이 보장
가상화 라이브러리가 “현재 뷰포트 크기가 얼마인지” 알아야 아이템 수를 계산할 수 있습니다.
상위 요소가 height: 100%를 가지도록 CSS 설정, 또는 height: 600px처럼 구체적인 픽셀 값을 부여해야 합니다. - 아이템 높이가 일정하면 더 쉽다
아이템 높이가 고정이면, 스크롤 위치에 따른 “몇 번째 아이템을 보여줄지” 계산이 간단해 성능이 좋습니다
동적 높이도 가상화할 수 있지만, 라이브러리 내부에서 별도의 측정 단계가 필요합니다. - 불필요한 리렌더링 방지
아이템 렌더 함수가 매번 새로 정의되면, VirtualizedList도 매번 재계산을 유발할 수 있습니다.
React.memo, useCallback, useMemo를 적절히 활용해 최적화해야합니다. - 스크롤 이벤트 활용 (Infinite Scrolling, Prev Data Loading 등)
예: startReached, endReached 같은 콜백으로 상단/하단 도달 시 추가 로딩(무한 스크롤) 구현 가능.
필요하다면 로딩 스피너를 Header나 Footer 컴포넌트로 표시할 수 있습니다.
적용하기 ( Mui + VirtuosoGrid )
import React from 'react';
import { VirtuosoGrid } from 'react-virtuoso';
import Box from '@mui/material/Box';
import Grid2 from '@mui/material/Unstable_Grid2';
import CircularProgress from '@mui/material/CircularProgress';
import ProcessCard from './ProcessCard';
const CardListContainer({ data, isPending, openInfoModal }) {
// 로딩 상태 표시
if (isPending) {
return <CircularProgress />;
}
return (
<Boxsx={{
height: '100%', // 상위 컨테이너도 100% 높이를 갖도록 설정 필요
width: '100%',
}}
>
<VirtuosoGrid
totalCount={data.length}
itemContent={(index) => {
const item = data[index];
return (
<Grid2 item xs={12} sm={6} md={4} lg={3}>
<ProcessCard data={item} openInfoModal={openInfoModal} />
</Grid2>
);
}}
components={{
List: React.forwardRef(({ style, children }, listRef) => (
<Grid2container
spacing={2}
ref={listRef}
style={style}
>
{children}
</Grid2>
)),
Item: ({ children, ...props }) => (
<div {...props}>{children}</div>
),
}}
style={{ height: '100%' }} // 내부에서 스크롤
/>
</Box>
);
}
Virtuoso : 주로 세로(Vertical) 목록에 최적화되어 있습니다. 아이템을 한 줄씩 리스트 형태로 렌더링할 때 주로 사용합니다.
VirtuosoGrid : 그리드(2차원) 형태로 여러 열(columns)을 구성하는 경우에 사용합니다. 예를 들어, MUI의 Grid처럼 카드 레이아웃을 여러 열로 배치해야 할 때 적합합니다.
- totalCount : 렌더링 할 아이템의 총 개수
- itemContnet : 반복되는 아이템을 어떻게 렌더링할지 정의하는 콜백 함수
- 인자로 index가 넘어오며, data[index]를 통해 실제 반복 할 아이템을 가져와 JSX로 반환한다. - components : VirtuosoGrid가 내부적으로 사용하는 컴포넌트를 커스터마이징한다.
- List : 전체 그리드를 감싸는 컴포넌트
- Item : 각 아이템을 감싸는 래퍼 컴포넌트
- style : VirtuosoGrid의 기본 컨테이너에 적용할 스타일
- height 100%로 설정해서 부모와 동일한 높이를 차지하게 지정해야 내부 스크롤이 제대로 동작하고 가상화 계산이 정확히 된다.
components 옵션 추가 설명
1. List에서 forwardRef 사용 ?
VirtuosoGrid는 내부에서 리스트 컨테이너의 크기(높이/너비)나 스크롤 위치를 직접 측정해야한다.
이를 위해 가상화 라이브러리가 DOM노드의 ref를 필요로한다.
List는 전체 그리드 래퍼이므로, 라이브러리가 이 래퍼의 참조(ref)를 가져다가 스크롤 계산, 높이 측정을 한다.
👉 Virtuoso는 리스트 컨테이너의 DOM노드에 접근하여 스크롤 계산과 높이 측정을 해야하므로, forwardRef를 사용해 Grid2 컴포넌트 내부의 실제 DOM 노드에 Virtuoso가 전달해준 ref를 연결(attach)해줍니다.
2. Item에서 props ?
VirtuosoGrid에서 한 개의 itemContent가 렌더될 때 그 내용을 item 컴포넌트로 감쌉니다.
따라서 각 아이템의 위치,스타일 등 가상화 라이브러리가 필요한 속성들이 props로 넘어올 수 있습니다.
즉, 라이브러리가 Item 컴포넌트에게 넘기는 다양한 속성(e.g. style, data-속성, onClick 등)을 그대로 <div>에 전파하기 위해 ...props를 사용합니다. Virtuoso가 아이템 배치를 위해 style, position, transform 등 여러 속성을 동적으로 부여할 수 있는데, ...props가 없다면 그것들이 DOM에 적용되지 않을 수 있습니다.
👉 Virtuoso가 부여한 속성이 DOM에 그대로 반영되어 정확한 위치/크기로 아이템이 배치
성능 측정 & 비교
1. 가상화 전 ( Array.map )
- JS 실행이 길다 (노란색 블록이 많음)
- 수천 개의 Card 컴포넌트를 한 번에 렌더링하기 때문에 JS 실행이 오래 걸려서 렌더링을 블로킹함
- React가 모든 컴포넌트를 한꺼번에 렌더링하려고 하면서 CPU 점유율이 높아지고, UI 반응 속도가 떨어짐
- 레이아웃 & 스타일 계산 (보라색 블록이 많음)
- 보라색이 많이 보이는 이유는 많은 DOM 요소를 브라우저가 한꺼번에 계산해야 하기 때문
- 많은 카드 컴포넌트를 배치하면서 레이아웃이 무거워지고, 스타일 계산이 반복됨
- 메인 스레드 과부하
- 메인 스레드가 JS 연산과 DOM 업데이트로 인해 꽉 차 있음.
- 사용자 입력(스크롤, 클릭) 처리가 늦어지고 스크롤 성능이 저하되며 FPS가 크게 떨어짐
2. 가상화 후
- JS 실행 시간이 급격히 줄어듦 (노란색 거의 없음)
- 불필요한 모든 카드 렌더링을 제거하고, 화면에 보이는 것만 렌더링하도록 최적화됨
- React가 Virtualized List를 활용하여, 화면에 필요한 컴포넌트만 렌더링하면서 실행 시간이 급감함
- 레이아웃 & 스타일 계산 감소 (보라색 줄어듦)
- DOM 업데이트 수가 줄었기 때문에, 브라우저의 스타일 재계산(Layout Recalculation) 부담이 줄어듦
- CSS 적용, Flex/Grid 등의 배치 연산이 줄면서 렌더링이 최적화되면서 FPS 50-60 유지
- 메인 스레드 여유 증가 → 부드러운 UI 가능
- 메인 스레드가 가벼워지고, 스크롤이 부드러워짐
- 이전에는 수천 개의 카드를 한꺼번에 계산했지만, 이제는 보이는 것만 처리해서 렌더링 지연이 사라짐
- 로딩 속도 급격히 개선 (총 시간 31.2초 → 2.33초)
- 첫 번째 캡처에서 JS 실행과 렌더링이 오래 걸렸던 것이 가상화 후 사라짐
- API 응답은 동일하지만, 렌더링 최적화 덕분에 사용자 경험(UX) 개선
가상화의 효과
대량의 데이터를 한 화면에서 효율적으로 보여주기 위해, 단순히 map으로 모든 컴포넌트를 렌더링하기보다는 가상화를 도입하여 필요한 부분만 그리는 방식이 필수적임을 깨달았습니다.
특히 react-virtuoso는 그리드 형태의 반응형 레이아웃을 적용하기 쉬우며, 내부적으로 사이즈 측정을 자동 처리해주어 동적 높이에도 유연하게 대응할 수 있었습니다.
결과적으로,
- JS 실행 시간 감소 (노란색 줄어듦) → 불필요한 렌더링 제거
- 스타일 계산 & 레이아웃 부담 감소 (보라색 줄어듦) → DOM 업데이트 최적화
- 메인 스레드 여유 증가 → 스크롤 & UI 성능 개선
- 전체 로딩 시간 31.2초 → 2.33초 → UX 대폭 향상
'FrontEnd > React' 카테고리의 다른 글
[React] lazy()와 Suspense를 활용한 동적 로딩 및 로딩 페이지 구현 (0) | 2025.02.04 |
---|---|
[vite-plugin-svgr] Vite와 React에서 SVG 파일을 자동으로 Import하고 사용하는 방법 (0) | 2025.01.13 |
[Zustand] Immer,Actions분리로 데이터그리드 상태 관리 리팩토링하기 (0) | 2024.12.24 |
[React-hook-form] 사용법 / Controller의 활용과 공통 컴포넌트 재사용하기 (0) | 2024.12.20 |
[React] 폼의 입력 필드 리렌더링 최적화하기 : React.memo 활용 (+useMemo 비교) (1) | 2024.11.28 |
- Total
- Today
- Yesterday
- piechart
- web
- frontend
- Vscode단축키
- package
- 깊은복사
- Legend
- Figma 기초
- 객체
- Figma Style
- javascript
- figma
- npm install
- BarChart
- chartjs
- x축스크롤
- echarts
- 객체복사
- 프론트엔드
- SASS
- VUE
- vscode
- package-lock
- 얕은복사
- SCSS
- Location
- Figma 버튼
- 환경설정
- Chart
- npm
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |