Ray Book
프론트엔드 인프라

트랜스파일러와 컴파일러

Babel → SWC → esbuild, 왜 Rust/Go로 갈아탔는가, 트랜스파일러의 내부 동작과 진화를 시각화합니다

infrababelswcesbuildtypescripttranspiler

트랜스파일러란

이전 글에서 번들러가 모듈을 합치기 전에 각 파일을 변환한다고 했습니다. 이 변환을 담당하는 도구가 트랜스파일러 (transpiler) 입니다.

트랜스파일러는 하나의 프로그래밍 언어를 비슷한 수준의 다른 언어로 변환하는 도구입니다. 컴파일러가 고수준 언어를 저수준 언어 (예: C → 기계어) 로 변환하는 것과 달리, 트랜스파일러는 같은 수준의 언어 간 변환 (예: TypeScript → JavaScript, JSX → JavaScript) 을 수행합니다.

// 변환 전: TypeScript + JSX
const Greeting: React.FC<{ name: string }> = ({ name }) => (
  <h1>Hello, {name}</h1>
);

// 변환 후: JavaScript
const Greeting = ({ name }) => (
  React.createElement("h1", null, "Hello, ", name)
);

트랜스파일러의 동작 과정

모든 트랜스파일러는 세 단계 파이프라인을 따릅니다: Parse → Transform → Generate . 이 과정의 중심에 AST (Abstract Syntax Tree, 추상 구문 트리) 가 있습니다.

1 / 4Source Code
원본 소스 (TSX)
TypeScriptJSX
interface User {
name: string;
age: number;
}
 
const Greeting = ({ user }: { user: User }) => (
<h1>Hello, {user.name}</h1>
);
개발자가 작성하는 코드에는 TypeScript 타입 구문과 JSX가 포함되어 있습니다. 브라우저는 이 구문을 직접 실행할 수 없으므로, 표준 JavaScript로 변환이 필요합니다.

1단계: Parse, 코드를 AST로

소스 코드 문자열을 파싱하여 트리 구조의 AST로 변환합니다. AST의 각 노드는 코드의 구문 요소 하나에 대응합니다.

// 소스 코드
const x = 1 + 2;

// AST (단순화)
{
  type: "VariableDeclaration",
  kind: "const",
  declarations: [{
    id: { type: "Identifier", name: "x" },
    init: {
      type: "BinaryExpression",
      operator: "+",
      left: { type: "NumericLiteral", value: 1 },
      right: { type: "NumericLiteral", value: 2 }
    }
  }]
}

텍스트로 코드를 다루면 정규표현식 치환 수준의 변환만 가능하지만, AST로 다루면 코드의 구조와 의미 를 이해한 변환이 가능합니다.

2단계: Transform, AST 변환

AST 노드를 순회하며 변환 규칙을 적용합니다. 이 단계에서 TypeScript 타입 정보 제거, JSX를 함수 호출로 변환, 최신 문법을 구형 문법으로 다운그레이드하는 작업이 이루어집니다.

// Babel 플러그인 예시, arrow function을 일반 function으로 변환
module.exports = function(babel) {
  return {
    visitor: {
      ArrowFunctionExpression(path) {
        // () => {} 를 function() {} 로 변환
        path.arrowFunctionToExpression();
      }
    }
  };
};

Babel의 플러그인 시스템은 이 visitor 패턴 을 사용합니다. 각 플러그인이 특정 노드 타입을 구독하고, 해당 노드를 만나면 변환 로직을 실행합니다.

3단계: Generate, AST에서 코드로

변환된 AST를 다시 JavaScript 문자열로 출력합니다. 이 단계에서 소스맵도 함께 생성하여, 디버깅 시 변환 전 원본 코드의 위치를 추적할 수 있습니다.

Babel, 플러그인의 힘

Babel은 2014년에 6to5라는 이름으로 시작했습니다. ES6 문법을 ES5로 변환하는 도구였지만, 플러그인 아키텍처 덕분에 범용 코드 변환 도구로 진화했습니다.

// babel.config.json
{
  "presets": [
    ["@babel/preset-env", { "targets": "> 0.25%, not dead" }],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/plugin-proposal-decorators"
  ]
}

프리셋 (preset) 은 관련된 플러그인의 묶음입니다.

  • @babel/preset-env, 대상 브라우저에 맞춰 필요한 문법 변환만 적용
  • @babel/preset-react, JSX 변환
  • @babel/preset-typescript, TypeScript 타입 구문 제거

Babel의 강점은 10년 넘게 축적된 플러그인 생태계 입니다. 데코레이터, 파이프라인 연산자 등 아직 표준화되지 않은 제안 단계의 문법도 Babel 플러그인으로 사용할 수 있습니다.

왜 느린가, JavaScript의 한계

