useMemo 캐시 적중 진단 — 매 렌더 새로 만들어지는 deps
무거운 계산을 useMemo로 감쌌는데 렌더 시간이 줄지 않는다. 각 단계는 질문 하나에 답하고, 그 답이 다음 질문을 만든다. 단계마다 추론이 아니라 관찰로 넘어간다.
1. 정말 이 계산이 느린가
React DevTools Profiler로 조작 하나를 녹화하고, flamegraph에서 그 컴포넌트의 렌더 시간(ms)을 본다. 여기서 이 컴포넌트가 병목이 아니면 useMemo를 볼 이유가 없다. 이 숫자는 5에서 수정 전후를 비교할 기준선이 된다.
2. 정말 재사용되고 있나
계산 함수 안에 카운터를 넣고 리렌더를 몇 번 일으킨다.
const rows = useMemo(() => {
console.count('calc')
return items.filter(fn)
}, [items])
calc가 렌더와 같이 늘어나면 한 번도 재사용되지 않고 있다. 개발 모드에서는 React가 계산 함수를 일부러 두 번 부르므로1 렌더당 2씩 뛰는 게 정상이다 — 보는 건 절대값이 아니라 렌더를 따라 늘어나는지다.
CDP로 읽을 때. Runtime.enable 뒤 Runtime.consoleAPICalled가 type: "count"로 온다. 숫자는 따로 오지 않고 args[0].value에 "calc: 3" 같은 문자열 하나로 오므로 /: (\d+)$/로 꺼낸다. console.countReset은 이벤트를 내지 않는다. Runtime.enable 전에 찍힌 메시지도 enable 시점에 다시 보내준다.2
코드를 고치지 않고 셀 때. 앱에 로그를 심을 수 없으면 V8 커버리지의 호출 횟수를 쓴다.
await send('Profiler.enable')
await send('Profiler.startPreciseCoverage', { callCount: true, detailed: true })
// 리렌더 한 번 일으키고
const { result } = await send('Profiler.takePreciseCoverage')
// result[].functions[].ranges[0].count = 그 사이 호출 횟수
takePreciseCoverage는 부를 때마다 카운터를 0으로 되돌린다.2 리렌더마다 한 번씩 부르면 그 렌더 동안의 호출 횟수가 바로 나온다. 다만 useMemo(() => …)의 화살표 함수는 functionName이 비어 있어 스크립트 url과 ranges 오프셋으로 찾아야 하고(번들이면 소스맵으로 되짚는다), 개발 모드의 두 번 호출도 그대로 세어진다.
3. 왜 매번 다시 계산하나
계산 쪽이 아니라 deps를 의심한다. React는 재료 안을 들여다보지 않고 Object.is로 같은 물건인지만 본다.1 매 렌더 새로 만들어지는 값이 deps에 있으면 내용이 같아도 매번 “다른 재료”다.
의심만 하지 말고 참조가 실제로 바뀌는지 찍는다.
const prev = useRef(items)
useEffect(() => {
console.log('items same?', Object.is(prev.current, items))
prev.current = items
})
매 렌더 false면 확정이다. Profiler 설정의 “Record why each component rendered while profiling”을 켜면 코드 없이 Props changed: items로도 보인다.
4. 누가 새 참조를 만드나
같은 비교를 부모 쪽에 두고 거슬러 올라간다. 렌더 중에 새 배열을 만들어 넘기고 있었다.
<List items={data.map(transform)} />
넘기는 쪽에서 참조를 붙잡아 둔다.
const items = useMemo(() => data.map(transform), [data])
Footnotes
-
“React will compare each dependency with its previous value using the
Object.iscomparison.” / “React will not throw away the cached value unless there is a specific reason to do that … This should be fine if you rely onuseMemosolely as a performance optimization.” / “In Strict Mode, React will call your calculation function twice in order to help you find accidental impurities.” (useMemo — React) ↩ ↩2 -
Chrome 154 headless 에서 원시 CDP 로 실측 (2026-09-30).
console.count('calc')세 번 →"calc: 1"·"calc: 2"·"calc: 3",countReset뒤 다시"calc: 1". 커버리지는 3회 호출 뒤take→ 3, 2회 더 호출 뒤take→ 2. ↩ ↩2