React Compiler의 동작 원리를 정리하면서, 컴파일러가 코드를 분석하는 방식 자체에 함수형 프로그래밍의 핵심 원칙이 녹아 있다는 걸 알게 됐습니다. 그런데 한 발 더 나아가서, 컴파일러의 분석 파이프라인을 코드 생성 직전에 멈추고 그 중간 결과물을 AI에게 넘기면 어떨까? 실제로 해봤더니, "함수형으로 바꿔줘"라고 하거나 "컴파일러 관점으로 분석해줘"라고 개념적으로 요청하는 것보다 훨씬 정확한 리팩토링이 나왔습니다.
React Compiler는 소스 코드를 최적화하기 위해 여러 단계의 정적 분석을 수행합니다. 이 분석 단계 각각이 함수형 프로그래밍의 원칙과 직접적으로 대응됩니다.
| 컴파일러 분석 | 핵심 동작 | 대응하는 함수형 원칙 |
|---|---|---|
| SSA 변환 | 모든 변수를 한 번만 할당 | 불변 바인딩 (const) |
| Effect 분석 | Read/Mutate/Freeze 분류 | 순수 함수, 부수효과 격리 |
| Reactive 분석 | 파생 가능한 값 식별 | 상태 최소화, 계산된 값 |
| Scope 생성 | 독립적 캐싱 단위 분리 | 관심사 분리 |
핵심은 이 분석 단계들이 코드 생성(메모이제이션 삽입) 이전에 수행된다는 점입니다. 코드 생성을 빼면, 컴파일러의 출력은 순수한 코드 분석 리포트가 됩니다.
React Compiler의 파이프라인은 이렇게 구성됩니다.
Source → AST → HIR → SSA → Effect 분석 → Reactive 분석 → Scope 생성 → Code 생성
↑
여기서 멈추면
분석만 남는다
babel-plugin-react-compiler의 내부 파이프라인을 활용해서, 코드 생성 직전까지의 분석 결과만 추출하는 CLI 도구를 만들 수 있습니다.
#!/usr/bin/env npx tsx
// analyze.ts — React Compiler 분석 단계만 추출하는 CLI
import { transformSync } from '@babel/core';
import fs from 'fs';
const sourceCode = fs.readFileSync(process.argv[2], 'utf-8');
// 각 파이프라인 단계를 가로채는 커스텀 로거
const pipelineLogger = {
logEvent(filename: string | null, event: any) {
if (event.kind === 'CompileSuccess') {
console.log('=== SSA Analysis ===');
printSSABindings(event.fnName, event.ir);
console.log('\n=== Effect Analysis ===');
printEffects(event.ir);
console.log('\n=== Reactive Analysis ===');
printReactiveValues(event.ir);
console.log('\n=== Scope Analysis ===');
printScopes(event.ir);
}
},
};
transformSync(sourceCode, {
plugins: [
[
'babel-plugin-react-compiler',
{
logger: pipelineLogger,
panicThreshold: 'none',
},
],
],
parserOpts: { plugins: ['jsx', 'typescript'] },
filename: 'input.tsx',
});
좀 더 직접적인 방법도 있습니다. 컴파일러의 내부 함수들을 단계별로 호출하되, 마지막 코드 생성만 빼는 것입니다.
// 컴파일러 파이프라인을 단계별로 실행
import { parse } from '@babel/parser';
import {
lower, // AST → HIR
enterSSA, // HIR → SSA
inferEffects, // Effect 분석
inferReactiveScopeVariables, // Reactive 분석
buildReactiveBlocks, // Scope 생성
// codegenFunction ← 이건 호출하지 않는다
} from 'babel-plugin-react-compiler/src/Entrypoint';
const ast = parse(sourceCode, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
});
const funcNode = ast.program.body[0];
const hir = lower(funcNode, env);
const ssa = enterSSA(hir);
inferEffects(ssa);
const reactive = inferReactiveScopeVariables(ssa);
const scopes = buildReactiveBlocks(reactive);
// scopes가 최종 분석 결과 — 여기서 멈춤
printAnalysis(scopes);
이 스크립트를 실행하면 코드 생성 없이 분석 결과만 나옵니다.
다음 코드를 분석 대상으로 넣어봅시다.
function OrderSummary({ items, coupon, user }) {
let total = 0;
let discountedTotal = 0;
let displayItems = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.inStock) {
total += item.price * item.quantity;
displayItems.push({
...item,
subtotal: item.price * item.quantity,
});
}
}
if (coupon && coupon.isValid) {
discountedTotal = total * (1 - coupon.discount);
} else {
discountedTotal = total;
}
const isVip = user.purchaseCount > 10;
const vipLabel = isVip ? 'VIP' : 'Standard';
const greeting = `${user.name}님 (${vipLabel})`;
return (
<div>
<h1>{greeting}</h1>
<ItemList items={displayItems} />
<span>{discountedTotal}원</span>
</div>
);
}
npx tsx analyze.ts OrderSummary.tsx
=== SSA Analysis ===
OrderSummary:
total_0 = 0 [initial]
total_1 = total_0 + item.price * item.quantity [loop body]
total_2 = φ(total_0, total_1) [loop merge]
discountedTotal_0 = total_2 * (1 - coupon.discount) [if branch]
discountedTotal_1 = total_2 [else branch]
discountedTotal_2 = φ(discountedTotal_0, discountedTotal_1) [merge]
displayItems_0 = [] [initial]
displayItems_1 = mutate(displayItems_0, push(...)) [loop body]
isVip_0 = user.purchaseCount > 10
vipLabel_0 = φ('VIP', 'Standard') [conditional]
greeting_0 = user.name + '님 (' + vipLabel_0 + ')'
재할당 변수: total(3), discountedTotal(3), displayItems(2)
φ 함수: total_2, discountedTotal_2, vipLabel_0
=== Effect Analysis ===
items → Read
items[i] → Read
item.price → Read
item.quantity → Read
item.inStock → Read
total += ... → Mutate (local accumulator)
displayItems.push → Mutate (local array)
{...item, subtotal} → Read(item) + Create(new object)
coupon?.isValid → Read
coupon?.discount → Read
user.purchaseCount → Read
user.name → Read
Mutate 연산: 2개 (total 누적, displayItems.push)
Read 연산: 11개
Create 연산: 1개
=== Reactive Analysis ===
reactive (props/state 의존):
items ← prop
coupon ← prop
user ← prop
total_2 ← items에서 파생
discountedTotal_2 ← items, coupon에서 파생
displayItems_1 ← items에서 파생
isVip_0 ← user에서 파생
greeting_0 ← user에서 파생
non-reactive (상수):
'VIP' ← string literal
'Standard' ← string literal
0 ← number literal
의존성 체인:
items → [total_2, displayItems_1] → discountedTotal_2
coupon → discountedTotal_2
user → isVip_0 → vipLabel_0 → greeting_0
=== Scope Analysis ===
scope_0 [deps: items, coupon]:
displayItems = items.filter(...).map(...)
total = items.reduce(...)
discountedTotal = coupon ? total * ... : total
→ JSX: <ItemList>, <span>
scope_1 [deps: user]:
isVip = user.purchaseCount > 10
vipLabel = isVip ? 'VIP' : 'Standard'
greeting = user.name + '님 (' + vipLabel + ')'
→ JSX: <h1>
scope_2 [deps: scope_0, scope_1]:
→ JSX: <div>
독립 scope: 2개 (scope_0과 scope_1은 의존성이 겹치지 않음)
코드 생성 단계가 빠져 있으니, 이건 순수한 분석 리포트입니다. 어떤 변수가 몇 번 재할당되는지, 어떤 연산이 변이를 일으키는지, 어떤 값이 어떤 props에 의존하는지, 어떤 연산들이 같은 scope로 묶이는지 — 구조화된 데이터로 나옵니다.
이 출력을 그대로 AI에게 넘기고 리팩토링을 요청합니다.
다음은 React Compiler가 OrderSummary 컴포넌트를 분석한 결과야.
코드 생성(메모이제이션 삽입) 직전 단계까지의 분석이야.
[위의 분석 출력 전체를 붙여넣기]
이 분석을 바탕으로 함수형 프로그래밍 원칙에 맞게 리팩토링해줘.
조건:
- SSA에서 φ 함수가 나온 변수는 단일 할당으로 변경
- Mutate로 분류된 연산은 Read로 변환
- Scope 경계를 코드 구조에 반영
- non-reactive 값은 컴포넌트 밖으로 추출 가능한지 검토
AI에게 "SSA 관점에서 분석해줘"라고 개념적으로 요청할 때는, AI가 직접 변수의 재할당 횟수를 세고 의존성을 추적해야 합니다. 이 과정에서 빠뜨리거나 잘못 판단할 수 있습니다. 반면 컴파일러가 이미 수행한 분석 결과를 넘기면, AI는 분석이 아니라 해석과 적용에 집중합니다.
같은 코드에 대해 세 가지 방식으로 리팩토링을 요청했을 때의 차이입니다.
"이 코드를 함수형으로 리팩토링해줘"
AI는 for를 forEach나 map으로 바꾸고, let을 const로 바꾸는 수준에서 멈추는 경우가 많습니다. total을 reduce로 바꿔도 displayItems의 push는 그대로 두거나, 변환은 하되 scope 분리까지는 가지 않습니다. 왜 바꿔야 하는지에 대한 분석 없이 패턴만 대입하기 때문입니다.
"SSA, Effect, Reactive, Scope 관점으로 분석한 뒤 리팩토링해줘"
AI가 직접 분석을 시도합니다. 대체로 방향은 맞지만, 변수의 재할당 횟수를 세거나 의존성 체인을 추적하는 과정에서 빠뜨리거나 잘못 판단하는 경우가 있습니다. 분석의 정확도가 AI의 추론 능력에 의존합니다.
"React Compiler의 분석 결과야. 이걸 바탕으로 리팩토링해줘."
[실제 컴파일러 출력]
AI가 분석을 직접 할 필요가 없습니다. 컴파일러가 이미 정확하게 수행한 분석 결과를 해석하고 적용만 하면 됩니다. total의 φ 함수 3개, displayItems의 Mutate 분류, scope 2개의 독립성 — 이 모든 게 이미 데이터로 주어져 있습니다.
접근 방식별 비교:
1단계: 코드 → AI가 패턴 매칭 → 표면적 변경
2단계: 코드 → AI가 분석 시도 → 근거 있지만 불완전한 변경
3단계: 코드 → 컴파일러가 분석 → AI가 적용 → 정확한 근거 기반 변경
| 항목 | 단순 요청 | 개념적 분석 | 컴파일러 출력 기반 |
|---|---|---|---|
| 분석 정확도 | 없음 | AI 추론에 의존 | 컴파일러 수준 |
| 재할당 감지 | 패턴 기반 | 대체로 정확 | 정확 (φ 함수 포함) |
| Effect 분류 | 안 함 | 가끔 누락 | 완전 |
| Scope 분리 | 안 함 | 시도하지만 불완전 | 정확 |
| 일관성 | 낮음 | 중간 | 높음 |
3단계 방식으로 얻은 리팩토링 결과입니다. 각 변환의 근거가 컴파일러 출력에서 직접 나옵니다.
function OrderSummary({ items, coupon, user }) {
// scope_0 [deps: items, coupon]
const inStockItems = items.filter((item) => item.inStock);
const displayItems = inStockItems.map((item) => ({
...item,
subtotal: item.price * item.quantity,
}));
const total = inStockItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const discountedTotal =
coupon?.isValid ? total * (1 - coupon.discount) : total;
// scope_1 [deps: user]
const vipLabel = user.purchaseCount > 10 ? 'VIP' : 'Standard';
const greeting = `${user.name}님 (${vipLabel})`;
return (
<div>
<h1>{greeting}</h1>
<ItemList items={displayItems} />
<span>{discountedTotal}원</span>
</div>
);
}
| 변환 | 컴파일러 분석 근거 |
|---|---|
total: let + += → reduce | SSA: total_0, total_1, total_2 = φ(...) — 3번 할당 |
displayItems: push → map | Effect: displayItems.push → Mutate |
discountedTotal: if/else → 삼항 | SSA: discountedTotal_2 = φ(d_0, d_1) |
isVip 인라인 | Scope: scope_1에서 단일 사용, 중간 바인딩 불필요 |
| scope 경계 주석 | Scope: scope_0 [items, coupon], scope_1 [user] 독립 |
let이 전부 사라졌고, 변이 연산이 없어졌고, 각 값이 선언과 동시에 확정됩니다. React Compiler가 다시 이 코드를 처리하면, 이미 scope가 분리되어 있으므로 최적화가 더 효과적으로 적용됩니다.
이 방식이 모든 리팩토링에 필요한 건 아닙니다. 간단한 유틸 함수나 이미 순수한 코드에 이 분석을 적용하는 건 과합니다. 효과가 큰 경우는 다음과 같습니다.
let + 재할당 + push 패턴이 반복되는 컴포넌트컴파일러 내부 API는 공식 지원 API가 아니므로 버전에 따라 바뀔 수 있습니다. 하지만 분석의 개념(SSA, Effect, Reactive, Scope)은 컴파일러 이론의 기본이라 프레임워크가 바뀌어도 유효합니다. 실제 프로젝트에서는 React Compiler Playground를 활용해서 중간 표현을 UI로 확인하는 것도 좋은 대안입니다.
React Compiler의 분석 파이프라인에서 코드 생성을 빼면, 코드의 구조적 특성을 정확하게 기술하는 분석 리포트가 남습니다. 이 리포트를 AI에게 넘기면, AI가 직접 분석하는 것보다 정확한 근거 위에서 리팩토링할 수 있습니다. 컴파일러는 분석을, AI는 해석과 적용을 — 각자 잘하는 일을 나누는 것입니다.