Front/React

React.memo, useMemo, useCallback 차이점과 활용법

thisisjustcode 2025. 1. 12. 19:28

React로 개발을 하다보면 원하지 않는 시점에 컴포넌트가 재렌더링 되는 경우가 있다. 이러한 현상을 보고 있으면 스트레스를 받아 성능을 최적화 해야겠다는 생각이 들게된다. 이럴 때 사용할 수 있는 기법 중 하나가 오늘의 주제 “Memoization” 이다.

 

Memoization 이란?

컴퓨터가 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로서 동일한 계산을 하지 않도록 하여, 속도를 높이는 기술이다. 보통 애플리케이션의 최적화를 위해 사용된다.


리액트에서 리렌더링이 일어나는 조건

  1. 컴포넌트의 state가 변경되었을 때
  2. 컴포넌트가 상속받은 props가 변경되었을 때
  3. 부모 컴포넌트가 리렌더링이 된 경우 모든 자식 컴포넌트는 리렌더링

React 에서 Memoization을 하는 대표적인 방법

Memoization 을 하는 대표적인 방법으로는 `React.memo`, `useMemo`, `useCallback` 3가지 방법이 있다.

 

React.memo

React.memo는 React 에서 제공하는 고차 컴포넌트(HOC)로, 함수형 컴포넌트의 렌더결과를 메모이징해 성능을 최적화할 수 있다.

고차함수
수를 인자로 받거나 또는 함수를 반환함으로서 작동 하는 함수.

 

작동 원리

`React.memo`는 props의 얕은 비교를 통해 이전과 현재의 props가 같은지 확인한다. 부모가 리렌더링되더라도 props가 변경되지 않는 한 React는 이를 리렌더링하지 않는다.

 

예제

React.memo 미적용

`React.memo`를 적용하지 않을 때는 부모 컴포넌트의 state 값만 변경됐지만 자식인 `MemoTestComponent`도 같이 리렌더링이 된다.

 

React.memo 적용

`React.memo`를 적용하면 부모 컴포넌트의 state 값이 변경돼도 `MemoTestComponent` 컴포넌트의 state, props 가 변경되지 않으면 리렌더링 되지 않는다.


useMemo

비용이 높은 복잡한 연산이 일어나는 경우, 값이 자주 변경되는 경우 `useMemo`를 사용하여 성능을 개선할 수 있다.

 

작동원리

초기 렌더링에서 반환값을 캐싱하고, 리렌더링하는 과정에서 특정 값이 바뀌었을 때만 계산을 실행한다. 만약 값이 바뀌지 않았다면 이전에 계산한 그 값을 그대로 사용한다. `useCallback`과 마찬가지로 의존성 배열의 값이 변경되지 않으면 이전에 계산한 값을 재사용한다.

 

예제

const dummyList = [...Array(29999998)].map((_, index) => {
    return {
        id: index,
        isSelected: index === 29999997,
    };
});

테스트를 위해 더미 데이터를 만들고 제일 마지막 요소의 `isSelected` 값을 `true` 로 넣어준다.

 

useMemo 미적용

`items` 의 요소들 중 `isSelected`가 `true` 인 요소를 찾아 반환하는 과정에서 29999998의 배열을 순회하기 때문에 `count`의 상태 값을 짧은 시간안에 여러 번 클릭 할 수록 성능이 심하게 저하된다. 

 

useMemo - 미적용

짧은 시간안에 `count` 값이 여러 번 변경 될 수록 성능이 심하게 저하된다.

 

useMemo 적용

`useMemo`를 감싸게 된다면 이전 값을 캐싱하여 보여주기 때문에 `items`의 `isSelected` 의 값이 변경되지 않는다면 다시 계산한 필요없이 이전 값을 반환한다.

 

useMemo - 적용

캐싱 된 값을 반환하기 때문에 성능 저하가 없다.


useCallback

React 에서 제공하는 기본 hook 중 하나로, 함수를 메모이징해서 성능을 최적화할 수 있는 기능을 제공한다.

 

작동원리

React는 컴포넌트가 렌더링 될 때 마다 다시 함수를 생성한다. 하지만 `useCallback` 으로 감싸주게 되면 첫 렌더할 때만 생성하고 그 이후에 의존생 배열의 값이 변경되지 않으면 이전 렌더링에서 이미 저장해 두었던 함수를 반환한다.

 

예제

useCallBack 미적용

`count`값을 변경하는 `handleClick` 함수를 생성하여 자식 컴포넌트의 props로 전달한다.  `count`값이 변경 될 때마다 부모 컴포넌트의 함수와 상태값이 새로 생성되는데, 이 때 `handleClick`함수가 새로 생성되어 props의 변경이 일어났기 때문에 자식 컴포넌트는 리렌더링 된다.

 

useCallback 적용

`useCallback`으로 감싸주게 된다면 의존성 배열의 값이 변경되지 않는다면 이전에 캐싱한 함수를 반환한다.

 

자식 컴포넌트에 memo를 감싼 이유

memo를 감싸지 않는다면 React 에 렌더링 과정에는 render phasecommit phase 가 있는데, `useCallback` 을 통해 함수에 변경점이 없음을 확인시켜 commit phase는 최적화 시켰지만 virtual DOM을 만들고 실제 DOM과 비교하는 render phase 과정은 최적화 시키지 못해 재렌더링이 일어난다. `memo` 로 감싸주게 된다면 props의 값이 변화가 없어 virtual DOM을 만들 필요 없음을 알려주고 render phase를 최적화 시켜 재렌더링이 일어나지 않게 만든다.

useCallback과 useMemo 무엇이 다른가
useCallback은 함수 자체를 캐싱하고, useMemo는 함수를 호출하고 그 결과를 캐싱한다.

최적화를 위해 모든 곳에 useCallback, useMemo, React.memo 를 사용해야 되나?

위 질문에 대해 두 가지 주장으로 나눠진다.

첫 번째 “최적화에도 비용이 들기 때문에 꼭 필요할 때만 최적화를 해야된다.” 라는 주장과 두 번째 “렌더링 과정의 비용이 크기 때문에 가능한 모든 곳에 memoization을 적용해야된다” 라는 주장이다. 이 두 주장에는 각각의 근거와 이유가 있겠지만, 내가 내린 결론은 React 공식 문서에도 언급되었듯이, 대부분의 경우 최적화가 큰 이익을 가져오지 않으며, 특정 상황에서만 최적화가 필요하다면 그때 진행하는 것이 적절하다는 것이다.

 

참고

'Front > React' 카테고리의 다른 글

React의 렌더링 과정(Render Phase 와 Commit Phase)  (0) 2025.01.06