React.memo 현명하게 사용하기

IT공부중·2021년 2월 8일
2

React

목록 보기
7/10

https://dmitripavlutin.com/use-react-memo-wisely/
이글을 읽고 제 나름대로 번역한 글입니다.

user interface 성능의 향상을 위해 React는 higher-order compoenent인 React.memo를 제공한다. React.memo로 컴포넌트를 감싸면, 리액트는 컴포넌트를 메모이제이션 하고 불필요한 렌더링을 건너 뛴다.

React.memo가 성능을 향상시키는 상황을 알아보고, 쓸모 없는 경우를 경고하고자 한다.

React.memo()

Dom의 업데이트를 결정할 때, 리액트는 컴포넌트를 처음으로 렌더링하고, 이전 렌더 결과와 비교를 할 것이다. 만약 렌더 결과가 다르다면 리액트는 돔을 업데이트한다.

현재와 이전 render 결과를 비교하는 것은 빠르다. 하지만 특정상황일 때 그 속도를 향상 시킬 수 있다.

React.memo()로 컴포넌트를 감쌌을 때, 리액트는 컴포넌트를 렌더링하고 그 결과를 메모이제이션한다. 다음 렌더 전에 만약 새로운 props가 이전 것과 같다면, React는 다음 렌더링을 건너 뛰고 메모이제이션한 결과를 재사용한다.

함수 컴포넌트 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를 반환한다. 이것은 기존의 Movie 컴포넌트와 똑같지만, 단 하나가 다르다.

MemoizedMovie이 렌더한 결과는 메모이제이션 된다. title이나 releaseDate props가 같으면 메모이제이션된 결과를 재사용하여 렌더링한다.

// First render. React calls MemoizedMovie function.
<MemoizedMovie 
  title="Heat" 
  releaseDate="December 15, 1995" 
/>

// On next round React does not call MemoizedMovie function,
// preventing rendering
<MemoizedMovie
  title="Heat" 
  releaseDate="December 15, 1995" 
/>

이로써 성능 향상을 얻을 수 있다. 메모 된 콘텐츠를 재사용하면 React는 구성 요소 렌더링을 건너 뛰고 가상 DOM 차이 검사를 수행하지 않는다.

Custom equality check of props

React.memo는 props 들 간의 비교에 얕은(shallow) 비교를 한다.

그래서 2번 째 인자로 커스텀 비교 함수를 넣을 수 있다.

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

areEqual 함수는 prevProps와 nextProps가 같으면 true를 반환해야한다. true라면 메모이제이션 된 결과를 사용한다.

예를 들어

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

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

이런 식으로 만들면 된다. moviePropsAreEqual은 prev와 next props 같다면 true를 반환할 것이다.

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

React.memo()로 컴포넌트를 감싸는 Best Case는 함수컴포넌트가 자주 렌더링 되고 보통 같은 props를 받을 때이다.

보통은 같은 props를 가지고 있는 컴포넌트도 부모 컴포넌트의 렌더링 때문에 같이 렌더링 될 것이다.

위에 정의한 Movie 컴포넌트를 재사용해보자. 새로운 부모 컴포넌트인 MovieViewsRealtime은 실시간으로 movie의 관객 수를 보여준다

이 어플리케이션은 정기적으로 서버로부터 투표를 받아서 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 props는 매번 새로운 props로 업데이트 되었다. 그래서 title이나 releaseDate는 같은 props임에도 Movie 컴포넌트가 같이 리렌더링 된다.

이 상황이 Movie 컴포넌트에 React.memo를 적용하기 좋은 예이다.

불필요한 리렌더링을 방지하기 위해 MemoizedMovie를 사용해보자.

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

props가 동일한 값으로 렌더링 되는 경우가 많을 때, output의 계산 비용이 무겁고 많이 들 때, React.memo()를 사용할 필요가 있다.

React.memo()를 피해야 할 때.

만약 컴포넌트가 같은 props로 자주 리렌더링 되는 것이 아니면, React.memo()를 사용할 필요는 없다.

잘못된 성능 관련 변경 사항은 오히려 성능을 저하시킬 수 있다. 현명하게 사용해야한다.

클래스형 컴포넌트에서는 PureComponent를 사용하거나 shouldComponentUpdate를 사용.

보통 다른 props로 리렌더링 되는 컴포넌트가 있다고 생각해보자. 이 경우에는 메모이제이션이 큰 효과를 줄 수 없다.

만약 React.memo로 감싸놓은 컴포넌트는 렌더링 마다 2가지 작업을 한다.

  1. 비교 함수를 호출하여 이전과 지금이 같은지를 비교한다.
  2. props 비교는 거의 항상 false를 반환하기 때문에, 이전과 현재의 결과가 달라진다.

이러면 성능 향상을 기대할 수 없고 불필요한 비교함수를 수행하게 된다.

React.memo()와 콜백함수

부모 컴포넌트는 렌더링 될 때마다 새로운 함수를 만든다. 다음 예를 보고 고쳐보자.

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

const MemoizedLogout = React.memo(Logout);

Logout 컴포넌트는 callback으로 onLogout을 받는다.

부모 컴포넌트인 MyApp컴포넌트는 렌더링마다 다른 콜백함수를 반환할 것이고, Logout은 메모이제이션 효과를 받지 못 할 것이다.

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

같은 username value를 넘겨주더라도 onLogout 콜백은 항상 재생성 되기 때문에 MemoizedLogout은 메모이제이션 효과를 받지 못 한다.

이를 고치기 위해 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>
  );
}

React.memo를 한 경우에도 상태가 변경되면 항상 컴포넌트를 리렌더링한다. props가 바뀔 경우도 마찬가지이다.

결론

React.memo는 함수 컴포넌트를 메모이제이션하기에 좋은 도구이다. 정확하게 사용한다면 이전과 현재 props가 같을 때 리렌더링하는 불필요한 상황을 예방할 수 있다.

props로 callback 함수를 사용할 때는 주의해야한다. useCallback 을 사용해서 동일한 함수를 props로 줄 수 있도록 해야한다.

profiling을 통해 성능 측정도 해보는 것을 잊지 말자.

profile
3년차 프론트엔드 개발자 문건우입니다.

0개의 댓글