React

[React] React의 메모이제이션 - useMemo(), useCallback(), React.memo

leejin_rho 2025. 2. 19. 03:37

React에서는 렌더링 시 발생되는 필요없는 작업들을 줄이기 위해 다양한 최적화 도구를 제공한다. 그 중 React의 메모이제이션을 도와주는 대표적인 도구가 useMemo(), useCallback(), React.memo이다.


각각 메모이제이션하는 값이 다른데, 순서대로 이를 정리해보고자 한다.

 

useMemo()

우선 useMemo는 비용이 큰 연산에 대한 결과를 저장해 두고, 이 저장된 값을 반환하는 훅이다.

 

가장 흔하게 사용이 되는데, 첫 번째 인수로는 어떠한 값을 반환하는 생성 함수를, 두 번째 인수로는 해당 함수가 의존하는 값의 배열을 전달한다. useMemo는 렌더링 발생 시 의존성 배열의 값이 변경됐을 때만 함수를 재실행한다. 그리고 함수의 반환값을 저장한다.

 

이러한 useMemo의 메모이제이션은 값 뿐만 아니라 컴포넌트도 가능하다.

다음과 같이 콜백 함수의 return값으로 컴포넌트를 넣으면 value값이 변할 때마다, 함수를 실행해 컴포넌트 자체의 바뀐 값을 저장한다.

 

하지만 이런식으로 컴포넌트 메모이제이션 하는 것은 컴포넌트 자체를 메모이제이션하는 것이 아니라, JSX 요소를 메모이제이션하는 것이므로, 비효율적인 방법이다.

 

그렇다면 컴포넌트와 JSX 요소의 차이점은 무엇일까?

JSX 문법으로 작성한 <Component />는 사실상 JavaScript 객체(React Element)로 변환된다. useMemo가 메모이제이션하는 것은 이 JSX 자체이다. 반면, 컴포넌트는 JSX 요소를 반환하는 함수 또는 클래스다. 그리고 컴포넌트가 호출될 때마다 새로운 JSX 요소(React Element 객체)가 생성된다.

 

 

React.memo

따라서 컴포넌트를 메모이제이션하고자할 때는 useMemo가 아닌 React.memo를 사용하는 것이 좋다.

React.memo는 컴포넌트가 리렌더링될 필요가 없으면, 컴포넌트 자체를 다시 호출하지 않는다. 즉, props가 변경되지 않으면 기존의 렌더링 결과를 재사용한다. 

만약 위의 MemoizedComponent 코드를 memo를 활용하면, 아래와 같이 ExpensiveComponent 컴포넌트 자체를 memo로 감싸면 된다.

const ExpensiveComponent = memo(({ value }) => {
  console.log("Child 렌더링");
  return <div>Value: {value}</div>;
});

 

또한 JSX 메모이제이션과 달리 부모가 렌더링되더라도 영향 받지 않는다. 따라서 컴포넌트의 메모이제이션이 필요할 때는 React.memo(Component) 를 사용하는 것이 좋다.

 

 

memo – React

The library for web and native user interfaces

react.dev

 

또한 리액트 공식 문서를 보면, 이 memo를 API로 분류하고 있음을 볼 수 있는데, 이 API는 우리가 아는 HTTP의 API가 아니라 React에서 제공하는 기능인 React API라고 한다.

 

 

useCallback

useMemo가 값을 기억했다면, useCallback은 콜백 함수 자체를 기억하는 훅이다.

 

useMemo와 마찬가지로 의존성 배열이 변경되지 않는 한 함수를 재생성 하지 않는다. 자바스크립트에서 함수는 값으로 표현될 수 있으므로 결국 useMemo와 비슷한 코드라고 할 수 있다.

 

만약 부모의 상태가 변경돼 재렌더링이 될때, React에서는 부모 컴포넌트가 리렌더링되면, 내부에서 정의된 함수도 다시 생성된다. 이때, 자식 컴포넌트의 props로 함수가 전달되면, 함수가 변경되었다고 인식되어 자식도 리렌더링되고, 이를 방지하기 위해 useCallback을 사용하게 된다.

 

따라서 결국 useCallback은 만약 자식 컴포넌트이면서 props로 함수를 받는다면 사용해주는 것이 좋다.

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("Clicked!");
  }, []); // 한 번만 생성된 이후부턴 재사용된다.

  return (
    <div>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

 

인라인 함수는 렌더링할 때마다 재생성되어 사용하지 않는 것이 좋다?

이전에 인라인 함수는 항상 함수가 새로 생성되어 좋지 않다는 이야기를 들었었다. 그래서 간단한 함수라도 최대한 일반함수로 구현하여 props로 전달하곤 했었는데, 하지만 useCallback을 공부하면서, 원래 자식 컴포넌트의 함수는 무조건 재생성되기 때문에 인라인 함수나, 일반 함수나 똑같다는 것을 알게 되었다.

 

인라인도 useCallback으로 최적화해주는 것이 가능하기 때문에, 자식 컴포넌트의 함수가 전달되고 있다면 useCallback을 사용해주는 것이 유리할 것 같다.