본문으로 건너뛰기
Previous
Next
Babel 완벽 가이드 | 파서·AST·파이프라인·트랜스파일러·Presets·Plugins·실전

Babel 완벽 가이드 | 파서·AST·파이프라인·트랜스파일러·Presets·Plugins·실전

Babel 완벽 가이드 | 파서·AST·파이프라인·트랜스파일러·Presets·Plugins·실전

이 글의 핵심

Babel 내부(@babel/parser, traverse, generator)와 Presets·Plugins·Polyfill, Webpack/Vite 통합을 한글로 정리. Babylon·AST·방문자 패턴·프로덕션 패턴까지 다룹니다.

이 글의 핵심

Babel로 JavaScript를 트랜스파일하는 완벽 가이드입니다. @babel/parser·AST·traverse/visitor·변환 파이프라인·@babel/generator 같은 내부 동작과 Presets, Plugins, Polyfills, Webpack/Vite 통합까지 실전 예제로 정리했습니다.

실무 경험 공유: Babel로 최신 JavaScript를 구형 브라우저에서 실행하면서, 브라우저 호환성 문제가 100% 해결된 경험을 공유합니다.

들어가며: “구형 브라우저를 지원해야 해요”

실무 문제 시나리오

시나리오 1: 최신 JavaScript를 사용하고 싶어요

구형 브라우저는 지원하지 않습니다. Babel로 변환합니다. 시나리오 2: Polyfill이 필요해요

Promise, Array.from 등이 없습니다. Babel이 자동으로 추가합니다. 시나리오 3: JSX를 사용하고 싶어요

브라우저는 JSX를 이해하지 못합니다. Babel로 변환합니다.

1. Babel이란?

핵심 특징

Babel은 JavaScript 트랜스파일러입니다. 주요 장점:

  • 최신 JavaScript: ES2024 → ES5
  • JSX: React 지원
  • TypeScript: 타입 제거
  • Polyfills: 자동 추가
  • Plugins: 확장 가능

2. 파서(Babylon)와 AST: 소스가 트리가 되는 과정

Babel 6 시절 Babylon이라는 이름으로 불리던 JavaScript 파서는 Babel 7에서 @babel/parser로 통합되었습니다. 오늘날 문서나 코드에서 “Babylon”을 보면, 대부분 현재의 @babel/parser와 동일한 계열의 ESTree 호환 AST를 생성한다고 이해하면 됩니다. 파서의 역할은 문자열 소스 코드를 토큰으로 쪼개고, 문법 규칙에 따라 추상 구문 트리(AST)로 만드는 것입니다.

AST는 “프로그램이 어떤 의미 구조를 갖는가”를 노드(node)자식 관계로 표현합니다. 예를 들어 const x = 1 + 2에는 변수 선언(VariableDeclaration), 바인딩(VariableDeclarator), 숫자 리터럴(NumericLiteral), 이항 표현식(BinaryExpression) 같은 노드가 계층적으로 달립니다. Babel 생태계에서는 @babel/types가 이런 노드의 생성·검증·타입 이름(예: t.isIdentifier)을 담당하고, 플러그인 작성 시 “어떤 형태의 노드인지”를 안전하게 다루게 해 줍니다.

파싱 단계에서 자주 쓰는 옵션으로는 JSX, TypeScript, 플러그인 문법(decorators 등) 활성화가 있습니다. 한 파일 안에 여러 문법이 섞이면, 파서 옵션이 실제 소스와 맞지 않아 파싱 오류가 나므로 babel.config.jspresets·plugins 선언 순서와 동일한 파서 플러그인이 로드되는지를 맞추는 것이 중요합니다.

아래는 @babel/parser로 문자열을 직접 파싱해 AST 객체를 얻는 최소 예시입니다. 실무에서는 보통 transformSync가 내부에서 이 과정을 수행합니다.

import { parse } from '@babel/parser';

const ast = parse('const n = 1 + 2;', {
  sourceType: 'module',
  plugins: ['jsx', 'typescript'],
});

// ast.type === 'File' → Program → ...
console.log(ast.program.body[0].type); // VariableDeclaration

AST를 한 번 출력해 보면(깊이가 깊으므로 JSON.stringify 대신 온라인 AST 탐색기나 IDE 확장으로 보는 것이 좋습니다), 플러그인이 “어디를 고치는지”가 직관적으로 이해됩니다. 즉, Preset·Plugin 작성은 결국 AST 상의 특정 type 패턴을 찾아 바꾸는 작업입니다.


3. 순회(traverse)와 방문자(visitor) 패턴

