React Compiler 시대, useCallback과 useMemo는 언제 써야 하나

React Compiler 시대, useCallback과 useMemo는 언제 써야 하나

측정 없이 추가한 메모이제이션이 오히려 성능을 망치는 이유—프로파일러가 먼저다.

useCallback useMemo React Compiler React Profiler 메모이제이션 렌더링 최적화 React.memo 성능 최적화
광고

useCallbackuseMemo는 React 생태계에서 가장 많이 오해받는 훅이다. 주니어 PR을 리뷰하다 보면 "성능 최적화를 위해 썼다"는 코멘트와 함께 거의 모든 함수와 배열이 메모이제이션으로 감싸져 있는 걸 심심찮게 발견한다. 그런데 실제로 React Profiler를 돌려보면 역설적인 결과가 나온다. 메모이제이션을 더 많이 쓸수록 비교 연산이 쌓이고, 인터랙션이 오히려 느려진다. dev.to에 최근 공유된 프로파일러 중심의 사례 분석은 이 패턴이 얼마나 광범위하게 퍼져 있는지를 잘 보여준다.

두 훅이 실제로 하는 일

먼저 개념부터 명확히 하자. useMemo는 의존성이 바뀌지 않으면 이전에 계산한 값을 재사용한다. useCallback은 함수 레퍼런스를 안정적으로 유지한다—본질적으로 useMemo의 함수 버전이다. 핵심은 이 두 훅 모두 그 자체로는 리렌더링을 막지 않는다는 점이다. React.memo로 감싼 자식 컴포넌트가 props의 참조 동일성을 체크할 때 비로소 의미가 생긴다. 가장 흔한 오해는 useMemo가 부모의 리렌더링 자체를 막아준다고 생각하는 것인데, 부모 컴포넌트는 state가 바뀔 때마다 여전히 함수 바디 전체를 실행한다. useMemo는 그 실행 안에서 특정 연산 하나를 건너뛸 뿐이다.

메모이제이션 과잉의 실제 사례

500행짜리 어드민 테이블에서 모든 셀 포매터를 useMemo로 감싸고, 각 행에 useCallback 핸들러를 주입했다고 상상해보자. 결과는 스크롤 버벅임이었다. 스크롤 이벤트마다 수백 개의 의존성 비교가 발생하고, 그 비용이 포매팅 연산 자체보다 훨씬 커진 것이다. 해법은 훅을 더 추가하는 게 아니라 리스트 가상화(@tanstack/react-virtual 같은 도구)와 컴포넌트 분리였다. 메모이제이션을 쓰기 전에 구조를 먼저 고쳐야 한다는 교훈이다.

또 하나의 전형적인 함정은 React.memo로 감싸지 않은 자식에게 useMemo로 안정화한 props를 넘기는 패턴이다. useMemo의 비용은 치렀는데, 자식은 어차피 매번 리렌더링된다. 훅 없이 메모이제이션 소비자가 없으면 그냥 낭비다.

프로파일러를 먼저 켜야 하는 이유

프로덕트 관점에서 성능 최적화는 "측정 → 병목 발견 → 해결"의 순서여야 한다. React DevTools의 Profiler 탭에서 인터랙션을 녹화하고 노란 막대(wasted render)를 찾는 것이 출발점이다. 이때 수정 순서는 명확하다: 먼저 구조를 개선하고(context 분리, state 위치 이동, 가상 리스트 도입), 그래도 React.memo로 감싼 자식이 안정적인 props를 받고도 리렌더링된다면 그때 useMemouseCallback을 추가한다.

실무에서 메모이제이션 훅을 추가할 때 // Profiler: Chart 40ms → 8ms 같은 주석을 남기는 습관도 여기서 나온다. 다음 개발자가 맥락 없이 삭제하지 않도록, 측정의 근거를 코드에 새겨두는 것이다.

언제 쓰고, 언제 쓰지 말아야 하나

실무에서 정리한 기준은 이렇다. useMemo는 계산 비용이 크고 그 결과를 React.memo 자식에게 넘길 때 쓴다. useCallback은 함수 레퍼런스를 React.memo 자식의 props로 넘길 때 쓴다. 반대로 원시값(string, number, boolean) props를 넘기거나, React.memo로 감싸지 않은 컴포넌트에 전달하거나, 1ms도 안 걸리는 필터·계산 로직에는 붙이지 않는 게 낫다. 훅의 오버헤드가 절약분보다 크다.

Context provider의 value 객체처럼 참조가 매 렌더마다 달라지는 경우엔 useMemo가 유의미하게 작동한다. 하지만 그 전에 context를 더 작게 쪼갤 수 없는지를 먼저 고민하는 게 맞다.

React Compiler와 앞으로의 전략

React 19와 함께 공식 논의가 이어지고 있는 React Compiler는 이 판을 바꿀 변수다. 컴파일 타임에 자동으로 메모이제이션을 적용한다면, 개발자가 수동으로 useCallbackuseMemo를 작성하는 부담은 크게 줄어든다. 하지만 컴파일러가 완전히 안착하기 전까지, 그리고 React Server Components가 이미 클라이언트 번들 자체를 줄여주고 있는 지금, 성능의 큰 그림은 여전히 서버 렌더링과 번들 사이즈 최소화에 있다. 메모이제이션 훅은 그 다음 단계의 미세 조정 도구다.

결국 원칙은 단순하다. 기본값은 훅 없음, 프로파일러가 문제를 보여줄 때만 추가. useCallback은 함수 레퍼런스를 안정화하고, useMemo는 값을 안정화한다. 둘 다 참조 동일성이 낭비된 작업을 막을 때만 의미가 있다. 측정 없이 추가된 메모이제이션은 최적화가 아니라 복잡도만 키우는 코드다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요