반응 창을 사용하여 대규모 목록 가상화

매우 큰 테이블과 목록은 사이트의 성능을 크게 저하시킬 수 있습니다. 가상화가 도움이 될 수 있습니다.

react-window는 대규모 목록을 효율적으로 렌더링할 수 있는 라이브러리입니다.

다음은 react-window로 렌더링되는 1,000개의 행이 포함된 목록의 예입니다. 최대한 빨리 스크롤해 보세요.

이것이 왜 유용할까요?

행이 많은 큰 표나 목록을 표시해야 할 때가 있습니다. 이러한 목록의 모든 항목을 로드하면 성능에 상당한 영향을 미칠 수 있습니다.

목록 가상화 또는 '창'은 사용자에게 표시되는 항목만 렌더링한다는 개념입니다. 처음에 렌더링되는 요소 수는 전체 목록의 매우 작은 하위 집합이며 사용자가 계속 스크롤하면 표시되는 콘텐츠의 '창'이 이동합니다. 이렇게 하면 목록의 렌더링 및 스크롤 성능이 모두 개선됩니다.

가상화된 목록의 콘텐츠 창
가상화된 목록에서 콘텐츠의 '창' 이동

'창'을 종료하는 DOM 노드는 재활용되거나 사용자가 목록을 아래로 스크롤할 때 즉시 최신 요소로 대체됩니다. 이렇게 하면 렌더링된 모든 요소의 수가 창 크기에 따라 유지됩니다.

react-window

react-window는 애플리케이션에서 가상화된 목록을 더 쉽게 만들 수 있는 작은 서드 파티 라이브러리입니다. 다양한 유형의 목록과 표에 사용할 수 있는 여러 기본 API를 제공합니다.

고정 크기 목록을 사용해야 하는 경우

크기가 동일한 항목이 긴 일차원 목록인 경우 FixedSizeList 구성요소를 사용하세요.

import React from 'react';
import { FixedSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
 
<div style={style}>
     
{/* define the row component using items[index] */}
 
</div>
);

const ListComponent = () => (
  <FixedSizeList
    height={500}
    width={500}
    itemSize={120}
    itemCount={items.length}
  >
    {Row}
  </
FixedSizeList>
);

export default ListComponent;
  • FixedSizeList 구성요소는 height, width, itemSize 속성을 사용하여 목록 내 항목의 크기를 제어합니다.
  • 행을 렌더링하는 함수가 FixedSizeList에 하위 요소로 전달됩니다. 특정 항목에 관한 세부정보는 index 인수(items[index])를 사용하여 액세스할 수 있습니다.
  • style 매개변수도 행 요소에 연결되어야 하는 행 렌더링 메서드에 전달됩니다. 목록 항목은 높이 및 너비 값이 인라인으로 할당된 절대 위치 지정되며 style 매개변수가 이를 담당합니다.

이 도움말 앞부분에 표시된 Glitch 예는 FixedSizeList 구성요소의 예를 보여줍니다.

크기가 가변적인 목록을 사용해야 하는 경우

VariableSizeList 구성요소를 사용하여 크기가 다른 항목 목록을 렌더링합니다. 이 구성요소는 고정 크기 목록과 동일한 방식으로 작동하지만 특정 값 대신 itemSize 속성의 함수를 예상합니다.

import React from 'react';
import { VariableSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
 
<div style={style}>
     
{/* define the row component using items[index] */}
 
</div>
);

const getItemSize = index => {
  /
/ return a size for items[index]
}

const ListComponent = () => (
 
<VariableSizeList
    height
={500}
    width
={500}
    itemCount
={items.length}
    itemSize
={getItemSize}
 
>
   
{Row}
 
</VariableSizeList>
);

export default ListComponent;

다음 삽입은 이 구성요소의 예시를 보여줍니다.

이 예에서는 itemSize 속성에 전달된 항목 크기 함수가 행 높이를 무작위로 지정합니다. 그러나 실제 애플리케이션에서는 각 항목의 크기를 정의하는 실제 로직이 있어야 합니다. 이러한 크기는 데이터를 기반으로 계산하거나 API에서 가져오는 것이 좋습니다.

격자

