React 컴파일러 정식 출시: 자동 최적화의 시작

React 컴파일러 정식 출시: 자동 최적화의 시작

🚀 React 컴파일러 정식 출시: 자동 최적화의 시작

🧩 "리렌더링은 문제인가? 아니면 렌더링 중 실행되는 코드가 문제인가?"

이 문장은 제가 지난 벨로그 글에서 강조한 핵심 메시지였습니다. 🔗 리렌더링 최적화의 오해 - 리렌더링이 많다고 느린 게 아니다 글에서, 저는 React의 Virtual DOM이 렌더링을 줄이는 기술이 아니라 커밋을 최적화하는 기술이라는 점을 설명하며, 진짜 병목은 “리렌더링 자체”가 아니라 “렌더링 중 실행되는 코드”라고 강조했습니다.

그런데 이제, React 컴파일러의 등장으로 이 고민조차 할 필요가 없어질지도 모릅니다.

React 팀이 드디어 **React Compiler (리액트 컴파일러)**의 첫 정식 버전을 공개했습니다. 수년간의 실험과 베타를 거쳐 발표된 이 컴파일러는, 리액트 개발자들이 오랫동안 겪어온 useMemo, useCallback, React.memo 지옥에서 벗어날 수 있도록 돕는 자동화된 빌드 타임 최적화 도구입니다.

React 컴파일러 v1.0


🧭 왜 React 컴파일러가 필요했을까?

문제 정의

대규모 리액트 앱에서 “리렌더링 감소”를 위해 우리는 다음과 같은 수동 최적화를 강박적으로 적용해왔습니다.

  • 상위 리렌더 → 하위 전파에 따른 불필요한 호출 폭증
  • 매 렌더마다 새로 생성되는 함수/객체 참조 변화로 인한 diff 낭비
  • useMemo/useCallback/React.memo보일러플레이트 남발의존성 배열 실수

이 방식은 두 가지 근본 한계를 가집니다:

  1. 정확성 비용: 의존성 배열이 한 번만 틀려도 버그/성능 퇴행.
  2. 스케일 비용: 컴포넌트가 많아질수록 수동 최적화 포인트가 기하급수적으로 증가.

목표(Design Goals)

React 컴파일러가 노리는 건 “개발자 개입 최소화, 정확성 우선”입니다.

  • 자동 메모이제이션: 의미 있는 입력이 바뀔 때만 재계산/재렌더.
  • 참조 안정화(Stable identity): 안전할 때 자동으로 동일 참조 유지.
  • 범용성: React DOM/Native, 기존 코드에도 무리 없이 적용.
  • 도구 통합: Babel/SWC, ESLint와 유기적 동작.

적용 범위

  • 클라이언트 렌더링 최적화가 중심. (RSC 대체가 아님)
  • React 17+ 호환, React 19에서 가장 매끄럽게 통합.

⚙️ React Compiler는 어떻게 작동할까?

아래 파이프라인으로 동작합니다.

1) 소스 수집 & 전처리 (Babel/SWC)

  • Babel 플러그인 babel-plugin-react-compiler가 JSX/TSX를 파싱하고 트랜스폼 훅을 탑재합니다. (Next.js는 SWC 트랜스폼으로도 구동 가능)
  • 컴포넌트 경계, 훅 호출, props/state/context 접근 지점을 마킹하고, 분석에 필요한 메타데이터를 수집합니다.

2) 고수준 IR(HIR) 구축

  • Babel AST를 **제어 흐름 그래프(CFG)**와 **데이터 흐름 그래프(DFG)**로 승격한 HIR로 투영합니다.

  • 이 HIR 위에서 다음 분석이 실행됩니다:

    • 반응성 루트 탐지: props/state/context/ref 등 값의 변경 가능성 추적.
    • 순수성/부수효과 분석: 렌더 경로에서 순수 계산부수효과(side-effect) 분리.
    • 별칭/이스케이프 분석: 객체/함수 참조가 외부로 유출되는지, 재사용이 안전한지 판단.
    • 클로저 캡처 분석: 이벤트 핸들러/콜백이 무엇을 캡처하는지, 캡처 값의 변화 여부 판별.

