SWC 컴파일러 심화 가이드 — 고급 설정·플러그인·Next.js·Vite·모노레포
이 글의 핵심
SWC는 Rust로 작성된 초고속 JavaScript·TypeScript 컴파일러입니다. 이 글에서는 렉서·파서·AST·변환 파이프라인을 이해한 뒤, .swcrc 고급 설정, WASM 플러그인과 프로그래매틱 API, Next.js·Vite 통합, 캐시·타깃·병렬화 기반 성능 최적화, Babel과의 선택 기준, 모노레포에서의 설정 계층을 다룹니다.
이 글의 핵심
SWC(Speedy Web Compiler)는 Rust로 구현된 JavaScript·TypeScript 컴파일러입니다. 단순히 “Babel보다 빠르다”는 인상을 넘어, 파이프라인 구조·설정 계층·도구별 통합 방식을 이해해야 실무에서 오판 없이 튜닝할 수 있습니다. 이 글은 고급 사용자를 위해 아키텍처 개요, .swcrc 심화 옵션, 플러그인 확장, Next.js·Vite 연동, 성능·모노레포 패턴을 한 흐름으로 정리합니다.
1. SWC의 핵심 아키텍처
1.1 처리 파이프라인 개요
SWC는 소스 문자열을 토큰(lexer)으로 쪼개고, 파서(parser)가 구문을 읽어 AST(abstract syntax tree)를 만든 뒤, 변환(transform) 단계에서 ECMAScript 하위 버전·JSX·TypeScript 등 목표에 맞게 트리를 수정하고, 마지막으로 코드 생성(codegen)으로 다시 문자열을 출력합니다. 이 구조는 Babel과 유사하지만, 핵심 루프가 Rust로 구현되어 단일 프로세스 내에서도 높은 처리량을 내는 데 유리합니다.
1.2 Lexer·Parser·AST
Lexer는 식별자, 리터럴, 연산자 등 최소 단위 토큰 스트림을 생성합니다. Parser는 문법 규칙에 따라 표현식·문장·모듈 구조를 복원하고, 내부적으로 SWC는 자체 AST 표현을 사용합니다(도구 체인에서 말하는 “SWC AST”). Transform 레이어는 이 AST에 패치를 가합니다. 예를 들어 TypeScript의 타입 어노테이션 제거, 최신 문법을 구형 런타임용으로 내리는 폴리필성 변환, JSX를 React.createElement 호출로 바꾸는 과정이 여기에 해당합니다.
1.3 EcmaScript 버전과 타깃
SWC는 jsc.target 등으로 출력 ECMAScript 버전을 지정합니다. 타깃이 낮을수록 더 많은 변환이 들어가고, 반대로 최신 엔진만 지원하면 변환량이 줄어 빌드가 가벼워집니다. 따라서 “프로젝트의 최소 런타임”과 “번들러가 추가로 할 일”을 함께 설계하는 것이 중요합니다.
1.4 모듈 시스템과 출력 형식
입력은 ESM·CommonJS 등이 될 수 있고, 출력은 module.type으로 es6/commonjs/umd 등을 선택합니다. 라이브러리 패키지를 배포할 때는 소비자가 Node와 브라우저 중 어디에서 읽는지에 따라 이 옵션이 곧 공개 API와 호환성을 결정합니다.
2. .swcrc 고급 설정
2.1 최소 구조와 확장
.swcrc는 JSON(또는 JSONC를 지원하는 도구에서는 주석 허용)으로, 보통 $schema로 자동 완성을 켜 두면 실수가 줄어듭니다. 기본적으로는 jsc(컴파일러 본체), module(출력 모듈 형식), 필요 시 minify, sourceMaps가 중심 축입니다.
{
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": true
},
"transform": {
"react": {
"runtime": "automatic",
"development": false,
"refresh": false
}
},
"target": "es2022",
"loose": false,
"externalHelpers": false,
"keepClassNames": true
},
"module": {
"type": "es6",
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
},
"minify": false,
"sourceMaps": true,
"inlineSourcesContent": true
}
위 예시에서 parser.syntax를 typescript로 두고 tsx를 켜면 TS+JSX를 한 번에 처리할 수 있습니다. jsc.target은 출력 언어 수준을 고정하고, transform.react.runtime: "automatic"은 React 17+ JSX 자동 런타임과 맞춥니다. keepClassNames는 프로덕션 압축 시에도 클래스 이름 유지가 필요한 디버깅·로깅 시나리오에서 유용합니다.
2.2 loose 모드의 의미와 트레이드오프
jsc.loose: true는 변환 결과를 더 단순한 형태로 만들어 출력 코드 크기와 빌드 비용을 줄이는 방향으로 동작하는 경우가 많습니다. 대신 스펙과 미묘하게 다른 엣지 케이스가 생길 수 있어, 라이브러리 코드나 엄격한 호환성이 필요한 패키지에서는 기본값인 false를 유지하는 편이 안전합니다.
2.3 externalHelpers와 런타임 패키지
externalHelpers: true로 두면 공통 헬퍼를 번들에 인라인하지 않고 @swc/helpers 같은 외부 패키지에 위임합니다. 여러 청크·패키지가 동일 헬퍼를 공유하면 중복 제거에 도움이 되지만, 패키지 의존성과 버전 정합성을 관리해야 합니다.
2.4 소스맵과 인라인 소스
sourceMaps: true는 디버깅에 필수에 가깝습니다. inlineSourcesContent를 켜면 원본 소스가 맵에 포함되어 원격 디버깅이 쉬워지지만 아티팩트 크기가 커집니다. CI 아티팩트에 소스를 넣지 않을 거라면, 배포 단계에서 소스맵을 별도 업로드(Sentry 등)하는 정책과 맞춰 조정합니다.
2.5 압축(minify) 옵션 심화
SWC는 구문 수준 압축(mangle 등)을 지원합니다. minify: true만 켜기보다, jsc.minify.compress / mangle 등 세부 옵션으로 디버그 심볼 유지 여부, 객체 프로퍼티 맹글 허용 범위를 제어할 수 있습니다. 라이브러리 배포 시 공개 프로퍼티 이름이 깨지면 외부 사용자가 깨지므로, export 경계를 기준으로 맹글 정책을 보수적으로 잡는 것이 좋습니다.
3. 커스텀 플러그인과 프로그래매틱 API
3.1 플러그인 모델의 현실
SWC는 실험적 WASM 플러그인 경로와, Node에서 @swc/core의 transform / transformSync로 파이프라인을 직접 호출하는 방식이 공존합니다. 팀 내부 규칙(예: 특정 API 사용 금지, 로깅 삽입)을 AST 변환으로 강제하려면, 플러그인 또는 변환 파이프를 설계해야 합니다.
3.2 @swc/core로 동기 변환 호출
아래는 Node 스크립트에서 SWC를 직접 호출하는 최소 예시입니다. 실제로는 설정 객체를 파일에서 읽거나, 모노레포 루트 설정을 병합해 주입합니다.
// scripts/swc-run.mjs — 예시: 소규모 코덱스 변환 파이프
import fs from 'node:fs';
import { transformSync } from '@swc/core';
const src = fs.readFileSync('input.tsx', 'utf8');
const out = transformSync(src, {
filename: 'input.tsx',
jsc: {
parser: { syntax: 'typescript', tsx: true },
target: 'es2022',
transform: { react: { runtime: 'automatic' } },
},
module: { type: 'es6' },
sourceMaps: true,
});
fs.writeFileSync('output.js', out.code);
if (out.map) fs.writeFileSync('output.js.map', out.map);
transformSync는 빌드 스크립트·코드젠·테스트 픽스처 생성에 적합합니다. 대규모 리포지토리에서는 비동기 transform과 작업 큐를 함께 쓰면 I/O와 CPU를 분리하기 쉽습니다.
3.3 플러그인을 설계할 때의 원칙
첫째, 변환은 가능한 한 국소적(local)으로 유지하고, 전역 상태에 의존하지 않습니다. 둘째, 구문 버전·JSX·TS 여부에 따라 파서 옵션이 달라지므로, 플러그인 입장에서는 입력이 어떤 프로파일로 파싱됐는지를 문서화합니다. 셋째, Next.js·Vite는 자체 레이어에서 추가 변환을 하므로, 동일 파일이 이중 변환될 때 충돌이 없는지 확인합니다.
3.4 Rust 네이티브 플러그인에 대해
조직 내에서 Rust 크레이트로 직접 플러그인을 붙이는 것은 강력하지만, 빌드·배포·바이너리 호환 비용이 큽니다. 대부분의 팀은 WASM 플러그인 또는 @swc/core 레벨의 전처리·후처리로 요구사항을 충족합니다.
4. Next.js와의 통합
4.1 Next.js 컴파일러와의 관계
Next.js는 내부적으로 SWC를 사용해 컴파일·번들 전처리를 수행합니다. 사용자는 next.config.js의 compiler 옵션으로 감정 표현식 제거, import 정리, styled-components 등 일부 동작을 제어합니다. 여기서 중요한 점은 “순수 SWC 문서의 옵션”과 “Next가 노출하는 옵션 집합”이 1:1로 대응하지 않는다는 것입니다. 따라서 Next 전용 프로젝트에서는 Next 문서의 compiler 섹션을 기준으로 삼는 것이 안전합니다.
4.2 Babel 설정이 있을 때
프로젝트 루트에 babel.config.js 등이 존재하면 Next.js가 Babel 경로를 택하는 경우가 있어 SWC 최적화 이점이 줄어들 수 있습니다. 마이그레이션 시에는 Babel 의존성을 제거하거나 최소화하고, 동일 동작을 SWC·번들러 레이어에서 재구성하는 전략이 필요합니다.
4.3 설정 예시(개념)
// next.config.mjs — 개념 예시 (프로젝트 버전에 맞게 조정)
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
experimental: {
// 버전별 실험 플래그는 Next 릴리스 노트를 반드시 확인
},
};
export default nextConfig;
운영 환경에서는 removeConsole 같은 옵션이 로그 전략과 충돌하지 않는지, 스테이징에서 먼저 검증해야 합니다.
5. Vite와의 통합
5.1 @vitejs/plugin-react-swc
Vite 환경에서는 @vitejs/plugin-react-swc가 일반적입니다. 이 플러그인은 Fast Refresh와 SWC 기반 변환을 묶어 주며, 대규모 프로젝트에서 개발 서버 재컴파일 시간을 줄이는 데 효과적입니다.
// vite.config.ts — 예시
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
export default defineConfig({
plugins: [react()],
});
5.2 ESLint·타입체크와의 역할 분담
SWC는 타입 검사를 대체하지 않습니다. Vite+SWC 프로젝트에서는 tsc --noEmit 또는 IDE의 TypeScript 서버로 타입을 검증하고, SWC는 트랜스파일에 집중시키는 구성이 일반적입니다.
6. 성능 최적화
6.1 타깃을 현실적으로 올리기
불필요하게 낮은 target은 변환량을 늘립니다. 지원 브라우저·Node LTS 범위를 데이터(Analytics, browserslist)로 확정하고, 그 이상의 ES 버전을 목표로 잡으면 SWC와 하위 도구 모두 이득을 봅니다.
6.2 캐시 전략
CLI·빌드 도구는 종종 디스크 캐시를 제공합니다. SWC를 직접 호출하는 커스텀 파이프라인이라면 입력 해시 기반 캐시(파일 내용·설정 문자열·도구 버전을 키에 포함)를 도입하는 것이 좋습니다. 캐시 키에서 @swc/core 버전을 빼면 재현성 문제가 생깁니다.
6.3 병렬화
모노레포에서 패키지 단위로 transform을 워커 풀에 분배하면 CPU 코어를 활용할 수 있습니다. 다만 동시 파일 수가 과도하면 I/O 병목이 되므로, 배치 크기를 점진적으로 튜닝합니다.
6.4 개발 vs 프로덕션
개발 모드에서는 소스맵·주석 유지·React Fast Refresh가 우선이고, 프로덕션에서는 압축·dead code elimination이 우선입니다. 동일 .swcrc를 양쪽에 쓰기보다 환경별 설정 분리를 권장합니다.
7. Babel vs SWC
| 관점 | Babel | SWC |
|---|---|---|
| 구현 | JavaScript 생태계 중심 | Rust 코어 |
| 플러그인 풍부함 | 매우 큼 | 성장 중, 도구별 지원 상이 |
| 빌드 속도 | 상대적으로 느릴 수 있음 | 대체로 빠름 |
| 타입스크립트 | 일반적으로 별도 도구와 병행 | 단일 파이프라인에 넣기 쉬움 |
선택 기준은 단순히 속도만이 아닙니다. 기존 Babel 매크로·맞춤 플러그인이 많다면 이주 비용이 큽니다. 반면 Greenfield이거나 Next·Vite가 SWC 경로를 잘 지원한다면 SWC 우선이 합리적인 경우가 많습니다.
8. 실전 모노레포 설정
8.1 단일 루트 .swcrc와 한계
모노레포 루트에 공통 .swcrc를 두고 앱·패키지가 공유하는 방식은 운영이 쉽습니다. 그러나 앱은 브라우저 타깃, 서버 패키지는 Node 18처럼 요구가 갈라지면 하나의 설정으로는 비효율 또는 호환성 문제가 생깁니다.
8.2 패키지별 설정 파일
각 패키지 디렉터리에 .swcrc를 두거나, 빌드 스크립트에서 --config-file로 경로를 명시합니다. 이때 루트 설정을 복제하지 않고 공통 조각을 npm 패키지로 빼두고 extends 패턴(도구가 지원하는 경우)이나 스크립트에서 객체 병합으로 중복을 줄입니다.
8.3 워크스페이스 스크립트 예시(개념)
{
"name": "root",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build:libs": "swc packages/core/src -d packages/core/dist --config-file packages/core/.swcrc",
"build:app": "pnpm --filter web build"
}
}
핵심은 “누가 어떤 설정으로 SWC를 호출하는가”를 빌드 그래프에 명시하는 것입니다. 암묵적 상속은 디버깅을 어렵게 만듭니다.
8.4 TypeScript project references와 정렬
모노레포에서 TS 프로젝트 참조를 쓰는 경우, SWC의 출력 디렉터리와 tsconfig의 outDir, 번들러의 엔트리가 서로 어긋나면 이중 소스·잘못된 해석이 발생합니다. 단일 소스 디렉터리와 명확한 산출물 경로를 팀 규칙으로 고정하세요.
9. 문제 해결 체크리스트
- 변환이 기대와 다름:
jsc.parser옵션(JSX·decorators·import assertions 등)과 파일 확장자가 일치하는지 확인합니다. - React Fast Refresh가 깨짐: Vite·Next 각각의 권장 플러그인/설정을 사용하고, HMR 경계에 익명 default export만 있는 파일이 과도하지 않은지 봅니다.
- 프로덕션만 오류:
NODE_ENV에 따라development플래그가 달라지는react변환,removeConsole등 환경 분기 설정을 점검합니다.
10. 맺음말
SWC는 속도뿐 아니라 파이프라인 단순화 측면에서도 매력적인 컴파일러입니다. 다만 도구마다 노출하는 옵션이 다르므로, “SWC 공식 문서 한 페이지”보다 “내가 쓰는 러너(Next, Vite, 커스텀 CLI)의 계약”을 기준으로 설정을 검증하는 태도가 필요합니다. 아키텍처·.swcrc·플러그인·통합·성능·모노레포를 위 흐름으로 정리해 두면, 팀 내 기술 공유와 온보딩 비용도 함께 줄일 수 있습니다.