컴포넌트 성능 최적화

taehyung·2023년 10월 13일

React.js

목록 보기
13/24

화면에서 사용하는 컴포넌트의 갯수가 많아지고, 한 번의 이벤트에 리렌더링되는 컴포넌트가 많아질수록 성능은 안좋아지게 됩니다.

체크를 할때마다 모든 리스트 아이템이 리렌더링 된다고 생각하면 어떤가요 즉각적인 반응이 오지않고 지연이 발생하게 됩니다.

할 일 버튼 클릭 을하여 체크박스를 활성화시키면, 아래와같이 렌더링 됩니다.

수많은 렌더링을 진행하였고 렌더링에 걸린 시간은 143.4ms 입니다.


렌더링 횟수 줄이기

클래스형 컴포넌트의 shouldComponentUpdate 라이프 사이클 기억하시나요? 리렌더링트리거가 발생하고 업데이트를 해야하는지에대한 논리값을 반환하는 함수였습니다.

함수형 컴포넌트에서는 React.memo 라는 함수가 이 역할을 합니다.
React.memo 는 props의 데이터가 변경되지 않았다면 리렌더링하지 않도록 해주는 함수입니다.

위와같은 구조의 TodoList 컴포넌트가 있다면, 현재 성능저하의 원인인 TodoItemList.js 컴포넌트를 React.memo 시켜줍니다.

export default React.memo(TodoListItem);

그러나 메모 하나만으로 성능이 최적화되지는 않습니다.

이제 TodoListItem 컴포넌트는 todo, onRemove, onToggle 이 변겅되지 않으면 리렌더링을 하지 않습니다.

하지만 현재 onRemove,onInsert,onToggle 함수는 디펜던시 ( 의존성 배열 )에 todos 를 가지고있습니다.

즉, todos state 의 상태가 변경되면 함수를 새로 만든다는 이야기입니다.

함수가 새로 만들어지는 상황을 방지하는 방법은 두 가지입니다.

첫번째, useState의 set함수를 함수형 업데이트로 진행한다.
두번째, useReduser를 사용한다.

useState의 함수형 업데이트는 set함수의 첫번째 파라미터는 현재 state의 값을 참조하고 있습니다. 규칙이고 이걸 이용한다면 함수의 디펜던시로 최신 state를 추적하지 않아도 된다는 말입니다.

const [number,setNumber] = useState(0);

const increase = () => {
	setNumber(number+1)
    setNumber((prevNum)=>{ prevNum + 1}) // 함수형 업데이트
}

단! useCallback, useMemo 등 디펜던시배열이 들어가는 함수 내에서 사용되는 props와 state는 반드시 디펜던시배열에 넣어야한다고 했습니다.

set함수는 최신 state값을 가지는 파라미터를 가지고있으므로 디펜던시에서 빼도 되는것입니다. 단순히 화면에 표현만 하는 경우에는 최신 state를 추적할 방법이 디펜던시밖에 없기에 무조건 디펜던시에 넣어야합니다. 규칙입니다.

제가 만든 TodoList 의 성능 차이를 예로 들어보겠습니다.

React.memo + useCallback 의 리렌더링 방지로 얻은 효과입니다.

167ms -> 4.3ms

드라마틱하죠? 사진을 자세히보시면 회색 박스들이 보이나요? react.memo 의 효과로 리렌더링되지 않은 컴포넌트를 나타냅니다.

useReducer 를 사용해도 같은 효과를 낼 수있습니다.

useReducer 는 언제 사용해야하나요??
useState 로 관리하는 상태를 업데이트하는 setState 함수의 사용이 잦아지면 고민해볼만 합니다. 단순히 true or false 의 간단한조작만 하는 상태의 경우는 당연히 state가 유리하겠죠?


불변성의 중요성

불변성이란? 기존값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 불변성을 지킨다고 합니다.

불변성이란 결국 참조하는 메모리의 값을 다르게 만들어준다는 것입니다.

const arr = [1,2,3,4]
const arrr = arr 
arr === arrr//true

arrr[0] = '1'

arr // ['1',2,3,4]
arrr // ['1',2,3,4]

//사본과 원본이 같은메모리 주소를 바라보기 때문에 사본수정시 원본도 수정 불변성X

const arr = [1,2,3,4]
const arrr = [...arr] //새로운 배열상자를 만들고, 그안에 기존 배열 풀어헤치기
arr===arrr //false

arrr[0] = '1'

arr // [1,2,3,4]
arrr // ['1',2,3,4]

리액트는 참조형 데이터의 데이터 변경을 감지하는 것을 최상단 [],{} 에서 진행합니다. 즉 2차원배열과 {a:{},b:{}} 객체 내부의 객체의 데이터 변화를 감지하지 않는다는 것입니다.

그래서 리액트에게 참조형 데이터 타입의 업데이트로 인한 리렌더링을 원할때에는반드시 최상단 배열과 객체의 메모리 주소를 변경해주어야 합니다.

참조형 데이터타입의 리렌더링의 방법은 위와 같습니다.

그럼 다중참조형 데이터타입의 불변성은 어떻게 유지할까요?

스프레드 연산자 ... 는 얕은 복사본을 가집니다. 즉 2차원 배열의 데이터를 변경하거나 객체내부의 객체의 데이터를 변경한다면 이또한 원본 배열과 객체의 값을 변경시켜버립니다.

const obj = {
  innerObj : {
    a:1,
    b:2
  }
}

const newObj = {
  ...obj,
  	{
  ...innerObj
	}
}

객체의 불변성을 완벽히 하려면 위와같이 하나하나 풀어헤쳐줘야합니다. 객체와 배열의 구조가 복잡해지면 복잡해질수록 난이도가 올라가겠죠?

이런 경우를 편하게 만들어주는 immer 라는 라이브러리가 있습니다. 추후에알아보겠습니다.


react-virtualized 를 사용한 렌더링 최적화
react-virtualized 는 React 기반의 가상 스크롤 라이브러리로, 대량의 데이터를 렌더링할 때 성능을 향상시키는 데 도움을 주는 도구입니다. 이 라이브러리는 긴 목록이나 그리드와 같이 많은 항목을 표시할 때, 화면에 현재 보이는 항목만 렌더링하고 스크롤 동작에 따라 동적으로 업데이트합니다. 이는 페이지 로딩 시 모든 항목을 렌더링하는 것보다 효율적인 방식으로 대규모 목록을 다루는 데 유용합니다.

예제

import React from 'react';
import { List } from 'react-virtualized';
import 'react-virtualized/styles.css'; // 스타일을 불러옵니다.

function VirtualizedListExample() {
  // 가상 스크롤에 사용할 항목의 데이터 배열
  const listData = Array.from({ length: 1000 }, (v, i) => `Item ${i + 1}`);

  // 항목을 렌더링하는 함수
  const rowRenderer = ({ key, index, isScrolling, isVisible, style }) => {
    return (
      <div key={key} style={style}>
        {listData[index]}
      </div>
    );
  };

  return (
    <List
      width={300}
      height={400}
      rowCount={listData.length}
      rowHeight={30}
      rowRenderer={rowRenderer}
    />
  );
}

export default VirtualizedListExample;
profile
Front End

0개의 댓글