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

1. 가상 스크롤링 알고리즘 소개 및 개념

React에서 가상 스크롤링 구현하기는 대량의 데이터를 효율적으로 렌더링하기 위한 핵심 기술입니다. 수천, 수만 개의 리스트 아이템을 한 번에 렌더링하면 브라우저 성능이 급격히 저하되고 사용자 경험이 나빠집니다. 가상 스크롤링(Virtual Scrolling)은 뷰포트에 보이는 영역의 아이템만 실제로 DOM에 렌더링하고, 나머지는 렌더링하지 않는 최적화 기법입니다. 이를 통해 10만 개의 아이템이 있어도 실제로는 10~20개만 렌더링하여 뛰어난 성능을 유지할 수 있습니다. 스크롤 위치에 따라 동적으로 렌더링할 아이템을 계산하고 교체하는 것이 핵심 원리입니다.

2. 동작 원리 상세 설명

가상 스크롤링의 동작 원리는 다음과 같습니다. 먼저 전체 컨테이너의 높이를 전체 아이템 수와 각 아이템의 높이를 곱한 값으로 설정합니다. 이렇게 하면 스크롤바가 실제 데이터 양을 반영하게 됩니다. 사용자가 스크롤할 때마다 현재 스크롤 위치(scrollTop)를 감지하고, 이를 기반으로 현재 뷰포트에 표시되어야 할 아이템의 시작 인덱스와 끝 인덱스를 계산합니다. 시작 인덱스는 Math.floor(scrollTop / itemHeight)로 구하고, 끝 인덱스는 startIndex + visibleCount로 계산합니다. 계산된 범위의 아이템만 실제 DOM에 렌더링하되, 위치를 올바르게 맞추기 위해 transform: translateY() 또는 padding-top을 사용하여 오프셋을 조정합니다. 스크롤 이벤트가 발생할 때마다 이 계산을 반복하여 렌더링할 아이템을 업데이트합니다. 버퍼(buffer) 개념을 도입하여 스크롤 방향으로 몇 개의 추가 아이템을 미리 렌더링하면 스크롤 시 깜빡임을 방지할 수 있습니다.

3. 시간/공간 복잡도 분석

시간 복잡도: 일반 렌더링의 경우 O(n)의 시간이 소요되며, n개의 모든 아이템을 DOM에 마운트해야 합니다. 가상 스크롤링을 사용하면 뷰포트에 보이는 k개의 아이템만 렌더링하므로 O(k)의 시간 복잡도를 가집니다. 여기서 k는 상수에 가까운 값(보통 10~30)이므로 실질적으로 O(1)로 간주할 수 있습니다. 스크롤 위치 계산도 O(1)의 산술 연산만 필요합니다.

공간 복잡도: 전체 데이터는 메모리에 O(n)으로 유지되지만, DOM 노드는 O(k)만 생성됩니다. 일반 렌더링은 O(n)의 DOM 노드를 생성하므로 메모리 사용량이 크게 차이납니다. 특히 각 DOM 노드가 이벤트 리스너나 복잡한 구조를 가질 경우 이 차이는 더욱 극명해집니다. 10,000개 아이템을 렌더링할 때 일반 방식은 10,000개의 DOM 노드가 필요하지만, 가상 스크롤링은 약 20개만 필요하여 500배의 메모리 절약 효과가 있습니다.

4. 단계별 구현 과정

4.1 기본 구조 설정

React에서 가상 스크롤링 구현하기의 첫 단계는 필요한 상태와 ref를 설정하는 것입니다.

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

const VirtualScroll = ({ items, itemHeight = 50, containerHeight = 600 }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);
  
  // 뷰포트에 보이는 아이템 개수 계산
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  
  // 버퍼: 위아래로 추가 렌더링할 아이템 수
  const bufferCount = 3;
  
  // 시작 인덱스 계산 (버퍼 포함)
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount);
  
  // 끝 인덱스 계산 (버퍼 포함)
  const endIndex = Math.min(
    items.length,
    startIndex + visibleCount + bufferCount * 2
  );
  
  // 실제 렌더링할 아이템 추출
  const visibleItems = items.slice(startIndex, endIndex);
  
  // 전체 컨테이너 높이
  const totalHeight = items.length * itemHeight;
  
  // 오프셋 계산 (렌더링된 아이템의 시작 위치)
  const offsetY = startIndex * itemHeight;
  
  return (
    
setScrollTop(e.target.scrollTop)} >
{visibleItems.map((item, index) => (
{item}
))}
); };

4.2 성능 최적화된 버전

스크롤 이벤트 최적화와 동적 높이 지원을 추가한 고급 구현입니다.

import React, { useState, useRef, useCallback, useMemo } from 'react';

const OptimizedVirtualScroll = ({ 
  items, 
  itemHeight = 50, 
  containerHeight = 600,
  overscan = 3 // 오버스캔 아이템 수
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);
  const [isScrolling, setIsScrolling] = useState(false);
  const scrollTimeout = useRef(null);
  
  // 스크롤 핸들러 최적화 (throttle)
  const handleScroll = useCallback((e) => {
    const newScrollTop = e.target.scrollTop;
    setScrollTop(newScrollTop);
    
    // 스크롤 중 상태 관리
    if (!isScrolling) setIsScrolling(true);
    
    // 디바운스로 스크롤 종료 감지
    clearTimeout(scrollTimeout.current);
    scrollTimeout.current = setTimeout(() => {
      setIsScrolling(false);
    }, 150);
  }, [isScrolling]);
  
  // 렌더링 범위 계산 (메모이제이션)
  const { startIndex, endIndex, offsetY } = useMemo(() => {
    const visibleCount = Math.ceil(containerHeight / itemHeight);
    const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const end = Math.min(
      items.length,
      start + visibleCount + overscan * 2
    );
    
    return {
      startIndex: start,
      endIndex: end,
      offsetY: start * itemHeight
    };
  }, [scrollTop, containerHeight, itemHeight, items.length, overscan]);
  
  const visibleItems = useMemo(
    () => items.slice(startIndex, endIndex),
    [items, startIndex, endIndex]
  );
  
  const totalHeight = items.length * itemHeight;
  
  return (
    
{visibleItems.map((item, index) => ( ))}
); }; // 아이템 컴포넌트 메모이제이션 const VirtualItem = React.memo(({ item, height, index }) => (
#{index + 1} {item}
));

