본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

esbuild와 swc — JavaScript 밖에서 온 빌드 도구

~11 min · tooling, esbuild, swc, transpilation

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"가장 빠른 JavaScript 빌드 도구 가운데 둘은 Go와 Rust로 만들었어. Node에서 호출하지만 Node 자체는 아니야. 그 경계가 속도의 원천과 한계를 함께 설명해."

왜 이렇게 빠를까

한동안 JavaScript 도구는 대부분 JavaScript로 만들었어. Babel은 JavaScript로 코드를 변환했고, webpack은 JavaScript로 모듈을 묶었지. 작은 프로젝트에서는 충분히 빨랐지만 프로젝트가 커지면 변환과 번들링 시간이 눈에 띄게 늘었어. 기존 도구의 핵심 작업이 한 프로세스의 JavaScript 실행 경로에 몰리는 경우가 많았기 때문이야.

esbuild는 2020년에 Evan Wallace가 Go로 만든 번들러고, swc는 2019년에 Donny Wang이 Rust로 만든 변환기야. 둘 다 네이티브 코드와 병렬 처리를 적극적으로 활용해서 기존 JavaScript 기반 도구보다 수십 배 빠른 경우가 있어. 그래서 여러 프런트엔드와 서버 도구가 이들을 직접 쓰거나 비슷한 네이티브 처리 계층을 채택했어.

Go로 만든 번들러 esbuild

esbuild는 두 가지 일을 맡아:

  • 변환: TS나 JSX를 JS로 바꾸고, 새 JavaScript 문법을 지정한 대상 버전에 맞춰 내려.
  • 번들링: import 그래프를 따라가며 파일을 합치고, 쓰지 않는 코드를 덜어 내고, 결과를 압축해.

npm 패키지로 설치하고 Node에서 호출하지만 무거운 작업은 node_modules에 함께 설치된 Go 바이너리가 처리해. Node 쪽 코드는 명령을 전달하고 결과를 받는 얇은 연결 계층이야. esbuild가 빠른 이유는 JavaScript 코드를 특별히 잘 최적화해서가 아니라, Node가 네이티브 프로그램을 지휘하기 때문이야.

Rust로 만든 변환기 swc

swc는 주로 TS와 JSX를 JavaScript로 바꾸는 변환 단계에 집중해. Next.js도 TypeScript와 JavaScript 변환에 swc를 사용해 왔어. esbuild가 변환과 번들링을 함께 맡을 수 있다면, swc의 중심 역할은 빠른 구문 변환이야.

둘 가운데 하나만 골라야 하는 건 아니야. swc로 소스를 변환하고 esbuild로 번들링할 수도 있어. 중요한 건 도구 이름보다 각 단계의 소유권이야. 무엇이 문법을 바꾸고, 무엇이 모듈 그래프를 묶는지 알아야 느려졌을 때 원인을 찾을 수 있어.

속도가 중요하지 않은 경우

2026년의 핵심은 필요 없는 단계를 빠르게 만드는 대신 그 단계를 없애는 거야. 지울 수 있는 TypeScript 문법만 쓴 Node 서비스라면 현재 Node가 소스를 바로 실행할 수 있어. 이 경우 esbuild나 swc가 아무리 빨라도 건너뛴 빌드 단계에는 도움이 되지 않아.

네이티브 도구의 속도가 실제로 빛나는 곳은 따로 있어:
  • 브라우저용 프런트엔드 빌드처럼 여러 파일을 변환하고 묶어야 할 때.
  • CLI를 단일 파일로 배포해야 할 때.
  • FaaS 배포물을 줄여 콜드 스타트를 개선해야 할 때.
  • TypeScript 변환량이 큰 모노레포에서 빌드 시간이 병목일 때.
빌드 결과물이 필요 없는 백엔드 서비스라면 배포 대상이 번들을 요구할 때만 esbuild를 들여와. 그렇지 않으면 의존성을 하나 덜 두는 편이 더 단순해.

플러그인이 다시 느리게 만든다

esbuild와 swc 자체는 빨라도 플러그인이 그 장점을 깎을 수 있어. 특히 변환할 때마다 JavaScript 플러그인으로 왕복하면 Go나 Rust에서 처리하던 빠른 경로가 자꾸 끊겨. 속도를 얻으려고 esbuild를 설치한 뒤 플러그인을 열 개 얹어 예전 webpack 수준으로 돌아가는 일이 흔해. esbuild를 쓰는데도 빌드가 느리다면 플러그인별 실행 시간과 네이티브 경계를 넘는 횟수부터 확인해.

Pippa의 고백

첫 Vite 프로젝트에서는 90초 걸리던 webpack 빌드가 vite build에서 2초 만에 끝났어. 처음에는 Vite 자체가 전부 빠른 줄 알았지. 아빠가 node_modules 안에서 실제 일을 하는 바이너리를 찾아보라고 했고, 거기서 플랫폼별 esbuild 바이너리를 봤어. Vite는 작업을 조율하고 esbuild가 무거운 변환을 맡는 구조였어. 이후 플러그인 때문에 빌드가 느려지면 JavaScript 콜백이 네이티브 처리 경로를 자꾸 끊는지부터 살펴봐.

Code

esbuild와 swc를 CLI에서 직접 쓰기·bash
# esbuild를 CLI에서 바로 사용
npx esbuild src/cli.ts \
  --bundle \
  --platform=node \
  --target=node22 \
  --format=esm \
  --outdir=dist \
  --packages=external

# 결과는 dist/cli.mjs, 외부 의존성은 node_modules에 유지

# 개발 중에는 변경을 감시하며 다시 번들
npx esbuild src/index.ts --watch --bundle --outdir=dist

# swc는 번들 없이 소스만 변환
npx swc src --out-dir dist --strip-leading-paths
# src/*.ts 구조를 따라 dist/*.js 생성
빌드 스크립트에서 esbuild 호출하기·javascript
// 빌드 스크립트에서 esbuild의 Node API 사용
import { build, context } from 'esbuild';

// 한 번만 빌드
await build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  platform: 'node',
  target: 'node22',
  format: 'esm',
  outfile: 'dist/index.mjs',
  packages: 'external',
});

// 개발 중에는 변경 감시
const ctx = await context({
  entryPoints: ['src/index.ts'],
  bundle: true,
  platform: 'node',
  format: 'esm',
  outfile: 'dist/index.mjs',
});
await ctx.watch();    // 변경되면 다시 빌드, 보통 약 50ms

External links

Exercise

크기에 상관없이 TypeScript 프로젝트 하나를 골라 세 경로를 비교해 봐. (a) tsc만 실행, (b) esbuild --bundle로 번들, (c) npx tsx로 소스 직접 실행. 각 경로의 실제 소요 시간과 설치 뒤 node_modules 크기를 기록해. 그러면 프런트엔드 도구가 Go와 Rust 계층을 택한 이유와, 백엔드에도 그 계층이 필요한지를 숫자로 판단할 수 있어.
Hint
(a)는 time npx tsc -p tsconfig.json, (b)는 time npx esbuild ... --bundle, (c)는 타입 검사까지 포함하려면 time npx tsx --type-check src/index.ts로 재. 한 번의 결과로 단정하지 말고 두세 번 반복해서 초기 실행과 캐시가 데워진 실행을 나눠 기록해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.