React Compiler가 나온 뒤 useMemo와 useCallback 최적화
2025년 11월 5일
useMemo와 useCallback은 불필요한 리렌더링을 막기 위해 썼다. 값이나 함수를 캐싱해두고, 의존성이 바뀌지 않으면 이전 결과를 재사용하는 방식이다. React 19에서 React Compiler가 정식 도입되면서 이 작업을 직접 할 필요가 없어졌다.
기존 방식의 문제
useMemo와 useCallback은 성능 최적화 도구지만, 잘못 쓰면 오히려 코드만 복잡해졌다.
// 최적화한다고 썼지만 의존성 배열 관리가 복잡해진다
const filtered = useMemo(() => {
return items.filter((item) => item.active);
}, [items]);
const handleClick = useCallback(() => {
onSelect(id);
}, [onSelect, id]);
의존성 배열에 뭘 넣어야 하는지 헷갈리고, 빠뜨리면 stale 클로저 버그가 생긴다. 실제로 성능 병목이 없는데 습관적으로 감싸는 경우도 많았다.
React Compiler가 하는 일
React Compiler는 빌드 타임에 컴포넌트 코드를 분석해서 자동으로 메모이제이션을 적용한다. 개발자가 직접 useMemo, useCallback을 쓰지 않아도 컴파일러가 어떤 값과 함수를 캐싱해야 하는지 판단한다.
// 컴파일러가 알아서 최적화한다
function List({ items, onSelect, id }) {
const filtered = items.filter((item) => item.active);
return (
<ul>
{filtered.map((item) => (
<li key={item.id} onClick={() => onSelect(id)}>
{item.name}
</li>
))}
</ul>
);
}
위 코드에서 filtered와 onClick 핸들러는 컴파일러가 의존성을 추적해서 필요할 때만 재계산되도록 변환한다.
React Compiler가 제대로 작동하려면
React Compiler의 자동 최적화는 Rules of React를 지킨다는 전제 위에서 동작한다. 규칙을 어기면 컴파일러는 해당 컴포넌트의 최적화를 포기한다.
지켜야 할 핵심은 세 가지다.
컴포넌트는 순수 함수여야 한다. 같은 props와 state가 들어오면 항상 같은 JSX를 반환해야 한다. 렌더링 중 외부 변수를 바꾸거나 사이드이펙트를 일으키면 컴파일러가 캐싱 여부를 판단할 수 없다.
props와 state는 읽기 전용이다. 컴파일러는 이 값들이 렌더링 사이에 바뀌지 않는다고 가정하고 최적화를 결정한다. items.push(...) 같은 직접 변경은 그 가정을 깨뜨린다.
훅은 항상 최상단에서 호출해야 한다. 조건문이나 반복문 안에서 훅을 호출하면 실행 순서가 달라질 수 있고, 컴파일러가 의존성을 정확히 추적하지 못한다.
useMemo와 useCallback 없이도 최적화가 되는 건 이 규칙들이 지켜진 코드에서만이다.