


2025년 10월 1일 React 19.2가 정식으로 출시가 되었다. 더불어 10월 7-8일에 “React Conf 2025” 가 진행되면서 최신 React에 대한 새로운 주제가 많이 발표되었다.
<Activity />, <ViewTransition/> 등 새로운 컴포넌트가 출시되었고, 부분 사전렌더링 등 새로운 React의 변화가 생겨나게되었다.
이 글에서 함수형 컴포넌트, Concurrent Mode 등과 같이 React의 주요 변화 라고 생각되는 React Complier 는 대해 알아보려고 한다.
React Complier는 이미 과거부터 도입을 할 것이라는 이야기가 많았고, 작년 베타버전이 출시되었었기 때문에 많은 사람들이 이 존재를 알고 있었을 것이다.
부족한 점을 보안하여 시간이 지나 드디어 19.2 버전을 통해 정식으로 출시가 되었다.
React Complier 는 “자동 메모이제이션을 통해 React 앱을 최적화하는 빌드 도구” 이다. 쉽게 말해 우리가 기존에 최적화를 위해 사용했던 React.memo, useMemo, useCallback 등을 빌드 시점에 자동으로 처리해주는 기능을 가지고 있는 것이다.
React 공식 문서에 올라온 Blog에서는 이 프로젝트는 “10년에 걸친 거대하고, 복잡한 엔지니어링 노력의 결실” 이라고 소개한다.
이를 통해 이 기능을 위해 얼마나 많은 고민들이 있었는지 느낄 수 있는 부분인 것 같다.

React는 기본적으로 상태(state)가 변경됨에 따라 해당 컴포넌트 함수를 다시 실행 (리렌더링)이 발생하게 된다.
즉, 부모가 렌더링이 됨에 따라 아래 자식 컴포넌트들도 렌더링이 되는 것이 기본 동작 매커니즘이다.
다음과 같은 코드가 있다고 해보자.
import { useState } from "react";
import "./App.css";
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h3>React Compiler ✅</h3>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<SubComponent />
</div>
);
}
export default App;
function SubComponent() {
const [random, setRandom] = useState(() => Math.floor(Math.random() * color.length))
const randomColor = color[random];
return (
<>
<h1>렌더링이 발생하면 색상이 바뀜</h1>
<h2 style={{ color: randomColor, marginTop: "30px" }}>SubComponent</h2>
</>
);
}
기존의 React 에서는 <App /> 컴포넌트에서 count 상태가 변경된다면, 아무 상관없는 <SubComponent/> 가 함께 렌더링되게 되었다.

<SubComponent/> 은 렌더링이 다시 일어난다고 해도 성능에 크게 문제가 되지는 않지만, 만약 아주 무거운 연산이나, 비동기 호출이 발생한다고 한다면, 성능에 큰 영향을 주게 될 것이다.
이를 개선하기 위해서 우리는 다음과 같이 React.memo, useMemo 등을 통해 메모이제이션을 하여 최적화해왔다.
// 수동으로 메모이제이션
import React, { useMemo } from "react";
const color = ["red", "green", "blue", "yellow"];
function SubComponent() {
const randomColor = useMemo(() => {
return color[Math.floor(Math.random() * color.length)];
}, []);
return (
<>
<h1>렌더링이 발생하면 색상이 바뀜</h1>
<h2 style={{ color: randomColor, marginTop: "30px" }}>SubComponent</h2>
</>
);
}
export default React.memo(SubComponent);
이렇게 불필요한 렌더링을 방지하기 위해 우리는 수동으로 메모이제이션을 수행을 하면서 다음과 같은 불편함을 마주할 수 있었다.
그렇다면 “어떤 기준”으로 메모이제이션을 수행해야 할까??
이러한 고민의 과정에서 발생하는 시간과 에너지 비용을 조금이라도 줄일 수 있을까?
개발자가 메모이제이션을 신경쓰지 않고, 본래 로직에 집중할 수 있을까?

이런 고민들이 프론트엔드 개발자의 소소한(?) 고민거리였다. 이러한 과정에서 React Compiler가 시작된 것이다.
“자동으로 메모이제이션을 해주면 좋겠다!!!”
React Conf 2021 에서 Xuan Huang 님을 통해 “React Forget” 이라는 이름으로 처음 공개가 되었었다.

