React.memo() 현명하게 써보자

Paul Kang·2021년 11월 16일
1
post-thumbnail

사용자들은 빠르고 즉각적인 사용자 경험을 원한다. 100ms 이하의 응답 지연은 사용자로 하여금 즉각적인 반응으로 느끼게 한다. 반면, 100ms~300ms의 응답 지연은 지연이 어느정도 느껴지기 마련이다.

사용자 경험의 향상을 위해 리액트는 고차 컴퍼넌트(Higher Order Component, HOC) React.memo()를 제공한다. 컴포넌트를 래핑(wrapping)함으로써 컴포넌트를 메모이징(Memoizing)하고 불필요한 리렌더링을 피할 수 있다.

이 글은 언제 React.memo()가 성능을 향상시키고, 언제 불필요한지 설명할 것이다.

1. React.memo()

DOM 업데이트를 결정할 때, 리액트는 먼저 컴포넌트를 렌더링한 뒤, 이전 렌더링 결과와 비교한다. 만약 렌더링 결과가 다르다면 그 때 DOM 업데이트를 한다.

이전 렌더링 결과와 현재의 렌더링 결과를 비교하는 것은 빠르다. 하지만 어떤 상황에선 이 속도를 더 높일 수 있다.

컴포넌트가 React.memo()로 래핑 되었을 때, 리액트는 컴포넌트를 렌더링하고 결과를 메모이징(Memoizing)한다. 다음 렌더링전에 props가 같다면, 리액트는 메모이징(Memoizing)된 내용을 재사용한다.

예시:

export function Movie({ title, releaseDate }) {
  return (
    <div>
      <div>Movie title: {title}</div>
      <div>Release date: {releaseDate}</div>
    </div>
  );
}
const MemoizedMovie = React.memo(Movie);
export { MemoizedMovie as Movie };

React.memo(Movie)는 메모이징(Memoizing)된 새로운 컴포넌트 MemoizedMovie를 리턴한다.

단 한가지의 차이점을 제외하고, 기존의 Movie 컴포넌트와 같은 렌더링 결과를 낸다.

MemoizedMovie의 렌더링 결과는 메모이징 되어있다. 만약 title이나 releaseDate 같은 props가 변경 되지 않는다면 다음 렌더링 때 메모이징 된 결과를 사용한다.

// First render - MemoizedMovie이 호출됨.
<MemoizedMovie 
  title="Heat" 
  releaseDate="December 15, 1995" 
/>
// Second render - MemoizedMovie 호출 x.
<MemoizedMovie
  title="Heat" 
  releaseDate="December 15, 1995" 
/>

메모이징 된 결과를 재사용함으로써 성능상의 이점을 얻을 수 있다.

  • 리액트는 렌더링을 스킵하고, virtual DOM이 달라진 부분을 체크하지 않는다.

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

기본적으로 React.memo()는 props 혹은 props 객체를 비교할 때 얕은(shallow) 비교를 한다.

props 비교를 직접 커스터마이징 하고 싶다면 두번째 인수로 비교 함수를 지정해주면 된다.

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

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

function moviePropsAreEqual(prevMovie, nextMovie) {
  return prevMovie.title === nextMovie.title
    && prevMovie.releaseDate === nextMovie.releaseDate;
}
const MemoizedMovie = React.memo(Movie, moviePropsAreEqual);

2. 언제 React.memo()를 사용할까?

2.1 같은 props로 렌더링이 자주 일어나는 Component

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

흔한 경우가 부모 컴포넌트에 의해 자식 컴포넌트가 같은 props로 렌더링 될 때이다.

예시: MovieViewsRealtime 실시간으로 업데이트되는 조회수를 가진 컴포넌트

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

이 어플리케이션은 매초마다 서버에서 데이터를 폴링(polling)해서 MovieViewsRealtime 컴포넌트의 views prop을 업데이트한다.

// 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 prop이 업데이트 될 때 마다 MoviewViewsRealtime 컴포넌트가 리렌더링 된다. 이때 Movie 컴퍼넌트 또한 title이나 releaseData가 이전과 똑같아도 리렌더링 된다.

