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

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

1. 가상 스크롤링 소개 및 개념

React에서 가상 스크롤링 구현하기는 대량의 데이터를 효율적으로 렌더링하기 위한 핵심 기술입니다. 수천, 수만 개의 아이템을 화면에 표시해야 할 때 모든 요소를 DOM에 렌더링하면 성능이 급격히 저하됩니다. 가상 스크롤링(Virtual Scrolling)은 실제로 화면에 보이는 영역(viewport)에 해당하는 아이템만 렌더링하고, 나머지는 빈 공간으로 처리하여 DOM 노드 수를 최소화하는 기법입니다. 이를 통해 10,000개 이상의 아이템도 부드럽게 스크롤할 수 있으며, 메모리 사용량과 렌더링 시간을 획기적으로 줄일 수 있습니다.

2. 동작 원리 상세 설명

가상 스크롤링의 핵심 원리는 ‘윈도잉(Windowing)’ 개념입니다. 전체 리스트의 높이를 계산하여 스크롤 컨테이너에 설정하되, 실제로는 현재 뷰포트에 보이는 아이템과 그 주변의 일부 아이템만 렌더링합니다. 스크롤 이벤트가 발생하면 현재 스크롤 위치를 기반으로 보이는 영역의 시작 인덱스와 끝 인덱스를 계산합니다. 예를 들어, 각 아이템 높이가 50px이고 뷰포트 높이가 500px이라면 한 번에 10개의 아이템만 보입니다. 스크롤 위치가 1000px이라면 20번째 아이템부터 렌더링을 시작하면 됩니다. 렌더링된 아이템들은 절대 위치(absolute positioning)를 사용해 정확한 위치에 배치되며, 위아래로 패딩을 추가하여 전체 리스트의 높이를 유지합니다. 이를 통해 스크롤바가 정상적으로 동작하고 사용자는 전체 리스트를 스크롤하는 것처럼 느낍니다.

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

시간 복잡도: 가상 스크롤링의 렌더링 시간 복잡도는 O(k)입니다. 여기서 k는 뷰포트에 표시되는 아이템 수로, 전체 데이터 크기 n과 무관하게 상수입니다. 일반적인 리스트 렌더링이 O(n)인 것과 비교하면 압도적인 성능 우위를 보입니다. 스크롤 이벤트마다 보이는 영역을 계산하는 작업은 O(1)의 산술 연산으로 처리됩니다.

공간 복잡도: 메모리 사용량 역시 O(k)입니다. DOM 노드는 화면에 보이는 아이템 수만큼만 생성되므로, 100만 개의 데이터가 있어도 실제로는 20-30개의 DOM 노드만 유지됩니다. 원본 데이터는 O(n)의 메모리를 사용하지만, 이는 어차피 필요한 데이터이므로 추가 오버헤드로 간주하지 않습니다. 이러한 효율성 덕분에 대용량 데이터셋에서도 안정적인 성능을 보장합니다.

4. 단계별 구현 과정

React에서 가상 스크롤링 구현하기를 단계별로 살펴보겠습니다.

4.1 기본 컴포넌트 구조 설계

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

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

const VirtualScroll = ({ items, itemHeight, containerHeight }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);
  
  // 뷰포트에 보이는 아이템 계산
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(startIndex + visibleCount + 1, items.length);
  
  // 전체 리스트 높이
  const totalHeight = items.length * itemHeight;
  
  // 스크롤 핸들러
  const handleScroll = (e) => {
    setScrollTop(e.target.scrollTop);
  };
  
  return (
    
{items.slice(startIndex, endIndex).map((item, index) => (
{item.content}
))}
); };

4.2 성능 최적화된 버전

스크롤 이벤트 최적화를 위해 throttle을 적용합니다:

import { useCallback, useMemo } from 'react';

const VirtualScrollOptimized = ({ items, itemHeight, containerHeight }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);
  
  // 오버스캔을 위한 버퍼 추가
  const overscan = 3;
  
  const { startIndex, endIndex, offsetY } = useMemo(() => {
    const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const visibleCount = Math.ceil(containerHeight / itemHeight);
    const end = Math.min(items.length, start + visibleCount + overscan * 2);
    const offset = start * itemHeight;
    
    return { startIndex: start, endIndex: end, offsetY: offset };
  }, [scrollTop, itemHeight, containerHeight, items.length]);
  
  // 스로틀 적용
  const handleScroll = useCallback((e) => {
    requestAnimationFrame(() => {
      setScrollTop(e.target.scrollTop);
    });
  }, []);
  
  const visibleItems = useMemo(
    () => items.slice(startIndex, endIndex),
    [items, startIndex, endIndex]
  );
  
  return (
    
{visibleItems.map((item, index) => (
{item.content}
))}
); };

