React에서 가상 스크롤링 구현하기 – 개념부터 구현까지 완벽 마스터

React에서 가상 스크롤링 구현하기란?

React에서 가상 스크롤링 구현하기는 대량의 데이터를 렌더링할 때 성능을 최적화하는 핵심 기술입니다. 수천, 수만 개의 리스트 아이템을 모두 DOM에 렌더링하면 브라우저가 느려지고 메모리 사용량이 급증합니다. 가상 스크롤링(Virtual Scrolling)은 뷰포트에 보이는 영역의 요소만 실제로 렌더링하고, 스크롤 시 동적으로 요소를 교체하는 방식으로 이 문제를 해결합니다. 이를 통해 10만 개 이상의 아이템도 부드럽게 스크롤할 수 있으며, 초기 렌더링 시간과 메모리 사용량을 획기적으로 줄일 수 있습니다.

가상 스크롤링의 동작 원리

가상 스크롤링의 핵심 원리는 윈도잉(Windowing)입니다. 전체 리스트의 높이를 계산하여 스크롤 컨테이너의 전체 크기를 설정하고, 현재 스크롤 위치를 기반으로 뷰포트에 보여야 할 아이템의 인덱스 범위를 계산합니다.

동작 과정은 다음과 같습니다:

  1. 뷰포트 높이 측정: 사용자에게 보이는 컨테이너의 높이를 파악합니다.
  2. 아이템 높이 계산: 각 아이템의 고정 높이 또는 동적 높이를 계산합니다.
  3. 가시 영역 계산: 현재 스크롤 위치(scrollTop)를 기준으로 화면에 보여야 할 시작 인덱스와 끝 인덱스를 산출합니다.
  4. 오프셋 조정: 렌더링되는 아이템들을 올바른 위치에 배치하기 위해 translateY 또는 paddingTop을 사용합니다.
  5. 버퍼 영역 추가: 스크롤 시 깜빡임을 방지하기 위해 뷰포트 위아래로 추가 아이템을 렌더링합니다.

스크롤 이벤트가 발생하면 가시 영역을 재계산하고, 변경된 범위의 아이템만 리렌더링하여 최소한의 DOM 조작으로 최적의 성능을 달성합니다.

시간 및 공간 복잡도 분석

시간 복잡도:

  • 초기 렌더링: O(V) – V는 뷰포트에 보이는 아이템 수 (일반적으로 10~30개)
  • 스크롤 업데이트: O(V) – 매 스크롤마다 가시 영역의 아이템만 재계산
  • 인덱스 계산: O(1) – 고정 높이인 경우 수식으로 즉시 계산
  • 동적 높이: O(log N) – 이진 탐색을 사용하는 경우

일반 렌더링이 O(N) (N = 전체 아이템 수)인 것에 비해, 가상 스크롤링은 데이터 크기와 무관하게 일정한 성능을 유지합니다.

공간 복잡도:

  • DOM 노드: O(V) – 뷰포트에 보이는 요소만 유지
  • 데이터 저장: O(N) – 전체 데이터는 메모리에 유지
  • 위치 캐시: O(N) – 동적 높이의 경우 각 아이템의 위치를 캐싱

10,000개 아이템의 경우, 일반 렌더링은 10,000개 DOM 노드를 생성하지만, 가상 스크롤링은 약 20~50개만 유지하여 메모리를 99% 이상 절감합니다.

단계별 구현 과정

1단계: 기본 구조 설정

먼저 필요한 state와 ref를 정의합니다:

import React, { useState, useRef, useEffect } from 'react';

function VirtualScroll({ items, itemHeight = 50, containerHeight = 600 }) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);
  
  // 뷰포트에 보일 아이템 수 계산
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  
  // 버퍼 추가 (위아래 5개씩)
  const bufferCount = 5;
  const totalVisible = visibleCount + bufferCount * 2;
  
  return (
    
setScrollTop(e.target.scrollTop)} > {/* 구현 내용 */}
); }

2단계: 가시 영역 인덱스 계산

현재 스크롤 위치에서 렌더링할 아이템의 시작/끝 인덱스를 계산합니다:

// 시작 인덱스 계산 (버퍼 포함)
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount);

// 끝 인덱스 계산
const endIndex = Math.min(
  items.length - 1,
  startIndex + totalVisible
);

// 렌더링할 아이템 추출
const visibleItems = items.slice(startIndex, endIndex + 1);

3단계: 오프셋 및 전체 높이 계산

스크롤 가능한 전체 영역과 현재 아이템들의 위치를 설정합니다:

// 전체 컨텐츠 높이
const totalHeight = items.length * itemHeight;

// 현재 렌더링 영역의 상단 오프셋
const offsetY = startIndex * itemHeight;

4단계: 최종 렌더링 구현

계산된 값들을 사용하여 실제 DOM을 구성합니다:

function VirtualScroll({ items, itemHeight = 50, containerHeight = 600 }) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);
  
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  const bufferCount = 5;
  const totalVisible = visibleCount + bufferCount * 2;
  
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount);
  const endIndex = Math.min(items.length - 1, startIndex + totalVisible);
  const visibleItems = items.slice(startIndex, endIndex + 1);
  
  const totalHeight = items.length * itemHeight;
  const offsetY = startIndex * itemHeight;
  
  return (
    
setScrollTop(e.target.scrollTop)} > {/* 전체 높이를 유지하는 스페이서 */}
{/* 가시 영역 아이템들 */}
{visibleItems.map((item, index) => (
{item.content}
))}
); }

5단계: 성능 최적화 – 스크롤 이벤트 최적화

스크롤 이벤트는 매우 빈번하게 발생하므로 throttle을 적용합니다:

const handleScroll = useCallback(
  throttle((e) => {
    setScrollTop(e.target.scrollTop);
  }, 16), // 약 60fps
  []
);

function throttle(func, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
}

최적화 방법

1. React.memo로 불필요한 리렌더링 방지

const VirtualItem = React.memo(({ item, style }) => (
  
{item.content}
));

2. 동적 높이 지원

아이템 높이가 다를 경우, 각 아이템의 높이를 측정하고 캐싱합니다:

const [heights, setHeights] = useState({});
const itemRefs = useRef({});

useEffect(() => {
  // 렌더링된 아이템의 실제 높이 측정
  Object.keys(itemRefs.current).forEach(key => {
    const element = itemRefs.current[key];
    if (element) {
      setHeights(prev => ({
        ...prev,
        [key]: element.offsetHeight
      }));
    }
  });
}, [visibleItems]);

3. Intersection Observer 활용

뷰포트 진입/이탈을 감지하여 더욱 정밀한 렌더링 제어가 가능합니다:

useEffect(() => {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // 이미지 lazy loading 등 추가 최적화
        }
      });
    },
    { root: containerRef.current, threshold: 0.1 }
  );
  
  return () => observer.disconnect();
}, []);

4. requestAnimationFrame 사용

스크롤 업데이트를 브라우저의 렌더링 사이클과 동기화합니다:

const rafId = useRef(null);

const handleScroll = (e) => {
  if (rafId.current) {
    cancelAnimationFrame(rafId.current);
  }
  
  rafId.current = requestAnimationFrame(() => {
    setScrollTop(e.target.scrollTop);
  });
};

실전 활용 예제

React에서 가상 스크롤링 구현하기 기술은 다양한 실전 시나리오에서 활용됩니다:

// 100,000개 아이템 렌더링 예제
function App() {
  const items = Array.from({ length: 100000 }, (_, i) => ({
    id: i,
    content: `아이템 ${i + 1}`
  }));
  
  return (
    
  );
}

활용 사례:

  • 대용량 데이터 테이블: 수만 개 행을 가진 데이터 그리드
  • 채팅 애플리케이션: 수천 개 메시지 기록 표시
  • 무한 스크롤 피드: 소셜 미디어 타임라인
  • 로그 뷰어: 대용량 로그 파일 실시간 모니터링
  • 이커머스 상품 목록: 수백~수천 개 상품 카탈로그

React에서 가상 스크롤링 구현하기를 마스터하면, 대규모 데이터를 다루는 모던 웹 애플리케이션에서 탁월한 사용자 경험을 제공할 수 있습니다. react-window, react-virtualized 같은 라이브러리도 동일한 원리로 작동하므로, 이 개념을 이해하면 라이브러리 사용 시에도 더 효과적으로 활용할 수 있습니다.

📚 함께 읽으면 좋은 글

1

재귀 함수 마스터하기 – 개념부터 구현까지 완벽 마스터

📂 알고리즘 해결
📅 2025. 9. 30.
🎯 재귀 함수 마스터하기

2

JavaScript로 이진 탐색 구현하기 – 개념부터 구현까지 완벽 마스터

📂 알고리즘 해결
📅 2025. 9. 30.
🎯 JavaScript로 이진 탐색 구현하기

3

Warning: Each child in a list should have a unique “key” prop 에러 해결법 – 원인 분석부터 완벽 해결까지

📂 React 에러
📅 2025. 9. 11.
🎯 Warning: Each child in a list should have a unique “key” prop

4

Hook “useState” is called conditionally 에러 해결법 – 원인 분석부터 완벽 해결까지

📂 React 에러
📅 2025. 9. 11.
🎯 Hook “useState” is called conditionally

5

Cannot read property ‘length’ of undefined 에러 해결법 – 원인 분석부터 완벽 해결까지

📂 React 에러
📅 2025. 9. 10.
🎯 Cannot read property ‘length’ of undefined

💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!

📢 이 글이 도움되셨나요? 공유해주세요!

여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨

🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏

💬 여러분의 소중한 의견을 들려주세요!

이 글을 읽고 새롭게 알게 된 정보가 있다면 공유해주세요!

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨

🔔 블로그 구독하고 최신 글을 받아보세요!

📚
다양한 주제
17개 카테고리

정기 업데이트
하루 3회 발행

🎯
실용적 정보
바로 적용 가능

💡
최신 트렌드
2025년 기준

🌟 알고리즘 해결부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨

📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!

답글 남기기