본문으로 건너뛰기
김신건의 로그

[Frontend] React Compiler 1.0: 자동 memoization 의 새 표준

· 수정 · 📖 약 5분 · 1,824자/단어 #react #compiler #performance #memoization #frontend
React Compiler, react-compiler, babel-plugin-react-compiler, automatic memoization, use memo, use no memo, React Forget, compiler-powered linting

정의

React Compiler 는 React 컴포넌트와 hook 의 불필요한 재계산 을 빌드 타임에 자동으로 잡아주는 optimizing compiler. 손으로 useMemo / useCallback / React.memo 를 박는 대신, 컴파일러가 값과 함수의 변경 여부 를 분석해 자동으로 캐싱한다.

핵심 약속 4가지:

  1. 자동 memoization: 손으로 쓰지 않아도 정확하게 박힌다.
  2. 조건부 memoization: if/return 이후의 값 까지 memoize 가능 (수동 hook 으로는 불가능했던 영역).
  3. Rules of React 강제: ESLint 와 함께 위반을 조용히 잡아낸다.
  4. 점진 도입: 'use memo' 디렉티브 하나로 컴포넌트 단위 부터 켤 수 있다.

v1.02025년 10월 7일 출시. 거의 10년에 걸친 컴파일러 실험 의 첫 안정 결과물이다.

왜 컴파일러인가?

React 의 멘탈 모델은 “매 렌더마다 새로 계산”. 깔끔하지만, 비싼 계산 이나 자식의 reference equality 가 필요한 곳에서는 다음 같은 코드가 불가피했다:

const filtered = useMemo(
  () => items.filter((it) => it.tag === activeTag),
  [items, activeTag],
);

const handleClick = useCallback(
  (id: string) => onSelect(id),
  [onSelect],
);

const Child = memo(function Child({ data }) {
  // ...
});

문제는 3가지:

  1. 디펜던시 배열을 사람이 관리: 빠뜨리면 stale, 너무 많이 넣으면 무의미.
  2. memoize 한 함수도 변하면 자식이 다시 렌더: 한 군데서 끊기면 사슬 전체가 무너진다.
  3. useMemo 자체가 비용: dependency 비교 + 캐시 슬롯 관리. 오히려 무겁다.

자동화는 비용 자체를 줄이는 것이 아니라 사람의 일관성을 보장 하기 위한 것. 컴파일러는 조건부 early-return 이후 의 값까지 memoize 한다.

컴파일러는 어떻게 동작하는가

Babel 이 만든 Abstract Syntax Tree (AST) 를 받아서, 자체 IR (HIR, High-Level Intermediate Representation) 로 lowering. HIR 은 Control Flow Graph (CFG) 기반이라 정확한 data-flow analysismutability inference 가 가능하다.

Source (JSX/TS)
  ↓ Babel parse
AST
  ↓ React Compiler lowering
HIR (CFG-based)
  ↓ multi-pass
    ├ data-flow analysis
    ├ mutability inference
    ├ scope formation (memoization 단위)
    └ rules-of-react validation
  ↓ codegen
Optimized output (AST)
  ↓ Babel print
JS

이 분리 덕에 다음이 가능:

  • 조건부 분기 이후 memoize: useMemo최상위 위치 제약이 사라진다.
  • 세분화 (granular) scope: 거대한 useMemo 하나가 아니라 필요한 값마다 따로.
  • lint 가 같은 분석을 재활용: eslint-plugin-react-hooks 의 새 규칙들이 compiler 의 HIR 분석 결과를 그대로 쓴다.

공식 예시 (조건부 return 후의 theme 값까지 memoize):

import { use } from 'react';

export default function ThemeProvider(props) {
  if (!props.children) {
    return null;
  }
  // 컴파일러는 conditional return 이후의 값도 memoize 한다
  const theme = mergeTheme(props.theme, use(ThemeContext));
  return (
    <ThemeContext value={theme}>
      {props.children}
    </ThemeContext>
  );
}

수동 useMemo 로는 불가능한 위치. 컴파일러만의 강점.

컴파일 모드와 디렉티브

모드동작용도
infer (기본)컴포넌트 / hook 으로 추론 되면 자동 컴파일새 코드, 전체 도입
annotation'use memo' 가 명시된 함수만 컴파일점진 도입 초기
all모든 함수를 컴파일 시도보통 권장하지 않음
syntaxparse 만 하고 실제 변환은 하지 않음debugging

함수 단위 디렉티브 (모드와 무관하게 우선):

function StableComponent() {
  'use memo';      // 이 컴포넌트는 *반드시* 컴파일
  // ...
}

function ProblematicComponent() {
  'use no memo';   // 이 컴포넌트는 *반드시* 컴파일에서 제외
  // 보통 Rules of React 위반을 발견한 임시 escape
}

권장 도입 흐름:

  1. compilationMode: 'annotation' 으로 켜고, 잘 검증된 컴포넌트부터 'use memo' 추가.
  2. ESLint 새 규칙으로 위반 코드 식별 + 수정.
  3. compilationMode: 'infer' 로 전환.
  4. 문제 발견된 컴포넌트만 'use no memo'임시 격리 + 원인 수정 후 제거.

설정 (babel.config.js)

// babel.config.js
module.exports = {
  plugins: [
    [
      'babel-plugin-react-compiler',
      {
        compilationMode: 'annotation',
        target: '18',                  // '17' | '18' | '19' (기본 '19')
        panicThreshold: 'none',        // 'all_errors' | 'critical_errors' | 'none'
        logger: {
          logEvent(filename, event) {
            if (event.kind === 'CompileSuccess') {
              console.log('Compiled:', filename);
            }
          },
        },
        gating: {
          source: 'my-feature-flags',           // 모듈 경로
          importSpecifierName: 'isCompilerEnabled', // 토글 함수
        },
      },
    ],
  ],
};