바로 이 상황에 React.memo()가 딱이다!

불필요한 리렌더링을 방지하기 위해 React.memo()로 메모이징을 해보자

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

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

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

profiling을 통해 React.memo()를 적용했을 때의 이점을 측정하자.

3. 언제 React.memo()를 피해야할까?

만약 컴포넌트가 무겁지 않고 같은 props로 자주 렌더링 되지 않는다면 React.memo()는 필요치 않다.
성능적으로 이점이 없다면 사용하지 않도록 하자.

성능 관련 변화가 잘못 적용된다면, 오히려 성능 악화를 불러 일으킬 수 있다. React.memo() 현명하게 사용하자!

클래스 컴포넌트를 React.memo()로 래핑하는 것은 바람직하지 않다. 만약 필요하다면, PureComponent를 확장해서 사용하거나 shouldComponentUpdate()를 커스터마이징해서 구현하는 것이 옳다.

3.1 필요없는 props 비교

렌더링될 때 props가 다른 경우가 대부분인 컴포넌트를 생각해 볼 때, 메모이제이션(Memoization)의 이점은 없다.

설령 props가 계속 변하는 컴포넌트를 React.memo()로 래핑할지라도, 리액트는 다음의 2가지 작업을 수행한다.

  1. 이전 props와 다음 props의 동등 비교를 위한 비교 함수를 호출한다.
  2. props 비교는 거의 항상 false를 리턴할 것이기에, 리액트는 이전 렌더링 결과와 현재의 렌더링 결과의 비교를 수행할 것이다.

당신은 성능 이점도 얻지 못한 채, 비교함수만 실행하게 될 것이다!

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

함수 sum1sum2 두 함수 모두 두 숫자를 더해주는 함수이다. 그러나 sum1sum2는 다른 함수 객체이다.

부모 컴퍼넌트가 자식 컴퍼넌트의 콜백 함수를 정의할 때, 새 함수 객체 인스턴스가 생성된다. 이것이 어떻게 메모이제이션을 막는지 보고, 고쳐보자.

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

function Logout({ username, onLogout }) {
  return (
    <div onClick={onLogout}>
      Logout {username}
    </div>
  );
}
const MemoizedLogout = React.memo(Logout);

콜백함수를 prop으로 받는 컴포넌트는 메모이제이션을 적용할 때 주의를 기울여야 한다. 부모 컴포넌트가 매 렌더링 때마다 다른 콜백 함수의 인스턴스를 넘겨줄 수 있기 때문이다.

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

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

Memoization is broken!

이를 해결하기 위해서는, onLogout prop은 매번 같은 콜백 함수 인스턴스를 받아야 한다. useCallback()을 적용해서 콜백 인스턴스를 렌더링 사이에서 지켜보자.

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

useCallback(() => { cookies.clear() }, []) 는 항상 같은 함수 인스턴스를 반환한다. (cookies 값이 바뀌지 않는다면) MemoizedLogout의 메모이제이션이 정상 작동 될 것이다!

5. React.memo()는 성능개선의 실마리

말그대로, 리액트는 성능개선의 도구로 메모이제이션을 사용한다. 대부분의 상황에서 리액트는 메모이제이션된 컴포넌트의 렌더링을 피하지만, 당신은 그것에 의존해선 안된다.

6. React.memo()와 hooks

hooks를 사용하는 컴포넌트는 메모이제이션을 달성하기 위해 자유롭게 React.memo()를 사용할 수 있다.

단, 리액트는 state 값이 바뀐다면 언제든 리렌더링 된다. 설령 컴포넌트가 React.memo()로 래핑되었다 할지라도...

7. 결론

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

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

마지막으로, 메모이제이션의 이점을 확인하기 위해 profiling을 사용하는 것을 잊지 말자.

한줄 요약: React.memo() 컴포넌트의 props가 바뀌지 않았다면, 리렌더링 하지 않도록 설정하여 성능 최적화를 해주는 착한 녀석.

profile
뭐든 기록하면 자산!

0개의 댓글