react-window는 다차원 목록 또는 그리드의 가상화를 지원합니다. 이 맥락에서 표시되는 콘텐츠의 '창'은 사용자가 가로 세로로 스크롤할 때 변경됩니다.

가상화된 그리드에서 콘텐츠 창을 이동하는 것은 2차원입니다.
가상화된 그리드에서 콘텐츠의 '창'을 이동하는 것은 2차원입니다.

마찬가지로 특정 목록 항목의 크기가 달라질 수 있는지에 따라 FixedSizeGridVariableSizeGrid 구성요소를 모두 사용할 수 있습니다.

  • FixedSizeGrid의 경우 API는 거의 동일하지만 높이, 너비, 항목 수가 열과 행 모두에 표시되어야 합니다.
  • VariableSizeGrid의 경우 각 프로퍼티에 값 대신 함수를 전달하여 열 너비와 행 높이를 모두 변경할 수 있습니다.

가상화된 그리드의 예는 문서를 참고하세요.

스크롤 시 지연 로드

많은 웹사이트는 사용자가 아래로 스크롤할 때까지 긴 목록의 항목을 로드하고 렌더링하기 위해 기다려 성능을 개선합니다. '무한 로드'라고 일반적으로 불리는 이 기법은 사용자가 끝에 가까운 특정 임곗값을 지나 스크롤할 때 목록에 새 DOM 노드를 추가합니다. 이는 목록의 모든 항목을 한 번에 로드하는 것보다 낫지만 사용자가 그보다 더 많이 스크롤한 경우에도 DOM이 수천 개의 행 항목으로 채워집니다. 이로 인해 DOM 크기가 지나치게 커져 스타일 계산 및 DOM 변형이 느려지면서 성능에 영향을 미치기 시작합니다.

다음 다이어그램은 이를 요약하는 데 도움이 될 수 있습니다.

일반 목록과 가상화된 목록의 스크롤 차이
일반 목록과 가상화된 목록의 스크롤 차이

이 문제를 해결하는 가장 좋은 방법은 react-window와 같은 라이브러리를 계속 사용하여 페이지에 작은 요소 '창'을 유지하되 사용자가 아래로 스크롤할 때 더 새로운 항목을 지연 로드하는 것입니다. 별도의 패키지인 react-window-infinite-loader를 사용하면 react-window로 이를 실행할 수 있습니다.

상위 App 구성요소에서 관리되는 상태의 예를 보여주는 다음 코드를 살펴보세요.

import React, { Component } from 'react';

import ListComponent from './ListComponent';

class App extends Component {
  constructor
(props) {
   
super(props);
   
this.state = {
      items
: [], // instantiate initial list here
      moreItemsLoading
: false,
      hasNextPage
: true
   
};

   
this.loadMore = this.loadMore.bind(this);
 
}

  loadMore
() {
   
// method to fetch newer entries for the list
 
}

  render
() {
   
const { items, moreItemsLoading, hasNextPage } = this.state;

   
return (
     
<ListComponent
        items
={items}
        moreItemsLoading
={moreItemsLoading}
        loadMore
={this.loadMore}
        hasNextPage
={hasNextPage}
     
/>
   
);
 
}
}

export default App;

loadMore 메서드는 무한 로더 목록이 포함된 하위 ListComponent에 전달됩니다. 이는 사용자가 특정 지점을 지나 스크롤하면 무한 로더가 콜백을 실행하여 더 많은 항목을 로드해야 하므로 중요합니다.

목록을 렌더링하는 ListComponent의 모습은 다음과 같습니다.

import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";

const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
 
const Row = ({ index, style }) => (
     
{/* define the row component using items[index] */}
 
);

 
const itemCount = hasNextPage ? items.length + 1 : items.length;

 
return (
   
<InfiniteLoader
      isItemLoaded
={index => index < items.length}
      itemCount
={itemCount}
      loadMoreItems
={loadMore}
   
>
     
{({ onItemsRendered, ref }) => (
       
<FixedSizeList
          height
={500}
          width
={500}
          itemCount
={itemCount}
          itemSize
={120}
          onItemsRendered
={onItemsRendered}
         
ref={ref}
       
>
         
{Row}
       
</FixedSizeList>
      )}
  </
InfiniteLoader>
 
)
};

export default ListComponent;

