본문 바로가기
공부/React & React Native

[React] Windowing으로 렌더링 성능 최적화하기

by Piva 2024. 6. 2.
  • 최근 렌더링 이슈를 경험했었다. 수 천개쯤 되는 다수의 데이터를 한 번에 렌더링했기에 발생하는 이슈였다. 이 이슈를 통해 알게 된 Windowing 기법과, 이를 구현할 수 있게 해주는 라이브러리에 대해 기록한다.

Windowing 기법이란?

  React 공식 페이지에서도 소개하고 있는 Windowing 기법이란, 간단히 말해 전체 목록의 일부만 렌더링하는 기법이다.

 

  수백, 혹은 수천개의 항목을 한 번에 렌더링 하는 건 큰 부하를 일으킬 뿐더러, 페이지에 보이지 않는 요소까지 렌더링하는 경우가 많기 때문에 비효율적이다. 따라서 Windowing 기법은 화면에 보이지 않는 요소들을 렌더링에서 제외하고 일부만 렌더링함으로써 성능을 최적화할 수 있게 해준다.

 

  사실 이 개념은 아주 어렴풋하게 알고 있었는데, 그 이유는 React Native의 FlatList (VirtualizedList)에서 이미 이 개념을 채용하고 있기 때문이다.

 

  VirtualizedList 문서를 살펴보면 아래와 같이 서술하고 있다.

  Virtualization massively improves memory consumption and performance of large lists by maintaining a finite render window of active items and replacing all items outside of the render window with appropriately sized blank space. The window adapts to scrolling behavior, and items are rendered incrementally with low-pri (after any running interactions) if they are far from the visible area, or with hi-pri otherwise to minimize the potential of seeing blank space.

  

  이를 요약하면: 

  • VirtualizedList는 메모리 사용량과 성능을 개선하기 위해 화면 상에 보이는 항목만 렌더링한다.
  • 스크롤에 따라 렌더링 되는 영역(Window)이 조정되며, 이에 가까운 항목들이 렌더링에 있어 높은 우선순위를, 먼 항목은 낮은 우선순위를 갖게 된다.

 

  ...이렇듯 대강은 알고 있던 개념이었지만 실제로 적용해본 적은 없었는데, Windowing을 적용할 수 있게 해주는 라이브러리가 존재한다는 것을 알았다.

 

 

 React에서 Windowing 사용하기(feat. react-virtualized)

    React에서 Windowing을 사용할 수 있게 도와주는 다양한 라이브러리가 존재한다.

 

  • react-virtualized
  • react-virtuoso
  • react-window
  • ...

 

  이번에 접한 라이브러리는 react-virtualized인데, 많이 사용되는 라이브러리인 동시에 다양한 기능을 제공한다(ex. 동적 크기 조정 등). 단점으로는 번들 크기가 다른 라이브러리에 비해 크다는 것.

 

  라이브러리 복습 겸 간단한 예제를 하나 만들어서 들고와봤다. (지난 Electron 예제를 열심히 써먹었다:) )

// App.tsx
// 중략

return (
  <div className="App">
    <h2>Todo List</h2>
    <div>
      {data != null && data.map((el: TodoItem, index) => {
        return (
          <TodoComponent key={`${el.title} ${index}`} {...el} id={index} onToggle={onToggleTodo} />
        );
      })}
    </div>
  </div>
);

 

수많은 요소가 한 번에 렌더링 되어있다.

 

  5천 건 정도의 더미 데이터를 만들고 map으로 띄우는 간단한 예제이다. 화면 상에는 보이지 않는 수많은 컴포넌트들이 동시에 렌더링 되고 있어, 개발자 도구를 조작하면 약간의 버벅임이 생길 정도였다.

 

  이제 이것을 최적화 하기 위해 react-virtualized를 사용해본다.

 

 

react-virtualized 적용하기

  리스트를 렌더링 하기 위해서는 react-virtualized의 List 컴포넌트를 사용하면 된다. List 컴포넌트는 아래의 prop을 필수로 전달해야 한다.

 

height 리스트 컴포넌트의 높이. 리스트 내에 얼만큼의 행이 렌더링 될지를 결정한다.
rowCount 리스트에 출력 될 행의 수.
rowHeight 고정된 행의 높이 값. 혹은 행의 높이를 반환하는 함수.
rowRenderer 각 행을 렌더링하는 함수. 이에 대해선 밑에서 좀 더 정리한다.
width 리스트 컴포넌트의 너비.

 

 