해당 Conf 에서는 위에서 언급한 메모이제이션으로 인한 UX / DX 를 개선하기 위해 도입하였다고 설명한다.
그리고, 데모 버전에서는 memoCache 라는 것을 활용해 변수의 변경 여부를 판단하여 불필요한 연산을 방지하는 아이디어를 제시해준다.
function TodoList({visibility, themeColor}) {
const [todos, setTodos] = useState(initialTodos);
let hasVisibilityChanged, hasThemeColorChanged, hasTodosChanged, memoCache;
const handleChange =
memoCache[0] ||
(memoCache[0] = todo => setTodos(todos => getUpdated(todos, todo)));
let filtered;
if (hasVisibilityChanged || hasTodosChanged) {
filtered = memoCache[1] = getFittered(todos, visibility);
} else {
filtered = memoCache[1];
}
return (
<div>
<ul>
{filtered.map(todo => (
<Todo key={todo.id} todo={todo} onChange={handleChange} />
))}
</ul>
<AddTodo setTodos={setTodos} themeColor={themeColor} />
</div>
);
}
간간히 블로그를 통해 소식이 전해지다가 드디어 React Conf 2024를 통해 기존 React Forget 의 이름을 대신하여 React Compiler라는 정식 명칭으로 오픈소스가 공개가 된다.

해당 발표에서는 instagram, Quest Store 등 Meta 의 일부 서비스에서 React Compiler를 도입하여 이미 유의미한 성과를 얻을 수 있었다고 한다.
| 지표 | 개선 효과 |
|---|---|
| 클릭 및 스크롤 상호작용 속도 | 2배 이상 향상 (Twice as fast) |
| 초기 로드 및 페이지 간 탐색 시간 | 최대 12% 향상 |
| Quest Store 전체 | 로드 및 탐색 속도 최소 4% 향상 |
| Instagram.com 전체 | 모든 라우트에서 평균 3% 향상 |

실제 Production에서 도입되어 메모리사용량 증가 없이 이렇게 유의미한 성과를 내고 있었다는 것이 React Complier 의 효과가 실제 체감으로 많이 나타난다는 것을 보여주었다.
당시 실험적인 버전으로 Babel 플러그인으로 공개가 되었었다.