Babel의 치명적 약점은 JavaScript로 작성되었다 는 것입니다.

파싱, AST 순회, 코드 생성은 모두 CPU 집약적 작업입니다. JavaScript는 인터프리터 언어이고, 가비지 컬렉션 오버헤드가 있으며, 싱글 스레드입니다. 수천 개의 파일을 변환할 때 이 한계가 드러납니다.

SWC, Rust의 등장

SWC (Speedy Web Compiler) 는 Rust로 작성된 트랜스파일러입니다. Babel과 같은 Parse → Transform → Generate 파이프라인을 따르지만, 네이티브 코드로 컴파일되어 실행됩니다.

// .swcrc
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true
    },
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    },
    "target": "es2020"
  }
}

SWC가 빠른 이유

  1. 네이티브 바이너리 , Rust로 작성되어 기계어로 컴파일됩니다. V8 인터프리터를 거치지 않습니다
  2. 메모리 효율 , Rust의 소유권 시스템으로 가비지 컬렉션 없이 메모리를 관리합니다
  3. 멀티스레드 , 여러 파일을 병렬로 변환합니다. 싱글 스레드 기준으로 Babel 대비 약 20배, 멀티코어 환경에서 약 60배 빠릅니다

SWC는 Next.js의 기본 트랜스파일러이며, Vite에서는 기본 @vitejs/plugin-react 대신 별도의 @vitejs/plugin-react-swc 플러그인을 선택하면 사용할 수 있습니다.

esbuild, Go의 접근

esbuild는 Go로 작성된 번들러 겸 트랜스파일러입니다. 순수 트랜스파일 성능에서는 SWC와 비슷하며, 번들링까지 통합되어 있어 빌드 파이프라인을 단순화합니다.

# TypeScript + JSX 변환
esbuild src/app.tsx --bundle --outfile=dist/app.js --target=es2020

esbuild의 특징:

  • 병렬 파싱 , Go의 고루틴으로 파일을 병렬 처리합니다
  • 메모리 효율 , 최소한의 AST 패스로 변환을 수행합니다
  • 통합 파이프라인 , 번들링과 트랜스파일이 하나의 프로세스에서 실행됩니다

Vite는 개발 환경에서 esbuild를 의존성 사전 번들링과 TS/JSX 소스 변환에 모두 기본으로 사용합니다.

속도 비교

벤치마크 결과는 환경에 따라 다르지만, 일반적인 경향은 명확합니다.

도구언어상대 속도 (Babel = 1x)주요 사용처
BabelJavaScript1x (기준)레거시 프로젝트, 커스텀 플러그인
SWCRust약 20x (싱글 스레드)Next.js, Vite, Parcel
esbuildGo약 20~25xVite (사전 번들링), 단독 번들러
tscJavaScript약 0.3x (타입 체크 포함)타입 체크 전용

tsc (TypeScript 컴파일러) 는 타입 체크까지 수행하므로 단순 트랜스파일보다 느립니다. 실전에서는 SWC나 esbuild로 빠르게 트랜스파일하고, tsc --noEmit으로 타입 체크만 별도 실행하는 패턴이 일반적입니다.

타입 체크의 트레이드오프

SWC와 esbuild가 빠른 이유 중 하나는 타입 체크를 하지 않는다 는 것입니다. TypeScript 코드에서 타입 구문만 제거할 뿐, 타입이 올바른지 검증하지 않습니다.

// SWC/esbuild: 타입 오류가 있어도 변환 성공
const x: string = 42; // tsc는 에러, SWC/esbuild는 통과

// 변환 결과
const x = 42; // 타입 정보만 제거

이것은 의도된 설계입니다. 타입 체크는 TypeScript 컴파일러의 타입 시스템 전체를 구현해야 하는 방대한 작업이며, 트랜스파일 속도와 트레이드오프 관계에 있습니다.

진화의 흐름

2014  Babel (6to5), JavaScript 기반, 풍부한 플러그인

2019  SWC, Rust 기반, Babel 호환, Next.js 채택

2020  esbuild, Go 기반, 번들러 + 트랜스파일러 통합

2024  TypeScript 5.5, 독립 선언 파일 생성 (isolatedDeclarations)

2025~ Rust/Go 기반 도구가 표준으로 정착

프론트엔드 도구 체인은 JavaScript에서 네이티브 언어 (Rust, Go) 로 이동하고 있습니다. 이 흐름은 트랜스파일러뿐 아니라 린터, 포매터, 번들러 모두에 영향을 미칩니다.

다음 단계

코드를 변환하는 도구를 이해했으니, 이제 코드의 품질을 검사하는 도구 를 살펴보겠습니다. 다음 글에서는 ESLint가 AST를 기반으로 코드를 어떻게 분석하는지, Prettier와 왜 분리하는지, Biome는 어떤 접근을 취하는지 알아봅니다.