노트

React Compiler

React Compiler

프런트엔드#react#performance · 연결된 개념 9개

쉽게 말하면

React Compiler는 꼼꼼한 비서가 원고를 미리 훑어보고 '여긴 지난번과 같으면 재사용' 표시를 곳곳에 달아 주는 것과 같아요. 개발자가 손으로 붙이던 useMemo·useCallback을 빌드할 때 자동으로 넣어 줘요.

비유가 깨지는 곳 비서는 원고가 규칙을 지킨다고 믿고 일해요. 렌더 중 ref 읽기나 호환되지 않는 라이브러리를 만나면 그 컴포넌트는 손대지 않고 넘겨요. 또 메모 코드가 들어가는 만큼 번들도 커져요.

React Compiler는 빌드 타임에 컴포넌트와 훅을 분석해 메모이제이션(Memoization) 코드를 자동으로 넣어 주는 컴파일러다. 개발자가 손으로 붙이던 useMemo·useCallback·memo를 대신한다. 2025년 10월 1.0이 나왔다.

동작 원리

컴포넌트마다 캐시 슬롯 배열을 만들고, 입력이 Object.is로 같으면 이전 결과를 꺼내 쓴다(개념 코드).

function Cart({ items }) {
  const $ = _c(2)                 // 슬롯 2개
  let total
  if ($[0] !== items) { total = items.reduce(sum, 0); $[0] = items; $[1] = total }
  else total = $[1]
  return <Summary total={total} />
}
  • 의존성을 자동으로 추론하니 deps 배열을 빠뜨릴 일이 없다
  • useMemo처럼 덩어리 단위가 아니라 식 단위로 잘게 메모한다
  • JSX(JavaScript XML)도 메모한다. props가 같으면 같은 엘리먼트 객체를 돌려줘서 React가 자식 렌더를 건너뛴다. 그래서 memo()와 비슷한 효과를 낸다
  • 주로 Babel 플러그인으로 동작하고, 컴포넌트(대문자, JSX 반환)와 훅(use…)만 대상이다

전제와 bailout

컴파일러는 코드가 React의 규칙과 렌더 순수성를 지킨다고 가정한다. 렌더 중 ref.current 읽기, props·state 변경, 호환되지 않는 라이브러리(값을 변경 가능한 객체로 주는 폼 라이브러리 등)를 감지하면 그 컴포넌트를 건드리지 않고 원본대로 둔다(bailout). 에러가 아니라 보수적인 보호 장치라 동작은 그대로이고, 자동 메모 혜택만 못 받는다. eslint-plugin-react-hooks가 이런 곳을 알려 준다.

실무 포인트

  • 새 코드에는 성능용 수동 메모를 쓰지 않는다. bailout된 컴포넌트, Effect 의존성으로 쓰이는 값 정도만 수동으로 남긴다
  • 이미 가벼운 렌더는 더 빨라지지 않는다. 무거운 리렌더를 줄이는 도구다
  • 컴포넌트마다 캐시 코드가 들어가 번들이 커진다. 효과와 비용을 재 보고 판단한다(번들 크기 줄이기)
  • 탈출구: 함수 첫 줄에 'use no memo'. 점진 도입은 compilationMode: 'annotation' + 'use memo'

컴파일러 기반 린트 규칙

eslint-plugin-react-hooks 7부터 기존 두 규칙(rules-of-hooks, exhaustive-deps)에 컴파일러 진단을 쓰는 규칙들이 더해졌고, recommended 프리셋에 들어 있다(2026-10 기준 7.1). 컴파일러를 켜지 않은 앱에서도 쓸 수 있어서 도입 전 준비 단계로 좋다. 자주 걸리는 것들은 다음과 같다.

  • set-state-in-effect: Effect 본문에서 곧바로 setState. 렌더를 한 번 더 돌리니 렌더 중 계산으로 바꾼다(Effect가 필요 없는 경우). ref로 잰 DOM 크기를 state에 넣는 경우는 예외다
  • immutability: props·state 등 추적 중인 값을 직접 변경(items.push(x); setItems(items))
  • refs: 렌더 중 ref.current 읽기·쓰기
  • purity: 렌더 중 Date.now()·Math.random() 같은 비순수 호출
  • preserve-manual-memoization: 기존 useMemo·useCallback의 deps가 빠져 컴파일러가 그 메모를 보존할 수 없는 경우
  • incompatible-library: 컴파일러가 건너뛰는 API 사용(내부 가변성과 메모이제이션). 기본은 경고
  • 그 밖에 set-state-in-render, static-components, globals, use-memo, error-boundaries 등이 있고, 실험 규칙까지 받으려면 recommended-latest를 쓴다