여기서 FixedSizeList 구성요소는 InfiniteLoader 내에 래핑됩니다. 로더에 할당된 속성은 다음과 같습니다.

  • isItemLoaded: 특정 항목이 로드되었는지 확인하는 메서드입니다.
  • itemCount: 목록의 항목 수 (또는 예상 수)
  • loadMoreItems: 목록의 추가 데이터로 확인되는 약속을 반환하는 콜백

렌더 프로퍼티는 목록 구성요소가 렌더링하는 데 사용하는 함수를 반환하는 데 사용됩니다. onItemsRenderedref 속성은 모두 전달해야 하는 속성입니다.

다음은 가상화된 목록에서 무한 로드가 작동하는 방식을 보여주는 예입니다.

목록을 아래로 스크롤해도 동일하게 느껴질 수 있지만, 이제 목록의 끝부분으로 스크롤할 때마다 random user API에서 사용자 10명을 검색하도록 요청이 전송됩니다. 이 모든 작업은 한 번에 하나의 결과 '창'만 렌더링하는 동안 실행됩니다.

특정 항목의 index를 확인하면 최신 항목에 대한 요청이 이루어졌는지, 항목이 아직 로드 중인지에 따라 항목에 다른 로드 상태가 표시될 수 있습니다.

예를 들면 다음과 같습니다.

const Row = ({ index, style }) => {
 
const itemLoading = index === items.length;

 
if (itemLoading) {
     
// return loading state
 
} else {
     
// return item
 
}
};

오버스캔

가상화된 목록의 항목은 사용자가 스크롤할 때만 변경되므로 더 새로운 항목이 표시될 때 빈 공간이 잠시 깜박일 수 있습니다. 이 가이드의 이전 예시를 빠르게 스크롤해 보면 이를 확인할 수 있습니다.

가상화된 목록의 사용자 환경을 개선하기 위해 react-windowoverscanCount 속성으로 항목을 오버스캔할 수 있도록 허용합니다. 이렇게 하면 항상 렌더링할 표시되는 '창' 외부의 항목 수를 정의할 수 있습니다.

<FixedSizeList
 
//...
 
overscanCount={4}
>
  {...}
</FixedSizeList>

overscanCountFixedSizeListVariableSizeList 구성요소 모두에 작동하며 기본값은 1입니다. 목록의 크기와 각 항목의 크기에 따라 항목을 2개 이상 오버스캔하면 사용자가 스크롤할 때 빈 공간이 눈에 띄게 깜박이는 것을 방지할 수 있습니다. 하지만 항목을 너무 많이 오버스캔하면 성능에 부정적인 영향을 줄 수 있습니다. 가상화된 목록을 사용하는 이유는 사용자가 특정 시점에 볼 수 있는 항목 수를 최소화하기 위함이므로 오버스캔된 항목 수를 최대한 적게 유지하세요.

FixedSizeGridVariableSizeGrid의 경우 overscanColumnsCountoverscanRowsCount 속성을 사용하여 각각 오버스캔할 열과 행의 수를 제어합니다.

결론

애플리케이션에서 목록과 테이블을 가상화할 위치를 잘 모르겠다면 다음 단계를 따르세요.

  1. 렌더링 및 스크롤 성능을 측정합니다. 이 도움말에서는 Chrome DevTools의 FPS 미터를 사용하여 목록에서 항목이 얼마나 효율적으로 렌더링되는지 살펴보는 방법을 보여줍니다.
  2. 성능에 영향을 미치는 긴 목록이나 그리드에는 react-window을(를) 포함합니다.
  3. react-window에서 지원되지 않는 특정 기능이 있는 경우 이 기능을 직접 추가할 수 없는 경우 react-virtualized를 사용하는 것이 좋습니다.
  4. 사용자가 스크롤할 때 항목을 지연 로드해야 하는 경우 가상화된 목록을 react-window-infinite-loader로 래핑합니다.
  5. 목록에는 overscanCount 속성을, 그리드에는 overscanColumnsCountoverscanRowsCount 속성을 사용하여 빈 콘텐츠가 표시되는 것을 방지합니다. 항목을 너무 많이 오버스캔하면 성능에 부정적인 영향을 미치므로 주의하세요.