[React] 성능 최적화 2 - React.memo

이재훈·2023년 6월 9일
0

React

목록 보기
16/27

컴포넌트 재사용

구현해 볼 것


현재 App에서 count, text state를 가지고 있습니다. count state가 변경되어도 Textview는 리렌더링 되지 않게 업데이트 조건을 걸어보겠습니다.

React.memo

React.memo는 고차 컴포넌트 입니다.
컴포넌트가 동일한 props로 동일한 결과를 렌더링해낸다면, React.memo를 호출하고 결과를 메모이징 하도록 래핑하여 경우에 따라 성능 향상을 누릴 수 있습니다. 즉, React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링 된 결과를 재사용 합니다.

const MyComponent = React.memo(function myComponent(props) {
	// props를 사용하여 렌더링
});

처음에는 React.memo 함수 안에 함수를 넣으면 무언가 좋아진 컴포넌트를 반환을 해서 상수에 저장된다 라고 이해할 수 있습니다. 같은 prop을 받으면 리렌더링 하지 않습니다. 물론 자기 자신의 state가 바뀌면 리렌더링 됩니다. React.memo는 부모에서 받은 prop에만 관계가 있습니다.

고차 컴포넌트란?

고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술입니다. 자세한 내용은 아래 링크에 들어가시면 보실 수 있습니다.
https://ko.legacy.reactjs.org/docs/higher-order-components.html

예제1

OptimizeTest.js

import { useState, useEffect } from "react";

const TextView = ({ text }) => {
  useEffect(() => {
    console.log(`update :: text : ${text}`);
  });
  return <div>{text}</div>;
};

const CountView = ({ count }) => {
  useEffect(() => {
    console.log(`update :: count : ${count}`);
  });
  return <div>{count}</div>;
};

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <CountView count={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
      <div>
        <h2>text</h2>
        <TextView text={text} />
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </div>
    </div>
  );
};

export default OptimizeTest;


새로운 컴포넌트를 3개를 만들었습니다. OptimizeTest에서 count, text state를 각각 CountView와 TextView에게 전달하고 렌더링하는 간단한 예제입니다. CountView와 TextView에는 useEffact를 사용하여 update 시 로그를 남기게 하였습니다.


+버튼을 눌러 CountView에게 전달하는 props를 변경하였습니다. CountView만 리렌더링 되는 것이 아니라 TextView도 같이 리렌더링 되는 것을 확인할 수 있습니다. 이것은 낭비입니다.

const TextView = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`update :: text : ${text}`);
  });
  return <div>{text}</div>;
});

TextView 컴포넌트를 React.memo로 감싸겠습니다. 이렇게 되면 props가 바뀌지 않으면 컴포넌트의 리렌더링은 일어나지 않습니다.

count를 올려도 TextView는 리렌더링 되지 않는 것을 확인할 수 있습니다.

OptimizeTest.js

import React, { useState, useEffect } from "react";

const TextView = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`update :: text : ${text}`);
  });
  return <div>{text}</div>;
});

const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`update :: count : ${count}`);
  });
  return <div>{count}</div>;
});

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <CountView count={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
      <div>
        <h2>text</h2>
        <TextView text={text} />
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </div>
    </div>
  );
};

export default OptimizeTest;

CountView에도 마찬가지로 React.memo로 감싸주엇습니다. 이제 text만 바꾸면 TextView만, count만 바꾸면 CountView만 리렌더링됩니다.

예제2

import React, { useState, useEffect } from "react";

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA update :: ${count}`);
  });
  return <div>{count}</div>;
});

const CounterB = React.memo(({ obj }) => {
  console.log(`CounterB update :: ${obj}`);
  return <div>{obj.count}</div>;
});

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>Button A</button>
      </div>
      <CounterB obj={obj} />
      <div>count B</div>
      <button
        onClick={() =>
          setObj({
            count: obj.count,
          })
        }
      >
        Button B
      </button>
    </div>
  );
};

export default OptimizeTest;

OptimizeTest에서 count와 obj 둘 다 값을 변경하지 않고 props를 전달하고 있습니다.

CounterA 컴포넌트와 CounterB 컴포넌트에는 React.memo를 사용하여 props 값이 변경되지 않았다면 리렌더링이 되지않게 설정하였습니다.

하지만....


button B를 누를 시 CounterB 컴포넌트는 계속 리렌더링이 되고 있습니다...!! ㅇ0ㅇ
그 이유는 prop인 obj가 객체이기 때문입니다. 기본적으로 자바스크립트에서 객체를 비교할 때는 얕은 비교를 하기 때문입니다. (객체 주소에 의한 비교)

다른 비교 동작을 원한다면, 두번째 인자로 별도의 비교함수를 제공하면 됩니다.

OptimizeTest.js

import React, { useState, useEffect } from "react";

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA update :: ${count}`);
  });
  return <div>{count}</div>;
});

const CounterB = ({ obj }) => {
  console.log(`CounterB update :: ${obj}`);
  return <div>{obj.count}</div>;
};

const areEqual = (preProps, nextProps) => {
  return preProps.obj.count === nextProps.obj.count;
};

const MemoizedCounterB = React.memo(CounterB, areEqual);

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1,
  });
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>Button A</button>
      </div>
      <MemoizedCounterB obj={obj} />
      <div>count B</div>
      <button
        onClick={() =>
          setObj({
            count: obj.count,
          })
        }
      >
        Button B
      </button>
    </div>
  );
};

export default OptimizeTest;
const CounterB = ({ obj }) => {
  console.log(`CounterB update :: ${obj}`);
  return <div>{obj.count}</div>;
};

const areEqual = (preProps, nextProps) => {
  return preProps.obj.count === nextProps.obj.count;
};

const MemoizedCounterB = React.memo(CounterB, areEqual);

// ... 생략

<MemoizedCounterB obj={obj} />

CounterB에 React.memo를 지우고, 얕은 비교가 아닌 obj의 count를 비교하는 함수인 areEqual을 선언했습니다. React.memo에는 컴포넌트와 비교함수를 넣은 MemoizedCounterB를 선언을 하였고, 실제로 OptimizeTest 컴포넌트 안에서는 CounterB 컴포넌트가 아닌 MemoizedCounterB를 사용하시면 됩니다.

이제 button B를 눌러도 리렌더링 되지 않는 것을 확인하였습니다.

이렇게 React.memo를 사용하여 렌더링 성능 최적화를 해보았습니다.


리액트 공식 홈페이지
https://ko.legacy.reactjs.org/docs/react-api.html#reactmemo
해당 게시글은 인프런 강의
"한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지(이정환)"
를 정리한 내용입니다. 쉽게 잘 설명해주시니 여러분도 강의를 듣는 것을 추천드립니다.

profile
부족함을 인정하고 노력하자

0개의 댓글