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

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

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

React에서 가상 스크롤링 구현하기는 대량의 데이터 목록을 효율적으로 렌더링하기 위한 핵심 기술입니다. 수천, 수만 개의 항목을 가진 리스트를 브라우저에서 표시할 때, 모든 항목을 DOM에 렌더링하면 심각한 성능 저하가 발생합니다. 가상 스크롤링(Virtual Scrolling)은 뷰포트에 보이는 항목만 실제로 렌더링하고, 스크롤 시 동적으로 항목을 교체하는 방식으로 이 문제를 해결합니다. 이 기법은 무한 스크롤, 대용량 데이터 테이블, 채팅 애플리케이션 등에서 필수적으로 사용되며, 사용자 경험을 크게 향상시킵니다.

2. 동작 원리 상세 설명

가상 스크롤링의 핵심 원리는 윈도잉(Windowing)입니다. 전체 데이터 목록 중에서 현재 화면에 표시되는 영역(뷰포트)에 해당하는 항목만 선택적으로 렌더링합니다. 구체적인 동작 과정은 다음과 같습니다:

  1. 컨테이너 높이 계산: 전체 리스트의 가상 높이를 계산합니다 (항목 개수 × 항목 높이)
  2. 스크롤 위치 추적: 사용자의 스크롤 이벤트를 감지하여 현재 스크롤 위치를 파악합니다
  3. 가시 범위 계산: 스크롤 위치를 기반으로 뷰포트에 보여야 할 시작/끝 인덱스를 계산합니다
  4. 오프셋 적용: 렌더링되는 항목들을 올바른 위치에 배치하기 위해 상단 오프셋을 적용합니다
  5. 버퍼 영역: 스크롤 시 깜빡임을 방지하기 위해 뷰포트 앞뒤로 추가 항목을 렌더링합니다

이 방식을 통해 10,000개의 항목이 있어도 실제로는 20-30개만 DOM에 존재하게 되어 렌더링 성능이 극적으로 향상됩니다.

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

시간 복잡도

  • 초기 렌더링: O(V) – V는 뷰포트에 보이는 항목 수
  • 스크롤 업데이트: O(V) – 매 스크롤마다 보이는 항목만 재계산
  • 인덱스 계산: O(1) – 스크롤 위치에서 시작/끝 인덱스 계산은 상수 시간

공간 복잡도

  • DOM 노드: O(V) – 전체 데이터 N에 관계없이 뷰포트 크기에만 비례
  • 데이터 메모리: O(N) – 전체 데이터는 메모리에 유지
  • 상태 관리: O(1) – 스크롤 위치, 인덱스 등 고정 크기 상태

일반 렌더링이 O(N)의 시간과 공간을 사용하는 반면, 가상 스크롤링은 O(V)로 제한되어 N이 커질수록 성능 이점이 극대화됩니다. 예를 들어, 10,000개 항목에서 20개만 렌더링하면 500배의 성능 향상을 기대할 수 있습니다.

4. 단계별 구현 과정

4.1 기본 컴포넌트 구조

먼저 React에서 가상 스크롤링 구현하기의 기본 골격을 만들어봅시다:

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

const VirtualScroll = ({ items, itemHeight, containerHeight }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  // 전체 리스트의 가상 높이
  const totalHeight = items.length * itemHeight;

  // 뷰포트에 보이는 항목 개수
  const visibleCount = Math.ceil(containerHeight / itemHeight);

  // 스크롤 위치에 따른 시작 인덱스
  const startIndex = Math.floor(scrollTop / itemHeight);

  // 끝 인덱스 (버퍼 포함)
  const endIndex = Math.min(
    startIndex + visibleCount + 1,
    items.length
  );

  // 실제 렌더링할 항목들
  const visibleItems = items.slice(startIndex, endIndex);

  // 상단 오프셋 (올바른 위치에 배치)
  const offsetY = startIndex * itemHeight;

  // 스크롤 이벤트 핸들러
  const handleScroll = (e) => {
    setScrollTop(e.currentTarget.scrollTop);
  };

  return (
    
{/* 전체 높이를 유지하는 스페이서 */}
{/* 실제 렌더링되는 항목들 */}
{visibleItems.map((item, index) => (
{item}
))}
); }; export default VirtualScroll;

4.2 성능 최적화된 버전

스크롤 이벤트 최적화와 버퍼 영역을 추가한 개선된 버전입니다:

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

const OptimizedVirtualScroll = ({
  items,
  itemHeight,
  containerHeight,
  overscan = 3, // 버퍼 항목 수
  renderItem
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  // 스크롤 핸들러 최적화
  const handleScroll = useCallback(
    debounce((e) => {
      setScrollTop(e.currentTarget.scrollTop);
    }, 16), // 약 60fps
    []
  );

  // 계산 결과 메모이제이션
  const { startIndex, endIndex, offsetY, visibleItems } = useMemo(() => {
    const totalHeight = items.length * itemHeight;
    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,
      visibleItems: items.slice(start, end)
    };
  }, [scrollTop, items, itemHeight, containerHeight, overscan]);

  return (
    
{visibleItems.map((item, index) => (
{renderItem ? renderItem(item, startIndex + index) : item}
))}
); }; export default OptimizedVirtualScroll;