주요 옵션:

  • target: '17' | '18': React 19 미만이면 react-compiler-runtime 을 의존성에 추가하고 target 을 명시. v1.0 의 놀라운 약속.
  • panicThreshold: 일반적으로 'none' 권장. 컴파일에 실패한 함수는 그대로 두고 넘어감.
  • gating: A/B 테스트나 feature flag 로 환경에 따라 켜고 끄기. Meta 가 내부적으로 쓰던 방식.

빌드 도구 매트릭스

빌드 도구지원 상태 (2026-06)
Babel1차 (babel-plugin-react-compiler)
Vite@vitejs/plugin-react 의 Babel 옵션 경유, 또는 vite-plugin-react-compiler
Next.jsnext.config.jsreactCompiler: true. 15.3.1+ 에서 swc plugin 으로 빌드 속도 크게 개선
swcexperimental. @kdy1dev 가 작업. Next.js 가 먼저 활용
oxc작업 중 (oxc-project/oxc#10048)
Rsbuild1차 지원
ExpoSDK 54 부터 기본 enable

새 프로젝트는 create-vite, create-next-app, create-expo-app 모두 컴파일러 enable 옵션 을 제공한다. 새 코드면 거의 무조건 켜는 것이 권장.

ESLint 통합

v1.0 부터 별도 eslint-plugin-react-compiler 는 폐지. eslint-plugin-react-hooksrecommended preset 안으로 통합되었다.

// eslint.config.js (Flat Config)
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  reactHooks.configs.flat.recommended,
]);

새 규칙 일부:

  • set-state-in-render: 렌더 중 setState 호출로 인한 무한 루프 패턴.
  • set-state-in-effect: Effect 안에서 불필요한 state 동기화 를 잡음.
  • refs: 렌더 중 ref 접근 같은 unsafe 사용을 잡음.

ESLint 만 켜는 것도 유의미: 컴파일러를 실제로 도입하지 않아도, Rules of React 위반을 컴파일러의 IR 수준 분석으로 잡아낼 수 있다.

useMemo / useCallback / React.memo 는 이제 필요 없나?

대부분의 경우 그렇다. 컴파일러는 손으로 쓴 것보다 더 정밀하게 memoize 한다.

다만 다음 세 경우는 escape hatch 로 그대로 둘 가치가 있다:

  1. useEffect 의 dependency 가 정확히 memoize 된 값이어야 할 때: 컴파일러가 언제 reference 가 바뀌는지 의 의도를 표현할 곳이 코드에 명시되어 있어야 안전.
  2. 외부 라이브러리에 stable reference 를 넘겨야 할 때: 라이브러리의 hook 내부가 dep array 를 보는 경우.
  3. 이미 깔린 기존 useMemo 를 제거: 컴파일 결과가 미세하게 달라질 수 있어, 코드를 건드리지 않고 두는 편이 안전. 새 코드는 안 써도 OK.

프로덕션 결과

Meta 가 자체 앱들에 수년간 적용한 결과 (공식 글):

  • Meta Quest Store: 초기 로드와 페이지 이동 최대 12% 개선. 일부 인터랙션은 2.5배 이상 빨라짐.
  • 메모리 사용량은 중립: memoize 추가 비용이 그만한 가치 가 있도록 균형.

오픈소스 사례:

  • Sanity Studio: 거대한 content editor 의 복잡한 트리에서 도입 회고 (discussion #33).
  • Wakelet: 측정된 수치 와 함께 도입 회고 (discussion #52).

v1.0 까지의 타임라인

시점이벤트
2017Meta 의 Prepack 실험. 종료되었지만 Hooks 설계의 토대 가 됨
2021Xuan Huang첫 prototype 데모
2024-02React Labs 글, 본격적인 Compiler 작업 상황 공개
2024-05React Conf 2024 에서 experimental 공개
2024-10Beta 출시. Meta 사내 사용량 확대
2025-10-07v1.0 stable, ESLint 통합, 새 프로젝트 기본 enable
2025-10-16React Conf 2025 에서 리뷰

다음

v1.0 은 시작점. 공식 글이 명시하는 방향:

  • 더 정밀한 memoization (현재보다 세분화 + 예측 가능성).
  • 더 많은 Rules of React 를 정적으로 검증.
  • swc / oxc 와의 통합 강화 → 빌드 속도.
  • Library precompilation: 라이브러리 저작자가 미리 컴파일된 결과 를 패키지에 동봉.

관련 위키

참고

이 글의 용어 (3개)
[Frontend] SPA Architecture: CSR, SSR, SSG, hydrationjavascript
정의 SPA (Single Page Application) 는 첫 로딩 시 받은 HTML / JS 번들 하나로 모든 화면 전환을 클라이언트에서 처리하는 웹 앱 구조. URL 변경…
[Javascript] 클로저javascript
정의 클로저 (Closure): 함수가 자신이 정의된 시점의 외부 변수들을 계속 참조할 수 있는 성질. 함수가 일급 객체 () 인 언어의 자연스러운 결과. 메커니즘은 + 함수 객…
[Voice AI] Agent 패턴: Supervisor, Handoff, ReAct, HITLai
정의 복잡한 음성 에이전트 = 여러 sub-agent + 조정 패턴. 단일 LLM 으로 처리하기 어려운 흐름. [!IMPORTANT] 각 sub-agent 내부는 VAD → ST…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기