4.3 동적 높이 지원

아이템 높이가 가변적인 경우를 처리하는 고급 구현:

const VirtualScrollDynamic = ({ items, estimatedItemHeight, containerHeight }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const [itemHeights, setItemHeights] = useState({});
  const containerRef = useRef(null);
  const itemRefs = useRef({});
  
  // 각 아이템의 누적 높이 계산
  const itemOffsets = useMemo(() => {
    const offsets = [0];
    for (let i = 0; i < items.length; i++) {
      const height = itemHeights[i] || estimatedItemHeight;
      offsets.push(offsets[i] + height);
    }
    return offsets;
  }, [items.length, itemHeights, estimatedItemHeight]);
  
  // 이진 탐색으로 시작 인덱스 찾기
  const findStartIndex = (scrollTop) => {
    let low = 0, high = itemOffsets.length - 1;
    while (low < high) {
      const mid = Math.floor((low + high) / 2);
      if (itemOffsets[mid] < scrollTop) {
        low = mid + 1;
      } else {
        high = mid;
      }
    }
    return Math.max(0, low - 1);
  };
  
  const startIndex = findStartIndex(scrollTop);
  const endIndex = useMemo(() => {
    let index = startIndex;
    let accumulatedHeight = 0;
    while (index < items.length && accumulatedHeight < containerHeight + scrollTop) {
      accumulatedHeight = itemOffsets[index + 1];
      index++;
    }
    return index;
  }, [startIndex, containerHeight, scrollTop, itemOffsets, items.length]);
  
  // ResizeObserver로 높이 측정
  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        const index = parseInt(entry.target.dataset.index);
        const height = entry.contentRect.height;
        setItemHeights(prev => ({ ...prev, [index]: height }));
      });
    });
    
    Object.values(itemRefs.current).forEach(ref => {
      if (ref) observer.observe(ref);
    });
    
    return () => observer.disconnect();
  }, [startIndex, endIndex]);
  
  return (
    
setScrollTop(e.target.scrollTop)} style={{ height: containerHeight, overflow: 'auto', position: 'relative' }} >
{items.slice(startIndex, endIndex).map((item, i) => { const index = startIndex + i; return (
itemRefs.current[index] = el} style={{ position: 'absolute', top: itemOffsets[index], width: '100%' }} > {item.content}
); })}
); };

5. 최적화 방법

오버스캔(Overscan): 뷰포트 위아래로 추가 아이템을 렌더링하여 빠른 스크롤 시 빈 화면이 보이는 현상을 방지합니다. 일반적으로 3-5개의 아이템을 오버스캔하면 좋습니다.

requestAnimationFrame 활용: 스크롤 이벤트 핸들러에서 requestAnimationFrame을 사용하면 브라우저의 렌더링 주기에 맞춰 업데이트되어 부드러운 스크롤을 제공합니다.

useMemo 적용: 보이는 아이템 목록, 인덱스 계산 등 반복적인 연산은 useMemo로 메모이제이션하여 불필요한 재계산을 방지합니다.

키 최적화: 리스트 아이템의 key는 인덱스 기반이 아닌 고유 ID를 사용하는 것이 좋지만, 가상 스크롤링에서는 절대 위치를 사용하므로 인덱스를 key로 사용해도 무방합니다.

CSS transform 사용: position: absolute 대신 transform: translateY()를 사용하면 GPU 가속을 활용하여 더 나은 성능을 얻을 수 있습니다.

6. 실전 활용 예제

React에서 가상 스크롤링 구현하기를 실제 프로젝트에 적용한 예제입니다:

// 대용량 데이터 테이블
const App = () => {
  const data = useMemo(() => 
    Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      content: `아이템 ${i + 1}`
    })),
    []
  );
  
  return (
    

가상 스크롤링 데모

); }; export default App;

이 예제는 10,000개의 아이템을 부드럽게 스크롤할 수 있으며, react-window나 react-virtualized 같은 라이브러리의 기본 원리를 이해하는 데 도움이 됩니다. 실무에서는 이러한 검증된 라이브러리를 사용하는 것을 권장하지만, 직접 구현해보면 내부 동작 원리를 깊이 이해할 수 있습니다.

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

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

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

5

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

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

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

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

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

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

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

여러분은 React에서 가상 스크롤링 구현하기에 대해 어떻게 생각하시나요?

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기