트랜스파일러란
이전 글에서 번들러가 모듈을 합치기 전에 각 파일을 변환한다고 했습니다. 이 변환을 담당하는 도구가 트랜스파일러 (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, 추상 구문 트리) 가 있습니다.
interface User {name: string;age: number;}const Greeting = ({ user }: { user: User }) => (<h1>Hello, {user.name}</h1>);
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가 빠른 이유
- 네이티브 바이너리 , Rust로 작성되어 기계어로 컴파일됩니다. V8 인터프리터를 거치지 않습니다
- 메모리 효율 , Rust의 소유권 시스템으로 가비지 컬렉션 없이 메모리를 관리합니다
- 멀티스레드 , 여러 파일을 병렬로 변환합니다. 싱글 스레드 기준으로 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=es2020esbuild의 특징:
- 병렬 파싱 , Go의 고루틴으로 파일을 병렬 처리합니다
- 메모리 효율 , 최소한의 AST 패스로 변환을 수행합니다
- 통합 파이프라인 , 번들링과 트랜스파일이 하나의 프로세스에서 실행됩니다
Vite는 개발 환경에서 esbuild를 의존성 사전 번들링과 TS/JSX 소스 변환에 모두 기본으로 사용합니다.
속도 비교
벤치마크 결과는 환경에 따라 다르지만, 일반적인 경향은 명확합니다.
| 도구 | 언어 | 상대 속도 (Babel = 1x) | 주요 사용처 |
|---|---|---|---|
| Babel | JavaScript | 1x (기준) | 레거시 프로젝트, 커스텀 플러그인 |
| SWC | Rust | 약 20x (싱글 스레드) | Next.js, Vite, Parcel |
| esbuild | Go | 약 20~25x | Vite (사전 번들링), 단독 번들러 |
| tsc | JavaScript | 약 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는 어떤 접근을 취하는지 알아봅니다.