3) 최적화 패스(Transforms)

  • 메모 캐시 삽입: 계산 지점에 슬롯 기반 캐시를 생성하고, 의존성이 바뀔 때만 재계산.
  • 리터럴 안정화: {}/[]/()=>{} 같은 리터럴 값/함수에 대해 참조 동일성을 자동 유지.
  • 표현식 호이스팅: 렌더마다 동일한 결과를 내는 불변 표현식을 컴포넌트 외곽으로 이동.
  • JSX 서브트리 캐시: props가 안정적이면 서브트리 재생성/전달을 건너뜀(사실상 React.memo 효과).
  • 가드 삽입: “입력이 동일 → 결과 재사용” 분기 생성으로 렌더 스킵 비용 최소화.

결과적으로 수동 useMemo/useCallback/React.memo대부분 자동화합니다.

4) 코드 방출(Emission)

컴파일 결과물은 다음과 같은 형태의 러프한 패턴을 가집니다(의사코드):

function Component(props) { const $cache = /* 내부 메모 캐시 */; const user = props.user; const name = $cache(0, () => format(user.name), [user.name]); // 입력이 같으면 재사용 const onClick = $cache(1, () => () => log(user.id), [user.id]); // 핸들러 참조 안정화 const view = $cache(2, () => ( <Child title={name} onClick={onClick} /> ), [name, onClick]); // JSX 서브트리 캐싱 return view; }
  • 실제 구현은 슬롯 인덱스/키를 이용한 고정 길이 캐시로, 빠른 비교낮은 오버헤드가 목표입니다.

5) 의존성 추적 규칙

  • 의미 기반: 단순 참조 동일성(===)만 보지 않고, HIR에서 추출한 반응성 소스 집합으로 변경 여부를 판단.
  • 동일 결과 보장 시 재사용: 포맷팅/매핑 등 순수 계산은 입력 불변 시 재사용됩니다.
  • 부수효과는 보호: 렌더 중 부수효과는 허용하지 않으며(React 규칙), 컴파일러가 경고/배제합니다.

6) 규칙 검증 & Lint 통합

  • ESLint의 react-hooksreact-compiler 규칙이 추가되어,

    • 렌더 중 setState금지 패턴
    • 조건부 훅 호출 등 규칙 위반
    • 과도한 의존성 등 취약 패턴정적 분석으로 조기에 경고합니다.

7) 옵트아웃/옵트인 지시어

  • 파일/컴포넌트 상단에 "use no memo"를 두면 컴파일러 최적화 제외.
  • 선택적 적용 모드에선 "use memo"가 선언된 컴포넌트만 대상 포함.

8) 빌드/런타임 통합

  • Babel: Vite/CRA 등에서 플러그인으로 사용.
  • SWC: Next.js 등에서 고속 트랜스폼.
  • 런타임 헬퍼: React 17/18에선 react-compiler-runtime로 보완, React 19에선 내장 헬퍼와 원활하게 동작.

🔄 기존 방식과 뭐가 달라졌을까?

React 컴파일러가 등장하면서 기존의 수동 최적화 패턴들은 대부분 불필요해졌습니다.

| 항목 | 기존 방식 | React 컴파일러 적용 시 | | ---------------- | --------------------- | ---------------- | | 값/함수 캐싱 | useMemo / useCallback | 자동 분석 및 캐싱 처리 | | 자식 컴포넌트 최적화 | React.memo | 자동으로 memo 수준 최적화 | | 조건부 반환 이후 메모이제이션 | 거의 불가 | 컴파일러가 전역적으로 분석함 |

