@swc/core 월 수천만 다운로드)이 레포가 무엇을 하는 물건인가.
당신이 const x = a ?? b 같은 최신 문법이나 TypeScript 타입을 쓰면, 구형 브라우저는 그걸 이해하지 못한다. 그래서 "코드를 읽어서(parse) → 다른 모양으로 바꾸고(transform) → 다시 코드로 출력(codegen)"하는 변환기가 필요하다. 지금까지 이 일은 JavaScript로 짠 Babel·tsc·Terser가 했는데, JS로 JS를 변환하다 보니 큰 프로젝트에서 빌드가 느렸다.
swc는 이 변환기 전체를 Rust로 다시 작성해 수십 배 빠르게 만들었다. 그리고 단순한 "빠른 Babel 대체품"을 넘어, 파서·AST·변환·압축(minify)·번들러·CSS/HTML 처리·Wasm 플러그인까지 묶은 컴파일러 인프라 플랫폼으로 자랐다. Next.js가 내부적으로 swc를 쓰는 것이 대표 사례다.
swc는 컴파일러 + 라이브러리다. Rust 사용자에게는 swc_ecma_parser 같은 crate로, JS 사용자에게는 @swc/core npm 패키지로 동시에 제공된다. 핵심 철학은 "작게 쪼갠 crate를 조합"이다 — 파서만, 코드젠만, 압축기만 따로 골라 쓸 수 있다. 현재 워크스페이스에는 약 120개의 crate가 있고, 각각 파싱·AST·visitor·변환(ES2015~ES2022)·모듈·React·TypeScript·CSS·HTML·XML·플러그인 런타임을 담당한다.
1 + 2 * 3은 "더하기 노드 아래에 1과 (곱하기 노드)"로 표현된다. swc의 모든 변환은 텍스트가 아니라 이 AST를 조작하는 방식으로 이뤄진다. crate swc_ecma_ast가 JS/TS의 모든 노드 타입을 정의한다.트렌딩 이유 · Babel·esbuild 등 경쟁 대비 장점.
JS 빌드 도구 시장은 오래 Babel(변환)+Terser(압축)+tsc(타입) 조합이 표준이었다. swc가 꾸준히 주목받는 이유는, 이 셋이 모두 JS로 작성돼 대형 프로젝트에서 느리다는 고질병을 Rust 네이티브로 정면 돌파했기 때문이다. 첫째 압도적 속도, 둘째 Babel 호환 변환 + TypeScript + 압축 + 번들 + 플러그인을 한 플랫폼에, 셋째 Next.js 채택으로 인한 사실상의 표준화다.
| 비교 항목 | Babel + Terser | esbuild | swc |
|---|---|---|---|
| 구현 언어 | JavaScript | Go | Rust |
| 변환 속도 | 기준(느림) | 매우 빠름 | 매우 빠름(멀티스레드) |
| Babel 호환성 | 원조 | 제한적 | 높음(plugin-* 대응) |
| TypeScript | preset 필요 | strip만 | 네이티브 + 타입 검사 일부 |
| 압축(minify) | Terser 별도 | 내장 | 내장(swc_ecma_minifier) |
| 플러그인 | JS 플러그인 | Go 플러그인 | Wasm 플러그인(언어 무관) |
| 라이브러리화 | npm | npm/Go | crate(Rust) + npm + wasm |
| 대표 채택처 | 전 생태계 | Vite(일부) | Next.js · Deno · Parcel · Turbopack |
Babel은 강력하지만 JS 런타임 위에서 돈다. JS는 단일 스레드 기반이라 여러 파일을 진짜 병렬로 변환하기 어렵고, AST를 객체로 다루는 비용·GC 부담이 크다. 프로젝트가 수천 파일이 되면 빌드가 분 단위로 늘어진다. Terser 압축, tsc 타입 검사까지 각각 다른 도구로 AST를 매번 새로 파싱하는 중복도 컸다.
swc는 한 번 파싱한 하나의 AST를 파싱→변환→압축→코드젠 전 과정에서 재사용한다. Rust의 무비용 추상화·진짜 멀티스레딩으로 파일들을 병렬 처리하고, 문자열은 swc_atoms로 인터닝해 메모리를 아낀다. 무엇보다 모든 단계를 독립 crate로 쪼개 필요한 부품만 골라 쓰게 했다 — "거대한 모놀리식 도구"가 아니라 "컴파일러 부품 상점"인 셈이다.
코어(Rust)·바인딩(Node/Wasm)·인프라 각각.
swc의 본체는 100% Rust이며, 하나의 Cargo 워크스페이스 안에 약 120개 crate가 들어 있다. crate 이름이 곧 역할을 말해 준다 — swc_ecma_*는 JavaScript/TypeScript(ECMAScript), swc_css_*는 CSS, swc_html_*는 HTML 처리다.
| 요소 | 역할 |
|---|---|
| Rust (edition 2021, MSRV 1.73) | 전체 코어 언어. 무비용 추상화·소유권으로 빠르고 안전. |
swc_ecma_parser / _lexer | JS·TS·JSX 소스 → AST. 손으로 작성한 재귀 하강 파서. |
swc_ecma_ast | JS/TS의 모든 AST 노드 타입 정의. |
swc_ecma_visit / swc_visit | Visit(읽기)·VisitMut·Fold(변환) 방문자 트레이트 — 매크로로 자동 생성. |
swc_ecma_transforms_* | 변환 모음: base(resolver·hygiene·fixer)·compat(ES3~ES2022)·module·react·typescript·optimization·proposal. |
swc_ecma_codegen | AST → JS 텍스트 + 소스맵 생성. |
swc_ecma_minifier | Terser 호환 압축기(트리 셰이킹·이름 단축). |
swc_bundler | 모듈 그래프 분석 후 단일 번들로 묶기. |
swc_common | span·hygiene·에러 리포팅·소스맵 등 공통 인프라. |
swc_atoms (hstr) | 문자열 인터닝 — 같은 식별자를 한 번만 저장해 메모리·비교 비용 절감. |
Rust 코어를 JavaScript 세계로 노출하는 층이다. bindings/ 디렉토리에 모여 있고, 네이티브(Node)와 WebAssembly 두 길이 있다.
| 요소 | 역할 |
|---|---|
napi-rs (binding_core_node) | Node.js 네이티브 애드온 생성. @swc/core가 OS·CPU별로 빌드된 .node 바이너리를 동적 로드. |
| WebAssembly (binding_core_wasm) | crate-type=["cdylib"]로 .wasm 빌드 — 브라우저·Deno·엣지 런타임에서 실행. |
swc_cli (bindings) | swc 커맨드라인 실행 파일. |
| 전용 바인딩 | minifier·html·react-compiler·typescript(ts_fast_strip)별 Node/Wasm 바인딩 분리. |
@swc/core 외 npm | @swc/helpers(런타임 헬퍼)·@swc/types(TS 타입 정의) 등 packages/에 위치. |
| 요소 | 역할 |
|---|---|
| Wasm 플러그인 | swc_plugin_runner + wasmtime/wasmer 백엔드. 사용자가 Rust로 짠 변환을 .wasm으로 컴파일해 swc에 끼움 — 언어·버전 독립. |
swc_core | 플러그인 작성자용 통합 진입 crate. 기능별 feature flag로 필요한 모듈만 활성화. |
| 모노레포 관리 | Rust는 Cargo workspace, JS는 pnpm workspace. 둘이 한 레포에 공존. |
| 코드 생성 | tools/generate-code·proc 매크로(ast_node·string_enum)로 보일러플레이트 자동 생성. |
| 릴리스 | changesets·git-cliff(CHANGELOG)·swc-releaser. crate는 "최신 버전끼리 항상 호환" 정책. |
| 테스트 | test262(ECMAScript 공식 적합성 스위트) + 골든 픽스처 비교. |
npm install @swc/core 한 줄로 네이티브 속도를 얻게 한다.Cargo.lock으로 함께 관리하는 구조. [workspace] members = ["crates/*", ...]로 선언한다. swc처럼 부품이 많은 프로젝트가 버전·의존성을 일관되게 유지하는 표준 방법이다.소스 → 파서 → AST → visitor/fold 변환 → 코드젠, 그리고 바인딩.
swc는 전형적인 3단계 컴파일러 파이프라인이다. ① 소스 텍스트를 토큰으로 쪼개고(lexer) 트리로 조립한다(parser → AST). ② AST를 여러 변환 패스(pass)에 차례로 통과시킨다. ③ 최종 AST를 다시 코드 텍스트로 출력한다(codegen). JS 사용자가 @swc/core를 호출하면 napi/wasm 바인딩을 거쳐 이 Rust 파이프라인이 돈다.
모든 변환은 AST를 재귀적으로 순회(traverse)하며 노드를 건드리는 방식으로 동작한다. swc는 이를 세 종류의 트레이트로 제공한다. 직접 트리를 재귀하는 코드를 짤 필요 없이, "내가 관심 있는 노드 타입의 메서드만 오버라이드"하면 나머지 순회는 자동으로 처리된다(swc_ecma_visit의 매크로가 생성).
| 트레이트 | 성격 | 쓰임 |
|---|---|---|
Visit | 읽기 전용(불변 참조) | 린트·분석·통계 — AST를 바꾸지 않고 정보만 수집. |
VisitMut | 제자리 수정(&mut) | 대부분의 현대 변환. 노드를 그 자리에서 고침. |
Fold | 값 소유·교체(소비 후 반환) | 노드를 통째로 다른 노드로 바꿀 때. node.fold_with(&mut pass). |
// 모든 식별자 `foo`를 `bar`로 바꾸는 변환 패스
use swc_ecma_ast::Ident;
use swc_ecma_visit::{VisitMut, VisitMutWith};
struct RenameFoo;
impl VisitMut for RenameFoo {
// Ident 노드를 만날 때만 개입 — 나머지 순회는 자동
fn visit_mut_ident(&mut self, n: &mut Ident) {
n.visit_mut_children_with(self); // 자식 먼저 순회
if &*n.sym == "foo" {
n.sym = "bar".into();
}
}
}
핵심은 visit_mut_ident만 정의했다는 점이다. 함수·블록·표현식 등 다른 모든 노드의 순회는 매크로가 생성한 기본 구현이 알아서 한다. 이것이 swc가 수백 개 변환을 짧은 코드로 유지하는 비결이다.
swc_ecma_transforms_base에는 다른 모든 변환이 의존하는 세 가지 핵심 패스가 있다. 이름이 같은 변수라도 스코프가 다르면 다른 변수로 구분해야 하는 위생(hygiene) 문제를 푸는 장치다.
| 패스 | 하는 일 |
|---|---|
resolver | 모든 식별자에 hygiene 마크(번호)를 붙인다. let a(바깥)과 let a(블록 안)을 a#0·a#1로 구분. |
hygiene | 마크가 다른 동명 식별자를 실제 다른 이름으로 바꾼다. a#1 → a1. |
fixer | 변환 후 깨진 AST를 보정. 연산자 우선순위에 맞춰 괄호를 자동 삽입 — (1+2)*3. |
큰 회사에 "김민수"가 여러 명 있다고 하자. 부서를 옮기며 코드를 합치다 보면 누가 누군지 헷갈린다. resolver는 모든 김민수에게 사원번호(#0, #1)를 붙여 일단 구분해 두는 단계다. 그 뒤 변환들이 마음껏 코드를 섞은 다음, hygiene이 마지막에 "김민수1, 김민수2"처럼 진짜 다른 이름으로 개명해 최종 코드에서 충돌이 없게 만든다. swc 변환이 안전하게 합성되는 토대다.
| 패턴 | swc에서의 구현 |
|---|---|
| 잘게 쪼갠 crate | 파서·AST·visit·변환·codegen·minify를 각각 독립 crate로. 필요한 것만 의존. |
| 방문자 패턴 + 코드 생성 | Visit/VisitMut/Fold를 매크로로 자동 생성 → 변환은 관심 노드만 작성. |
| 단일 AST 재사용 | 한 번 파싱한 AST를 변환·압축·codegen이 공유 (중복 파싱 제거). |
| 문자열 인터닝 | swc_atoms로 식별자를 한 번만 저장, 비교는 포인터로. |
| 플러그인 = Wasm | 변환을 .wasm으로 격리 — 언어·버전 독립, 샌드박스 안전. |
| 이중 바인딩 | 같은 코어를 napi(네이티브)와 wasm(이식성) 두 경로로 노출. |
| proc 매크로 활용 | ast_node·string_enum으로 노드·열거형 보일러플레이트 제거. |
Cargo 워크스페이스(crates/) + JS 바인딩(bindings/·packages/).
① ARCHITECTURE.md로 전체 그림 → ② swc_ecma_ast에서 AST 노드가 어떻게 생겼나 → ③ swc_ecma_visit/src/lib.rs로 Visit/Fold 트레이트의 형태 → ④ swc_ecma_transforms_base의 resolver·hygiene·fixer로 기반 패스 → ⑤ bindings/binding_core_node/src/lib.rs로 Rust 코어가 어떻게 Node로 노출되는지. 코어의 통찰은 거의 이 다섯 곳에 있다.
대규모 컴파일러를 Rust로 설계하는 법에서 무엇을 배우나.
swc는 "텍스트 → AST → 변환 → 텍스트"라는 컴파일러의 정석 흐름을 가장 깔끔하게 보여 주는 실전 코드다. 각 단계가 독립 crate라 경계가 명확하다. swc_ecma_parser로 입력을, swc_ecma_codegen으로 출력을 다루고, 그 사이를 변환 패스가 채운다. 컴파일러 강의의 추상 개념을 실제로 수백만 명이 쓰는 코드로 확인할 수 있다.
play.swc.rs 플레이그라운드에서 같은 TS 코드를 target만 바꿔(es5 vs es2022) 출력 차이를 비교하고, "어느 변환 패스가 그 차이를 만들었을지" 추론해 보라.AST 같은 트리를 다룰 때 매번 재귀 코드를 짜면 실수가 잦다. swc의 Visit/VisitMut/Fold는 "순회는 프레임워크가, 개입은 내가"를 구현한 모범 사례다. 읽기/제자리 수정/소유 교체의 세 변형이 각각 언제 적합한지를 코드로 익히면, 다른 언어의 AST·DOM·설정 트리 작업에도 그대로 옮길 수 있다.
swc_ecma_visit를 의존성으로 추가해, "코드 안의 console.log 호출 개수를 세는" Visit 구현체를 30줄로 작성해 보라(읽기 전용이므로 Visit 사용).120개 crate를 하나의 워크스페이스로 묶고, [workspace.dependencies]에서 버전을 중앙 관리하는 방식은 대형 Rust 프로젝트의 표준 청사진이다. 더해 같은 레포에서 pnpm 워크스페이스(JS)와 Cargo 워크스페이스(Rust)가 공존하는 폴리글랏 모노레포 구성도 배울 점이다.
[workspace.dependencies]로 끌어올려 보라.swc의 가장 실용적인 교훈은 "빠른 코어는 Rust로, 사용성은 바인딩으로"다. 같은 코어 로직을 napi-rs로는 Node 네이티브 애드온, wasm으로는 브라우저/엣지용으로 동시에 내보낸다. 성능이 중요한 부분만 Rust로 짜고 나머지 생태계는 그대로 쓰는 이 전략은 esbuild·Biome·Rolldown 등 차세대 도구의 공통 패턴이다.
require해 호출하는 최소 애드온을 만들어 보라. 그다음 wasm-pack으로 같은 함수를 브라우저용 wasm으로도 빌드."쓰는 것"과 "빌드/기여하는 것"의 요구사항이 다르다.
| 항목 | 요구사항 |
|---|---|
| JS 사용(소비) | Node v10+ (개발은 v20+). npm install @swc/core — 미리 빌드된 네이티브 바이너리 자동 설치. |
| Rust 사용(crate) | Rust 툴체인. MSRV 1.73. cargo add swc_ecma_parser 등으로 부품만 선택. |
| 코어 빌드/기여 | Rust(rust-toolchain 고정 버전) + pnpm 10 + Node 20+. cargo build + pnpm build. |
| 플랫폼(네이티브) | napi 타깃 다수: macOS(x64·arm64)·Windows(x64·arm64·x86)·Linux(gnu·musl, arm 포함) 등. |
| Wasm 런타임 | 브라우저·Deno·Cloudflare Workers 등 wasm 지원 환경에서 바이너리 없이 실행. |
| 플러그인 빌드 | 플러그인은 wasm32-wasip1 타깃으로 컴파일. swc_core 버전을 호스트와 맞춰야 함. |
| 라이선스 | Apache-2.0 — 상용 포함 자유롭게 사용 가능. |
swc_core ABI로 빌드돼야 동작한다. 버전이 어긋나면 플러그인이 로드되지 않으므로, Next.js 등에서 플러그인을 쓸 때는 swc_core 버전 매칭이 가장 흔한 트러블 지점이다(공식 "selecting-swc-core" 문서 참고).난이도별 5단계 — 플레이그라운드부터 Wasm 플러그인까지.
play.swc.rs에서 TypeScript + JSX 코드를 붙여 넣고, target·jsx·minify 옵션을 바꿔 출력이 어떻게 달라지는지 본다. "타입은 어디서 사라지고", "JSX는 무엇으로 바뀌는지"를 눈으로 확인하는 것이 목표.
Node 프로젝트에서 @swc/core를 설치하고 transformSync(code, options)로 TS 문자열을 JS로 변환하는 20줄 스크립트를 작성한다. .code와 .map(소스맵)을 모두 출력해 본다.
Rust 프로젝트에 swc_ecma_parser·swc_common을 추가하고, JS 문자열을 파싱해 얻은 Module AST를 {:#?}로 디버그 출력한다. 간단한 표현식(a + b * c)이 어떤 트리로 표현되는지 관찰.
swc_ecma_visit로 "모든 console.log를 제거하는" VisitMut 패스를 작성한다. 파서로 AST를 만들고 → 패스를 적용하고 → swc_ecma_codegen으로 다시 코드를 출력하는 전체 파이프라인을 직접 연결해 본다.
공식 플러그인 템플릿(swc_core 기반)으로 간단한 변환(예: 특정 함수명 치환)을 작성해 wasm32-wasip1로 빌드하고, .swcrc의 jsc.experimental.plugins에 등록해 @swc/core에서 실제로 동작시킨다. swc_core 버전 매칭에 주의.
6주 코스 — Rust 기초부터 컴파일러 플러그인까지.
| 주차 | 주제 | 실습 · 참고 |
|---|---|---|
| 1주차 | Rust 기초 — 소유권·트레이트·enum·매크로 개념 | The Rust Book · 미니 CLI 작성 |
| 2주차 | 컴파일러 기초 — 렉서·파서·AST·재귀 하강 | swc_ecma_ast 구조 읽기 · 과제 3 |
| 3주차 | 방문자 패턴 — Visit/VisitMut/Fold | swc_ecma_visit 정독 · 과제 4 |
| 4주차 | 변환·스코프·hygiene — resolver/hygiene/fixer | ARCHITECTURE.md · transforms_base 읽기 |
| 5주차 | Rust↔JS 바인딩 — napi-rs·wasm-pack | napi-rs 튜토리얼 · binding_core_node 분석 |
| 6주차 | Wasm 플러그인 + 모노레포 운영 | 플러그인 템플릿 · Cargo/pnpm 워크스페이스 구성 |
본문·소스에 나온 용어 빠른 참조.
| 용어 | 의미 |
|---|---|
| swc | Speedy Web Compiler. Rust로 작성한 JS/TS 컴파일러·번들러 플랫폼. |
| 트랜스파일 | 한 언어를 같은/유사 언어로 변환(최신 JS/TS → 구형 JS). |
| AST | 코드를 트리로 표현한 추상 구문 트리. swc 변환의 작업 대상. |
| lexer / parser | 텍스트→토큰 / 토큰→AST. swc_ecma_lexer·swc_ecma_parser. |
| codegen | AST→코드 텍스트 + 소스맵 생성. swc_ecma_codegen. |
| Visit / VisitMut / Fold | AST 순회 트레이트: 읽기 / 제자리 수정 / 소유 교체. |
| transform pass | AST를 한 번 순회하며 바꾸는 변환 단위. 여러 개를 체인으로 연결. |
| resolver | 식별자에 hygiene 마크를 붙여 스코프를 구분하는 기반 패스. |
| hygiene | 동명이지만 다른 변수를 실제 다른 이름으로 만드는 위생 처리. |
| fixer | 변환 후 깨진 AST(괄호·우선순위 등)를 보정하는 패스. |
| minify | 코드 압축(공백·이름 단축·죽은 코드 제거). swc_ecma_minifier. |
| bundler | 여러 모듈을 의존성 그래프로 묶어 한 파일로. swc_bundler. |
| napi-rs | Rust 코드를 Node 네이티브 애드온으로 빌드하는 도구. |
| WebAssembly (wasm) | 브라우저·엣지에서 도는 이식성 바이너리 포맷. |
| Cargo workspace | 여러 Rust crate를 한 레포·한 lock으로 묶는 구조. |
| swc_core | 플러그인 작성자용 통합 진입 crate. ABI 버전 매칭 중요. |
| swc_atoms | 문자열 인터닝 — 식별자 메모리·비교 비용 절감. |
| test262 | ECMAScript 공식 적합성 테스트 스위트. |