React 무한스크롤 성능 개선: 가상화로 렌더링 속도 2배 향상

혜연·2025년 6월 10일
0

Next.js

목록 보기
19/20

1️⃣ 문제 인식

1차 개발 당시, 스터디 카드 리스트를 무한 스크롤 방식으로 먼저 구현했다. 하지만 실제 배로 후, 초기 진입 속도나 리스트 로딩 속도가 느리다고 체감했다.
그래서 useMemo, useCallback을 부모 컴포넌트인 StudyList와 자식 컴포넌트인 StudyCard에 골고루 적용해봤다.

그런데.. 메모제이션이 전혀 적용되지 않았다.

무한스크롤이 발생하거나, 필터링 조건이 바뀌거나, 새로운 스터디가 등록될 때마다 자식 컴포넌트까지 전부 재렌더링되면서, useMemo로 감싼 로직(today, 모집 마감 여부 등)도 매번 다시 실행되었다.

  • 카드 수가 적을 때는 눈에 띄지 않지만,
  • 카드 수가 많아지면 점점 느려질 수밖에 없는 구조다.

2️⃣ 성능 측정: 무한 스크롤 기준

실제 카드 수를 500개 이상으로 늘려주고, Chrome Devtools Profiler로 렌더링 속도를 측정했다.

  • 무한스크롤 시, 카드 하나 렌더링에 약 2.4ms ~ 4.1ms 소요됨
  • 누적되는 DOM 구조로 인해, 스크롤이 길어질수록 성능 저하

3️⃣ 개념 정리: 가상화 vs 무한스크롤

항목무한스크롤가상화
방식스크롤 시 새로운 데이터 추가 로딩화면에 보이는 것만 DOM에 렌더링
DOM 수계속 누적(계속 누적 (10 → 20 → … 1000)항상 일정 (뷰포트 기준)
렌더링 비용데이터 많을수록 ↑데이터 수와 무관

4️⃣ react-window 도입

가상화를 위해 react-window을 사용했다.
TypeScript 사용 시 타입 설치도 필요하다:

npm install react-window
npm install @types/react-window

Grid 컴포넌트 적용 예시

          <Grid
            columnCount={columnCount}
            columnWidth={cardWidth + 18}
            height={700} // 뷰포트 높이(px), 조정 가능
            rowCount={Math.ceil(studies.length / columnCount)}
            rowHeight={cardHeight + 24}
            width={gridWidth}
            onItemsRendered={({ visibleRowStopIndex }) => {
              const lastVisibleIndex = (visibleRowStopIndex + 1) * columnCount;
              if (
                hasMore &&
                !loading &&
                lastVisibleIndex >= studies.length - columnCount
              ) {
                fetchStudies();
              }
            }}
          >
            {({ columnIndex, rowIndex, style }) => {
              const idx = rowIndex * columnCount + columnIndex;
              if (idx >= studies.length) return null;
              return (
                <div
                  style={{
                    ...style,
                    left: (style.left as number) + 12 / 2, // 양 옆 gap/2씩
                    top: (style.top as number) + 12 / 2,
                    width: cardWidth,
                    height: cardHeight,
                  }}
                >
                  <StudyCard key={studies[idx].id} study={studies[idx]} />
                </div>
              );
            }}
          </Grid>
  • height와 width: 뷰포트 크기
  • rowCount, columnCount: 카드 개수에 맞게 계산
  • onItemsRendered: 스크롤 끝 감지 후 fetch 호출
    나는 반응형 대응을 위해 window.innerWidth를 기준으로 카드 가로 폭, 열 수 등을 계산해서 반영했다.

5️⃣ 가상화 적용 후 성능 측정

같은 조건에서 다시 Profiler 측정하기

  • 카드 하나당 1.4ms ~ 1.9ms로 렌더링 시간 감소
  • 첫 렌더링 시 뷰포트에 맞춰 18개만 렌더링
  • 이후 스크롤 시 6개씩 동적으로 교체
  • F12 개발자 도구로 html 확인시 18개만 계속 렌더링됨

렌더링 성능이 약 2배 이상 개선되었고
전체 카드 수와 무관하게 일정한 성능 유지 가능해짐


가상화 적용하기 쉬웠는데 렌더링 시간이 바로 감소돼서 좋았다.
실제 사용자 경험을 고려하면 성능과 UX 모두 잡을 수 있는 조합이라고 느꼈다. 👍

0개의 댓글