[React] React.memo() 현명하게 사용하기

원문: https://dmitripavlutin.com/use-react-memo-wisely

이 글은 언제 React.memo() 가 성능을 향상하는데 도움이 되는지, 또 언제 불필요한지 구분하는 데 도움이 될 것입니다.

React는 UI 성능을 증가시키기 위해 고차 컴퍼넌트(Higher Order Component, HOC) React.memo()를 제공하는데, 이것은 렌더링 결과를 메모이징(Memoizing)함으로써, 불필요한 리렌더링을 건너뛰는 방식을 사용합니다.

1. React.memo()

React는 먼저 컴퍼넌트를 렌더링(rendering) 한 뒤, 이전 렌더된 결과와 비교하여 만약 렌더 결과가 이전과 다르다면, React는 DOM을 업데이트를 진행합니다.

컴퍼넌트가 React.memo()로 래핑 될 때, React는 컴퍼넌트를 렌더링하고 결과를 메모이징(Memoizing)하여 다음 렌더링 때 props가 같다면 저장된 내용을 재사용합니다.

예시를 살펴보자. 함수형 컴퍼넌트 Movie가 React.memo()로 래핑 되어 있다.

export function Movie({ title, releaseDate }) {
  return (
    <div>
      <div>Movie title: {title}</div>
      <div>Release date: {releaseDate}</div>
    </div>
  );
}

export const MemoizedMovie = React.memo(Movie);

React.memo(Movie)는 새로 메모이징된 컴퍼넌트인 MemoizedMovie를 반환합니다. 만약 title이나 releaseData 같은 props가 변경 되지 않는다면 다음 렌더링 때 메모이징 된 내용을 그대로 사용하게 됩니.

// React는 MemoizedMovie 함수를 호출한다.
<MemoizedMovie
  movieTitle="Heat"
  releaseDate="December 15, 1995"
/>

// 다시 렌더링 할 때 React는 MemoizedMovie 함수를 호출하지 않는다.
// 리렌더링을 막는다.
<MemoizedMovie
  movieTitle="Heat"
  releaseDate="December 15, 1995"
/>

메모이징 한 결과를 재사용 함으로써, React에서 리렌더링을 할 때 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점을 누릴 수 있습니다.

1.1 props 동등 비교 커스터마이징

React.memo()는 props 혹은 props의 객체를 비교할 때 얕은(shallow) 비교 방식을 사용합니다.

(비교 방식을 수정하고 싶다면 React.memo() 두 번째 매개변수로 비교함수를 만들어 넘겨주면 된다.)

React.memo(Component, [areEqual(prevProps, nextProps)]);

areEqual(prevProps, nextProps) 함수는 prevProps와 nextProps가 같다면 true를 반환할 것이다.

예를들어 Movie의 props가 동일한지 수동으로 비교해보자.

function moviePropsAreEqual(prevMovie, nextMovie) {
  return (
    prevMovie.title === nextMovie.title &&
    prevMovie.releaseDate === nextMovie.releaseDate
  );
}

const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);

moviePropsAreEqual() 함수는 이전 props와 현재 props가 같다면 true를 반환할 것 입니다.

2. 언제 React.memo()를 써야할까

2.1 같은 props로 렌더링이 자주 일어나는 컴퍼넌트

React.memo()는 함수형 컴퍼넌트에 적용되어 같은 props에 같은 렌더링 결과를 제공합니다.

React.memo()를 사용하기 가장 좋은 케이스는 함수형 컴퍼넌트가 같은 props로 자주 렌더링 될거라 예상될 때이다.

(일반적으로 부모 컴퍼넌트에 의해 하위 컴퍼넌트가 같은 props로 리렌더링 될 때)

위에서 정의한 Movie를 다시 사용해서 예시를 들어보자. 여기 Movie의 부모 컴퍼넌트인 실시간으로 업데이트되는 영화 조회수를 나타내는 MovieViewsRealtime 컴퍼넌트가 있습니다.

function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <Movie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}

이 어플리케이션은 주기적(매초)으로 서버에서 데이터를 폴링(Polling)해서 MovieViewsRealtime 컴퍼넌트의 views를 업데이트한다.

// Initial render
<MovieViewsRealtime
  views={0}
  title="Forrest Gump"
  releaseDate="June 23, 1994"
/>

// After 1 second, views is 10
<MovieViewsRealtime
  views={10}
  title="Forrest Gump"
  releaseDate="June 23, 1994"
/>

// After 2 seconds, views is 25
<MovieViewsRealtime
  views={25}
  title="Forrest Gump"
  releaseDate="June 23, 1994"
/>

// etc

views가 새로운 숫자가 업데이트 될 때 마다 MoviewViewsRealtime 컴퍼넌트 또한 리렌더링 된다. 이때 Movie 컴퍼넌트 또한 title이나 releaseData가 같음에도 불구하고 리렌더링 됩니다.

이때가 Movie 컴퍼넌트에 메모이제이션을 적용할 적절한 케이스다.

MovieViewsRealtime에 메모이징된 컴퍼넌트인 MemoizedMovie를 대신 사용해 성능을 향상해보자.

function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <MemoizedMovie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}

title 혹은 releaseDate props가 같다면, React는 MemoizedMovie를 리렌더링 하지 않을 것이다. 이렇게 MovieViewsRealtime 컴퍼넌트의 성능을 향상할 수 있습니다다.

