Tanstack Virtual 적용기

JavaScriptReact

Tanstack Virtual 이란?

Tanstack Virtual은 커다란 스크롤링 요소들을 효율적으로 렌더링하기 위한 가상화 라이브러리이다.

AnimatedImage.gif

아무리 많은 요소가 있어도 실제 DOM에서는 일정한 수 만큼만 렌더링하는 기법이다. DOM에 렌더링 되는 요소가 많아질수록 어느 순간부터 성능이 급격하게 저하되므로 보여줘야 할 요소가 많을 경우 이러한 가상화 기법을 적용해볼 수 있다.

Virtualize 라이브러리는 Tanstack 외에도 react-virtualized, react-window 등이 있다.

문제

Image.png

기존에는 위처럼 grid로 구성되어 있었으며, viewport에 따라 보여지는 카드 개수가 달라지게 했다.

Tanstack Virtual 예제를 보면 알겠지만, Tanstack Virtual을 사용하게 되면 스크롤 div 안에 아이템들을 absolute로 적용하고, transform을 통해 위치를 조정하도록 되어있다.

즉, 기존 방식에서 부모에다가 Virtualizer 만 씌우면 되는 문제가 아니었다. Tanstack Virtual을 사용하면 한 row 혹은 column에 한 개의 요소만 들어가도록 해야한다.

해결

기존 리스트 아이템은 1차원 배열로 되어있었고, 이를 그냥 map을 돌려 렌더링 하고 있다.

const itemList = [item1, item2, item3, item4, item5, item6, item7];

return <div className='grid grid-cols-1 tablet:grid-cols-3 desktop: grid-cols-5'>
  itemList.map(item => <Card item={item} />)
</div>

위에서 설명한 것처럼 한 row에 한 요소만 들어가야 하므로 아래처럼 코드를 변경했다.

let columnSize = 3; // 1, 3, 5

const chunkedItemList = chunkArray(itemList, columnSize);
// [[item1, item2, item3], [item4, item5, item6], [item7]]

return <div>
  chunkedItemList.map(list => <div className='grid grid-cols-1 tablet:grid-cols-3 desktop: grid-cols-5'>
    {list.map(item => <Card item={item} />)}
    </div>)
</div>

기존 아이템 리스트를 1차원 배열에서 columnSize 만큼 쪼게 2차원 배열로 만들었다.

그림으로 살펴 보면 이런 느낌이다.

Image.png

신경 써야 하는건 한 줄에 하나의 요소만 들어가게 하는 것, 그리고 그 한 요소안에 몇 개의 아이템을 넣을지 이다!

실제 코드는 아래와 같다.

const [columnSize, setColumnSize] = useState(COLUMN_SIZE.desktop);

const list = chunkArray(
  flatPages(data) ?? [],
  columnSize
);

const rowVirtualizer = useVirtualizer({
  count: list?.length,
  getScrollElement: () => document.getElementById("main-section"),
  estimateSize: () => 390,
  overscan: 1,
});

useEffect(() => {
  if (isLabtop) setColumnSize(COLUMN_SIZE.labtop);
  else if (isMobile) setColumnSize(COLUMN_SIZE.mobile);
  else setColumnSize(COLUMN_SIZE.desktop);
}, [isLabtop, isMobile]);

{
  rowVirtualizer.getVirtualItems()?.map((virtualRow) => {
    const row = list[virtualRow.index];
    if (!row) return null;
    return (
      <div
        key={virtualRow.key}
        className={`absolute top-0 left-0 w-full`}
        style={{
          transform: `translateY(${virtualRow.start}px)`,
        }}
      >
        <div className="grid grid-cols-1 laptop:grid-cols-3 desktop:grid-cols-5 w-full gap-6 place-items-center items-stretch">
          {row.map((item) => (
            <Card
              key={item.designId}
              item={item}
              isVault={isVault}
            />
          ))}
        </div>
      </div>
    );
  });
}

결과로 아래처럼 특정 개수만 실제 DOM에 그려지도록 가상화할 수 있었다!

AnimatedImage.gif