개발 노트/프론트엔드

React Compiler 도입 후 useMemo·useCallback을 걷어내는 기준

Roslyn 2026. 9. 6. 09:00
반응형

프로젝트에서 useMemo와 useCallback이 몇 개나 쓰이고 있는지 세어본 적이 있습니다. 생각보다 많았고, 더 문제는 그중 상당수가 왜 붙였는지 기억나지 않는 것이었습니다. 언젠가 성능이 걱정돼서 붙였고, 그 뒤로 아무도 건드리지 않은 코드였습니다.

React Compiler가 자리를 잡으면서 이 코드들을 어떻게 할지 결정해야 하는 시점이 왔습니다. 컴파일러가 알아서 메모이제이션을 해준다면 손으로 붙인 것들은 중복입니다. 다만 전부 지우면 되는 것도 아닙니다. 여전히 손으로 남겨야 하는 자리가 있고, 그 구분을 모르고 일괄 삭제하면 조용히 느려지거나 이상하게 동작합니다.

컴파일러가 해주는 일과 해주지 않는 일

React Compiler는 컴포넌트와 훅을 분석해서 렌더 사이에 재사용할 수 있는 값을 자동으로 붙잡아 둡니다. 결과적으로 컴포넌트 안에서 만들어지는 값과 함수의 참조가 불필요하게 새로 만들어지는 일이 줄어듭니다. 손으로 useMemo를 감싸던 이유의 상당 부분이 여기서 해소됩니다.

다만 컴파일러가 다루는 범위는 컴포넌트와 훅 안쪽입니다. 그 바깥으로 값이 나가는 순간, 즉 리액트가 관리하지 않는 코드에 참조를 넘길 때는 이야기가 달라집니다. 그래서 저는 판단 기준을 "이 값이 리액트 안에서만 쓰이는가"로 잡았습니다.

상황손으로 남길지 여부이유

자식 컴포넌트에 props로 넘기는 함수대체로 제거컴파일러가 다루는 범위 안입니다
파생 상태 계산(필터, 정렬)대체로 제거비용이 작다면 자동 처리로 충분합니다
외부 라이브러리에 넘기는 옵션 객체남김참조 동일성이 라이브러리 동작을 좌우합니다
비용이 매우 큰 순수 계산남김의도를 코드에 명시해두는 편이 안전합니다
effect 의존성으로 들어가는 값확인 후 판단재실행 횟수가 바뀌면 부수 효과가 달라집니다

외부 라이브러리에 참조를 넘길 때

지도 라이브러리, 차트 라이브러리, 에디터처럼 리액트 바깥에서 자기 상태를 관리하는 것들이 있습니다. 이런 라이브러리에 옵션 객체나 콜백을 넘길 때 참조가 매번 바뀌면 내부에서 재초기화가 일어나기도 합니다. 저는 웹소설 초고를 다루는 에디터를 붙일 때 이 문제를 겪었습니다. 겉으로는 잘 도는데 커서가 가끔 튀는 증상이었고, 원인은 매 렌더마다 새 설정 객체가 들어간 것이었습니다.

비용이 큰 순수 계산

수천 건을 훑어 통계를 만드는 계산처럼 확실히 무거운 작업은 명시적으로 남겨두는 편이 낫습니다. 자동 최적화가 걸린다고 해도, 이 계산은 비싸다는 신호를 코드에 남기는 것 자체가 다음 사람에게 도움이 됩니다. 여기서 다음 사람은 대개 몇 달 뒤의 저입니다.

// 남겨둘 만한 경우: 외부 라이브러리에 넘기는 옵션
const editorOptions = useMemo(() => ({
  placeholder,
  autoSave: true,
  onSave: handleSave,
}), [placeholder, handleSave]);

useEffect(() => {
  const editor = createEditor(ref.current, editorOptions);
  return () => editor.destroy();
}, [editorOptions]);

전부 지우면 생기는 문제

