RSS듀오랩스
React

React useMemo와 useCallback은 언제 써야 할까?

작성자
듀오랩스 대표·8분 읽기

리뷰에서 이런 코드를 자주 만납니다.

function OrderRow({ order, onSelect }) {
  const total = useMemo(() => order.price * order.qty, [order.price, order.qty]);
  const label = useMemo(() => `${order.no} · ${order.customer}`, [order.no, order.customer]);
  const handleClick = useCallback(() => onSelect(order.id), [onSelect, order.id]);
  return <li onClick={handleClick}>{label} {total}</li>;
}

곱셈 한 번, 문자열 합치기 한 번, <li> 에 넘기는 함수 하나를 모두 메모이제이션했습니다. 성능을 챙긴 코드처럼 보이지만, 이 세 줄은 대부분 아무것도 빠르게 만들지 않습니다.

useMemo 와 useCallback 은 붙일수록 빨라진다는 생각

useMemo 는 계산 결과를, useCallback 은 함수를 렌더링 사이에 기억합니다. 다시 계산하지 않으니 빨라진다고 이해하면, 붙이지 않을 이유가 없어 보입니다. 그래서 「일단 감싸 두자」가 습관이 됩니다.

메모이제이션은 공짜가 아닙니다. 매 렌더링마다 의존성 배열을 만들고, 이전 값과 하나씩 비교하고, 이전 결과를 메모리에 들고 있습니다. 곱셈 한 번보다 이 비교가 더 비쌀 수 있습니다. 그리고 의존성 배열을 틀리면 오래된 값을 쓰는 버그가 새로 생깁니다.

계산이 비싼지는 재 봐야 안다

useMemo 문서는 비싼 계산을 판단하는 방법을 구체적으로 적습니다. 수천 개의 객체를 만들거나 순회하는 것이 아니라면 대개 비싸지 않다고 하면서, 확실히 하려면 console.time 으로 재 보라고 권합니다.

console.time("filter array");
const visibleTodos = filterTodos(todos, tab);
console.timeEnd("filter array");

문서가 제시하는 기준은 합계가 1ms 이상이면 메모이제이션을 고려할 만하다는 것입니다. 그리고 useMemo 로 감싼 뒤 같은 상호작용에서 시간이 실제로 줄었는지 다시 재 보라고 합니다. 첫 렌더링은 useMemo 로도 빨라지지 않고, 개발 환경은 운영보다 느리며 사용자의 기기는 개발자의 기기보다 느릴 수 있다는 점도 함께 적습니다.

앞의 totallabel 은 이 기준에 한참 못 미칩니다. 감쌀 이유가 없습니다.

useCallback 이 의미 있는 두 경우

함수는 계산 결과가 아니라서 「비싼 계산」 기준이 맞지 않습니다. useCallback 문서는 함수를 기억하는 것이 가치 있는 경우를 두 가지로 한정합니다.

첫째, 그 함수를 memo 로 감싼 컴포넌트에 props 로 넘길 때입니다. 리렌더링 글에서 memo 는 props 를 Object.is 로 비교하므로, 렌더링마다 새로 만든 함수를 넘기면 memo 가 무력해진다고 했습니다. 이때만 함수를 고정할 이유가 생깁니다.

둘째, 그 함수가 다른 훅의 의존성으로 쓰일 때입니다. 효과의 의존성 배열에 들어가는 함수가 렌더링마다 바뀌면 효과도 렌더링마다 다시 실행됩니다.

문서는 그 밖의 경우에는 useCallback 으로 감싸는 이점이 없다고 적습니다. 앞의 handleClickmemo 컴포넌트가 아니라 평범한 <li> 에 넘기므로 두 경우 어디에도 해당하지 않습니다.

useMemo 도 같은 두 경우에 계산 비용과 상관없이 쓸모가 있습니다. memo 컴포넌트에 넘기는 객체나 배열, 효과의 의존성으로 쓰이는 객체를 고정할 때입니다.

없으면 틀리는 코드라면 먼저 고칠 것

문서에는 메모이제이션을 쓰는 태도에 관한 문장이 있습니다.

You should only rely on useMemo as a performance optimization. If your code doesn't work without it, find the underlying problem and fix it first.

useMemo 를 지우면 동작이 틀려지는 코드라면, 메모이제이션이 성능이 아니라 정확성을 떠받치고 있다는 뜻입니다. 예를 들어 렌더링마다 새 객체가 만들어져 효과가 무한히 다시 실행되는 것을 useMemo 로 막고 있다면, 원래 문제는 효과가 불필요한 의존성을 갖는 구조에 있습니다. React 는 필요하면 기억해 둔 값을 버릴 수 있다고 문서가 말하므로, 정확성을 여기에 맡기면 언젠가 깨집니다.

React Compiler 가 바꾸는 것

이 판단을 사람이 매번 하는 부담을 줄이려는 것이 React Compiler 입니다. 문서는 컴파일러를 빌드 시점에 React 앱을 자동으로 최적화하는 도구로 소개합니다. 손으로 하는 메모이제이션은 번거롭고 틀리기 쉬우며 유지할 코드를 늘린다고 하면서, 컴파일러가 이 최적화를 자동으로 해 준다고 설명합니다. memouseCallback 문서 맨 위에도 컴파일러가 이를 자동으로 적용해 손으로 쓸 일이 줄어든다는 안내가 붙어 있습니다.

그렇다면 이미 써 둔 useMemouseCallback 은 어떻게 할까요. 컴파일러 문서는 대부분의 경우 컴파일러의 메모이제이션이 사람이 쓴 것만큼 또는 그보다 정밀하다고 하면서도, 어떤 값을 기억할지 직접 제어해야 할 때를 위한 탈출구로 두 훅을 계속 쓸 수 있다고 적습니다.

저는 컴파일러가 이 글의 기준을 무의미하게 만든다고 보지 않습니다. 컴파일러를 쓰든 안 쓰든 「효과의 의존성으로 쓰이는 객체가 매번 바뀌면 효과가 다시 실행된다」는 모델은 그대로이고, 컴파일러가 무엇을 기억하는지 이해하려면 그 모델이 필요합니다. 달라지는 것은 그 모델을 손으로 코드에 옮기는 일의 양입니다.

메모이제이션을 붙이기 전에 묻는 순서

질문 그렇다면
실제로 느린가 (Profiler, console.time) 아니면 붙이지 않는다
구조로 줄일 수 있는가 (state 를 아래로, children 으로 넘기기) 구조부터 바꾼다
계산이 1ms 이상인가 useMemo
memo 컴포넌트의 props 이거나 훅의 의존성인가 useMemo / useCallback 으로 고정
지우면 동작이 틀려지는가 메모이제이션 말고 원래 문제를 고친다

여기까지가 확실한 부분

비싼 계산의 판단 방법과 1ms 기준, useCallback 이 가치 있는 두 경우, useMemo 에 정확성을 맡기지 말라는 문장, React Compiler 의 설명과 기존 훅을 탈출구로 쓸 수 있다는 안내는 react.dev 문서 기준입니다. 비교 비용이 곱셈보다 클 수 있다는 부분은 일반적인 판단이고, 특정 코드에서의 차이는 재 봐야 알 수 있습니다. 컴파일러를 도입할 때 어떤 코드가 최적화 대상에서 빠지는지는 컴파일러 문서에서 따로 확인해야 합니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.