React

[React] 'react-virtuoso' Windowing 최적화하기 (스크롤 성능개선, 뒤로가기 처리)

차노도리 2024. 9. 28. 21:49

Windowing의 개념

Windowing은 가상화 기법의 일종으로, 대량의 데이터를 렌더링 할 때 성능을 최적화하기 위해 사용되는 방법 중 하나이다.
화면 전체를 한 번에 렌더링 하는 대신, 화면에 보이는 영역과 오버스캔 영역만 렌더링 한다.

주로 리스트나 테이블에서 수천 개 이상의 아이템을 렌더링해야 할 때 사용되며, 화면에 보이는 항목들만 렌더링 하고 사용자가 스크롤할 때 새로운 항목을 점진적으로 렌더링 하는 방식이다. 이 과정에서 보이지 않는 항목은 메모리와 DOM에서 제외되어 성능이 최적화된다.

 

Windowing 장점

  • 수천 개의 리스트 아이템을 한 번에 렌더링 하지 않고, 화면에 보이는 부분만 렌더링 하므로 DOM 크기가 줄어들어 렌더링 성능과 초기 로딩 속도가 크게 개선된다.

 

Windowing 단점

  • 사용자가 빠르게 스크롤 시 아이템이 제대로 렌더링 되지 않아서 깜빡임 현상이 발생할 수 있다. (Over Scan을 통하여 어느 정도 해결 가능)
  • 작은 리스트에서 Windowing 적용하는 계산 비용이 오히려 성능을 저하시킬 수 있다.

 

 

React Windowing 라이브러리 비교

라이브러리 비교 요약

라이브러리 특징 장점 단점 적합한 상황
React Window 경량화된 가상화 라이브러리 간단한 사용법, 가벼운 성능 복잡한 기능 부족 간단한 리스트나 그리드가 필요할 때
React Virtualized 기능이 풍부한 가상화 라이브러리 다양한 고급 기능 제공, 유연성 복잡하고 무거운 사용법 대규모 데이터 및 복잡한 레이아웃이 필요할 때
Virtuoso 가변 크기 항목에 최적화된 라이브러리 가변 크기 처리, 자동 스크롤 관리 매우 대규모 데이터 처리에 부족할 수 있음 동적 데이터를 다루는 프로젝트

 

react-virtuoso  선택 이유

  • 컴포넌트의 크기가 동적으로 변하는 상황이 많았기 때문에,  가변 크기의 컴포넌트를 자동으로 처리해 주는 라이브러리 선택

 

react-virtuoso 적용하기


공식 문서 : https://virtuoso.dev/

 

Getting Started with React Virtuoso | React Virtuoso

React Virtuoso is a set of React components that display large data sets using virtualized rendering. The Virtuoso components automatically handle items with variable sizes and changes in items' sizes.

virtuoso.dev

 

  1. 라이브러리 설치
npm install react-virtuoso
  1. Virtuoso 적용하기 
  • Virtuoso Props
    • useWindowScroll :  브라우저의 기본 스크롤바를 활용하여 스크롤
    • totalCount : 전체 데이터의 총길이를 설정
    • data : 렌더링 할 데이터 리스트
    • overscan : 화면에 보이지 않는 영역에 미리 렌더링 할 높이
    • itemContent :  data 리스트를 순회하면서 각 항목을 렌더링 할 컴포넌트를 정의
import { Virtuoso } from "react-virtuoso";
interface WindowingScrollTemplateProps {
  datas: data[];
}
export default function WindowingScrollTemplate({
  datas,
}: WindowingScrollTemplateProps) {
  return (
    <Virtuoso
      useWindowScroll
      totalCount={datas.length || 0}
      data={datas || []}
      overscan={3000}
      itemContent={(itemIndex, item) => {
        return <ItemComponent data={item} key={`item-${itemIndex}`} />;
      }}
    />
  );
}
  1.  

 

 

페이지 이동후 스크롤 복원하기

 

스크롤 이벤트가 발생할 때 현재 화면에 보이는 아이템의 인덱스를 저장해 두었다가, 뒤로 가기를 통해 페이지에 다시 접근할 경우 저장된 정보에 맞춰 스크롤 위치를 복원할 수 있다.

 

Virtuoso에서 스크롤 초기 설정을 위한 주요 Props

  1. initialTopMostItemIndex
    • 초기 렌더링 시, 데이터 리스트의 특정 인덱스를 기준으로 스크롤 위치를 설정한다.
  2. initialScrollTop
    • 초기 렌더링 시, 픽셀 단위로 스크롤 위치를 설정한다.
    • document에서는 initialTopMostItemIndex를 권장하고 있으며, 이는 인덱스 기반으로 보다 안정적인 스크롤 제어를 제공
    • 렌더링 완료 전에 스크롤이 이동되어 예상대로 동작하지 않는 경우가 있는 거 같다.

Virtuoso에서 사용할 추가 Props

  • itemsRendered : 리스트가 렌더링 될 때 호출되며, 보이는 DOM 정보를 제공한다. 이 정보를 활용해 현재 보이는 아이템들을 추적할 수 있다.
  • isScrolling : 스크롤이 이동 중인지 멈췄는지 여부를 나타낸다. 스크롤이 멈췄을 때 특정 작업을 트리거할 수 있다.

 

