← 목록으로
AI 코드 분석으로 함수형 리팩토링하기

React Compiler의 동작 원리를 정리하면서, 컴파일러가 코드를 분석하는 방식 자체에 함수형 프로그래밍의 핵심 원칙이 녹아 있다는 걸 알게 됐습니다. 그런데 한 발 더 나아가서, 컴파일러의 분석 파이프라인을 코드 생성 직전에 멈추고 그 중간 결과물을 AI에게 넘기면 어떨까? 실제로 해봤더니, "함수형으로 바꿔줘"라고 하거나 "컴파일러 관점으로 분석해줘"라고 개념적으로 요청하는 것보다 훨씬 정확한 리팩토링이 나왔습니다.

컴파일러의 분석이 보여주는 것

React Compiler는 소스 코드를 최적화하기 위해 여러 단계의 정적 분석을 수행합니다. 이 분석 단계 각각이 함수형 프로그래밍의 원칙과 직접적으로 대응됩니다.

컴파일러 분석핵심 동작대응하는 함수형 원칙
SSA 변환모든 변수를 한 번만 할당불변 바인딩 (const)
Effect 분석Read/Mutate/Freeze 분류순수 함수, 부수효과 격리
Reactive 분석파생 가능한 값 식별상태 최소화, 계산된 값
Scope 생성독립적 캐싱 단위 분리관심사 분리

핵심은 이 분석 단계들이 코드 생성(메모이제이션 삽입) 이전에 수행된다는 점입니다. 코드 생성을 빼면, 컴파일러의 출력은 순수한 코드 분석 리포트가 됩니다.

컴파일러 분석을 CLI로 추출하기

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에 넘기기

이 출력을 그대로 AI에게 넘기고 리팩토링을 요청합니다.

다음은 React Compiler가 OrderSummary 컴포넌트를 분석한 결과야.
코드 생성(메모이제이션 삽입) 직전 단계까지의 분석이야.

[위의 분석 출력 전체를 붙여넣기]

이 분석을 바탕으로 함수형 프로그래밍 원칙에 맞게 리팩토링해줘.
조건:
- SSA에서 φ 함수가 나온 변수는 단일 할당으로 변경
- Mutate로 분류된 연산은 Read로 변환
- Scope 경계를 코드 구조에 반영
- non-reactive 값은 컴포넌트 밖으로 추출 가능한지 검토

AI에게 "SSA 관점에서 분석해줘"라고 개념적으로 요청할 때는, AI가 직접 변수의 재할당 횟수를 세고 의존성을 추적해야 합니다. 이 과정에서 빠뜨리거나 잘못 판단할 수 있습니다. 반면 컴파일러가 이미 수행한 분석 결과를 넘기면, AI는 분석이 아니라 해석과 적용에 집중합니다.

세 가지 접근 비교

같은 코드에 대해 세 가지 방식으로 리팩토링을 요청했을 때의 차이입니다.

1단계: 단순 요청

"이 코드를 함수형으로 리팩토링해줘"

AI는 forforEachmap으로 바꾸고, letconst로 바꾸는 수준에서 멈추는 경우가 많습니다. totalreduce로 바꿔도 displayItemspush는 그대로 두거나, 변환은 하되 scope 분리까지는 가지 않습니다. 왜 바꿔야 하는지에 대한 분석 없이 패턴만 대입하기 때문입니다.

2단계: 개념적 분석 요청

"SSA, Effect, Reactive, Scope 관점으로 분석한 뒤 리팩토링해줘"

AI가 직접 분석을 시도합니다. 대체로 방향은 맞지만, 변수의 재할당 횟수를 세거나 의존성 체인을 추적하는 과정에서 빠뜨리거나 잘못 판단하는 경우가 있습니다. 분석의 정확도가 AI의 추론 능력에 의존합니다.

3단계: 컴파일러 분석 결과 전달

"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 + +=reduceSSA: total_0, total_1, total_2 = φ(...) — 3번 할당
displayItems: pushmapEffect: 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 패턴이 반복되는 컴포넌트
  • 여러 props에서 파생되는 계산: 의존성 그래프가 복잡해서 scope 분리가 필요한 경우
  • 성능 최적화가 필요한 컴포넌트: React Compiler가 최적화하기 좋은 형태로 만들고 싶을 때

컴파일러 내부 API는 공식 지원 API가 아니므로 버전에 따라 바뀔 수 있습니다. 하지만 분석의 개념(SSA, Effect, Reactive, Scope)은 컴파일러 이론의 기본이라 프레임워크가 바뀌어도 유효합니다. 실제 프로젝트에서는 React Compiler Playground를 활용해서 중간 표현을 UI로 확인하는 것도 좋은 대안입니다.

정리

React Compiler의 분석 파이프라인에서 코드 생성을 빼면, 코드의 구조적 특성을 정확하게 기술하는 분석 리포트가 남습니다. 이 리포트를 AI에게 넘기면, AI가 직접 분석하는 것보다 정확한 근거 위에서 리팩토링할 수 있습니다. 컴파일러는 분석을, AI는 해석과 적용을 — 각자 잘하는 일을 나누는 것입니다.

References