기존 코드베이스에 처음 켜면 위반이 많이 나온다. 한꺼번에 고칠 필요는 없고, 규칙별로 끄거나 경고로 낮춘 뒤 하나씩 켜 가면 된다. 위반한 컴포넌트는 컴파일러가 건너뛸 뿐이라 점진적으로 최적화 범위가 넓어진다.

도입 판단 축

  • 런타임 이득: 리렌더가 이미 가볍거나 서버 렌더·정적화 비중이 크면 체감 이득이 거의 없을 수 있다. 프로파일러로 실제 느린 인터랙션이 있는지 먼저 본다
  • 번들 비용: 메모 코드가 늘어 번들이 커진다. 모바일 비중이 높으면 압축 후 크기를 직접 재 본다
  • 코드 단순화: 수동 useMemo·useCallback과 deps 실수를 줄이고, 새 코드에서 "메모할까" 고민이 사라진다. 성능보다 이쪽이 주된 이득일 때가 많다
  • 위험과 전환 비용: 비호환 라이브러리, 렌더 중 ref 접근 등 건너뛰거나 동작이 달라질 수 있는 곳을 핵심 경로(결제 등)부터 확인해야 한다. 수동 메모를 지운 뒤 되돌리면 다시 써야 한다
  • 중간 선택지: compilationMode: 'annotation'으로 무거운 컴포넌트에만 'use memo'를 붙여 비용을 국소화한다

수동 도구의 원리는 memo·useMemo·useCallback, 순수성 규칙은 부수 효과와 Effect.

출처: React - eslint-plugin-react-hooks · React - set-state-in-effect · React - preserve-manual-memoization

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 재조정(Reconciliation)

    재조정(Reconciliation)은 React가 이전 렌더의 엘리먼트 트리와 새 트리를 비교(Diffing)해, 실제 DOM(Document Object Model)에 필요한 최소한의 변경만 반영하는 과정이다. 두 트리를 완벽하게 비교하면 O(n³)이라, React는 두 가지 가정을 두어 O(n)으로 줄인다.

  • 상태 내리기와 컴포지션으로 리렌더 줄이기

    상태 내리기(State Colocation)는 state를 실제로 쓰는 컴포넌트 가까이로 옮겨, state가 바뀔 때 다시 렌더되는 범위를 좁히는 기법이다. 메모이제이션(Memoization)보다 먼저 시도할 구조적 최적화다.

  • React로 보는 SOLID

    SOLID는 원래 객체 지향 설계 원칙이지만 React 컴포넌트·props·훅에도 그대로 옮겨 볼 수 있다. 원칙 자체보다 "어떤 코드 모양이 되는가"를 짧게 정리한다.

  • 변수·컬렉션 캡슐화

    넓은 범위에서 쓰이는 데이터에 직접 접근하지 못하게 하고, 읽고 쓰는 함수를 통해서만 다루게 하는 리팩터링. 데이터를 옮기거나 바꿀 때 고쳐야 할 곳이 접근 함수 한 곳으로 좁아진다.

  • 구성요소 줄이기

    셀 수 있는 모든 구성요소(파일, 폴더 깊이, 클래스, 함수, 분기, 변수, 테이블, 필드, 테스트 케이스…)는 비용이라는 관점. 심플 디자인의 네 번째 규칙을 실무 기준으로 풀어낸 것이다.

보기 옵션