React Compiler v1.0 완전 가이드 — 자동 메모이제이션 원리와 실전 적용
불필요한 재렌더를 막기 위해 useMemo, useCallback, React.memo를 수동으로 작성하고, 의존성 배열이 올바른지 매번 검토하던 경험이 있으신가요? React를 어느 정도 써본 분이라면 이 수동 최적화 작업이 얼마나 번거롭고 실수하기 쉬운지 공감하실 겁니다. 2025년 10월 7일, React 팀은 이 문제를 빌드 타임에 자동으로 해결하는 React Compiler v1.0을 정식 출시했습니다.
컴파일러의 효과는 이미 실제 서비스에서 확인됩니다. Meta Quest Store에서 최대 12%의 로드 성능 개선(InfoQ 보고), Sanity Studio에서 렌더 시간 20~30% 감소(Sanity 공식 문서), Wakelet에서 LCP 10% 향상(DebugBear 분석)이 보고됐습니다. 다만 이 수치들은 각 팀의 특정 코드베이스에서 측정된 결과이며, 실제 효과는 프로젝트 구조에 따라 크게 달라집니다.
Next.js 15, Expo SDK 54가 React Compiler를 기본값으로 활성화하고 있는 지금, 이 도구를 이해하는 것은 React 성능 최적화 경험이 있는 프론트엔드 개발자라면 빠르게 파악해두어야 할 변화입니다. 이 글에서는 React Compiler의 동작 원리부터 실전 적용법, 그리고 놓치기 쉬운 함정까지 한 번에 정리합니다.
핵심 개념
React Compiler란 무엇인가
React Compiler는 Babel 플러그인 형태로 동작하는 빌드 타임 최적화 도구입니다. 번들러가 코드를 처리하는 시점에 개입해 React 컴포넌트와 훅을 분석하고 최적화된 코드를 생성합니다.
내부 동작 흐름을 살펴보면, Babel이 소스 코드를 파싱해 생성한 **AST(Abstract Syntax Tree, 추상 구문 트리)**를 컴파일러가 자체 형식인 **HIR(High-level Intermediate Representation)**로 변환합니다. 이후 여러 분석 단계를 순차적으로 거쳐 최적화를 적용합니다.
HIR(High-level Intermediate Representation): 소스 코드를 직접 분석하는 대신 사용하는 중간 표현 형식입니다. AST보다 추상 수준이 높아 제어 흐름 그래프(control flow graph)로 변환하기에 유리하며, 이를 통해 컴파일러가 변수의 생존 범위와 데이터 흐름을 정밀하게 추적할 수 있습니다.
컴파일러 패스(pass): 컴파일러가 코드를 여러 번 순차적으로 분석하는 단계를 의미합니다. 각 패스에서 데이터 흐름 분석, 변경 가능성(mutability) 검사, 최적화 삽입 등 특정 작업을 수행합니다.
이 분석을 바탕으로 컴파일러가 "안전하다"고 판단한 코드에 한해 세분화된 조건부 메모이제이션을 자동으로 삽입합니다.
자동 메모이제이션의 동작 방식
컴파일러의 메모이제이션은 컴포넌트 단위에 그치지 않습니다. JSX 표현식, 인라인 객체 리터럴, 콜백 함수처럼 세밀한 단위로도 캐싱을 자동으로 적용할 수 있어 수동 메모이제이션으로는 달성하기 어려운 최적화가 가능합니다.
아래는 컴파일러가 무엇을 대신해주는지 보여주는 개념적 비교입니다.
// 개발자가 작성하는 코드 (Before)
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
}// 컴파일러가 생성하는 최적화 결과 (개념적 표현 — 실제 출력은 내부 캐싱 API를 사용하는 저수준 코드입니다, After)
const TodoList = React.memo(function TodoList({ todos, onToggle }) {
const handleToggle = useCallback((id) => onToggle(id), [onToggle]);
const memoizedList = useMemo(
() =>
todos.map(todo => (
<li key={todo.id} onClick={() => handleToggle(todo.id)}>
{todo.text}
</li>
)),
[todos, handleToggle]
);
return <ul>{memoizedList}</ul>;
});개발자는 평소처럼 코드를 작성하면 되고, 컴파일러가 빌드 시점에 React.memo, useMemo, useCallback을 자동으로 삽입합니다. onToggle 콜백처럼 렌더마다 새로 생성되던 함수 참조도 자동으로 안정화됩니다.
적용의 전제 조건
React Compiler는 React의 규칙(Rules of React)을 준수하는 코드에만 최적화를 적용합니다. 규칙을 위반하는 코드는 조용히 최적화 대상에서 제외되며(무음 실패, Silent Failures), 별도 에러 없이 스킵됩니다. 대표적인 규칙 위반 사례는 다음과 같습니다.
| 위반 패턴 | 예시 |
|---|---|
| props 직접 변경 | props.list.push(item) |
| 렌더 중 사이드 이펙트 | 렌더 함수 내 fetch() 직접 호출 |
| 복잡한 try/catch 구조 | 컴파일러가 흐름 분석을 포기하는 패턴 |
| Hook 규칙 위반 | 조건문 내부 Hook 호출 |
실전 적용
예시 1: Next.js에서 한 줄로 전체 앱에 적용
Next.js를 사용하고 있다면 설정 파일 한 줄로 전체 앱에 React Compiler를 활성화할 수 있습니다.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;Next.js는 SWC 기반으로 컴파일러를 통합하므로 별도의 Babel 설정 없이 즉시 적용됩니다. 기존 코드베이스에 영향을 최소화하면서 점진적으로 도입하고 싶다면 compilationMode: 'annotation'을 함께 설정하는 방법도 있습니다.
// next.config.ts — 점진적 도입 (opt-in 모드)
const nextConfig: NextConfig = {
reactCompiler: {
compilationMode: 'annotation',
},
};
export default nextConfig;opt-in 모드에서는 파일 최상단에 "use memo" 디렉티브를 추가한 컴포넌트에만 컴파일러가 적용됩니다.
// components/ExpensiveComponent.tsx
"use memo"; // 이 파일의 컴포넌트에만 컴파일러 적용
export function ExpensiveComponent({ data }) {
// 무거운 연산이 포함된 컴포넌트
return <div>{/* ... */}</div>;
}예시 2: Vite 프로젝트에서 Babel 플러그인으로 적용
Vite 기반 프로젝트라면 babel-plugin-react-compiler를 추가하는 방식으로 적용할 수 있습니다. 현재 React Compiler 공식 패키지는 Babel 기반으로 안정적으로 제공됩니다. @vitejs/plugin-react-swc를 사용하는 프로젝트의 경우 SWC용 React Compiler 플러그인 지원이 아직 실험적 단계이므로, 안정적인 적용을 위해서는 @vitejs/plugin-react(Babel 기반)로 전환하는 방법을 권장합니다.
pnpm add -D babel-plugin-react-compiler// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});npm create vite@latest의 최신 스캐폴드 템플릿에는 React Compiler 옵션이 이미 포함되어 있으므로, 새 프로젝트라면 템플릿 선택 시 활성화 여부를 바로 선택할 수 있습니다.
예시 3: 도입 전 코드베이스 사전 진단
기존 프로젝트에 React Compiler를 도입하기 전에 react-compiler-healthcheck CLI로 컴파일 가능 비율을 미리 파악해두시면 좋습니다.
npx react-compiler-healthcheck@latest실제 출력에는 전체 컴파일 결과와 함께 최적화 불가 원인이 분류되어 표시됩니다.
Successfully compiled 1,231 out of 1,411 components.
Components skipped (180):
- Rules of React violations: 89 components
- Unsupported syntax patterns: 56 components
- Side effects detected in render: 35 components이 리포트를 통해 "어떤 패턴을 먼저 수정하면 컴파일 적용 비율을 높일 수 있는지" 우선순위를 파악할 수 있습니다. Sanity Studio의 경우 87%가 컴파일 적용 가능했으며(Sanity 공식 문서), 이 비율이 실제 성능 개선 폭과 비례하는 경향이 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 코드 간결화 | useMemo, useCallback, React.memo를 수동으로 작성할 필요가 없습니다 |
| 자동 최적화 | 정적 분석을 통해 개발자가 놓친 최적화 포인트도 자동으로 처리됩니다 |
| 유지보수성 향상 | 의존성 배열 오류(exhaustive-deps)로 인한 버그 가능성이 줄어듭니다 |
| 점진적 도입 | opt-in/opt-out 모드를 통해 기존 코드베이스에 단계적으로 적용할 수 있습니다 |
| 프레임워크 통합 | Next.js, Expo, Vite 등 주요 프레임워크의 공식 지원이 완료되어 있습니다 |
| 세밀한 캐싱 | JSX 표현식, 인라인 객체 리터럴, 콜백 단위의 캐싱도 자동으로 적용됩니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 무음 실패(Silent Failures) | 최적화 불가 코드를 조용히 스킵하여 어떤 컴포넌트가 최적화됐는지 파악이 어렵습니다 | React DevTools의 Memo ✨ 배지로 확인하거나 ESLint 규칙으로 사전 진단합니다 |
| 부분적 최적화 | 렌더 방식(HOW)은 최적화하지만 렌더 여부(WHETHER)는 제어하지 않습니다. 예를 들어 부모 컴포넌트의 count 상태가 변경되면, 자식에 전달되는 props가 달라지지 않아도 자식이 재렌더될 수 있으며 컴파일러는 이 재렌더 자체를 막지는 않습니다 |
구조적인 불필요 렌더 문제는 상태 설계 수준에서 해결하는 것이 적절합니다 |
| 코드 패턴 제약 | props 직접 변경, 복잡한 try/catch 등 비지원 패턴이 존재합니다 | react-compiler-healthcheck로 사전 파악 후 해당 패턴을 정리합니다 |
| 가변적 효과 | 코드베이스 특성에 따라 성능 개선 폭이 0~60%까지 크게 차이 납니다 | 도입 전 실제 환경에서 측정하여 기대치를 현실적으로 설정합니다 |
| React 규칙 준수 필수 | Rules of React를 위반하는 기존 코드는 컴파일 대상에서 자동 제외됩니다 | eslint-plugin-react-hooks v6+로 위반 코드를 사전에 파악하고 수정합니다 |
| 서드파티 라이브러리 호환성 | Zustand, Jotai 같은 상태 관리 라이브러리나 Framer Motion 같은 애니메이션 라이브러리와 함께 사용할 때 예상치 못한 동작이 발생할 수 있습니다 | "use no memo" 디렉티브로 해당 컴포넌트를 컴파일러 대상에서 제외하는 방법을 활용합니다 |
eslint-plugin-react-hooksv6+: React Compiler 관련 lint 규칙이 통합된 버전입니다.set-state-in-render,set-state-in-effect,refs등 새 규칙이 포함되어 있으며, 별도의eslint-plugin-react-compiler설치 없이 사용할 수 있습니다.
"use no memo"디렉티브: 특정 컴포넌트를 컴파일러 대상에서 명시적으로 제외하고 싶을 때 파일 최상단에 추가합니다. 컴파일러와 충돌하는 서드파티 라이브러리를 래핑한 컴포넌트나 사이드 이펙트가 복잡하게 얽힌 레거시 컴포넌트에 활용할 수 있습니다.
실무에서 가장 흔한 실수
- DevTools의
Memo ✨배지를 최적화 성공의 보장으로 해석하는 것 — 이 배지는 컴파일러가 해당 컴포넌트를 처리했다는 표시일 뿐, 실제 성능이 개선됐음을 의미하지 않습니다. 성능 측정은 Lighthouse나 React Profiler로 별도로 진행하는 것을 권장합니다. - 기존의 잘못된
useMemo/useCallback을 정리하지 않고 컴파일러를 도입하는 것 — 잘못된 의존성 배열을 가진 수동 메모이제이션 코드가 남아 있으면 컴파일러의 분석 결과와 충돌할 수 있습니다. 도입 전 ESLint로 코드를 먼저 정리하는 것을 권장합니다. - 전체 앱에 한 번에 적용하는 것 — 특히 규모가 큰 레거시 코드베이스에서는 opt-in 모드(
compilationMode: 'annotation')로 시작해 컴포넌트 단위로 점진적으로 확인하며 적용하는 방법이 안전합니다.
마치며
직접 코드베이스에 React Compiler를 적용해보면, 수동으로 의존성 배열을 관리하던 시절에 작성했던 보일러플레이트 코드가 놀라울 만큼 많이 사라진다는 것을 체감할 수 있습니다. React Compiler v1.0은 메모이제이션 관리의 인지 부담을 컴파일러에 위임함으로써 개발자가 비즈니스 로직에 집중할 수 있는 환경을 만들어줍니다. 단, 코드베이스 특성에 따라 효과의 폭은 다르므로 도입 전 실측을 통해 기대치를 현실적으로 설정하는 것이 중요합니다.
지금 바로 시작해볼 수 있는 3단계:
- 기존 프로젝트 진단 —
npx react-compiler-healthcheck@latest를 실행해 현재 코드베이스의 컴파일 가능 비율을 확인해볼 수 있습니다. 80% 이상이면 즉시 도입 효과를 기대해볼 만합니다. - ESLint 업그레이드 및 위반 코드 정리 —
pnpm add -D eslint-plugin-react-hooks@latest로 v6+를 설치한 뒤, 새 lint 규칙으로 Rules of React 위반 코드를 파악하고 수정하는 것이 선행되면 좋습니다. - opt-in 모드로 점진적 적용 — Next.js라면
compilationMode: 'annotation'을, Vite라면 Babel 플러그인을 설정한 뒤"use memo"디렉티브로 성능이 중요한 컴포넌트부터 하나씩 적용해볼 수 있습니다. React DevTools와 React Profiler로 적용 전후를 비교하며 효과를 확인해보시면 좋습니다.
다음 글: React Compiler와 함께 설계된 React 19의 핵심 기능 — Actions API와
use훅이 데이터 페칭 패턴을 어떻게 바꾸는지 살펴볼 예정입니다.
참고 자료
- React Compiler v1.0 공식 블로그 | react.dev
- React Compiler 공식 문서 (Introduction) | react.dev
- React Compiler 설치 가이드 | react.dev
- Meta's React Compiler 1.0 Brings Automatic Memoization to Production | InfoQ
- React Compiler: An Introduction, Pros, Cons & When to Use It | DebugBear
- React Compiler RC: What it means for React devs | LogRocket
- React Compiler and Sanity | Sanity 공식 문서
- React Compiler's Silent Failures | acusti.ca
- (번역) 리액트 컴파일러 이해하기 | emewjin.log
- React Conf 2025 Recap | react.dev
- Next.js reactCompiler 설정 공식 문서 | Next.js
- React Compiler | Expo 공식 문서