리액트 메모이제이션

8/20/2025FE

이전에 여러 사내 라이브러리를 제작할 때는 최적화가 중요했기 때문에 성능에 영향을 받을 수 있는 값이나 함수, 컴포넌트에는 메모이제이션을 많이 적용하고는 했습니다.
그러나 실 프로덕트를 개발하면서는 오히려 메모이제이션이 영향을 주지 않는 경우가 많았기 때문에 메모이제이션 기법을 거의 사용하지 않았는데, 이보다는 메모이제이션을 적용하는 명확한 기준이 있으면 좋을 것 같다는 생각이 들었습니다.
그래서 겸사겸사 해보는 메모이제이션 정리 📝


메모이제이션이란?

메모이제이션은 비싼 연산 결과를 저장해두고, 동일 입력이 있을 때 캐시했던 결과를 반환하는 최적화 기법입니다.
리액트에서는 리렌더링이 잦게 발생하기 때문에, 무거운 연산이 있을 때 이런 메모이제이션을 적용해서 최적화를 하고는 합니다.

리액트의 리렌더링 조건

  1. 내부 상태 값이 바뀔 때
  2. 컴포넌트가 읽는 외부 상태가 바뀔 때 (스토어 구독 등)
  3. 부모가 리렌더링될 때
  4. props가 바뀔 때
  5. forceUpdate 등으로 강제 갱신할 때

위는 간단하게 축약해서 정리해본 리액트가 리렌더링하는 조건입니다. 5번을 제외하고는 전부 리액트가 감지하는 컴포넌트 관련 변화라고 볼 수 있어요.


이처럼 리액트는 컴포넌트 내부에서 일어나는 어떤 변화든 전부 확인하는데, 기본적으로 얕은 비교를 수행하기 때문에 개발자가 의도하지 않은 변화를 감지하기도 합니다.
이것이 리액트에서 불필요한 리렌더링이 발생하는 이유이기도 하고요.


이런 불필요한 리렌더링을 최적화하고자 리액트 16부터는 세 가지 기법을 제공합니다.

  1. memo -> 컴포넌트 최적화
  2. useMemo -> 값 최적화
  3. useCallback -> 함수 최적화

아래에서 각각의 기법에 대해 간단하게 정리해보겠습니다.


memo

React v16.6에서 추가된 컴포넌트 메모이제이션 기법입니다.


HOC 형태의 memo는 컴포넌트를 감싸서 props가 이전과 같으면 리렌더링을 건너뛰도록 만듭니다.
props를 얕은 비교하기 때문에 props가 객체나 함수처럼 항상 새 참조를 가지면 memo가 무의미해질 수 있습니다. 그럴 때는 useMemouseCallback을 조합하면 동일한 참조를 사용하도록 만들 수 있기 때문에 메모이제이션을 의도대로 활용하기 좋습니다.


또한 커스텀 비교 함수를 두 번째 인자로 전달 받아 사용할 수 있습니다.

커스텀 비교 함수

React.memo(Component, (prevProps: Props, nextProps: Props): boolean => {
  // 원하는 비교 로직
});

커스텀 비교 함수는 두 가지 인자를 가지는데요. 차례대로 컴포넌트의 이전 / 다음 파라미터입니다.


비교 함수에서는 boolean 값을 반환하는데, 리렌더링이 필요할 때는 true, 아니면 false를 반환하면 됩니다.
(리액트에서는 기본적으로 Object.is()를 사용하여 컴포넌트를 비교 -> 리렌더링합니다.)


만약 props 중 특정 값이 바뀔 때만 컴포넌트를 리렌더링하고 싶다면, 그럴 때 이 커스텀 비교 함수를 활용하기 좋습니다.

React.memo(Component, (prevProps, nextProps) => prevProps.name !== nextProps.name);

만약 위처럼 로직을 작성했다면 name prop이 바뀔 때만 Component가 리렌더링 됩니다.


useMemo & useCallback

React.useMemo(() => value, []);
React.useCallback(fn, []);

React v16.8에서 추가된 값 / 함수 메모이제이션 기법입니다.


useMemo에서는 계산이 복잡하거나 비용이 큰 연산의 결과를,
useCallback에서는 매 렌더링마다 새롭게 생성되는 함수를,


의존성이 바뀌지 않는 한 재사용할 수 있도록 만들어주는 훅입니다.


객체나 함수의 참조가 바뀌지 않도록 할 수 있기 때문에, memo와의 조합이 좋습니다.


다만 의존성 비교 비용이 있기 때문에 과도한 사용은 오히려 오버헤드를 발생시킬 가능성이 있습니다.
때문에 React Profiler로 실제 성능 이득 여부를 비교한 뒤 사용하는 것이 적절합니다.


React Profiler 성능 비교

React Profiler로 성능을 비교하는 방법은 크게 두 가지로 보면 깔끔합니다.

  1. React DevTools Profiler로 렌더링 시간, 횟수 비교
  2. <Profiler> 컴포넌트로 렌더링 시간 비교