AST는 트리이므로, 모든 노드를 한 번씩 도는 깊이 우선 순회가 필요합니다. @babel/traverse는 이를 방문자(visitor) 객체로 표현합니다. 키는 노드 타입 이름(예: Identifier, CallExpression)이고, 값은 해당 노드에 들렀을 때 실행할 함수입니다. enterexit들어올 때·나갈 때 훅을 나눌 수 있어, 자식 노드를 먼저 처리할지 부모를 먼저 처리할지 제어할 수 있습니다.

방문자 패턴의 실무적 의미는 다음과 같습니다. 첫째, 특정 API 사용 패턴(예: require('foo'))만 골라 검사할 수 있습니다. 둘째, 스코프 정보와 결합하면 path.scope를 통해 변수가 어디에 선언되었는지 추적할 수 있습니다. 셋째, 플러그인은 visitor만 export하면 Babel 코어가 알아서 traverse를 호출하므로, 변환 로직을 선언적으로 작성할 수 있습니다.

import { parse } from '@babel/parser';
import traverse from '@babel/traverse';

const ast = parse('function f(a) { return a + 1; }', { sourceType: 'module' });

traverse(ast, {
  FunctionDeclaration(path) {
    // path.node: AST 노드, path: NodePath (부모·스코프·replace API)
    path.node.id.name = 'renamed';
  },
});

NodePath는 단순한 노드 래퍼가 아니라 부모 경로, 형제, 스코프, 제거·삽입·치환 API를 제공합니다. 플러그인에서 path.replaceWith, path.insertBefore, path.remove 등을 쓰면 부모와의 연결이 일관되게 유지되도록 Babel이 처리합니다. 잘못된 수동 조작은 AST가 깨질 수 있으므로, 가능하면 @babel/types의 빌더로 새 노드를 만든 뒤 replaceWith하는 패턴을 권장합니다.


4. 변환 파이프라인: parse → traverse → generate

@babel/coretransform 계열 API는 대략 다음 파이프라인으로 이해할 수 있습니다.

  1. Parse: 소스 문자열 → AST (@babel/parser).
  2. Traverse + Plugins: 등록된 플러그인의 visitor를 순회하며 AST 변환. Preset은 내부적으로 여러 플러그인을 순서대로 펼칩니다.
  3. Generate: 변환된 AST → 코드 문자열 (@babel/generator). 이 단계에서 소스 맵이 연결됩니다.

플러그인 적용 순서는 결과에 직접 영향을 줍니다. 예를 들어 TypeScript 문법을 먼저 제거해야 하는지, polyfill 삽입이 먼저여야 하는지는 파이프라인 설계 문제입니다. 그래서 babel.config.js에서 plugins 배열의 앞쪽이 먼저 실행된다는 규칙(문서화된 동작)을 기준으로 순서를 맞춥니다.

아래는 파이프라인을 수동으로 이어 붙이는 형태의 예시입니다. 디버깅·실험용으로 유용합니다.

import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import * as t from '@babel/types';

const src = 'const x = () => 1;';
const ast = parse(src, { sourceType: 'module' });

traverse(ast, {
  ArrowFunctionExpression(path) {
    path.replaceWith(t.functionExpression(null, [], t.blockStatement([t.returnStatement(t.numericLiteral(1))])));
  },
});

const { code, map } = generate(ast, { sourceMaps: true }, src);

프로덕션 빌드에서는 보통 한 번의 transformSync 호출로 이 과정이 캡슐화되지만, 커스텀 플러그인을 작성하거나 성능을 프로파일링할 때는 위 단계를 머릿속으로 분리해 두면 원인 분석이 빨라집니다.


5. 코드 생성(@babel/generator)과 소스 맵

@babel/generator는 AST를 다시 읽기 가능한 JavaScript 문자열로 직렬화합니다. 이 과정에서 공백·들여쓰기·쉼표 위치 같은 스타일은 기본 포맷터 규칙을 따르며, 필요하면 옵션으로 조정할 수 있습니다. 중요한 점은 변환 후에도 디버깅 가능해야 한다는 것이고, 그래서 inputSourceMap이나 Babel의 sourceMaps: true 설정과 함께 원본 파일과 줄 매핑이 유지됩니다.

소스 맵이 끊기면 브라우저 개발자 도구에서 번들 내부가 아닌 원본 TS/JSX 줄로 스택이 잡히지 않습니다. Babel 단에서 할 수 있는 일은 (1) babel-loader/@rollup/plugin-babel 등에서 소스 맵 플래그를 켜는 것, (2) 입력 소스 맵을 다음 단계로 전달하는 것입니다. 이후 Webpack/Rollup/Vite가 최종 번들 맵을 합성합니다.

import { transformSync } from '@babel/core';

const out = transformSync(sourceCode, {
  filename: 'src/app.tsx',
  sourceMaps: true,
  // 이전 단계(예: TS)에서 온 맵이 있으면 전달
  // inputSourceMap: previousMap,
  presets: ['@babel/preset-typescript', '@babel/preset-react'],
});
// out.map — 디버깅·다음 툴 체인으로 전달

