React Hooks - React.memo

박정호·2022년 8월 30일
0

React Hook

목록 보기
8/12
post-thumbnail

🚀 Start

React.memo를 호출하면 결과를 메모이징하도록 래핑하여 경우에 따라 성능을 향상시킬 수 있다. 마치 useCallback, useMemo와 같은 역할을 하는 듯하다. 그렇다면 useCallback, useMemo와 다르게 어떤 역할을 하는지 살펴보자.

🪝 React.memo

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

이때, 렌더링된 결과와 이전 결과의 비교를 빠르게 하여 과정의 속도를 높여 줄 수 있는 것이 React.memo이다.

⭐️ React.memo는 메모이징을 통해 이전결과와 다음결과를 비교하여 재사용 유무를 따진다.
그렇게 되면, React에서 리렌더링 시 virtualDOM에서 달라진 부분을 확인하지 않아도 되고, DOM의 업데이트(리렌더링)이 일어나지 않아도되므로 성능이 향상되는 것이다!

💡 주의) 단, React.memo는 성능 최적화를 위하여 사용된다. 렌더링을 '방지'하기 위해서 사용하면 버그를 만들 수 있다.

React,memo는 props의 변화에만 영향을 주는 것으로, 메모이징을 통해 값을 비교하여 렌더링을 막는 것은 맞지만, 렌더링을 막기위한 목적으로 사용되면 안된다는 것이다.

🙂 사용방법

React.memo는 hook이 아닌 고차 컴포넌트(Higher Order Component)이다.

다음과 같은 경우 React.memo로 Movie컴포넌트를 래핑하고, Movie컴포넌트에 대한 값들을 메모이징한 컴포넌트인 MemoizedMovie를 반환한다.
만약 title, releaseDate와 같은 props에 변화가 없다면 메모이징된 내용을 그대로 사용하면 되는 것이다.

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

export const MemoizedMovie = React.memo(Movie);

⭐️ 그렇다면 메모이징을 통한 이전 결과와 다음 결과를 어떻게 비교할까?

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

React.memo()는 props 혹은 props의 객체를 비교할 때 얕은(shallow) 비교를 기본 동작으로 한다.
만약, 비교방식을 수정하고 싶다면 React.memo의 두번째 매개변수에 비교함수를 제공하면 된다.

✏️ Example 1.

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

✏️ Example 2.

  • moviePropsAreEqual() 함수는 이전 props와 현재 props가 같다면 true를 반환할 것이다.
function moviePropsAreEqual(prevMovie, nextMovie) {
  return (
    prevMovie.title === nextMovie.title &&
    prevMovie.releaseDate === nextMovie.releaseDate
  );
}

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

👍 언제 사용할까?

👉 같은 props의 잦은 렌더링

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

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

✏️ 일반적으로 부모 컴퍼넌트에 의해 하위 컴퍼넌트가 같은 props로 리렌더링 될 때가 있다.

다음과 같은 경우 MovieViewsRealtime이라는 컴포넌트의 props인 views에 변화가 생겼다. 그렇다면 자식 컴포넌트인 Movie는 아무런 변화가 없어도 재렌더링되어야 하는 것이다.

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

따라서, 메모이지징된 컴포넌트인 MemoizedMovie를 대신 사용하면 된다. title,releaseDate에 변화가 없다면 React는 MemoizedMovie를 리렌더링할 필요가 없게 되어 성능이 향상되는 것이다.

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

👎 언제 사용하지말까?

사용해야 하는 이유에 대해서 알아봤었다. 만약 그 이유가 아니라면 사용하지 않는 것이 좋다. 왜냐하면 성능향상에 도움이 되지 않는 메모이제이션은 오히려 더 악화시킬 수가 있기 때문이다.

즉, 같은 props에 대한 렌더링이 아닌 값이 계속해서 렌더링될때 마다 props값이 변하는 상황이라면 메모이징해서 비교하고 또 비교해봤자 비교함수는 false만 계속해서 반환할 것이기 때문에 React.memo의 사용은 무의미해진다.

그리고 클래스 기반 컴포넌트를 React.memo로 래핑하는 것은 적절하지 않다. 만약 클래스 기반의 컴퍼넌트에서 메모이제이션이 필요하다면 PureComponent를 확장하여 사용하거나, shouldComponentUpdate() 메서드를 구현하는 것이 적절하다.

⭐️ 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

✏️ 다음은 함수의 동등성으로 인해 메모이제이션이 막아지는 상황이다.

Logout컴포넌트는 onLogout이라는 콜백함수와 username을 props로 받아왔다.
앞서 보았듯이 username이라는 props가 변화가 없다면 메모이징을 통해 재사용이 가능하다. 하지만, 함수의 동등성으로 props로 받아온 onLogout은 함수이기 때문에 새로운 콜백이 이루어져 리렌더링이 일어날 수 밖에 없게 된다.

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

const MemoizedLogout = React.memo(Logout);

이 문제를 해결하려면 onLogout이라는 props를 매번 동일한 콜백 인스턴스로 설정해주어야 한다.

⭐️ 동일하게 보존하기 위해서 사용하는 것이 바로 useCallback인 것이다.

useCallback을 통해 항상 같은 함수 인스턴스를 반환하게 하고, 메모이제이션된 MemoizedLogout이 동작하도록 하면 해결된다.

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>
  );
}

💡 중요) How?

새롬게 만들어진 함수는 기존의 함수와 같은 함수가 아니다. 메모리 주소가 다르기 때문인데, 따라서 useCallback을 통해 기존의 함수 자체를 저장해놓고 다시 사용하는 것이다. 그러면 렌더링되어도 기존의 함수 그대로 사용이 가능하다.

자세한 useCallback 내용: https://velog.io/@pjh1011409/React-Hooks-useCallBack

💡 중요)

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

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

그리고 메모이제이션의 성능상 이점을 측정하기 위해 profiling을 사용해야 한다.

👉 React Profiler를 통한 성능 측정: 성능 측정

출처 및 참고하기 좋은 사이트
-https://ui.toast.com/weekly-pick/ko_20190731

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글