1. React DevTools Profiler로 비교

개발 빌드는 오버헤드가 크기 때문에 프로덕션 빌드로 비교하는 것이 좋습니다.
3 ~ 5회 정도 반복해서 평균을 측정한 뒤 비교합니다.


아래는 대표적으로 비교할 수 있는 수치 목록입니다.

  1. 커밋 수
  2. 커밋 당 전체 렌더 시간 (commit duration)
  3. 특정 컴포넌트의 렌더링 횟수 (render count)
  4. 특정 컴포넌트가 자기 렌더에 쓴 순수 시간 (self time)
  5. (Why did this render 옵션 활성화 후) 이전에 발생했던 원인이 사라졌는지 확인

각 메모이제이션 기법마다 목표와 지표는 조금씩 다를 수 있습니다.
저는 아래처럼 정리해봤습니다.

  • memo
    • 목표 : props가 같을 때 자식 컴포넌트 리렌더 스킵
    • 지표 : 자식 컴포넌트가 특정 커밋에서 아예 렌더에 등장하지 않음 (render count 감소), 전체 commit duration 감소
  • useMemo
    • 목표 : "비싼 계산"을 덜 수행
    • 지표 : 그 값을 계산 / 소비하는 컴포넌트의 self time 감소, 전체 commit duration 감소
  • useCallback
    • 목표 : 함수 prop 참조 안정 -> 자식 컴포넌트 리렌더 횟수 감소
    • 지표 : 자식 컴포넌트의 render count 감소, "Why did this render: prop ** changed" 원인 사라짐
2. <Profiler> 컴포넌트로 렌더링 시간 비교
const onRender(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
) {
  console.table([{ id, phase, actualDuration, baseDuration, startTime, commitTime }]);
}

<Profiler id="wanted" onRender={onRender}>
  <Component />
</Profiler>
  • id
    • 커밋된 <Profiler> 트리의 문자열 id 프로퍼티입니다.
  • phase
    • "mount" | "update" | "nested-update"
    • 트리가 최초로 마운트되었는지 또는 props, state, hook의 변경으로 인해 리렌더링되었는지 알 수 있습니다.
  • actualDuration
    • 현재 업데이트에 대해 Profiler와 자식들을 렌더링하는 데 소요된 시간 (밀리초 단위) 입니다.
    • 이상적으로는 최초 마운트 이후에 많이 감소해야 합니다.
    • actualDuration의 평균과 총합을 비교합니다.
  • baseDuration
    • 최적화 (메모이제이션) 없이 Profiler 하위 트리의 가상 (추정) 렌더 시간 (밀리초 단위) 입니다.
    • 최악의 렌더링 비용 (ex: 최초 마운트, 메모이제이션 없는 트리) 을 추정하기 때문에 actualDuration과 비교하여 메모이제이션이 작동하는지 확인할 수 있습니다.
    • baseDuration이 줄어들면, 구조적으로 작업이 더 가벼워진 신호입니다.
  • startTime
    • 렌더 시작 시점의 타임스탬프입니다.
  • commitTime
    • 커밋 시점의 타임스탬프입니다.

다만, Profiler는 CPU와 메모리에 추가적인 오버헤드를 더하기 때문에 프로덕션 빌드에서는 기본적으로 비활성화되어 있습니다.
프로덕션 프로파일링을 사용하려면 프로파일링 기능이 활성화된 특수한 프로덕션 빌드를 사용해야 합니다.


정리

(어쩌다보니 React Profiler에 대해서도 다뤘지만) 간단하게 리액트에서 제공하는 세 가지 메모이제이션 기법에 대해서 정리해봤습니다.


기본적으로 리액트는 충분히 빠르기 때문에 memo, useMemo, useCallback 없이도 충분히 잘 동작합니다.
오히려 불필요한 최적화를 사용하여 코드 복잡도만 높일 수 있기 때문에, 프로파일링 데이터를 기반으로 적절한 판단이 필요합니다.
물론 이것도 라이브러리 개발이라면 다른 이야기가 되겠지만요. 어디까지나 B2B / B2C 같은 실 프로덕트 개발에 입각해서 적어봤습니다 XD


특히 React 19에서는 React Compiler를 사용할 수 있는데, 여기에서는 메모이제이션 자동 최적화가 포함되어 있기 때문에 수동으로 사용하는 memo, useMemo, useCallback이 필요하지 않은 경우도 있다고 합니다.
19 업데이트 이후에 Compiler에 대해 다룬 글도 많았었는데, 나중에 이 부분도 공부해봐야 할 것 같아요 👩‍💻

기법 역할 사용 시기
memo 컴포넌트 메모이제이션 props 변경이 거의 없는 무거운 컴포넌트
useMemo 값 메모이제이션 무거운 계산의 반복을 줄이고 싶을 때
useCallback 함수 메모이제이션 함수 prop 때문에 불필요한 컴포넌트 렌더링을 줄이고 싶을 때
리액트 메모이제이션 : 🐢