[React]React.memo()를 통한 성능 최적화

Jay·2022년 4월 28일
1

※ 다음 글은 해당 포스트의 번역본입니다.

리액트는 별도로 성능 최적화를 시키지 않아도 자체적으로 성능을 어느 정도 최적화합니다. React.memo는 렌더링 되는 리액트 컴포넌트들의 최적화가 더 잘 이뤄지도록 도와줍니다.

이 글에서는 React.memo를 활용하여 리액트의 성능을 향상시키는 방법과, 당신이 맞닥뜨릴 수 있는 흔한 에러들, 그리고 React.memo를 사용해선 안 되는 경우에 대해 알아볼 것입니다.

목차

1. React.memo란 무엇인가?

React.memo는 순수 함수 컴포넌트와 훅의 렌더링 성능을 향상시키는 기능입니다.

Memo는 메모이제이션(memoization)에서 유래됐습니다. React.memo로 감싸진 함수의 결과값은 메모리에 저장되는데, 같은 인풋값으로 해당 함수를 재호출할 시, 저장해 둔 결과값을 반환해 줍니다.

순수 함수에 사용되기 때문에, 만일 인수가 바뀌지 않는다면, 결과값 또한 바뀌지 않습니다. React.memo는 함수로 하여금 이러한 경우에서의 실행을 방지합니다.

링크에서 시각화 된 예시를 볼 수 있습니다.

파란색 컴포넌트는 세 개의 하위 컴포넌트를 가지고 있습니다. 한 개의 인풋란과 2개의 검정색 타일이 있습니다. 노란색은 각 컴포넌트의 페인트 개수를 나타냅니다.

인풋란에 무언가를 입력하면, 키보드를 입력할 때마다 왼쪽의 검정색 컴포넌트와 파란색 컴포넌트의 내용은 변경되지만 오른쪽 컴포넌트는 변경되지 않는 걸 볼 수 있습니다.

오른쪽 타일은 프롭스가 바뀌지 않는 경우 렌더링 되는 것을 막기 위해 React.memo로 감싸져 있습니다.

2. 사용법

기본 사용 방법은 다음과 같습니다. 함수 컴포넌트를 React.memo함수로 감싸주면 됩니다.

const Tile = React.memo(() => {
  let eventUpdates = React.useRef(0);
  return (
    <div className="black-tile">
      <Updates updates={eventUpdates.current++} />
    </div>
  );
});

이러한 작은 변경은 컴포넌트의 렌더링 성능을 향상시키는 데에 도움을 주겠지만, 복잡한 컴포넌트 처리에 있어선 몇 가지 이슈들을 발생시킬 수 있습니다.

3. 흔한 에러

React.memo가 예상대로 작동하지 않는다구요? React.memo를 사용하기 시작하면서 마주할 수 있는 몇 가지 흔한 에러들을 소개합니다.

  • 객체 처리

    타일 컴포넌트로 객체 데이터를 넘겨 준다고 해 봅시다.
const App = () => {
  const updates = React.useRef(0);
  const [text, setText] = React.useState('');
  const data = { test: 'data' };

  return (
    <div className="app">
      <div className="blue-wrapper">
        <input
          value={text}
          placeholder="Write something"
          onChange={(e) => setText(e.target.value)}
        />
        <Updates updates={updates.current++} />
        <Tile />
        <TileMemo data={data} />
      </div>
    </div>
  );
};

별안간 React.memo가 작동을 멈추고, 키를 입력할 때마다 컴포넌트가 렌더링 되는 걸 볼 수 있습니다.

React.memo가 더 이상 작동하지 않는 이유는 이것이 컴포넌트 속성을 얕은 수준으로 비교하기 때문입니다. data 변수는 App이 업데이트 될 때마다 재 선언됩니다. 그러므로 이 객체는 이전과 다른 참조값을 갖게 되므로 이전과 같은 객체라고 볼 수 없게 됩니다.

  • areEqual로 해결하기

    React.memo는 두 번째 인자를 통해서 해당 문제를 위한 해결책을 제공합니다. 이 두 번째 인자는 areEqual이라는 함수를 받는데, 이는 컴포넌트를 언제 업데이트 해야할 지를 컨트롤 하는 데에 사용됩니다.
