고양이hyebin
무한 스크롤과 가상 스크롤
November 24, 2024

과제로 가로로 무한히 스크롤할 수 있는 기능을 구현해야 했습니다. 처음 문제를 읽고 자연스럽게 무한 스크롤 방식으로 구현을 시작했는데, 다시 문제를 읽어보니 가상 스크롤로 구현하라는 문장을 발견해 급히 코드를 수정했던 기억이 납니다.

그때 헤맸던 기억을 더듬어 가상 스크롤이 무엇인지, 무한 스크롤과의 차이점, 그리고 가상 스크롤을 어떻게 구현하는지 정리해보겠습니다.

무한스크롤

무한 스크롤
무한 스크롤

우리는 평소에 무한 스크롤을 자주 경험해봤습니다. 스크롤을 내릴 때마다 쇼핑 목록, 뉴스기사, 수많은 정보들등 새로운 데이터를 계속 불러와서 끊김 없이 내용을 제공하는 방식입니다. 사용자가 다음 페이지를 클릭하지 않고도 계속해서 콘텐츠를 볼 수 있어서 편리한 경험을 제공할 수 있습니다

하지만 문제점이 있습니다. 데이터 양이 많아질수록 DOM에 추가되는 요소들이 많아지고, 브라우저가 처리해야 할 DOM 트리 크기가 커져 성능 저하가 발생할 수 있습니다. 특히 모바일 환경에서는 메모리 사용량이 급증하여 앱이 느려지거나 중단될 위험이 있습니다.

무한 스크롤 구현 예제 코드

무한 스크롤 구현 원리
무한 스크롤 구현 원리

무한 스크롤은 스크롤이 가능한 콘텐츠가 이미 렌더링이 된 상태이기 때문에 시작 인덱스와 마지막 인덱스를 알고있습니다.

const itemHeight = 35;
 
export const NonVirtualizedList = ({
  numberOfItems,
}: {
  numberOfItems: number;
}) => {
  const listItems = Array.from({ length: numberOfItems }, (_, index) => (
    <ListItem key={index} index={index} />
  ));
 
  return (
    <ul
      className="overflow-y-scroll w-full h-[500px] border-2 border-black"
      onScroll={(e) => {
        console.log('Scrolling ', e.currentTarget.scrollTop);
      }}
    >
      {listItems}
    </ul>
  );
};
 
const ListItem = ({ index }: { index: number }) => {
  const height = `${itemHeight}px`
  return (
    <li style={{ height: height }} className="text-center">
      List Item Index - {index}
    </li>
  );
};

만약 numberOfItems를 무한한 숫자로 설정하면 어떻게 될까요? 모든 요소가 DOM에 렌더링되면서 브라우저가 이를 처리하지 못해 프로젝트가 먹통이 될 것입니다. 어떻게 해결할 수 있을까요?

가상스크롤

무한 스크롤의 문제를 해결할 수 있는 방법은 바로 가상스크롤입니다.

가상 스크롤
가상 스크롤

가상스크롤은 무엇일까요 ? 대량의 데이터를 효율적으로 렌더링하기 위한 기술입니다. ‘윈도윙’이라는 기술을 사용해 현재 화면에 보이는 데이터만 렌더링하고, 나머지는 제거하여 브라우저 성능을 최적화합니다.

윈도윙 기법?

윈도윙은 현재 화면에 보이는 영역의 데이터만 DOM에 추가하고, 스크롤에 따라 필요 없는 요소는 제거하는 기법입니다. React의 공식 문서에서도 긴 리스트를 처리할 때 윈도윙 기법을 권장합니다.

리액트에서 윈도윙 기술 설명

리액트에서 윈도윙 기술은 긴 데이터 목록(수백에서 수천 개 행)을 효과적으로 렌더링하기 위한 방법입니다. 이 기술을 사용하면, 한 번에 화면에 보이는 데이터만 렌더링하고 나머지는 생략하여 DOM 노드 수를 줄이고 성능을 크게 개선할 수 있습니다.

react-windowreact-virtualized는 이러한 윈도윙을 구현하는 데 많이 사용되는 라이브러리입니다. 이들은 목록, 그리드, 테이블 등 다양한 형태의 데이터를 효율적으로 렌더링할 수 있도록 돕습니다.
또한, 특정 요구 사항에 맞춰 더 세부적인 커스터마이징이 필요하다면, Twitter처럼 직접 윈도윙 구성 요소를 만들어 활용할 수도 있습니다.

가상 스크롤 구현

가상 스크롤 구현 원리
가상 스크롤 구현 원리

간단한 원리는 스크롤 위치를 기준으로 보이는 항목의 시작 인덱스와 끝 인덱스를 계산한 뒤 해당 항목만 렌더링합니다. 그리고 각 아이템은 상위 박스(relative)를 기준으로 절대 위치(absolute) 로 배치하여 상위 박스의 높이( top:100px 이와 같이 설정)를 설정 해 스크롤바가 동작하게 구현합니다.

import { useState } from 'react';
 
const itemHeight = 35;
const windowHeight = 500;
const overscan = 20;
 
