useMemo — 적중은 Object.is(deep-equal 아님)
useMemo 적중 조건은 Object.is(참조/원시 동등)이지 deep-equal이 아니다. 그래서 인라인 items={data.map(...)}은 내용이 같아도 매 렌더 miss(참조가 매번 새로움). 정확성은 별개 — 순수 가정(매 렌더 f() = φ(deps))이 서면 적중이든 miss든 반환값은 항상 fresh φ(deps). stale closure(deps에 없는 변수 포획)는 그 가정 자체를 깨서 stale 값을 낸다.
useDeferredValue — items와 무관 (핵심)
deferred 궤적은 자기 입력(query)에만 의존하고 items 참조 안정성과 무관(증명이 rfl인 이유 — items가 계산에 안 들어감). 그래서 items가 매 렌더 새로 만들어져도 죽는 건 useMemo 캐시 한 층뿐, deferred는 안 흔들린다.
안전성: deferred는 항상 지금까지 본 입력 중 하나(미래·날조값 아님). 수렴: 입력이 안정화되고 저우선순위 렌더가 무한 선점 안 되면 따라잡음 — 그 격차 구간이 isStale(opacity dim 창). 연속 타이핑 시 영영 못 따라잡을 수 있는데 버그가 아니라 의도(그 내내 dim 유지).
형식 닻: renderDeferred_const_in_items(items에 상수), converge(안정화 후 따라잡음).
useMemo는 성능 힌트지 의미적 보장이 아니다 — 정확성을 캐시에 의존하면 안 된다. React는 특별한 이유가 있으면 캐시를 버린다: 개발 중 HMR, 초기 마운트 중 suspend, 미래 기능(오프스크린/가상 리스트). 그래서 useMemo 결과로 “한 번 정해지면 유지돼야 하는 값”을 만들면, 캐시가 폐기되는 순간 값이 바뀐다.
// ❌ 캐시 폐기 시 색이 재생성 → flicker
const colors = useMemo(() => getRandomColors(baseTheme), [baseTheme])
// ✅ 영속성이 정확성 조건이면 state로
const [colors, setColors] = useState(() => generateAccentColors(baseTheme))
const [prevTheme, setPrevTheme] = useState(baseTheme)
if (baseTheme !== prevTheme) {
// 렌더 중 prev 비교 = derived state
setPrevTheme(baseTheme)
setColors(generateAccentColors(baseTheme))
}
판별 기준: “이 값이 재계산되면 틀린 동작인가, 아니면 그냥 느린 동작인가.” 틀림 → state/ref. 느림 → useMemo.