🧩 "리렌더링은 문제인가? 아니면 렌더링 중 실행되는 코드가 문제인가?"
이 문장은 제가 지난 벨로그 글에서 강조한 핵심 메시지였습니다. 🔗 리렌더링 최적화의 오해 - 리렌더링이 많다고 느린 게 아니다 글에서, 저는 React의 Virtual DOM이 렌더링을 줄이는 기술이 아니라 커밋을 최적화하는 기술이라는 점을 설명하며, 진짜 병목은 “리렌더링 자체”가 아니라 “렌더링 중 실행되는 코드”라고 강조했습니다.
그런데 이제, React 컴파일러의 등장으로 이 고민조차 할 필요가 없어질지도 모릅니다.
React 팀이 드디어 React Compiler (리액트 컴파일러)의 첫 정식 버전을 공개했습니다. 수년간의 실험과 베타를 거쳐 발표된 이 컴파일러는, 리액트 개발자들이 오랫동안 겪어온 useMemo, useCallback, React.memo 지옥에서 벗어날 수 있도록 돕는 자동화된 빌드 타임 최적화 도구입니다.
대규모 리액트 앱에서 “리렌더링 감소”를 위해 우리는 다음과 같은 수동 최적화를 강박적으로 적용해왔습니다.
useMemo/useCallback/React.memo의 보일러플레이트 남발과 의존성 배열 실수이 방식은 두 가지 근본 한계를 가집니다:
React 컴파일러가 노리는 건 “개발자 개입 최소화, 정확성 우선”입니다.
아래 파이프라인으로 동작합니다.
babel-plugin-react-compiler가 JSX/TSX를 파싱하고 트랜스폼 훅을 탑재합니다. (Next.js는 SWC 트랜스폼으로도 구동 가능)Babel AST를 제어 흐름 그래프(CFG)와 데이터 흐름 그래프(DFG)로 승격한 HIR로 투영합니다.
이 HIR 위에서 다음 분석이 실행됩니다:
{}/[]/()=>{} 같은 리터럴 값/함수에 대해 참조 동일성을 자동 유지.React.memo 효과).결과적으로 수동
useMemo/useCallback/React.memo를 대부분 자동화합니다.
컴파일 결과물은 다음과 같은 형태의 러프한 패턴을 가집니다(의사코드):
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;
}
ESLint의 react-hooks에 react-compiler 규칙이 추가되어,
setState 등 금지 패턴"use no memo"를 두면 컴파일러 최적화 제외."use memo"가 선언된 컴포넌트만 대상 포함.react-compiler-runtime로 보완, React 19에선 내장 헬퍼와 원활하게 동작.React 컴파일러가 등장하면서 기존의 수동 최적화 패턴들은 대부분 불필요해졌습니다.
| 항목 | 기존 방식 | React 컴파일러 적용 시 |
|---|---|---|
| 값/함수 캐싱 | useMemo / useCallback | 자동 분석 및 캐싱 처리 |
| 자식 컴포넌트 최적화 | React.memo | 자동으로 memo 수준 최적화 |
| 조건부 반환 이후 메모이제이션 | 거의 불가 | 컴파일러가 전역적으로 분석함 |
물론 특정 상황에서는 여전히 useMemo, useCallback을 사용할 수 있습니다. 예를 들어, useEffect의 의존성 배열을 명확히 관리하고 싶을 때는 수동 제어가 유용할 수 있습니다.
React Compiler는 다음의 일정으로 진행되었습니다:
v1.0.0 정식 안정 버전 출시현재는 React 17 이상에서 호환 가능하며, React 19 환경에서 가장 자연스럽게 통합됩니다. Expo, Vite, Next.js 등 주요 툴에서 설정만으로 쉽게 사용할 수 있도록 대응되고 있습니다.
✅ 정식 버전은 프로덕션 수준에서 충분히 검증되었고, Meta의 대형 앱들에도 이미 적용되었습니다.
개발자 커뮤니티에서도 직접 적용한 결과를 공유하고 있으며, 일부는 렌더링 블로킹 시간이 0ms로 줄었다는 사례도 있습니다.
복잡한 UI일수록 최적화 효과가 크며, 단순한 앱에서는 차이가 미미할 수 있습니다.
React Compiler는 단순한 성능 도구 그 이상입니다. 다음과 같은 의미 있는 변화를 예고합니다:
개발자는 이제 "렌더링을 줄이기 위한 요령"보다는 정확한 상태 관리와 표현에만 집중하면 됩니다. 프레임워크가 최적화를 책임지는 시대가 온 셈입니다.
좌측에 원본 코드, 우측 상단의 Compiled 탭에서 컴파일된 코드를 확인할 수 있습니다.
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'],
},
}),
],
});
콘솔 로그로 즉시 체감: 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가 증가하지 않거나 최소로 유지되면, 컴파일러의 스킵/캐시가 적용된 것입니다.
npm run dev

브라우저에서 React DevTools를 열고 Components 탭으로 이동합니다.
App 트리에서 MyApp 또는 하위 컴포넌트를 선택합니다.
컴파일러가 메모이제이션한 컴포넌트/서브트리에는 반짝이는 Memo 배지(메모 아이콘)가 표시됩니다.
DevTools의 Highlight updates(업데이트 하이라이트)를 켜고 상태 변화를 트리거해 보세요.

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

이전 글에서 말했듯, 리렌더링은 죄가 아니고 렌더 중의 ‘비용’이 문제입니다. React 컴파일러는 이 비용을 도구 체인에서 자동으로 줄여 개발자가 일일이 useMemo/useCallback을 배치하던 시대를 넘어가게 합니다. 이제 우리의 포커스는 “얼마나 적게 리렌더링할까?”가 아니라, “의미가 바뀔 때만 다시 그리자”로 바뀝니다.
위의 Playground와 데모 코드를 그대로 돌려 보며 Memo ✨ 배지와 콘솔 카운트로 체감을 추천합니다. 다음 글에서는 DevTools로 최적화가 어떻게 표시되는지 더 깊게 들여다보고, Next.js에서의 적용 포인트와 실전 패턴/안티패턴을 정리할 예정입니다. 궁금한 점이나 실험 결과가 있다면 댓글로 남겨 주세요—함께 보완해보겠습니다.
참고