컴퍼넌트가 같은 props로 자주 렌더링되거나, 무겁고 비용이 큰 연산이 있는 경우, React.memo()로 컴퍼넌트를 래핑할 필요가 있다.

(profiling을 통해 React.memo()의 이점을 측정해보기)

3. 언제 React.memo()를 사용하지 말아야 할까

만약 위에서 언급한 상황에 일치하지 않는다면 React.memo()를 사용할 필요가 없을 가능성이 높습니다.

경험적으로, 성능적인 이점을 얻지 못한다면 메모이제이션을 사용하지 않는것이 좋다.

성능 관련 변경이 잘못 적용 된다면 성능이 오히려 악화될 수 있다. React.memo()를 현명하게 사용하라.

또한, 기술적으로는 가능하지만 클래스 기반의 컴퍼넌트를 React.memo()로 래핑하는것은 적절하지 않습니다.

3.1 쓸모없는 props 비교

렌더링될 때 props가 다른 경우가 대부분인 컴포넌트를 생각해보면, 메모이제이션 기법의 이점을 얻기 힘들다.

props가 자주 변하는 컴퍼넌트를 React.memo()로 래핑할지라도, React는 두 가지 작업을 리렌더링 할 때마다 수행할 것이다.

이전 props와 다음 props의 동등 비교를 위해 비교 함수를 수행한다.
비교 함수는 거의 항상 false를 반환할 것이기 때문에, React는 이전 렌더링 내용과 다음 렌더링 내용을 비교할 것이다.
비교 함수의 결과는 대부분 false를 반환하기에 props 비교는 불필요하게 된다.

4. React.memo() 와 콜백 함수

함수 객체는 "일반" 객체와 동일한 비교 원칙을 따른다. 함수 객체는 오직 자신에게만 동일하다.

몇가지 함수를 비교해보자.

function sumFactory() {
  return (a, b) => a + b;
}

const sum1 = sumFactory();
const sum2 = sumFactory();

console.log(sum1 === sum2); // => false
console.log(sum1 === sum1); // => true
console.log(sum2 === sum2); // => true

sumFactory()는 팩토리 함수이다. 이 함수는 2가지 숫자를 더해주는 화살표 함수를 반환한다.

함수 sum1과 sum2는 팩토리에 의해 생성된 함수다. 두 함수 모두 두 숫자를 더해주는 함수이다. 그러나 sum1과 sum2는 다른 함수 객체이다.

부모 컴퍼넌트가 자식 컴퍼넌트의 콜백 함수를 정의한다면, 새 함수가 암시적으로 생성될 수 있다. 이것이 어떻게 메모이제이션을 막는지 보고, 수정해보자.

Logout 컴퍼넌트는 콜백 prop인 onLogout을 갖는다.

function Logout({ username, onLogout }) {
  return <div onClick={onLogout}>Logout {username}</div>;
}

const MemoizedLogout = React.memo(Logout);

함수의 동등성이란 함정 때문에, 메모이제이션을 적용할 때는 콜백을 받는 컴퍼넌트 관리에 주의해야한다. 리렌더를 할 때 마다 부모 함수가 다른 콜백 함수의 인스턴스를 넘길 가능성이 있다.

function MyApp({ store, cookies }) {
  return (
    <div className="main">
      <header>
        <MemoizedLogout
          username={store.username}
          onLogout={() => cookies.clear()}
        />
      </header>
      {store.content}
    </div>
  );
}

동일한 username 값이 전달되더라고, MemoizedLogout은 새로운 onLogout 콜백 때문에 리렌더링을 하게 된다.

메모이제이션이 중단되게 되는 것이다.

이 문제를 해결하려면 onLogout prop의 값을 매번 동일한 콜백 인스턴스로 설정해야만 한다.useCallback()을 이용해서 콜백 인스턴스를 보존시켜보자.

const MemoizedLogout = React.memo(Logout);

function MyApp({ store, cookies }) {
  const onLogout = useCallback(() => {
    cookies.clear();
  }, []);
  return (
    <div className="main">
      <header>
        <MemoizedLogout username={store.username} onLogout={onLogout} />
      </header>
      {store.content}
    </div>
  );
}

useCallback(() => { cookies.clear() }, []) 는 항상 같은 함수 인스턴스를 반환한다. MemoizedLogout의 메모이제이션이 정상적으로 동작하도록 수정되었다.

5. React.memo() 은 성능 개선의 도구다

엄밀히 말하면, React에서는 성능 개선을 위한 하나의 도구로 메모이제이션을 사용한다.

대부분의 상황에서 React는 메모이징 된 컴퍼넌트의 리렌더링을 피할 수 있지만, 렌더링을 막기 위해 메모이제이션에 의존하면 안된다.

6. 결론

React.memo()는 함수형 컴퍼넌트에서도 메모이제이션의 장점을 얻게 해 주는 훌륭한 도구다. 올바르게 적용 된다면 변경되지 않은 동일한 prop에 대해 리렌더링을 하는 것을 막을 수 있다.

다만, 콜백 함수를 prop으로 사용하는 컴퍼넌트에서 메모이징을 할 때 주의하라. 그리고 같은 렌더링을 할 때 이전과 동일한 콜백 함수 인스턴스를 넘기는지 확실히 하라.

그리고 메모이제이션의 성능상 이점을 측정하기 위해 profiling을 사용하는 것을 잊지 말아라.

좋은 웹페이지 즐겨찾기