[React] memoization

리렌더링의 발생 조건

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

2) 컴포넌트가 내려받은 props가 변경됐을 때

3) 부모 컴포넌트가 리렌더링된 경우 모든 자식 컴포넌트도 리렌더링

 

 

잦은 리렌더링이 좋지 않은 이유

1) UX 관점에서 계속해서 바뀌는 화면을 보는 것은 사용자에게 좋지 않음

2) 비용이 발생하는 작업을 줄여 최적화해야 함

- memo: 컴포넌트를 캐싱

- useCallback: 함수를 캐싱

- useMemo: 값을 캐싱

 

 

React.memo

memo

부모 컴포넌트가 리렌더링되면 자식 컴포넌트는 모두 리렌더링 됨

변화가 없는 자식 컴포넌트는 리렌더링되지 않게 하기 위해서 memo를 사용할 수 있음

 

예제

function App() {
  const [count, setCount] = useState(0);
  console.log("App 컴포넌트 렌더링");

  const onPlusButtonClickHandler = () => {
    setCount(count + 1);
  };

  const onMinusButtonClickHandler = () => {
    setCount(count - 1);
  };

  return (
    <>
      <h3>카운트 예제</h3>
      <p>현재 카운트: {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div
        style={{
          display: "flex",
          marginTop: "10px",
        }}
      >
        <Box1></Box1>
        <Box2></Box2>
        <Box3></Box3>
      </div>
    </>
  );
}

- state가 선언돼있는 가장 상위 컴포넌트 App에 자식컴포넌트 Box1, 2, 3이 있는 경우

- 버튼을 눌러 state가 변할 때마다 자식 컴포넌트는 변화가 없음에도 리렌더링 된다

 

React.memo를 사용하려면?

캐싱해서 사용할 컴포넌트를 export 할 때 React.memo로 감싸서 사용한다

import { memo } from 'react';

// ...

export default memo(Box1);

- 처음 렌더링이 될때를 제외하고 실제로 변화가 있는 App 컴포넌트만 렌더링 되고 있다.

 

 

useCallback

- 함수 자체를 기억해둔다.

- Box1이 count를 초기화해주는 initCount를 props로 받았다고 가정해보자

 

// App.jsx
const initCount = () => {
  setCount(0);
};

<Box1 initCount={initCount}></Box1>

// Box1.jsx
<button onClick={initCount}>초기화</button>

- Box1은 memoization되어있음에도 불구하고 setCount가 호출될 때 같이 리렌더링된다.

- 이유는 함수형 컴포넌트를 사용하기 때문!

- 즉, App이 리렌더링되면서 참조형타입인 함수 또한 기존의 것을 사용하는 것이 아니라 다시 생성된다

- Box1 입장에서는 props가 변한 것이 되므로 다시 렌더링 되는 것

 

개선 방안

initCount라는 함수를 메모리에 저장해두고, 특정 조건이 아닌 경우 새로 생성되지 않도록 해야 함!

- App.jsx에서 initCount를 useCallback으로 감싸 변하지 않도록 하기

- useCallback(() => {캐싱할 함수}, [의존성배열])

-> 의존성 배열에서 변화가 생기지 않는 한 함수가 새로 생성되지 않음

-> 첫 마운트 이후 함수의 참조가 바껴야할 일이 없으므로 의존성배열에 []를 할당

const initCount = useCallback(() => {
  setCount(0);
}, []);

 

유의할 점

위 예시에서 setCount는 count가 0인 상태에서 계속 머물러있다.

따라서 initCount를 호출했을 때 setCount(0)은 잘 실행되지만, initCount내에서 count를 출력하면 버튼을 얼마나 눌렀던 상관없이 0이 나온다.

count가 바뀔 때마다 그 변경사항도 반영하고 싶다면 count가 바뀔 때 initCount가 새로운 정보를 담아 다시 생성되어야하고, 이렇게 구현하기 위해선 의존성 배열에 count를 담아줄 수 있다.

const initCount = useCallback(() => {
  setCount(0);
}, [count]);

 

 

useMemo

사용 방법

useMemo 내부에 캐싱할 값을 담고, 의존성 배열을 추가한다.

 

예제 1

function HeavyComponent() {
  const [value, setValue] = useState(0);

  // 가상의 무거운 작업
  const heavyWork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };

  const sampleValue = heavyWork();

  return (
    <div>
      <p>나는 {sampleValue}를 가져오는 엄청 무거운 작업을 할거야!</p>
      <button
        onClick={() => {
          setValue(value + 1);
        }}
      >
        누르면 아래 count가 올라가요!
      </button>
      <br />
      {value}
    </div>
  );
}

export default HeavyComponent;

- 가상의 무거운 작업을 포함한 컴포넌트

- 한번 리렌더링이 될 때마다 많은 횟수로 for문을 반복한다

- 새로고침을 한 번 할 때마다 꽤 시간이 걸리는 것을 확인할 수 있음!

 

- 위와 같이 렌더링 될때마다 계산되어야하는 값이 아닌 경우 값을 캐싱해둘 필요가 있다

 

  const sampleValue = useMemo(() => heavyWork(), []);

- 캐싱해둘 값인 heavyWork()의 연산 결과를 useMemo에 담아두고, 마운트 됐을 때만 값을 받아오도록 한 예시

- 처음 페이지가 마운트 됐을 때만 시간이 소요되고, 이후 setState로 리렌더링 될 때는 시간이 소요되지 않음

 

 

예제 2

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

function ObjectComponent() {
  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>
        내 이름은 {me.name}이구, 나이는 {me.age}야!
      </div>
      <br />
      <div>
        <button
          onClick={() => {
            setIsAlive(!isAlive);
          }}
        >
          누르면 살았다가 죽었다가 해요
        </button>
        <br />
        생존여부 : {me.isAlive}
      </div>
      <hr />
      필요없는 숫자 영역이에요!
      <br />
      {uselessCount}
      <br />
      <button
        onClick={() => {
          setUselessCount(uselessCount + 1);
        }}
      >
        누르면 숫자가 올라가요
      </button>
    </>
  );
}

export default ObjectComponent;

 

- me의 값이 바꼈을 때만 console.log를 찍고싶었는데, uslessCount 버튼을 눌렀을 때도 매번 콘솔이 찍힌다!

- 이 또한 마찬가지로 setUselessCount가 호출되면서 컴포넌트가 리렌더링 -> 컴포넌트 내부의 참조타입인 me 객체가 새로 선언됨 -> me를 참조할 수 있는 주소값이 달라졌으므로 useEffect는 me가 변했다고 생각하여 내부 로직을 실행하기 때문이다!

- 이 때도 me에대한 참조가 isAlive의 변동이 있을 때만 변하도록 하기 위해 useMemo를 활용할 수 있다

 

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

isAlive에 변화가 있을 때만 새로운 객체를 반환하므로 다른 변화에 의해서 새로운 me가 생성되지 않는다!

 

memoization을 많이 활용하면 좋을까?

아니다! 캐싱을 해둔다는 것은 별도의 메모리를 사용한다는 것으로 남발하면 오히려 성능 저하가 일어날 수 있음

따라서 반드시 필요한 경우에만 사용한다.

 

 

'React' 카테고리의 다른 글

[React] Effect가 필요하지 않을 수도 있습니다  (0) 2024.08.18
[React] useSyncExternalStore  (0) 2024.08.18
[React] useContext  (0) 2024.08.16
[React] useRef  (0) 2024.08.16
[React] useState  (0) 2024.08.16