tsup에서 tsdown으로: Rust 기반 Rolldown과 Oxc가 TypeScript 라이브러리 빌드를 수십 배 단축하는 원리
라이브러리 빌드가 느리다는 걸 체감하게 되는 순간이 있습니다. CI에서 패키지를 하나 고쳤는데 빌드 파이프라인이 2분 넘게 돌아가거나, 로컬에서 pnpm build 치고 커피 한 잔 타러 갔다 와야 할 때죠. 저도 모노레포에서 공유 패키지를 여럿 관리하다 보니 그 답답함을 꽤 오래 느꼈습니다.
이 글은 tsup으로 TypeScript 라이브러리를 빌드하고 있는 분들을 위한 글입니다. tsup을 처음 접하는 분이라면 먼저 tsup 기본 사용법을 보고 오시면 맥락이 더 잘 잡힙니다.
그러다 tsdown으로 옮기면서 상황이 완전히 달라졌습니다. 제 모노레포의 공유 패키지 빌드가 2분 30초에서 18초로 줄었고, .d.ts(TypeScript 타입 선언 파일) 생성만 따로 보면 최대 8배까지 빨라졌습니다. 핵심은 이것입니다: Rust 기반 번들 엔진 Rolldown과 Oxc 위에 올라간 tsdown은 특히 멀티 엔트리 라이브러리나 모노레포 환경에서 빌드 시간을 수십 배 단축시킬 수 있으며, 옮기는 데 생각보다 시간이 안 걸립니다.
어떤 경우에 효과가 크고 어떤 경우에는 기대에 못 미칠 수 있는지도 솔직하게 이야기하겠습니다.
핵심 개념
tsdown은 무엇이고, 왜 지금 주목받는가
tsdown은 한마디로 "tsup의 정신적 후계자"입니다. tsup이 esbuild를 번들 엔진으로 삼은 것처럼, tsdown은 Rolldown을 엔진으로 사용합니다.
Rolldown: Vue.js 창시자 Evan You가 설립한 VoidZero에서 개발하는 Rust 기반 JavaScript 번들러. 2026년 1월 1.0 RC가 릴리즈되었고, Vite 8 Beta에서 기존 esbuild(개발 서버)와 Rollup(프로덕션 빌드)을 단일 엔진으로 대체하는 역할을 맡았습니다.
Rolldown은 Rust로 작성되어 Rollup 대비 10~30배 빠르고, esbuild에 필적하는 속도를 냅니다(tsdown 공식 벤치마크 기준). 그리고 Rollup 호환 API를 유지하기 때문에 기존 Rollup 플러그인 생태계를 그대로 활용할 수 있다는 것도 큰 장점입니다.
tsdown의 전체 스택을 정리하면 이렇습니다:
| 구성 요소 | 역할 |
|---|---|
| Rolldown | Rust 기반 코어 번들 엔진 (esbuild 수준 속도 + Rollup 호환 API) |
| Oxc | Rust 기반 TypeScript 파서 / 트랜스파일러 / .d.ts 생성기 |
| tsdown | Rolldown + Oxc 위에 라이브러리 번들링 워크플로를 추상화한 도구 |
이 조합이 강력한 이유는 .d.ts 생성 병목이 거의 사라지기 때문입니다. .d.ts는 라이브러리를 배포할 때 함께 제공하는 TypeScript 타입 선언 파일로, 라이브러리를 사용하는 쪽에서 타입 정보를 얻기 위해 반드시 필요합니다. 문제는 이 파일을 만드는 게 생각보다 오래 걸린다는 점인데, 기존 tsup은 tsc(TypeScript 컴파일러)를 거치거나 rollup-plugin-dts를 사용했고, 이게 전체 빌드 시간의 절반 이상을 차지하는 경우도 많았습니다. tsdown은 Oxc가 이 작업을 훨씬 빠르게 처리합니다.
isolatedDeclarations + Oxc 조합이 가져오는 변화
솔직히 처음엔 isolatedDeclarations라는 TypeScript 옵션이 이렇게 큰 영향을 미칠 줄 몰랐습니다. TypeScript 5.5에서 추가된 이 옵션을 활성화하면, 각 파일의 타입 선언을 독립적으로(파일 간 의존성 없이) 생성할 수 있게 됩니다.
isolatedDeclarations: TypeScript 5.5+에서 추가된 컴파일러 옵션. 각 파일이 다른 파일의 타입 정보 없이도 독립적으로
.d.ts를 생성할 수 있도록 강제합니다. 코드 작성에 약간의 제약이 생기지만, 그 대가로 타입 선언 생성 속도가 극적으로 향상됩니다.
// tsconfig.json
{
"compilerOptions": {
"isolatedDeclarations": true
}
}이 덕분에 tsdown이 Oxc를 활용해서 .d.ts 파일을 병렬로, 극도로 빠르게 만들어낼 수 있습니다. 단, 이 옵션을 켜면 처음에 타입 에러가 몇 개 나올 수 있습니다. 가장 흔한 패턴은 내보내는 함수에 반환 타입이 없는 경우입니다:
// isolatedDeclarations 위반 예시 — 반환 타입 추론 불가
export function createClient() { // ❌ 반환 타입 명시 필요
return new HttpClient()
}
// 수정 후
export function createClient(): HttpClient { // ✅
return new HttpClient()
}이런 패턴이 몇 곳 정도 나오는데, 수정하고 나면 .d.ts 생성 속도에서 확연한 차이를 느낄 수 있습니다. 처음엔 귀찮아 보이지만, 오히려 공개 API의 타입을 명시적으로 선언하게 되어 라이브러리 유지보수 측면에서도 좋은 습관이 됩니다. Oxc가 아직 모든 복잡한 타입 패턴을 완벽히 지원하진 않아서, 드물게 unsupported syntax 경고가 뜰 수 있습니다. 이런 경우에는 에러 메시지가 꽤 명확하게 어떤 표현이 문제인지 알려줍니다.
실전 적용
자동 마이그레이션 CLI 30초 체험
가장 간단한 시작점은 공식 마이그레이션 CLI입니다. --dry-run 플래그로 먼저 어떤 변경이 일어나는지 확인해볼 수 있어서, 부담 없이 시도해보시기 좋습니다.
# 변경사항 미리 확인 (실제 파일 변경 없음)
npx tsdown migrate --dry-run
# 실제 마이그레이션 실행
npx tsdown migrateCLI가 자동으로 처리해주는 것들은 다음과 같습니다:
| 자동 처리 항목 | 내용 |
|---|---|
| 파일 리네임 | tsup.config.ts → tsdown.config.ts |
| import 교체 | from 'tsup' → from 'tsdown' |
| 의존성 업데이트 | package.json의 tsup 제거, tsdown 추가 |
| 스크립트 업데이트 | package.json의 tsup 명령어 → tsdown |
마이그레이션 후 수동으로 확인해야 하는 것은 bundle: false → unbundle: true 변환 정도입니다. 설정을 수동으로 옮겼다면 반드시 체크해보시는 것을 권장합니다.
설정 파일 자체는 거의 그대로입니다. 눈으로 보면 import 하나 바뀐 수준이죠:
// tsup.config.ts (기존)
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})// tsdown.config.ts (이후)
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true, // Oxc가 처리 — 훨씬 빠릅니다
})멀티 엔트리 환경에서 체감하는 진짜 차이
단일 엔트리의 작은 라이브러리라면 솔직히 체감 효과가 크지 않을 수 있습니다. Node.js 프로세스 시작 비용이 있어서 1~2초짜리 빌드가 0.5초로 줄어도 "와!"라는 감탄이 나오기 어렵죠. 하지만 엔트리가 여럿이거나 의존성 그래프가 복잡한 경우엔 이야기가 달라집니다.
멀티 엔트리 설정을 처음 만들었을 때 실수한 부분이 있는데, entry에 나열한 경로가 하나라도 잘못되면 조용히 해당 엔트리가 스킵되는 경우가 있었습니다. 경로를 한 번 확인해보시면 좋습니다.
// tsdown.config.ts — 멀티 엔트리 설정 예시
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: [
'src/index.ts',
'src/utils/index.ts',
'src/components/index.ts',
'src/hooks/index.ts',
],
format: ['esm', 'cjs'],
dts: true,
sourcemap: true,
clean: true,
})실제로 제가 4개 엔트리를 가진 UI 컴포넌트 패키지에서 time pnpm build로 측정했을 때 결과가 이렇게 나왔습니다:
# tsup (이전)
$ time pnpm build
✓ Building entry: src/index.ts, src/utils/index.ts, src/components/index.ts, src/hooks/index.ts
✓ Build success
pnpm build 43.21s user 4.87s system 98% cpu 48.612 total
# tsdown (이후)
$ time pnpm build
✓ Build success
pnpm build 4.89s user 0.61s system 97% cpu 5.634 total약 8.6배 차이가 났는데, 이 패키지는 .d.ts 생성 비중이 높았기 때문에 Oxc 처리 이점을 풀로 받은 케이스입니다.
TresJS(Three.js용 Vue 렌더러)도 tsdown으로 전환하면서 번들 크기가 50% 줄었는데, 이 경우에는 tsdown 자체의 성능보다 CJS/UMD 포맷을 제거하고 ESM만 남긴 결정이 주요 원인이었습니다. tsdown의 ESM 우선 설계 철학이 불필요한 포맷을 정리하는 계기가 된 것이죠. 빌드 속도 개선과는 별개로 봐야 합니다.
모노레포 CI를 위한 일괄 전환
CI에서 여러 공유 패키지를 순차로 빌드하는 모노레포라면 효과가 특히 큽니다. 모노레포 전환 첫날 CI 완료 알림이 왔을 때 잠깐 파이프라인이 실패한 줄 알았습니다. 2분 30초 걸리던 게 18초로 줄어서 "이게 맞나?" 싶었거든요.
Makeplane/Plane 프로젝트도 PR #7679에서 공유 패키지와 라이브 서버 빌드 전체를 tsdown으로 옮겼는데, TypeScript 5.8.3과 tsdown 0.14.x로 툴체인 버전을 통일하면서 빌드 파이프라인이 눈에 띄게 빨라졌습니다.
pnpm 모노레포에서는 루트에서 --filter를 이용하는 방식이 더 편합니다:
# 각 패키지에 tsdown 설치 (루트에서)
pnpm add -D tsdown --filter @myorg/shared-utils
pnpm add -D tsdown --filter @myorg/ui-components
# 루트에서 필터로 마이그레이션 실행
pnpm exec tsdown migrate --filter @myorg/shared-utils
pnpm exec tsdown migrate --filter @myorg/ui-components
# 또는 각 패키지 디렉터리에서 직접 실행하는 방식도 가능합니다
cd packages/shared-utils && npx tsdown migrate
cd packages/ui-components && npx tsdown migrate장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 빌드 속도 | 일반 빌드 tsup 대비 약 2배, .d.ts 생성 시 최대 8배 빠름 (멀티 엔트리 + isolatedDeclarations 활성화 기준, 공식 벤치마크) |
| 마이그레이션 편의성 | 자동 마이그레이션 CLI 제공, 설정 구조가 tsup과 거의 동일 |
| zero-config DTS | package.json의 types 필드 또는 declaration: true만으로 .d.ts 자동 생성 |
| 플러그인 호환성 | Rollup 플러그인 생태계를 그대로 활용 가능. unplugin, 일부 Vite 플러그인도 지원 |
| ESM 우선 설계 | 현대 패키지 배포 관행에 맞는 설계 철학 |
| Oxc 연동 | isolatedDeclarations 활성화 시 초고속 타입 선언 병렬 생성 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 패키지 크기 | tsdown 자체 설치 용량이 tsup보다 상당히 큼 | devDependencies 영향이라 배포에는 무관 |
| stub 모드 미지원 | unbuild의 stub 모드에 해당하는 기능 없음 | 모노레포 링킹은 pnpm link 또는 workspace 프로토콜로 대체 |
| watch 모드 이슈 | 여러 useConfigs 블록 + watch 모드 동시 사용 시 컴파일 블로킹 버그 보고 |
단일 config 블록으로 통합하거나 이슈 픽스 릴리즈 대기 |
| Oxc DTS 호환성 | isolatedDeclarations 활성화 시 일부 복잡한 타입 패턴에서 unsupported syntax 경고 발생 가능 |
에러 메시지 확인 후 해당 위치에 명시적 타입 선언 추가 |
| 소규모 라이브러리 체감 한계 | 단일 엔트리 소규모 라이브러리는 Node.js 시작 비용 때문에 차이가 미미 | 기대치를 현실적으로 조정 |
| 생태계 성숙도 | tsup 대비 플러그인과 레퍼런스 생태계가 아직 작음 | 필요한 플러그인이 Rollup용으로 있는지 먼저 확인 |
unplugin: Rollup, Vite, Webpack 등 여러 번들러에서 동작하는 통합 플러그인 인터페이스. tsdown에서 이 생태계의 플러그인을 그대로 활용할 수 있어서 생태계 격차를 일부 메워줍니다.
실무에서 가장 흔한 실수
isolatedDeclarations없이 DTS 속도를 기대하는 경우 — tsdown의 타입 선언 초고속 생성은 이 옵션이 활성화되어야 진가를 발휘합니다. tsconfig에 추가하지 않으면 개선 폭이 예상보다 작을 수 있습니다.bundle: false옵션을 그냥 복붙하는 경우 — tsdown에서는unbundle: true로 바뀌었습니다. 자동 마이그레이션 CLI를 사용했다면 처리됐겠지만, 수동으로 설정을 옮겼다면 반드시 확인해보시는 것을 권장합니다.- watch 모드 + 여러 config 블록 조합을 그대로 가져오는 경우 — 현재 알려진 버그가 있어서, watch 모드를 쓴다면 config 블록을 하나로 통합하는 방향이 안전합니다.
마치며
tsdown은 단순히 "tsup의 빠른 버전"이 아니라, Rust 기반 JavaScript 툴체인이 라이브러리 번들링 워크플로에 실질적인 변화를 가져올 수 있다는 것을 보여주는 도구입니다. 특히 멀티 엔트리나 모노레포 환경이라면, 옮기는 비용 대비 얻는 게 훨씬 많습니다.
지금 바로 시작해볼 수 있는 3단계:
- 마이그레이션 실행 — 기존 tsup 프로젝트에서
npx tsdown migrate --dry-run으로 어떤 파일이 어떻게 바뀌는지 확인한 후,npx tsdown migrate로 실제 전환할 수 있습니다. 설정이 거의 그대로 유지되어 대부분의 경우 수 분 안에 끝납니다. - 빌드 시간 측정 —
time pnpm build로 이전과 비교해보시면 효과를 바로 확인할 수 있습니다. 특히 멀티 엔트리 또는 모노레포 환경이라면 차이가 더욱 명확하게 나타납니다. isolatedDeclarations활성화 — tsconfig에"isolatedDeclarations": true를 추가해보시는 것을 권장합니다. 처음엔 반환 타입 명시 요구 등으로 타입 에러가 몇 개 나올 수 있지만, 이를 수정하고 나면.d.ts생성 속도에서 확연한 차이를 느낄 수 있습니다.
CI에서 여러 패키지를 빌드하는 모노레포 환경이라면, 지금 당장 dry-run 한 번 돌려보시는 것을 권합니다.
참고 자료
- tsdown 공식 문서 | tsdown.dev
- tsup에서 tsdown으로 마이그레이션 공식 가이드 | tsdown.dev
- tsdown 벤치마크 | tsdown.dev
- tsdown 선언 파일(.d.ts) 옵션 | tsdown.dev
- tsdown GitHub 저장소 | github.com/rolldown/tsdown
- TresJS의 tsdown 마이그레이션 사례 | tresjs.org
- Makeplane/Plane tsdown 마이그레이션 PR #7679 | github.com
- Switching from tsup to tsdown | alan.norbauer.com
- Rolldown 1.0 발표 | voidzero.dev
- Vite 8 Beta 발표 | voidzero.dev
- tsup vs tsdown vs unbuild 2026 비교 | pkgpulse.com
- e18e 에코시스템 tsup → tsdown 마이그레이션 이슈 | github.com
- Dual publish ESM and CJS with tsdown | dev.to