initialTopMostItemIndex를 활용한 스크롤 복원 방법

itemsRendered를 사용하여 리스트가 렌더링 될 때 보이는 아이템의 정보를 저장한다. 이를 통해 화면에 보이는 아이템의 인덱스를 추적할 수 있다.

isScrolling을 활용해, 스크롤이 멈췄을 때 현재 스크롤 위치를 저장한다. 스크롤 이벤트가 끝난 시점에 정확한 인덱스나 스크롤 위치를 저장하는 것이 중요하다.

 

주의사항

  • 뒤로 가기를 통해 페이지에 다시 접근하지 않았을 경우에는, 저장된 스크롤 값을 제거하는 로직이 필요하다. 페이지 이동 시 상태를 관리하여 불필요한 스크롤 복원이 일어나지 않도록 해야 한다.
  • itemsRendered에서 제공되는 아이템 정보의 offsetTop 값은, useWindowScroll를 사용하는 경우에도 기본 브라우저 스크롤의 offsetTop 값이 아닌 Virtuoso 내부의 offsetTop 값을 반환한다. 이 점을 고려하여 스크롤 복원 로직을 작성해야 한다.

예시 코드)

import { Virtuoso } from "react-virtuoso";
import { useRef } from "react";

interface WindowingScrollTemplateProps {
  datas: data[];
}

export default function WindowingScrollTemplate({
  datas,
}: WindowingScrollTemplateProps) {
  const scrollInfoCookie = getCookie(SCROLL_INFO);
  const virtuosoRef = useRef<HTMLDivElement | null>(null);
  const viewItemRef = useRef<data[]>([]);

  const handleItemRender = (items: data[]) => {
    viewItemRef.current = items;
  };

  // 현재 가운데에 있는 아이템의 Index 찾기
  const handleFindViewIndex = () => {
    const scrollY = window.scrollY;
    const innerHeight = window.innerHeight;
    const virtuosoOffsetTop = virtuosoRef.current?.offsetTop || 0;

    if (!virtuosoRef.current || viewItemRef.current.length === 0) {
      return;
    }

    const viewportMiddle =
      Math.round(scrollY + innerHeight / 2) - virtuosoOffsetTop;

    const visibleItem = viewItemRef.current.find(
      (item) =>
        item.offset <= viewportMiddle &&
        item.offset + item.size >= viewportMiddle
    );

    const scrollIndex = visibleItem?.index;

    if (!scrollIndex || scrollIndex <= 1) {
      clearCookie(SCROLL_INFO);
      return;
    }

    setCookie(SCROLL_INFO, JSON.stringify({ scrollIndex }));
  };

  return (
    <div ref={virtuosoRef}>
      <Virtuoso
        useWindowScroll
        totalCount={datas.length}
        data={datas}
        overscan={3000}
        itemsRendered={handleItemRender}
        isScrolling={(isScrolling) => {
          if (!isScrolling) {
            handleFindViewIndex();
          }
        }}
        {...(scrollInfoCookie?.scrollIndex > 0 && {
          initialTopMostItemIndex: {
            index: scrollInfoCookie.scrollIndex,
            align: "center",
          },
        })}
        itemContent={(itemIndex, item) => (
          <ItemComponent data={item} key={`item-${itemIndex}`} />
        )}
      />
    </div>
  );
}

 

 

 

스크롤이 많을 경우 페이지 성능비교 (약 1,200건의 데이터 리스트 렌더링)

랜더링 해야할 DOM이 많을 경우Windowing에 장점이 부각된다.

 

체크 방법 네트워크 요청 시간 비교 리소스 양
데이터 캐싱되어있는 경우 렌더링 비교 약 9.5초 -> 약 2초 약 43 MB -> 약 2 MB
데이터 캐싱 안 되어있는 경우 렌더링 비교 약 12 초 -> 약 6초 약 46 MB -> 약 5MB

 

 

데이터 캐싱 되어 있을 때 네트워크 요청 비교

윈도윙 미 적용 (데이터 캐싱 되어있음)
윈도윙 적용 (데이터 캐싱 되어있음)

 

데이터 캐싱 안되어 있을 때 네트워크 요청 비교 

윈도윙 미 적용 (데이터 캐싱 안되어있을 때)
윈도윙 적용 (데이터 캐싱 안되어 있을 때)

 

결론

대량의 DOM 요소를 렌더링 하는 페이지에서는 Windowing 기술을 도입함으로써 네트워크 요청 시간이 단축되고, 렌더링 성능을 크게 개선할 수 있다. 특히 수천 개 이상의 리스트나 데이터를 효율적으로 처리할 때 유용하다.

하지만 DOM 요소가 적거나 데이터 양이 작은 페이지에서는, 불필요한 연산이 추가되므로 오히려 성능 저하를 일으킬 수 있다. 따라서 데이터 양과 사용 시나리오를 고려해 적절한 상황에서 Windowing을 적용하는 것이 중요하다.

또한, 초기 화면 진입 시의 스크롤 위치에 대한 고민도 필요하다. 사용자가 페이지를 다시 방문하거나 뒤로 가기를 통해 접근할 때, 스크롤 복원과 관련된 로직은 성능과 사용자 편의성을 동시에 고려해한다.