물론 특정 상황에서는 여전히 useMemo, useCallback을 사용할 수 있습니다. 예를 들어, useEffect의 의존성 배열을 명확히 관리하고 싶을 때는 수동 제어가 유용할 수 있습니다.


📅 출시 일정 및 현재 안정성은?

React Compiler는 다음의 일정으로 진행되었습니다:

  • 2023년 말: React Conf 2023에서 초기 베타 소개
  • 2024년 초: 오픈소스 및 React 19 RC와 함께 공개
  • 2025년 10월: v1.0.0 정식 안정 버전 출시

현재는 React 17 이상에서 호환 가능하며, React 19 환경에서 가장 자연스럽게 통합됩니다. Expo, Vite, Next.js 등 주요 툴에서 설정만으로 쉽게 사용할 수 있도록 대응되고 있습니다.

✅ 정식 버전은 프로덕션 수준에서 충분히 검증되었고, Meta의 대형 앱들에도 이미 적용되었습니다.


⚡️ 얼마나 빨라지는가? 성능 벤치마크 사례

  • Meta Quest Store 앱에서 페이지 로딩 속도 최대 12% 개선
  • 사용자 인터랙션 반응 속도 최대 2.5배 향상
  • 메모리 사용량은 큰 차이 없이 안정 유지

개발자 커뮤니티에서도 직접 적용한 결과를 공유하고 있으며, 일부는 렌더링 블로킹 시간이 0ms로 줄었다는 사례도 있습니다.

복잡한 UI일수록 최적화 효과가 크며, 단순한 앱에서는 차이가 미미할 수 있습니다.


🧑‍💻 개발자에게 주는 변화와 의미

React Compiler는 단순한 성능 도구 그 이상입니다. 다음과 같은 의미 있는 변화를 예고합니다:

  • 성능 최적화를 자동화 → 비즈니스 로직에 집중 가능
  • 코드 품질 향상 → 렌더링 관련 버그 사전 예방
  • 생산성 증가 → 보일러플레이트 감소, 유지보수 용이

개발자는 이제 "렌더링을 줄이기 위한 요령"보다는 정확한 상태 관리와 표현에만 집중하면 됩니다. 프레임워크가 최적화를 책임지는 시대가 온 셈입니다.


🧪 이어지는 실습: React 19 + Compiler 동작 확인

0) 가장 빠른 검증 — React Playground

1) React 19 프로젝트 생성 (Vite + Babel 플러그인 경로)

React Compiler는 Babel, Vite, Metro, Rsbuild 등 다양한 빌드 도구에서 사용할 수 있습니다. 초기 안정 버전은 Babel 플러그인 래퍼 형태지만, swc/oxc 팀과 협력 중이라 **Next.js v15.3.1+**에서는 SWC로 호출되는 컴파일러를 옵션으로 활성화할 수 있습니다. 컴파일러는 항상 다른 변환보다 먼저 실행되어야 합니다(소스 정보를 보존해야 정적 분석이 정확함). Vite 사용 시 @vitejs/plugin-react의 babel.plugins 첫 번째 위치에 넣으세요. 참고: https://ko.react.dev/learn/react-compiler/installation SWC 기반 템플릿은 Babel 플러그인 주입이 어려우므로, Babel 기반 @vitejs/plugin-react를 사용합니다.

저는 vite 를 사용하였습니다.

# 프로젝트 생성 npm create vite@latest react-compiler-lab -- --template react # 컴파일러 설치 npm install --save-dev --save-exact babel-plugin-react-compiler@latest

한번 더 주의! React 컴파일러는 Babel 플러그인 파이프라인에서 먼저 실행되어야 합니다 . 컴파일러는 적절한 분석을 위해 원본 소스 정보가 필요하므로 다른 변환 작업보다 먼저 코드를 처리해야 합니다.

Vite를 사용하는 경우 vite-plugin-react에 플러그인을 추가할 수 있습니다.

// vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [ react({ babel: { plugins: ['babel-plugin-react-compiler'], }, }), ], });

2) 데모 페이지(App.jsx) — 컴파일러 체감용

콘솔 로그로 즉시 체감: Child render 카운트를 보며, theme 토글 시 자식이 스킵되는지 확인합니다. DevTools 의 렌더링 하이라이트 기능으로도 확인가능합니다. useMemo/useCallback은 사용하지 않습니다(컴파일러 자동 최적화 관찰 목적).

import { useState } from "react"; function Child({ label, onIncrement }) { console.count("Child render"); return ( <div> <p>{label}</p> <button onClick={onIncrement}>increment</button> </div> ); } export default function App() { const [count, setCount] = useState(0); const [dark, setDark] = useState(false); // 매 렌더마다 새로 만들어지지만, 입력이 같으면 컴파일러가 참조 안정화/캐시 const onIncrement = () => setCount((c) => c + 1); const label = "STATIC LABEL"; // 항상 동일 return ( <div data-theme={dark ? "dark" : "light"}> <button onClick={()=> setDark((v)=> !v)}> theme: {dark ? "dark" : "light"} </button> <Child label={label} onIncrement={onIncrement} /> <p>count: {count}</p> </div> ); }

관찰 포인트

  • DevTools → Components에서 Child 옆에 Memo ✨ 배지가 표시됩니다.

  • theme 토글을 여러 번 눌러도 콘솔의 Child render가 증가하지 않거나 최소로 유지되면, 컴파일러의 스킵/캐시가 적용된 것입니다.

3) 로컬에서 동작 확인

npm run dev
  • 빌드 산출물에서 문자열 react.memo_cache_sentinel 또는 react/compiler-runtime을 검색하면 컴파일된 흔적을 확인할 수 있습니다.

  • 더 확실히 보려면 npm run build && npm run preview 후, dist 산출물 소스맵에서 검색하세요.

4) React DevTools로 “Memo 배지” 확인

  • 브라우저에서 React DevTools를 열고 Components 탭으로 이동합니다.

  • App 트리에서 MyApp 또는 하위 컴포넌트를 선택합니다.

  • 컴파일러가 메모이제이션한 컴포넌트/서브트리에는 반짝이는 Memo 배지(메모 아이콘)가 표시됩니다.

  • DevTools의 Highlight updates(업데이트 하이라이트)를 켜고 상태 변화를 트리거해 보세요.

5) 테스트 화면

5-1 컴파일러 사용 전

5-2 컴파일러 사용 후

찍히지 않는 console.count("Child render"); 를 확인하세요!


🧾 마치며

이전 글에서 말했듯, 리렌더링은 죄가 아니고 렌더 중의 ‘비용’이 문제입니다. React 컴파일러는 이 비용을 도구 체인에서 자동으로 줄여 개발자가 일일이 useMemo/useCallback을 배치하던 시대를 넘어가게 합니다. 이제 우리의 포커스는 “얼마나 적게 리렌더링할까?”가 아니라, **“의미가 바뀔 때만 다시 그리자”**로 바뀝니다.

위의 Playground와 데모 코드를 그대로 돌려 보며 Memo ✨ 배지와 콘솔 카운트로 체감을 추천합니다. 다음 글에서는 DevTools로 최적화가 어떻게 표시되는지 더 깊게 들여다보고, Next.js에서의 적용 포인트와 실전 패턴/안티패턴을 정리할 예정입니다. 궁금한 점이나 실험 결과가 있다면 댓글로 남겨 주세요—함께 보완해보겠습니다.

참고

  • React docs
  • React Conf 2023 / 2024 요약
JP
이중표Frontend Engineer

3년차 프론트엔드 개발자. Next.js, React, TypeScript 기반 웹 애플리케이션 개발 전문. 대규모 트래픽 환경에서 SSR·ISR 렌더링 전략 설계 경험.

이력서 보기