드디어, 정식으로 React Compiler 1.0이 출시가 된다.
그렇다면 React Complier 는 어떻게 자동으로 메모이제이션을 가능하게 해줄까??
먼저, React Complier 를 통해 어떻게 코드가 변환되는지 알아보자.
React Compoent 가 결과적으로 어떻게 만들어지는지는 React 공식 문서에서 제공하는 React Complier Playground 를 통해 쉽게 확인해볼 수 있다.
위에서 작성했던 예시코드를 React Complier 를 통해 변환 하면 다음과 같이 변환된다.
import { c as _c } from "react/compiler-runtime";
import { useState } from "react";
import "./App.css";
function App() {
const $ = _c(6);
const [count, setCount] = useState(0);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => setCount(_temp);
$[0] = t0;
} else {
t0 = $[0];
}
let t1;
if ($[1] !== count) {
t1 = (
<button onClick={t0}>
count is
{count}
</button>
);
$[1] = count;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <SubComponent />;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t1) {
t3 = (
<div>
{t1}
{t2}
</div>
);
$[4] = t1;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
function _temp(count_0) {
return count_0 + 1;
}
const color = ["red", "green", "blue", "yellow"];
export default function SubComponent() {
const $ = _c(3); // 캐시 생성
const [random] = useState(_temp2);
const randomColor = color[random];
let t0;
// 처음 렌더링
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <h1>렌더링이 발생하면 색상이 바뀜</h1>;
$[0] = t0;
} else {
//
t0 = $[0];
}
let t1;
if ($[1] !== randomColor) {
t1 = (
<>
{t0}
<h2 style={{ color: randomColor, marginTop: "30px" }}>SubComponent</h2>
</>
);
$[1] = randomColor;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function _temp2() {
return Math.floor(Math.random() * color.length);
}
살짝은 복잡해보이지만, 사실 별거 없다.
_c 라는 함수를 통해 캐시배열($)을 생성하고, 해당 캐시가 처음 렌더링 된 것이면 $[0]에 초기화를 해주고, 렌더링 되었던 이력이 있다면, 기존 $[0]에서 꺼내서 사용한다.
비교적 이해하기 쉬운 로직이지만, 위와 같은 결과물을 얻기 위해서 다음 이미지와 같은 과정으로 동작한다.

컴파일러라는 이름 답게(?) 아주 복잡한 파이프라인을 통해 수행 되고 있다. 내용이 무수히 많고, 복잡하게 작성되어있기 때문에, 한번에 이해하기에는 어려움이 있다.
그래서 대략적으로 흐름의 큰 관점에서 보면, 다음과 같은 과정을 통해 컴파일러가 동작한다.
React Component → 추상구문트리(AST) → HIR → SSA → Reactive Scopes → Reactive Function → React Component
이 과정을 통해서 React 코드의 데이터 흐름과 가변성을 면밀히 파악하여 렌더링에 사용되는 값을 세부적으로 메모이제이션 할 수 있도록 한다.
Javascript 로 이루어진 React Component 를 React Complier 가 쉽게 이해할 수 있는 AST 구조로 변경하는 과정이 처음으로 일어난다.
AST(Abstract Syntax Tree)
코드의 “구조”를 트리형태로 이해할 수 있도록 나타내는 방식
이때 이 과정은 Babel 이라는 도구를 통해 변환 된다. 아래 사이트를 통해서도 쉽게 변환 내용을 확인할 수 있다.

다음으로는 앞서 변환된 AST를 CFG(Control Flow Graph) 기반 HIR 을 생성 한다.
HIR은 React Team에서 Complier를 개발하면서 도입한 개념이다. 컴파일러를 설계할 때, 사용되는 중간 표현(Intermediate Representation, IR)을 직접 설계한 것이라고 할 수 있다.
HIR 은 반복문 등의 고수준의 정보(조건문 등)를 보존하며, “제어 흐름” 을 그래프의 형태를 코드로 표현을 한다.

React Team에서 제어의 흐름을 표현하기 위해 고안한 HIR 은 Rust Complier에서 따왔다고 한다.
이렇게 AST를 통해 HIR 만들어 통해 React Complier 내에서 훨씬 더 정확한 분석과 유형 추론이 가능해졌다고 한다.

중간 표현(Intermediate Representation, IR) 의 하나인 SSA (Static Single Assignment)는 컴파일러를 설계하고 만들 때 사용되는 개념 중 한 가지이다.
SSA 는 “각 변수를 정확히 한 번 할당 할 것을 요구하고, 사용하기 전에 정의하는 것” 표현법을 의미한다.
예를 들어 다음과 같은 변수가 있다고 해보자.
x = 1
y = x + 5 // 여기서 x는 1
x = 10
z = x + y // 여기서 x는 10
하나의 x 변수가 사용되는 위치에 따라 재할당 되면서 각각 다른 값을 가지며 사용된다.
SSA로 변환한 다는 것은 다음과 같이 표현하는 것이라고 상각하면 된다.
x1 = 1
y1 = x1 + 5 // x1는 1
x1 = 10
z1 = x2 + y1 // x2는 10
이렇게 표현함으로써 컴파일러는 변수의 흐름을 조금 더 쉽게 이해하고, 분석하기 쉬워지게 된다.
이렇게 흐름 분석의 효율성을 위해 HIR의 모든 식별자가 SSA 기반 식별자로 업데이트가 된다.
SSA로 변환 된 HIR 내용이 유효한 React 인지 유효성 검사를 진행하게 된다. 여기에 Hook, State 등 React 필수 규칙에 대한 내용을 잘 지켰는지 확인하게 된다.
이후 생성/변경 되는 값의 범위와, 해당 값이 생성/변경 되는데 필요한 명령들의 집합을 결정하는 Reactive Scopes 를 결정한다.
컴파일러가 React Scopes를 결정하게 되면, HIR에서 해당 Scope 내용을 명시적으로 표현하도록 변환한다.
이 과정에서 자동 메모이제이션을 가능하게 하는 많은 로직들이 들어가는데, 너무 많고 복잡해서 이 부분은 넘어가도록 한다.
위 과정이 모두 끝나면 제어의 흐름을 표현한 HIR 과 Component 를 트리 형태로 나타낸 AST 를 결합하여 ReactiveFunction 이라는 것으로 변환 시키게 된다.
이렇게 생성된 Reactive Function은 다시 Babel을 통해 AST 형태로 변환 되고, 이를 Javascript (React Component) 로 변환 되어 앞서 React Complier 를 통해 변환된 형태를 확인할 수 있게 된다.
export default function SubComponent() {
const $ = _c(3); // 캐시 생성
const [random] = useState(_temp2);
const randomColor = color[random];
let t0;
// 처음 렌더링
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <h1>렌더링이 발생하면 색상이 바뀜</h1>;
$[0] = t0;
} else {
//
t0 = $[0];
}
let t1;
if ($[1] !== randomColor) {
t1 = (
<>
{t0}
<h2 style={{ color: randomColor, marginTop: "30px" }}>SubComponent</h2>
</>
);
$[1] = randomColor;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
function _temp2() {
return Math.floor(Math.random() * color.length);
}
규칙을 위반할 수 있는 코드를 발견하면 앱의 동작을 변경하는 위험을 감수하기보다는 안전하게 최적화를 건너 뛰도록 했다고 한다.
그래서 반드시 React 규칙을 잘 지켜야 메모이제이션이 올바르게 동작하도록 할 수 있다.
이를 위해서 새로운 eslint 플러그인을 함께 출시햇다. eslint-plugin-react-hooks 을 새롭게 제공함으로 React Compolier가 잘 동작하도록 돕는다.
또한 해당 플러그인에는 최적화할 수 없는 코드를 식별하는 데 도움이 되는 ESLint 규칙이 포함되어 있다. ESLint 규칙에서 오류가 감지되면, 컴파일러는 해당 컴포넌트나 훅의 최적화를 건너뛰도록 한다.
eslint.config.js 파일에서 다음과 같이 설정하면 된다.
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks'
import { defineConfig } from 'eslint/config'
export default defineConfig([
{
extends: [reactHooks.configs['recommended-latest']]
},
])
React Compiler로 최적화된 구성 요소는 React DevTools에서 "Memo ✨" 배지를 표시한다. 쉽게 DevTool을 통해 메모이제이션이 잘 되었는지 확인해볼 수 있다.

소개 블로그에 따르면, 대부분의 경우 React Compiler 를 사용하는 것이 작성한 코드보다 훨씬 정확하게 메모이제이션을 해준다고 한다.
또한 React Compiler 는 useMemo/ 를 useCallback사용할 수 없는 경우에도 메모이제이션을 수행할 수 이씩 때문에 React Compiler 사용을 권장한다고 한다.
하지만, React Compiler를 사용하는 것은 메모이제이션에 대한 “제어권”을 모두 React 에 맞기는 것이기 때문에 메모이제이션에 대해 개발자가 직접 제어하고 싶을 때가 분명 있을 것이다.
그래서 여전히 useMemo과 useCallback은 React 컴파일러와 함께 메모이제이션되는 값을 제어하는데 사용할 수 있다고 한다.
결론적으로 기본적으로 React Compiler를 사용하고, 정확한 제어를 위해 필요한 경우 useMemo , useCallback 을 사용하는 것이 좋겠다.
이외에도 React Complier 사용을 하면서 더욱 자세하게 사용할 수 있는 내용을 공식문서에서 제공한다.
컴파일러 제어, 버전 호환성, 디버깅, 로깅 등 자세한 사용 내용은 아래 링크를 통해 확인 할 수 있다.
이번 글을 정리하면서 React 팀에서 지속적으로 DX 향상을 위한 많은 고민들을하고 있다는 것을 많이 느꼈다.
처음 React를 접한 이후 Functional Component, Fiber, Concurrent Features, React Server Componet 그리고 React Compomlier 까지.. 정말 많은 발전이 있었다.
단순히 UI를 표현하는 기능에 머무리지 않고,너 나은 성능, 더 나은 개발자 경험까지 지속적으로 발전하는 모습이 있었기에 오랜시간 프론트엔드에서 가장 인기있는 라이브러리로 군림하고 있는 것이 아닌가 싶다.
이를 기억하며, 현재의 머물러있지 않고, 문제를 고민하고 지속적 성장하는 프론트엔드 개발자로 성장하고 싶다.
React Conf 2025 (Day 1) | GeekNews