export const VirtualizedList = ({
  numberOfItems,
}: {
  numberOfItems: number;
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const startIndex = Math.max(Math.floor(scrollTop / itemHeight) - overscan);
  const endIndex = Math.min(
    numberOfItems,
    Math.floor((scrollTop + windowHeight) / itemHeight) + overscan
  );
 
  const generateRows = () => {
    let items: JSX.Element[] = [];
    for (let i = startIndex; i < endIndex; i++) {
      items.push(<ListItem key={i} index={i} />);
    }
 
    return items;
  };
 
const windowHeight = `${windowHeight}px`
const itemHeight = `${numberOfItems * itemHeight}px`
 
  return (
    <div
      className="overflow-y-scroll w-full border-2 border-black relative"
      style={{ height: windowHeight }}
      onScroll={(e) => {
        setScrollTop(e.currentTarget.scrollTop);
      }}
    >
      <div
        style={{
          height: itemHeight,
        }}
      >
        {generateRows()}
      </div>
    </div>
  );
};
 
const ListItem = ({ index }: { index: number }) => {
  const itemHeight = `${itemHeight}px`
  const itemTop =  `${itemHeight * index}px`
  return (
    <div
      style={{
        height: itemHeight,
        top: itemTop,
        backgroundColor: index % 2 === 0 ? '#f0f0f0' : '#ffffff',
      }}
      className="text-center w-full absolute"
    >
      List Item Index - {index}
    </div>
  );
};

가상 스크롤 최적화 버전

이전 버전 보다 렌더링되는 항목 수 정확히 계산하여, 필요한 범위만 DOM에 추가합니다. 또 transform: translateY를 사용한 위치 설정으로 CSS 성능을 최적화했습니다.

import { useState } from 'react';
 
const itemHeight = 35;
const windowHeight = 500;
const overscan = 20;
 
export const VirtualizedListOptimized = ({
  numberOfItems,
}: {
  numberOfItems: number;
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
  let renderedNodesCount = Math.floor(windowHeight / itemHeight) + 2 * overscan;
  renderedNodesCount = Math.min(numberOfItems - startIndex, renderedNodesCount);
 
  const generateRows = () => {
    let items: JSX.Element[] = [];
    for (let i = 0; i < renderedNodesCount; i++) {
      const index = i + startIndex;
      items.push(<ListItem key={index} index={index} />);
    }
 
    return items;
  };
 
const windowHeight = `${windowHeight}px`
const itemHeight = `${numberOfItems * itemHeight}px`
const transform = `translateY(${startIndex * itemHeight}px)`
  return (
    <div
      className="overflow-y-scroll w-full border-2 border-black"
      style={{ height: windowHeight }}
      onScroll={(e) => {
        setScrollTop(e.currentTarget.scrollTop);
      }}
    >
      <div
        style={{
          height: itemHeight,
        }}
      >
        <div
          style={{
            transform: transform ,
          }}
        >
          {generateRows()}
        </div>
      </div>
    </div>
  );
};
 
const ListItem = ({ index }: { index: number }) => {
  const itemHeight = `${itemHeight}px`
  return (
    <div
      style={{
        height: itemHeight,
        backgroundColor: index % 2 === 0 ? '#f0f0f0' : '#ffffff',
      }}
      className="text-center w-full"
    >
      List Item Index - {index}
    </div>
  );
};

가상스크롤 라이브러리 비교

가장 많이 사용하는 가상 스크롤 라이브러리를 npmtrends에서 비교해보았습니다.

  1. @tanstack/react-virtual
  2. react-virtualized
  3. react-virtuoso
  4. react-window
npmtrends 라이브러리 사용 수
npmtrends 라이브러리 사용 수
npmtrends 표 비교
npmtrends 표 비교

라이브러리를 선택하는 기준은 사람마다 다를 수 있습니다. 저는 라이브러리를 선택할 때 유지보수가 활발히 이루어지고 있는지, 커스터마이징이 용이한지, 그리고 공식 문서나 예제가 잘 정리되어 있는지를 중점적으로 봅니다. 이러한 기준으로 4가지 라이브러리를 비교한 결과, 지속적인 업데이트와 잘 정리된 문서를 제공하는 @tanstack/react-virtualreact-virtuoso를 선택했습니다.

@tanstack/react-virtual

  • 최신 라이브러리로, React Query 등을 만든 TanStack 팀에서 제작.
  • 가볍고 성능 최적화가 잘 되어 있음.
  • 가상화된 리스트뿐 아니라 테이블, 그리드 등 다양한 레이아웃을 지원.
  • TypeScript 지원 및 문서가 잘 정리되어 있음.

react-virtuoso

  • 사용하기 쉽고 직관적인 API 제공.
  • 가변 크기 리스트나, 무한 스크롤 같은 고급 기능 지원.
  • 스타일링 및 데이터 렌더링 커스터마이징이 간편.
  • 활발한 업데이트와 좋은 문서 제공.

마무리

지금까지 무한 스크롤과 가상 스크롤의 차이점에 대해서 설명해봤습니다. 다음 포스팅에서는 각 라이브러리를 활용해 가상 스크롤을 어떻게 구현하고 커스텀 할지 방법에 대해서 다뤄보도록 하겠습니다 :)

참고

https://www.youtube.com/watch?v=Yz4eK-4LKXg