const TileMemo = React.memo(() => {
  let updates = React.useRef(0);
  return (
    <div className="black-tile">
      <Updates updates={updates.current++} />
    </div>
  );
}, (prevProps, nextProps) => {
  if (prevProps.data.test === nextProps.data.test) {
    return true; // 프롭스가 같다면
  }
  return false; // 프롭스가 다르다면 -> 컴포넌트를 업데이트
});
  • React.useMemo

위 문제에 대한 대안으로는 React.useMemo()를 사용해서 객체를 감싸는 방법이 있습니다. 이렇게 했을 경우, 변수를 메모이제이션하여 새로운 객체를 생성하지 않도록 합니다.

const data = React.useMemo(() => ({
  test: 'data',
}), []);

useMemo의 두 번째 인자는 변수의 의존성 배열입니다. 이들 중 하나라도 값이 변경되면, 리액트는 값을 재연산합니다.

예제에서는 빈 배열이므로 그러한 일은 일어나지 않을 겁니다.

  • 함수 처리

    자바스크립트에서의 함수는 우리가 이전에 겪었던 문제의 원인이 되는 객체처럼 작동합니다. onClick 함수는 App이 업데이트 될 때마다 선언됩니다. 그렇게 되면, TileMemo는 참조가 변경되었으므로 onClick도 변경됐다고 생각하게 되죠.
const App = () => {
  const updates = React.useRef(0);
  const [text, setText] = React.useState('');
  const onClick = () => {
    console.log('click');
  };

  return (
    <div className="app">
      <div className="blue-wrapper">
        <input
          value={text}
          placeholder="Write something"
          onChange={(e) => setText(e.target.value)}
        />
        <Updates updates={updates.current++} />
        <Tile />
        <TileMemo onClick={onClick} />
      </div>
    </div>
  );
};
  • React.useCallback

    우리가 객체와 관련된 문제를 해결한 방식과 같이 함수도 메모이제이션을 통해 해결할 수 있습니다.
const onClick = React.useCallback(() => {
  console.log('click');
}, []);

여기서의 두 번째 인자도 의존성 배열입니다. 배열 내의 데이터가 변경되는 경우 계산을 유발합니다.

4. 모든 곳에 React.memo를 사용하면 안 되는 이유?

당신은 React.memo에 아무 단점도 없기에 모든 함수 컴포넌트를 React.memo로 감싸야겠다고 생각할 수도 있습니다. 문제는 React.memo는 함수를 캐시에 저장한다는 겁니다. 이 말인 즉슨, 메모리에 결과값이 저장된다는 거죠.

이러한 다량 혹은 대량의 컴포넌트들을 저장하는 것은 더 많은 메모리 소비로 이어질 수 있습니다. 그러므로 크기가 큰 컴포넌트를 메모이제이션할 때는 주의해야 합니다.

컴포넌트의 프롭스가 자주 바뀌는 경우에도 Reacrt.memo의 사용을 주의해야 합니다. React.memo는 프롭스와 메모이제이션 된 프롭스를 비교하는 데에 추가적인 간접비을 소비합니다. 이는 성능에 크게 영향을 미칠 뿐만 아니라 React.memo를 통해 얻어낼 수 있는 성능 최적화 또한 잃게 되는 겁니다.

리액트는 이미 매우 효율적으로 랜더링 성능을 최적화합니다. 불필요한 리렌더링을 최적화하기 위해 시간을 낭비해서는 안 됩니다. 첫 번째 단계는 언제나 성능의 병목화 현상을 측정하고 확인하는 단계가 되어야 합니다. 어떤 컴포넌트가 가장 많이 렌더링 되는지 확인하기 이전에 리액트 앱의 개요를 파악하는 것도 좋은 아이디어입니다. 이러한 컴포넌트에 React.memo를 적용해야 큰 영향을 가질 수 있습니다.

profile
개발할래요💻

0개의 댓글