4.3 동적 높이 지원 버전

항목마다 높이가 다른 경우를 처리하는 고급 구현입니다:

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

const DynamicVirtualScroll = ({ items, containerHeight, estimatedItemHeight = 50 }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const [itemHeights, setItemHeights] = useState({});
  const containerRef = useRef(null);
  const itemRefs = useRef({});

  // 항목 높이 측정
  useEffect(() => {
    const newHeights = {};
    let hasChanges = false;

    Object.entries(itemRefs.current).forEach(([index, element]) => {
      if (element) {
        const height = element.getBoundingClientRect().height;
        const prevHeight = itemHeights[index];
        
        if (height !== prevHeight) {
          newHeights[index] = height;
          hasChanges = true;
        }
      }
    });

    if (hasChanges) {
      setItemHeights(prev => ({ ...prev, ...newHeights }));
    }
  });

  // 누적 높이 계산
  const getItemOffset = useCallback((index) => {
    let offset = 0;
    for (let i = 0; i < index; i++) {
      offset += itemHeights[i] || estimatedItemHeight;
    }
    return offset;
  }, [itemHeights, estimatedItemHeight]);

  // 스크롤 위치에서 인덱스 찾기
  const findStartIndex = useCallback((scrollTop) => {
    let offset = 0;
    for (let i = 0; i < items.length; i++) {
      const height = itemHeights[i] || estimatedItemHeight;
      if (offset + height > scrollTop) {
        return i;
      }
      offset += height;
    }
    return 0;
  }, [items.length, itemHeights, estimatedItemHeight]);

  const startIndex = findStartIndex(scrollTop);
  const endIndex = Math.min(
    items.length,
    startIndex + Math.ceil(containerHeight / estimatedItemHeight) + 5
  );

  const visibleItems = items.slice(startIndex, endIndex);
  const offsetY = getItemOffset(startIndex);
  const totalHeight = getItemOffset(items.length);

  const handleScroll = (e) => {
    setScrollTop(e.currentTarget.scrollTop);
  };

  return (
    
{visibleItems.map((item, index) => { const actualIndex = startIndex + index; return (
(itemRefs.current[actualIndex] = el)} > {item}
); })}
); }; export default DynamicVirtualScroll;

5. 최적화 방법

5.1 스크롤 이벤트 최적화

  • Throttle/Debounce: 스크롤 이벤트를 제한하여 불필요한 재계산 방지
  • RequestAnimationFrame: 브라우저 렌더링 사이클에 맞춰 업데이트
const handleScroll = useCallback((e) => {
  requestAnimationFrame(() => {
    setScrollTop(e.currentTarget.scrollTop);
  });
}, []);

5.2 메모이제이션 활용

  • useMemo: 계산 비용이 높은 값들을 캐싱
  • React.memo: 항목 컴포넌트의 불필요한 리렌더링 방지
const VirtualItem = React.memo(({ item, style }) => (
  
{item}
));

5.3 CSS 최적화

  • will-change: 브라우저에 변경 사항 힌트 제공
  • transform: position 대신 transform 사용으로 GPU 가속 활용
  • contain: CSS contain 속성으로 레이아웃 계산 범위 제한

5.4 라이브러리 활용

React에서 가상 스크롤링 구현하기를 처음 시도한다면, 검증된 라이브러리 사용을 권장합니다:

  • react-window: 가볍고 빠른 가상 스크롤 라이브러리
  • react-virtualized: 기능이 풍부한 완전한 솔루션
  • @tanstack/react-virtual: 현대적이고 유연한 훅 기반 라이브러리

6. 실전 활용 예제

대용량 사용자 목록 렌더링

import React from 'react';
import VirtualScroll from './VirtualScroll';

const UserList = () => {
  // 10,000명의 사용자 데이터
  const users = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `User ${i}`,
    email: `user${i}@example.com`
  }));

  const renderUser = (user) => (
    
{user.name}
{user.name}
{user.email}
); return (

사용자 목록 (10,000명)

); }; export default UserList;

무한 스크롤 구현

const InfiniteVirtualScroll = ({ loadMore, hasMore }) => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  const handleScroll = async (e) => {
    const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
    
    // 하단 100px 이내로 스크롤하면 추가 로드
    if (scrollHeight - scrollTop - clientHeight < 100 && hasMore && !isLoading) {
      setIsLoading(true);
      const newItems = await loadMore();
      setItems(prev => [...prev, ...newItems]);
      setIsLoading(false);
    }
  };

  return (
    
  );
};

이제 React에서 가상 스크롤링 구현하기의 모든 핵심 개념과 실전 활용 방법을 마스터했습니다. 대량의 데이터를 효율적으로 렌더링하여 사용자 경험을 극대화할 수 있습니다!

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

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

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

5

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

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

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

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

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

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

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

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

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기