[TIL] React.memo / useCallback / useMemo

hanbyul.choi·2023년 6월 20일
0

[TIL]

목록 보기
21/39
post-thumbnail

React.memo / useCallback / useMemo

리렌더링이 발생하는 경우

  1. 컴포넌트에서 state가 바뀌었을 때

  2. 컴포넌트가 내려받은 props가 변경되었을 때

  3. 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두

리액트에서 리렌더링이 빈번하게, 자주 일어난다는 것은 좋지않다.
비용이 발생하는 것은 최대한 줄여야하기 때문.
리액트에서 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법이 아래이다.

  • memo(React.memo) : 컴포넌트를 캐싱
  • useCallback : 함수를 캐싱
  • useMemo : 값을 캐싱

여기서 캐싱이라는 것은 메모리에 저장해두고 필요할 때 쓰겠다는 뜻.


memo(React.memo)

아래에 리턴문을 가지고 있는 카운트 컴포넌트를 예시로 들어보자

return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div style={boxesStyle}>
        <Box1 />
        <Box2 />
        <Box3 />
      </div>
    </>
  );

현재 컴포넌트에서 useState를 사용하여 {count} 값을 변경시키면 하위에 있는 Box1, Box2, Box3 모두 부모컴포넌트의 상태변경으로 인해 리렌더링 된다.

이처럼 리렌더링할 이유가 없기때문에 예외처리를 하기 위해 React.memo를 사용한다.

export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);

우리가 export할 때 React.memo로 컴포넌트를 감싸주면 부모컴포넌트의
state가 변경되더라도 자식 컴포넌트들은 리렌더링이 되지 않는다.


useCallback

Box1이 count를 초기화 해 주는 코드라고 가정해보자.

// count를 초기화해주는 함수
  const initCount = () => {
    setCount(0);
  };

  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div style={boxesStyle}>
        <Box1 initCount={initCount} />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

현재 컴포넌트에서 useState를 사용하여 {count} 값을 변경시키면 하위에 있는 Box1만 리렌더링이 된다. (현재 각 box를 React.memo로 감싼상황)

그 이유는 현재 컴포턴트가 리렌더링 되면서 함수를 다시 선언하기 때문이다.

자바스크립트에서는 함수도 객체의 한 종류.
따라서 모양은 같더라도 다시 만들어지면 그 주솟값이 달라지고 이에 따라 하위 컴포넌트인 Box1.jsxprops가 변경됐다고 인식한다.

// 변경 전
const initCount = () => {
  setCount(0);
};

// 변경 후
const initCount = useCallback(() => {
  setCount(0);
}, []);

위와 같이 useCallback을 사용해서 함수를 감싸주면 뒤에오는 dependency array 여부에 따라 마운트될 때만 혹은 특정값이 업데이트 될 때 새롭게 함수를 할당한다.

여기서 짚고 넘어갈 부분이 있다. 아래코드를 보게되면,

const initCount = useCallback(() => {
  console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, []);

위와 같이 코드를 변경했을 때 참조되는 count값은 초기 마운팅 되었을때 값이다. 따라서 직전 값이 뭐였는지 추적이 안된다.

const initCount = useCallback(() => {
  console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, count]);

따라서 dependency array에 count를 넣어주어 상태변경 시 마다 새롭게 함수를 할당해주어야 한다.


useMemo

동일한 값을 반환하는 함수를 계속 호출해야 하면 필요없는 렌더링을 하기때문에 이부분을 보완하기 위해 useMemo를 사용한다.

예를 들어 아래와 같이 시간이 오래걸리는 작업을 수행하는 함수가 있다고 가정해보자.

여기서 반환되는 값의 변화가 없을 시에는 함수를 수행하지 않고 값만 저장하여 반환하는 것이다.

const heavyWork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };

	// CASE 1 : useMemo를 사용하지 않았을 때
  const value = heavyWork();

	// CASE 2 : useMemo를 사용했을 때
  // const value = useMemo(() => heavyWork(), []);

위와 같은 구문이 리렌더링될 때마다 heavyWork라는 시간이 오래걸리는 함수이가 매번 작동한다. 따라서 CASE 2 처럼 useMemo를 사용하여 값을 캐싱한다.


또 다른 사용 예시를 살펴보자.
이번에는 객체를 dependency array로 참조하는 useEffect 훅을 사용한다고 가정했을때,

  const [isAlive, setIsAlive] = useState(true);
  const [uselessCount, setUselessCount] = useState(0);

  const me = {
    name: "Ted Chang",
    age: 21,
    isAlive: isAlive ? "생존" : "사망",
  };

  useEffect(() => {  // 객체를 참조
    console.log("생존여부가 바뀔 때만 호출해주세요!");
  }, [me]);

  return (
    <>
      <div>
        <button
          onClick={() => {
            setIsAlive(!isAlive);
          }}
        />
        <br />
        생존여부 : {me.isAlive}
      </div>
      <hr />
      필요없는 숫자 영역이에요!
      <br />
      {uselessCount}
      <br />
      <button
        onClick={() => {
          setUselessCount(uselessCount + 1);
        }}
      >
        +1 증가버튼
      </button>
    </>
  );

위와 같은 컴포넌트에서는 객체와 관련없는 카운트를 1씩 증가시켜도 useEffect가 동작한다. 그 이유는 컴포넌트가 리렌더링 되면서 객체의 주소값이 새롭게 할당되기 때문이다.

따라서 useEffect는 변화를 감지하고 동작하게 된다.
아래와 같이 메모를 사용해야 우리가 원하는대로 작동하게 된다.

const me = useMemo(() => {
  return {
    name: "Ted Chang",
    age: 21,
    isAlive: isAlive ? "생존" : "사망",
  };
}, [isAlive]);

여기까지 React.memo / useCallback / useMemo 를 사용하여 컴포넌트에 불필요한 리렌더링을 줄여 최적화하는 방법에 대해 살펴보았다.

0개의 댓글