← 목록으로
데이터 기반 함수형 리팩토링

리팩토링을 하려면 먼저 코드의 구조를 파악해야 합니다. 어떤 변수가 어디서 변하는지, 어떤 연산이 부수효과를 일으키는지, 어떤 값이 어떤 입력에 의존하는지. 이걸 사람이 눈으로 읽어서 판단하면 빠뜨리는 부분이 생기고, AI에게 "이 코드 리팩토링해줘"라고 던지면 프롬프트 표현에 따라 결과가 달라집니다.

이 문제를 고민하다가 React Compiler를 스터디하게 됐습니다. React Compiler는 메모이제이션을 자동 삽입하는 도구로 알려져 있지만, 그 내부를 들여다보면 본질은 정적 분석기입니다. SSA 변환, 부수효과 분류, 의존성 추적, 스코프 분리—코드 생성 직전까지의 파이프라인은 소스 코드의 구조를 정밀하게 분석하는 과정입니다.

여기서 아이디어가 나왔습니다. 코드 생성 단계를 빼면, 컴파일러의 출력은 순수한 구조화된 분석 데이터가 됩니다. 이 데이터를 AI에게 넘기면, 프롬프트 표현이나 대화 컨텍스트가 아니라 항상 동일한 분석 데이터를 근거로 리팩토링합니다. 같은 코드를 넣으면 같은 분석이 나오고, 리팩토링 전후를 다시 분석에 넣어 수치로 비교할 수 있습니다.

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

React Compiler는 메모이제이션을 위해 여러 단계의 정적 분석을 수행합니다.

분석 단계핵심 동작추출되는 데이터
SSA 변환모든 변수에 고유 버전 부여변수별 할당 횟수, φ 함수 위치
Effect 분석각 연산의 부수효과 분류Read/Mutate/Freeze 분류표
Reactive 분석props/state 의존성 추적의존성 체인, 파생 관계
Scope 생성독립적 단위 분리scope별 의존성, 포함 연산

이 분석 단계들은 모두 코드 생성(메모이제이션 삽입) 이전에 수행됩니다. 동일한 소스 코드에 대해 항상 같은 결과를 내는 결정적(deterministic) 분석이라는 점이 핵심입니다.

분석 데이터 추출하기

React Compiler의 파이프라인은 이렇게 구성됩니다.

Source → AST → HIR → SSA → Effect 분석 → Reactive 분석 → Scope 생성 → Code 생성
                                                                        ↑
                                                               여기서 멈추면
                                                            분석 데이터만 남는다

babel-plugin-react-compilerlogger 옵션에서 debugLogIRs 콜백을 등록하면, 파이프라인의 각 단계가 완료될 때마다 HIRFunction/ReactiveFunction 등 내부 IR 데이터를 그대로 받아볼 수 있습니다. React Compiler Playground도 이 방식으로 중간 결과를 시각화합니다.

// babel-plugin-react-compiler의 logger.debugLogIRs 활용
const snapshots = [];

const pluginOptions = {
  logger: {
    debugLogIRs(result) {
      // 매 파이프라인 단계마다 호출됨
      snapshots.push({ stage: result.name, kind: result.kind, value: result.value });
    },
    logEvent(filename, event) { /* CompileSuccess, CompileError 등 */ },
  },
};

// 파이프라인 완료 후, snapshots에 각 단계의 IR이 누적됨
// snapshots에서 원하는 단계의 HIRFunction/ReactiveFunction을 꺼내 분석

분석 데이터로 알 수 있는 것

다음 코드를 분석 대상으로 넣어봅시다.

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>
  );
}

이 코드를 분석하면 다음과 같은 구조화된 데이터를 얻습니다.

  • SSA 분석: total이 4번 할당됨(초기값, 루프 헤더 φ, 루프 내 누적, inStock 분기 합류 φ). discountedTotal은 3번 할당(if 분기, else 분기, 합류 φ). 루프 변수 idisplayItems까지 포함하면 φ 함수가 총 6개 존재
  • Effect 분석: displayItems.push가 Mutate로 분류됨(배열 in-place 수정). total += ...은 원시값 재할당이므로 Mutate가 아니라 새 값 생성(SSA에서 새 버전 할당). props 접근은 Read, 객체 리터럴 생성({...item})은 Create, JSX 전달은 Freeze
  • Reactive 분석: totaldisplayItemsitems에서 파생, discountedTotalitemscoupon에서 파생, greetinguser에서 파생
  • Scope 분석: items/coupon에 의존하는 연산(scope_0), user에 의존하는 연산(scope_1), 그리고 두 scope의 결과를 결합하는 JSX 반환(scope_2)으로 총 3개의 scope가 생성됨. scope_0과 scope_1은 서로 독립적

이 데이터 각각이 리팩토링의 근거가 됩니다. φ 함수가 나온 변수는 reduce나 삼항 연산자로 단일 할당으로 바꿀 수 있고, Mutate로 분류된 pushmap으로, 원시값 재할당(+=)은 reduce로 대체할 수 있고, 독립된 scope는 코드 구조에 그대로 반영할 수 있습니다.

데이터 기반 리팩토링

이 분석 데이터를 AI에 넘기면, AI는 분석을 직접 수행할 필요 없이 해석과 적용에 집중합니다.

다음은 React Compiler가 OrderSummary 컴포넌트를 분석한 결과야.

[분석 데이터 붙여넣기]

이 분석을 바탕으로 함수형 프로그래밍 원칙에 맞게 리팩토링해줘.

핵심은 컴파일러가 내놓는 분석 데이터가 결정적(deterministic)이라는 점입니다. 같은 소스 코드를 넣으면 프롬프트 표현, AI 모델 버전, 대화 컨텍스트와 무관하게 항상 같은 분석 결과가 나옵니다. 리팩토링의 근거가 되는 데이터가 고정되어 있으므로 AI의 출력도 일관성을 가집니다.

또한 리팩토링 결과를 다시 분석에 넣으면 수치로 비교 평가할 수 있습니다.

지표리팩토링 전리팩토링 후
재할당 변수 수2 (total, discountedTotal)0
φ 함수 수60
Mutate 연산1 (push)0
원시값 재할당1 (+=)0
독립 scope3 (코드 구조에 미반영)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_0total_1(φ)total_2(+=)total_3(φ) — 4번 할당, 원시값 재할당 제거
displayItems: pushmapEffect: displayItems.push → Mutate — 배열 in-place 변이 제거
discountedTotal: if/else → 삼항SSA: discountedTotal = φ(d_if, d_else) — 분기 병합 제거
isVip 인라인Scope: scope_1에서 단일 사용, 중간 바인딩 불필요
scope 경계 주석Scope: scope_0 [items, coupon], scope_1 [user] 독립, scope_2에서 결합

let이 전부 사라졌고, 변이 연산이 없어졌고, 각 값이 선언과 동시에 확정됩니다. React Compiler가 다시 이 코드를 처리하면, 이미 scope가 분리되어 있으므로 최적화가 더 효과적으로 적용됩니다.

정리

React Compiler의 분석 파이프라인에서 코드 생성을 빼면, 코드의 구조적 특성을 정확하게 기술하는 분석 데이터가 남습니다. 이 데이터를 AI에게 넘기면, 프롬프트나 컨텍스트에 의존하지 않고 항상 동일한 근거 위에서 리팩토링할 수 있습니다. 리팩토링 전후를 다시 분석에 넣어 수치로 비교하면 개선 여부를 객관적으로 평가할 수 있습니다. 컴파일러는 분석을, AI는 해석과 적용을 — 각자 잘하는 일을 나누는 것입니다.

References