rowRenderer

  공식 문서에 따르면 Virtualized list에서는 성능을 위해 rowRenderer를 사용하여 각 행을 렌더링한다. rowRenderer는 아래와 같은 몇 가지의 패러미터를 받는다.

index 각 행의 index.
isScrolling 현재 리스트가 스크롤 되고 있는지, 아닌지의 여부를 가리킨다.
isVisible 현재의 행이 리스트에서 보이고 있는지, 아닌지를 가리킨다.
key 렌더링된 행 내부에서의 유니크한 키.
parent 부모 리스트 reference.
style 각 행에 적용되는 style 객체. 렌더링 될 행 요소에 반드시 넘겨져야 한다!

 

  이를 참고하여 List를 위의 코드에 적용하면 아래와 같아진다.

 

import { List, ListRowRenderer } from 'react-virtualized';

// 중략

const rowRenderer: ListRowRenderer = ({ index, isScrolling, isVisible, parent, style, key }) => {
  if (data == null) return;
  const el = data[index];
  return (
    <TodoComponent key={key} {...el} id={index} onToggle={onToggleTodo} style={style} />
  );
};

return (
  <div className="App">
    <h2>Todo List</h2>
    <div>
      {data != null && (
        <List
          height={820}
          rowCount={data.length}
          rowHeight={50}
          rowRenderer={rowRenderer}
          width={400}
        />
      )}
    </div>
  </div>
);
  • rowRenderer 함수를 통해 key와 style, index를 Todo 컴포넌트로 넘겨준다.
  • key는 React에서 배열 내 아이템을 특정할 수 있게 하는 중요한 수단이므로, 꼭 넘겨주도록 한다.
    (참고: https://react.dev/learn/rendering-lists)
  • style 또한 위에서 강조했듯, 리스트 내 각 행의 위치를 잡아주는 역할을 하기 때문에 꼭 넘겨주어야 한다.

 

react-virtualized로 windowing이 적용된 모습

 

  적용이 완료된 후 개발자 도구로 리스트를 살펴보면, 적용 전과 달리 일부의 컴포넌트만이 렌더링되어있는 것을 확인할 수 있을 뿐 아니라, 스크롤에 따라 컴포넌트가 업데이트 되는 것을 확인할 수 있다.

 

  참고로 위에서 style을 넣는 것을 몇 번이고 강조했는데... 이는 style을 전달하지 않을 경우 리스트가 깨지기 때문이다.

 

style이 적용 안 된 모습

 

  위는 style을 빼먹었을 때의 모습인데, 스크롤을 내릴 때 심한 깜빡거림과 함께 리스트가 정상적으로 갱신되지 않는 모습을 보인다. 문서를 찬찬히 읽어보고 style의 전달이 필수임을 깨닫고는 금방 수정할 수 있었다.

 

 

유동적인 크기의 리스트 렌더링?

  위에서는 고정된 높이/너비 값을 가지는 컴포넌트로 구성된 목록을 렌더링하였지만, react-virtualized의 AutoSizer & CellMeasurer를 사용하면 고정되지 않은 크기의 컴포넌트로 구성된 목록 또한 windowing을 사용해 렌더링 하는 것이 가능하다.

 

  다만 CellMeasurer 문서에서도 나와있듯, 리스트 내 컴포넌트의 높이나 너비를 하나하나 계산하기 때문에 필연적으로 성능 저하가 발생할 수 밖에 없다. 예시로 사용자가 한 번에 많은 수의 행이나 열을 스크롤링 할 때 느려질 수 밖에 없다고.

 


 

예상보다 24배 많은 콘텐츠에 프론트가 대처하는 법 - 리디주식회사

리디는 매년 연말 한 번에 많은 종수의 작품을 할인 판매하는 마크다운 이벤트를 진행합니다. 2022년 마크다운 이벤트는 총 6,107개의 작품을 가지고 진행되었는데요. 이 이벤트를 개선하는 과정

ridicorp.com

  • 마침 이번 이슈와 관련된 글이라 흥미롭게 읽었다. 이번 기회에 렌더링 최적화의 중요성을 뼈저리게 느꼈기도 하고, 언젠가 또 목록 렌더링을 해야 할 날엔 적극적으로 Windowing을 도입해보기로...

'공부 > React & React Native' 카테고리의 다른 글

React-Query 찍어먹기  (1) 2024.03.04
React Testing에 대하여  (0) 2023.11.22
Notifee 정리  (1) 2023.05.20
[0814 ~ 0815] React-Native 강의 정리  (0) 2021.08.16
[0812~0813] React-Native 강의 정리  (0) 2021.08.14