React Compiler의 동작 원리, HIR 기반 분석, 자동 메모이제이션, 설치와 설정, ESLint 통합, 실전 적용 전략을 다룹니다.
지금까지 React 19의 서버 중심 기능들을 살펴보았습니다. 이번 장에서는 클라이언트 사이드 성능을 근본적으로 개선하는 React Compiler를 다룹니다. React Compiler는 빌드 타임에 컴포넌트를 분석하여 자동으로 메모이제이션을 적용하는 최적화 도구로, 2025년 10월에 안정 버전 1.0이 출시되었습니다.
React는 상태가 변경되면 해당 컴포넌트와 모든 자식 컴포넌트를 다시 렌더링합니다. 이를 최적화하기 위해 개발자들은 수동으로 메모이제이션을 적용해왔습니다.
import { useMemo, useCallback, memo } from 'react';
const ExpensiveList = memo(function ExpensiveList({
items,
onSelect,
}: {
items: Item[];
onSelect: (id: string) => void;
}) {
const sortedItems = useMemo(
() => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleSelect = useCallback(
(id: string) => {
onSelect(id);
},
[onSelect]
);
return (
<ul>
{sortedItems.map(item => (
<ListItem key={item.id} item={item} onSelect={handleSelect} />
))}
</ul>
);
});
const ListItem = memo(function ListItem({
item,
onSelect,
}: {
item: Item;
onSelect: (id: string) => void;
}) {
return (
<li onClick={() => onSelect(item.id)}>
{item.name}
</li>
);
});이 코드에서 useMemo, useCallback, memo는 모두 불필요한 재렌더링을 방지하기 위한 것입니다. 하지만 이 접근법에는 여러 문제가 있습니다.
memo로 감싼 컴포넌트에 전달하는 모든 함수와 객체도 메모이제이션해야 효과가 있습니다.React Compiler는 이 모든 문제를 빌드 타임 자동화로 해결합니다.
// useMemo, useCallback, memo가 불필요
function ExpensiveList({
items,
onSelect,
}: {
items: Item[];
onSelect: (id: string) => void;
}) {
const sortedItems = items.slice().sort((a, b) =>
a.name.localeCompare(b.name)
);
return (
<ul>
{sortedItems.map(item => (
<ListItem key={item.id} item={item} onSelect={onSelect} />
))}
</ul>
);
}
function ListItem({
item,
onSelect,
}: {
item: Item;
onSelect: (id: string) => void;
}) {
return (
<li onClick={() => onSelect(item.id)}>
{item.name}
</li>
);
}코드가 훨씬 간결해졌지만, Compiler가 자동으로 필요한 메모이제이션을 적용하므로 성능은 동일하거나 더 좋습니다.
React Compiler는 Babel 플러그인으로 구현되어 있으며, 다음 단계를 거칩니다.
React Compiler는 컴포넌트 단위가 아닌 값 단위로 메모이제이션을 적용합니다.
function UserCard({ user, theme }: { user: User; theme: Theme }) {
// Compiler는 이 스타일 객체를 theme이 변경될 때만 재생성
const cardStyle = {
background: theme.cardBg,
color: theme.text,
};
// 이 계산은 user.posts가 변경될 때만 재실행
const recentPosts = user.posts
.filter(p => p.published)
.slice(0, 5);
// 이 JSX는 관련 값이 변경될 때만 재생성
return (
<div style={cardStyle}>
<h2>{user.name}</h2>
<ul>
{recentPosts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}수동으로 useMemo를 적용했다면 cardStyle과 recentPosts 각각에 별도의 useMemo가 필요했을 것입니다. Compiler는 이를 자동으로 처리하며, 조건부 반환 이후의 값도 정확하게 메모이제이션합니다.
Compiler의 핵심 기능 중 하나는 값의 변경(mutation)을 정밀하게 추적하는 것입니다.
function TodoList({ todos }: { todos: Todo[] }) {
// Compiler는 이 배열이 새로 생성되었고,
// sort가 원본을 변경하지 않음을 이해
const sorted = [...todos].sort((a, b) =>
a.priority - b.priority
);
// filteredTodos는 sorted에서 파생되므로
// todos가 변경되지 않으면 재계산하지 않음
const filteredTodos = sorted.filter(t => !t.completed);
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}pnpm add --save-dev --save-exact babel-plugin-react-compiler@latestReact Compiler는 정확한 버전을 고정(--save-exact)하는 것이 권장됩니다. 향후 버전에서 메모이제이션 전략이 변경될 수 있으며, 예기치 않은 동작 변화를 방지합니다.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;Next.js 15.3.1 이상에서 기본 지원됩니다.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
const ReactCompilerConfig = {
// 컴파일러 옵션
};
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig],
],
},
}),
],
});React Compiler는 다음 환경을 지원합니다.
| React 버전 | 지원 | 추가 패키지 |
|---|---|---|
| React 19+ | 완전 지원 | 불필요 |
| React 18 | 지원 | react-compiler-runtime 필요 |
| React 17 | 지원 | react-compiler-runtime 필요 |
# React 17/18에서 사용할 경우
pnpm add react-compiler-runtimeReact Compiler의 ESLint 규칙은 eslint-plugin-react-hooks의 recommended 프리셋에 포함되어 있습니다. 별도의 플러그인이 필요 없습니다.
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
plugins: {
'react-hooks': reactHooks,
},
rules: reactHooks.configs.recommended.rules,
},
];// 렌더링 중 setState: set-state-in-render
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // 렌더링 중 상태 변경 - 무한 루프
return <p>{count}</p>;
}
// 렌더링 중 ref 접근: refs
function Input({ ref }: { ref: React.Ref<HTMLInputElement> }) {
// ref.current는 렌더링 중에 읽으면 안 됨
console.log(ref.current?.value);
return <input ref={ref} />;
}Compiler는 이러한 Rules of React 위반을 빌드 타임에 감지하고 진단 메시지를 제공합니다.
const nextConfig: NextConfig = {
experimental: {
reactCompiler: {
compilationMode: 'annotation',
},
},
};compilationMode: 'annotation'으로 설정하면, 'use memo' 디렉티브가 있는 파일만 컴파일합니다.
'use memo'; // 이 파일만 Compiler 적용
function OptimizedComponent() {
// Compiler가 이 컴포넌트를 최적화
}React Compiler가 적용된 후에도 기존의 useMemo, useCallback은 그대로 동작합니다. Compiler는 이들을 추가 힌트로 활용합니다.
function Component({ data }: { data: Data }) {
// Compiler가 적용되면 이 useMemo는 사실상 불필요
// 하지만 제거하지 않아도 됨 - Compiler가 적절히 처리
const processed = useMemo(
() => expensiveProcess(data),
[data]
);
return <Display data={processed} />;
}Compiler가 안정적으로 동작하는 것을 확인한 후, 기존 메모이제이션 코드를 점진적으로 제거할 수 있습니다. 하지만 급하게 제거할 필요는 없습니다.
Meta에서 React Compiler를 프로덕션에 적용한 결과는 인상적입니다.
| 지표 | 개선 |
|---|---|
| 초기 로드 | 최대 12% 빠름 |
| 페이지 전환 | 최대 12% 빠름 |
| 특정 인터랙션 | 최대 2.5배 빠름 |
| 메모리 사용량 | 중립 (증가 없음) |
Compiler는 순수한 React 코드에 최적화되어 있습니다. React의 규칙을 위반하는 코드(렌더링 중 외부 상태 변경, ref 잘못 사용 등)에는 예상치 못한 동작이 발생할 수 있습니다.
// 렌더링 중 외부 변수 변경 - 순수하지 않음
let globalCounter = 0;
function BadComponent() {
globalCounter++; // 외부 상태 변경
return <p>{globalCounter}</p>;
}이런 코드는 Compiler 없이도 버그를 유발할 수 있으므로, Compiler 도입을 계기로 코드 품질을 개선하는 것이 좋습니다.
React Compiler가 도입되면 대부분의 경우 React.memo가 불필요해집니다.
// Before: React.memo로 감싸야 했던 컴포넌트
const MemoizedCard = memo(function Card({ title, content }: CardProps) {
return (
<div>
<h2>{title}</h2>
<p>{content}</p>
</div>
);
});
// After: Compiler가 자동으로 최적화
function Card({ title, content }: CardProps) {
return (
<div>
<h2>{title}</h2>
<p>{content}</p>
</div>
);
}다만, React.memo에 커스텀 비교 함수를 전달하는 경우는 Compiler가 자동으로 대체하지 않습니다.
// 이 패턴은 Compiler가 대체하지 않음 - 유지 필요
const DeepCompareList = memo(
function List({ items }: { items: Item[] }) {
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
},
(prev, next) => isDeepEqual(prev.items, next.items)
);useMemo, useCallback, React.memo를 수동으로 작성할 필요가 없어집니다.다음 장에서는 ref 개선, 메타데이터 지원, 리소스 로딩 API 등 React 19의 DX 개선사항들을 다룹니다.
이 글이 도움이 되셨나요?
React 19의 DX 개선사항을 다룹니다. ref를 일반 props로 전달하는 방법, 컴포넌트 내 메타데이터 태그, 리소스 프리로딩 API를 살펴봅니다.
React 19에서 강화된 Suspense의 고급 패턴, 스트리밍 SSR, 중첩 Suspense 전략, 배칭 동작, Partial Pre-rendering을 다룹니다.
React 19 애플리케이션의 성능을 극대화하는 전략을 다룹니다. 번들 최적화, 렌더링 성능, Core Web Vitals 개선, 측정 도구 활용법을 배웁���다.