4.3 동적 높이 지원

아이템마다 높이가 다를 때 사용하는 고급 구현입니다.

const DynamicVirtualScroll = ({ items, estimatedItemHeight = 50, containerHeight = 600 }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const itemHeights = useRef(new Map());
  const itemPositions = useRef([]);
  
  // 아이템 위치 계산
  useEffect(() => {
    let offset = 0;
    const positions = [];
    
    items.forEach((item, index) => {
      positions[index] = offset;
      const height = itemHeights.current.get(index) || estimatedItemHeight;
      offset += height;
    });
    
    itemPositions.current = positions;
  }, [items, estimatedItemHeight]);
  
  // 높이 측정 콜백
  const measureItem = useCallback((index, element) => {
    if (element) {
      const height = element.getBoundingClientRect().height;
      if (itemHeights.current.get(index) !== height) {
        itemHeights.current.set(index, height);
      }
    }
  }, []);
  
  // 이진 탐색으로 시작 인덱스 찾기
  const findStartIndex = (scrollTop) => {
    let left = 0;
    let right = itemPositions.current.length - 1;
    
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const position = itemPositions.current[mid];
      
      if (position === scrollTop) return mid;
      if (position < scrollTop) left = mid + 1;
      else right = mid - 1;
    }
    
    return Math.max(0, right);
  };
  
  const startIndex = findStartIndex(scrollTop);
  const endIndex = Math.min(items.length, startIndex + 20);
  
  return (
    
setScrollTop(e.target.scrollTop)} >
{items.slice(startIndex, endIndex).map((item, idx) => { const index = startIndex + idx; return (
measureItem(index, el)} style={{ position: 'absolute', top: itemPositions.current[index], width: '100%' }} > {item}
); })}
); };

5. 최적화 방법

1. 스크롤 이벤트 최적화: 스크롤 이벤트는 초당 수십 번 발생하므로 throttle이나 debounce를 적용해야 합니다. requestAnimationFrame을 사용하면 브라우저 리페인트 주기에 맞춰 최적화할 수 있습니다.

const throttledScroll = useCallback(
  throttle((scrollTop) => setScrollTop(scrollTop), 16), // 60fps
  []
);

2. 메모이제이션: useMemo와 React.memo를 활용하여 불필요한 재계산과 리렌더링을 방지합니다. 특히 visibleItems 계산과 개별 아이템 컴포넌트에 적용하면 효과적입니다.

3. CSS 최적화: will-change: transform을 사용하여 GPU 가속을 활성화하고, transform: translateY()top보다 성능이 우수합니다. contain: layout style paint를 추가하면 레이아웃 계산을 격리할 수 있습니다.

4. 오버스캔 전략: 스크롤 방향을 감지하여 해당 방향으로만 버퍼를 추가하면 렌더링 효율이 높아집니다. 빠른 스크롤 시 오버스캔을 증가시키고, 느린 스크롤 시 감소시키는 동적 조정도 가능합니다.

5. 가상화 라이브러리 활용: 복잡한 요구사항이 있다면 react-window나 react-virtualized 같은 검증된 라이브러리를 사용하는 것이 효율적입니다.

6. 실전 활용 예제

// 10만 개 아이템 렌더링 예제
const App = () => {
  const largeDataset = useMemo(
    () => Array.from({ length: 100000 }, (_, i) => `아이템 #${i + 1}`),
    []
  );
  
  return (
    

가상 스크롤링 데모

총 {largeDataset.length.toLocaleString()}개 아이템

); }; export default App;

이 구현은 대시보드, 로그 뷰어, 채팅 애플리케이션, 데이터 그리드 등 대량의 리스트를 표시하는 모든 곳에서 활용할 수 있습니다. React에서 가상 스크롤링 구현하기를 마스터하면 어떤 규모의 데이터도 부드럽게 처리할 수 있는 고성능 애플리케이션을 개발할 수 있습니다.

📚 함께 읽으면 좋은 글

1

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

📂 알고리즘 해결
📅 2025. 10. 8.
🎯 React에서 가상 스크롤링 구현하기

2

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

📂 알고리즘 해결
📅 2025. 10. 2.
🎯 React에서 가상 스크롤링 구현하기

3

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

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

4

배열 정렬 알고리즘 성능 비교 – 개념부터 구현까지 완벽 마스터

📂 알고리즘 해결
📅 2025. 10. 7.
🎯 배열 정렬 알고리즘 성능 비교

5

배열 정렬 알고리즘 성능 비교 – 개념부터 구현까지 완벽 마스터

📂 알고리즘 해결
📅 2025. 10. 7.
🎯 배열 정렬 알고리즘 성능 비교

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

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

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

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

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

React에서 가상 스크롤링 구현하기에 대한 여러분만의 경험이나 노하우가 있으시나요?

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기