icon안동민 개발노트

대규모 리스트 렌더링 최적화


 대규모 리스트를 렌더링할 때 성능 최적화는 매우 중요합니다.

 이 실습에서는 다양한 최적화 기법을 적용하여 효율적인 리스트 렌더링 방법을 알아보겠습니다.

1. 기본 리스트 컴포넌트 구현

 먼저, 최적화되지 않은 기본 리스트 컴포넌트를 만들어봅시다.

import React, { useState } from 'react';
 
function ListItem({ item }) {
  return <div>{item.text}</div>;
}
 
function LargeList({ items }) {
  return (
    <div>
      {items.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </div>
  );
}
 
function App() {
  const [items] = useState(() => 
    Array.from({ length: 10000 }, (_, index) => ({
      id: index,
      text: `Item ${index}`
    }))
  );
 
  return <LargeList items={items} />;
}

 이 기본 구현은 10,000개의 아이템을 한 번에 모두 렌더링하므로 성능 문제가 발생할 수 있습니다.

2. 가상화(Virtualization) 적용

 react-window 라이브러리를 사용하여 가상화를 구현해 보겠습니다.

npm install react-window
import React, { useState } from 'react';
import { FixedSizeList as List } from 'react-window';
 
const Row = ({ index, style, data }) => (
  <div style={style}>
    {data[index].text}
  </div>
);
 
function VirtualizedList({ items }) {
  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={35}
      width={300}
      itemData={items}
    >
      {Row}
    </List>
  );
}
 
function App() {
  const [items] = useState(() => 
    Array.from({ length: 10000 }, (_, index) => ({
      id: index,
      text: `Item ${index}`
    }))
  );
 
  return <VirtualizedList items={items} />;
}

 이제 화면에 보이는 아이템만 렌더링되므로 성능이 크게 향상됩니다.

3. 리스트 아이템 메모이제이션

 React.memo를 사용하여 리스트 아이템을 메모이제이션합니다.

const Row = React.memo(({ index, style, data }) => (
  <div style={style}>
    {data[index].text}
  </div>
));

 이렇게 하면 각 아이템의 내용이 변경되지 않는 한 리렌더링되지 않습니다.

4. useCallback과 useMemo 활용

 부모 컴포넌트에서 전달하는 함수와 데이터를 최적화합니다.

import React, { useState, useCallback, useMemo } from 'react';
import { FixedSizeList as List } from 'react-window';
 
const Row = React.memo(({ index, style, data }) => {
  const { items, onItemClick } = data;
  return (
    <div style={style} onClick={() => onItemClick(items[index].id)}>
      {items[index].text}
    </div>
  );
});
 
function VirtualizedList({ items }) {
  const onItemClick = useCallback((id) => {
    console.log(`Clicked item ${id}`);
  }, []);
 
  const itemData = useMemo(() => ({
    items,
    onItemClick
  }), [items, onItemClick]);
 
  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={35}
      width={300}
      itemData={itemData}
    >
      {Row}
    </List>
  );
}

 useCallback으로 onItemClick 함수를 메모이제이션하고, useMemoitemData 객체를 메모이제이션하여 불필요한 리렌더링을 방지합니다.

5. 성능 비교 및 분석

 React Developer Tools의 Profiler를 사용하여 최적화 전후의 성능을 비교합니다.

  1. 최적화 전 : 초기 렌더링 시간이 길고, 스크롤 시 프레임 드롭이 발생할 수 있습니다.
  2. 가상화 적용 후 : 초기 렌더링 시간이 크게 단축되고, 스크롤이 부드러워집니다.
  3. 메모이제이션 적용 후 : 아이템 클릭 등의 상호작용 시 리렌더링이 최소화됩니다.

 Profiler를 사용하여 렌더링 시간, 커밋 횟수 등을 측정하고 비교합니다.

6. 추가 최적화 팁

  1. 청크 로딩 : 대량의 데이터를 한 번에 로드하지 않고, 필요한 만큼만 청크로 나누어 로드합니다.
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
 
const loadMoreItems = useCallback(() => {
  setIsLoading(true);
  // 추가 아이템 로드 로직
  setIsLoading(false);
}, []);
 
// 리스트의 끝에 도달했을 때 loadMoreItems 호출
  1. 데이터 정규화 : 중복 데이터를 제거하고 플랫한 구조로 데이터를 저장하여 업데이트를 최적화합니다.
  2. 불변성 유지 : 상태 업데이트 시 불변성을 유지하여 React의 비교 알고리즘 효율을 높입니다.

완성된 대규모 리스트 컴포넌트

import React, { useState, useCallback, useMemo } from 'react';
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
 
const Row = React.memo(({ index, style, data }) => {
  const { items, onItemClick } = data;
  return (
    <div 
      style={{
        ...style,
        display: 'flex',
        alignItems: 'center',
        padding: '0 10px',
        borderBottom: '1px solid #eee'
      }} 
      onClick={() => onItemClick(items[index].id)}
    >
      {items[index].text}
    </div>
  );
});
 
function OptimizedLargeList({ items }) {
  const onItemClick = useCallback((id) => {
    console.log(`Clicked item ${id}`);
  }, []);
 
  const itemData = useMemo(() => ({
    items,
    onItemClick
  }), [items, onItemClick]);
 
  return (
    <AutoSizer>
      {({ height, width }) => (
        <List
          height={height}
          itemCount={items.length}
          itemSize={35}
          width={width}
          itemData={itemData}
        >
          {Row}
        </List>
      )}
    </AutoSizer>
  );
}
 
function App() {
  const [items] = useState(() => 
    Array.from({ length: 10000 }, (_, index) => ({
      id: index,
      text: `Item ${index}`
    }))
  );
 
  return (
    <div style={{ height: '100vh' }}>
      <OptimizedLargeList items={items} />
    </div>
  );
}
 
export default App;

 이 최종 버전에서는 다음과 같은 최적화 기법이 적용되었습니다.

  1. 가상화 : react-window를 사용하여 보이는 아이템만 렌더링합니다.
  2. 메모이제이션 : React.memo로 Row 컴포넌트를 최적화합니다.
  3. useCallback : 아이템 클릭 핸들러를 메모이제이션합니다.
  4. useMemo : itemData 객체를 메모이제이션하여 불필요한 리렌더링을 방지합니다.
  5. AutoSizer : 컨테이너 크기에 따라 리스트 크기를 자동으로 조정합니다.

 이러한 최적화 기법들을 적용함으로써, 대규모 리스트를 효율적으로 렌더링하고 부드러운 사용자 경험을 제공할 수 있습니다. 실제 애플리케이션에서는 데이터 로딩, 상태 관리, 에러 처리 등 추가적인 고려사항이 있을 수 있으며, 이러한 요소들도 성능에 영향을 미칠 수 있으므로 종합적인 접근이 필요합니다.

 성능 최적화는 지속적인 과정이며, 실제 사용자 경험과 측정된 성능 지표를 바탕으로 계속해서 개선해 나가야 합니다. React Developer Tools의 Profiler를 정기적으로 사용하여 성능 병목 지점을 식별하고, 필요에 따라 추가적인 최적화를 적용하는 것이 중요합니다.