또한 AST를 수동으로 많이 바꿀수록 생성된 코드의 가독성이 떨어질 수 있으므로, 라이브러리 배포용 빌드에서는 compact 옵션이나 이후 단계의 minifier(Terser, esbuild)에 맡기는 것이 일반적입니다.


6. 프로덕션 Babel 패턴

실서비스에서 Babel은 “최신 문법을 옛날 브라우저에 맞춘다”를 넘어, 번들 크기·빌드 속도·일관된 런타임 헬퍼까지 고려해야 합니다.

@babel/plugin-transform-runtime + @babel/runtime: 헬퍼 함수(classCallCheck 등)를 각 파일마다 중복 삽입하지 않고 공유 모듈으로 빼서 번들 크기와 캐시 효율을 좋아지게 합니다. corejs 옵션과 함께 쓰면 폴리필 주입 전략과 맞물리므로, preset-env의 useBuiltIns와 역할이 겹치지 않게 설계해야 합니다.

preset-envmodules: 라이브러리 제작 시 modules: false로 두어 ESM 형태를 유지하고, 애플리케이션 번들러가 트리 쉐이킹을 하게 하는 패턴이 흔합니다. 반면 테스트 환경(env.test)에서는 node: 'current'CommonJS 출력을 기대하는 도구와 맞추기도 합니다.

캐시: babel-loadercacheDirectory: true, 또는 빌드 도구의 영구 캐시는 대규모 모노레포에서 필수에 가깝습니다. 플러그인 순서나 babel.config.js가 바뀌면 캐시가 무효화되어야 하므로, CI에서는 캐시 키에 설정 파일 해시를 포함합니다.

플러그인 최소화: 가능한 한 문법 한 종류당 한 플러그인 원칙을 지키고, 실험 단계 문법은 TC39 단계와 @babel/plugin-* 버전을 주석으로 고정해 팀 합의를 남깁니다.

디버깅: 변환 결과가 이상하면 (1) npx babel path/to/file.js --show-config 등으로 실제 병합 설정을 확인하거나, 빌드 도구가 제공하는 설정 덤프를 사용하고, (2) AST 탐색기로 어느 visitor에서 노드가 바뀌는지 역추적합니다.

// 라이브러리용 babel.config.js 스케치 — 앱과 분리해 생각하기
module.exports = {
  presets: [
    ['@babel/preset-env', { modules: false, bugfixes: true }],
    ['@babel/preset-react', { runtime: 'automatic' }],
  ],
  plugins: [
    ['@babel/plugin-transform-runtime', { useESModules: true }],
  ],
};

이상으로 Babel의 파싱·순회·파이프라인·생성 흐름과 운영 시 유의점을 정리했습니다. 이후 절에서는 설치법과 Preset·Plugin 구성, 번들러 연동을 계속합니다.


7. 설치 및 기본 설정

설치

npm install -D @babel/core @babel/cli @babel/preset-env

.babelrc

{
  "presets": ["@babel/preset-env"]
}

실행

npx babel src --out-dir dist

8. Presets

@babel/preset-env

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "browsers": ["> 1%", "last 2 versions", "not dead"]
        },
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

@babel/preset-react

npm install -D @babel/preset-react
{
  "presets": [
    "@babel/preset-env",
    [
      "@babel/preset-react",
      {
        "runtime": "automatic"
      }
    ]
  ]
}

@babel/preset-typescript

npm install -D @babel/preset-typescript
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ]
}

9. Plugins

@babel/plugin-transform-runtime

npm install -D @babel/plugin-transform-runtime
npm install @babel/runtime
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

@babel/plugin-proposal-decorators

npm install -D @babel/plugin-proposal-decorators
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
  ]
}

10. Polyfills

core-js

npm install core-js
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

수동 Import

import 'core-js/stable';
import 'regenerator-runtime/runtime';

11. Webpack 통합

npm install -D babel-loader
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
    ],
  },
};

12. Vite 통합

Vite는 기본적으로 esbuild를 사용하지만, Babel을 추가할 수 있습니다.

npm install -D @vitejs/plugin-react
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['@babel/plugin-proposal-decorators'],
      },
    }),
  ],
});

13. 실전 예제: React 라이브러리

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          browsers: ['> 1%', 'last 2 versions', 'not dead'],
        },
        modules: false,
        useBuiltIns: 'usage',
        corejs: 3,
      },
    ],
    [
      '@babel/preset-react',
      {
        runtime: 'automatic',
      },
    ],
    '@babel/preset-typescript',
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,
      },
    ],
  ],
  env: {
    test: {
      presets: [
        [
          '@babel/preset-env',
          {
            targets: {
              node: 'current',
            },
          },
        ],
      ],
    },
  },
};