정규식으로 useMemo를 한 번에 걷어내고 싶은 유혹이 있습니다. 실제로 해보면 아래 같은 일이 생깁니다.

• effect가 더 자주 돕니다: 의존성 배열에 들어가던 값의 참조가 매번 바뀌면 effect 재실행 횟수가 늘어납니다. 네트워크 요청이 붙어 있으면 호출량이 바로 늘어납니다.

• 외부 라이브러리가 재초기화됩니다: 앞서 말한 경우입니다. 오류가 나지 않고 체감으로만 이상해져서 원인 찾기가 오래 걸립니다.

• 무거운 계산이 반복됩니다: 컴파일러 최적화가 적용되지 않은 파일이 섞여 있으면 그대로 비용을 냅니다.

• 회귀 시점을 알 수 없게 됩니다: 한 커밋에서 백 군데를 바꾸면 어디서 문제가 생겼는지 되짚기 어렵습니다.

규칙을 어긴 코드에는 최적화가 안 붙습니다

이 부분이 중요합니다. React Compiler는 리액트 규칙을 지킨 코드에서만 안전하게 최적화합니다. 아래 같은 코드가 있으면 해당 부분은 최적화 대상에서 빠집니다.

• 조건부 훅 호출: if 문이나 반복문 안에서 훅을 부르는 코드입니다.

• 렌더 중 변이: 렌더링 도중 props나 상태 객체를 직접 수정하는 코드입니다.

• 렌더 중 부수 효과: 렌더 본문에서 외부 값을 바꾸거나 요청을 보내는 코드입니다.

즉 "컴파일러가 해주겠지"라고 믿고 손으로 붙인 것을 지웠는데, 정작 그 파일은 최적화 대상이 아니었던 상황이 생길 수 있습니다. 그래서 걷어내기 전에 린트 규칙을 먼저 켜고 위반을 정리하는 순서가 필요합니다.

점진적으로 걷어내는 절차

제가 쓰는 순서는 다섯 단계입니다. 한 번에 하나씩만 진행합니다.

• 1. 측정 먼저: 대표 화면 서너 개에서 상호작용 응답 시간과 렌더 횟수를 기록합니다. 기준선이 없으면 나중에 좋아졌는지 나빠졌는지 말할 수 없습니다.

• 2. 규칙 위반 정리: 린트 경고를 모두 해소합니다. 이 단계에서 걸린 파일이 곧 최적화가 안 되고 있던 파일입니다.

• 3. 파일 단위 적용: 화면 하나 또는 파일 하나 범위로만 손으로 붙인 메모이제이션을 제거합니다. 커밋도 그 단위로 나눕니다.

• 4. 회귀 확인: 1단계에서 잰 항목을 다시 재고, 화면 동작을 직접 눌러 확인합니다. 특히 외부 라이브러리가 붙은 화면을 봅니다.

• 5. 예외 표시: 남기기로 한 자리에는 왜 남겼는지 한 줄 주석을 답니다. 이 한 줄이 다음 정리 때 같은 고민을 반복하지 않게 해줍니다.

속도가 답답해 보여도 이 편이 결국 빠릅니다. 저는 화면 단위로 나눠서 며칠에 걸쳐 진행했고, 중간에 한 번 되돌린 적이 있는데 범위가 좁아서 5분 만에 정리됐습니다.

마치며

React Compiler는 손으로 붙인 메모이제이션의 상당 부분을 대체합니다. 다만 외부 라이브러리에 참조를 넘기는 자리, 비용이 확실히 큰 순수 계산, effect 의존성 안정화가 필요한 자리는 여전히 판단이 필요합니다. 그리고 규칙을 어긴 코드에는 최적화가 붙지 않으므로 린트 정리가 먼저입니다.

오늘 할 수 있는 한 가지는 프로젝트에서 useMemo와 useCallback이 쓰인 자리를 세어보고, 그중 외부 라이브러리로 값이 나가는 것에 표시를 해두는 일입니다. 그 표시가 곧 남길 목록이고, 나머지가 정리 대상입니다.

반응형