14. 브라우저 타겟

.browserslistrc

> 1%
last 2 versions
not dead

package.json

{
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

정리 및 체크리스트

핵심 요약

  • Babel: JavaScript 트랜스파일러
  • 파서·AST: @babel/parser(구 Babylon 계열)가 소스를 AST로 만들고, 플러그인은 노드 패턴을 바꿉니다
  • traverse·visitor: @babel/traverseNodePath와 스코프 정보를 넘기며 변환을 수행합니다
  • 파이프라인: parse →(플러그인 순회)→ generate, 소스 맵은 생성 단계와 이후 번들러에서 이어집니다
  • Presets: 설정 묶음
  • Plugins: 변환 규칙
  • Polyfills: 자동 추가
  • Webpack/Vite: 통합 가능
  • 브라우저 타겟: 유연한 설정

구현 체크리스트

  • Babel 설치
  • Presets 설정
  • Plugins 추가
  • Polyfills 설정
  • (선택) 커스텀 플러그인·AST 이해로 디버깅 시간 단축
  • Webpack/Vite 통합
  • 브라우저 타겟 설정
  • 프로덕션 빌드
  • 번들 크기 확인

같이 보면 좋은 글


이 글에서 다루는 키워드

Babel, AST, @babel/parser, Transpiler, JavaScript, TypeScript, Polyfill, Build Tools, Frontend

내부 동작과 핵심 메커니즘

이 글의 주제는 「Babel 완벽 가이드 | 파서·AST·파이프라인·트랜스파일러·Presets·Plugins·실전」입니다. 앞선 튜토리얼을 구현·런타임 관점에서 다시 압축합니다. 시스템·런타임 경계(스케줄링, I/O, 메모리, 동시성)를 기준으로 “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)·동시성이 어디서 터지는가”를 한 장면으로 그리면 장애 분석이 빨라집니다.

처리 파이프라인(개념도)

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]

경계에서의 지연·실패(시퀀스 관점)

sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(프로세스·런타임·게이트웨이)
  participant D as 의존성(외부 API·DB·큐)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)

알고리즘·프로토콜·리소스 관점 체크포인트

  • 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(버퍼 경계, 프로토콜 상태, 트랜잭션 격리, 파일 디스크립터 상한)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 동일 입력에 동일 출력이 보장되는 순수 층과, 시간·네트워크·스레드 스케줄에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합, GC·할당, 캐시 미스처럼 누적 비용을 의심 목록에 넣습니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때(소켓 버퍼, 큐 깊이, 스트림) 어디서 어떤 신호로 속도를 줄일지 정의합니다.

프로덕션 운영 패턴

실서비스에서는 기능과 함께 관측·배포·보안·비용·규제가 동시에 요구됩니다.

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율/지연 분위수(p95/p99), 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시 계층·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션 호환성·플래그가 문서화되어 있는가
용량피크 트래픽·디스크·파일 디스크립터·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 가능한 한 프로덕션에 가깝게 맞추는 것이 재현율을 높입니다.


확장 예시: 엔드투엔드 미니 시나리오

「Babel 완벽 가이드 | 파서·AST·파이프라인·트랜스파일러·Presets·Plugins·실전」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값이 기대 범위인지 본다.

의사코드 스케치(프레임워크 무관)

handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)        // 경계에서 거절
  authorize(validated, ctx)                  // 권한·테넌트
  result = domainCore(validated)             // 순수에 가까운 규칙
  persistOrEmit(result, idempotentKey)       // I/O: 멱등·재시도 정책
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성 불안정, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정이 로컬과 다름프로필·시크릿·기본값, 지역 리전단일 소스(예: 스키마 검증된 설정)와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

자주 묻는 질문 (FAQ)

Q. Babylon과 @babel/parser는 같은가요?

A. Babel 7 이전에는 파서가 Babylon이라는 별도 패키지로 불리기도 했고, 현재는 @babel/parser 하나로 통합되어 동일한 역할을 합니다. 예전 글이나 플러그인 이름에 Babylon이 남아 있을 수 있습니다.

Q. TypeScript와 함께 사용해야 하나요?

A. TypeScript는 타입 체크를 하고, Babel은 변환을 합니다. 함께 사용하면 좋습니다.

Q. esbuild와 비교하면 어떤가요?

A. esbuild가 훨씬 빠릅니다. Babel은 더 많은 변환 옵션을 제공합니다.

Q. 번들 크기가 커지나요?

A. Polyfill을 추가하면 커질 수 있습니다. useBuiltIns: ‘usage’로 최소화할 수 있습니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 거의 모든 프론트엔드 프로젝트에서 사용하고 있습니다.