Migrating a Real TypeScript OSS Library from tsup to tsdown
실제 TypeScript 오픈소스 라이브러리를 tsup에서 tsdown으로 옮기기
is-kit의 빌드 도구를 tsup에서 tsdown으로 바꾸며 출력 파일명과 패키지 계약을 검증한 사례입니다. 빌드 성공만으로는 배포 패키지의 호환성을 보장하지 못하며, npm 패키지를 임시 소비자 프로젝트에 설치해 확인해야 한다고 설명합니다.
- 주제
AI 요약
TypeScript 런타임 타입 가드 라이브러리 is-kit은 tsup에서 tsdown으로 빌드 도구를 바꿨습니다. 설정 변경은 작았지만, 첫 빌드 결과가 기존 패키지 계약과 달라졌습니다. 글은 빌드 성공 여부가 아니라 실제로 배포할 패키지가 기존 소비자에게 같은 파일과 동작을 제공하는지 검증한 과정을 소개합니다.
기존 패키지 계약
마이그레이션 전 [email protected]는 [email protected]로 단일 진입점 src/index.ts를 빌드했습니다. 결과물은 ESM과 CJS, 번들된 타입 선언 파일이었고, package.json의 exports는 dist/index.mjs, dist/index.js, dist/index.d.ts를 가리켰습니다. 선언 파일에 배너를 넣고, 패키지를 묶어 소비자 환경에서 확인하는 테스트도 이미 갖추고 있었습니다.
저자는 tsdown이 Rolldown 기반이며 tsup 사용 프로젝트의 마이그레이션 경로로 설계됐다는 점을 도입 이유로 들었습니다. 목표는 빌드 성공이 아니라 기존 사용자가 받는 패키지의 계약을 유지하는 것이었습니다. 라이브러리에서는 출력 파일명도 호환성의 일부가 될 수 있기 때문입니다.
설정은 비슷했지만 출력 파일은 달라졌습니다
저자는 tsup.config.ts를 tsdown.config.mts로 바꿨습니다. 패키지에 "type": "module"이 없으므로 .mts를 사용해 Node가 설정 파일을 해석하는 과정에서 생기는 경고를 피했습니다. 진입점, ESM·CJS 형식, 선언 파일 생성, 정리, 출력 디렉터리, esnext 타깃, 선언 파일에만 적용하는 배너 설정은 거의 그대로 옮겼습니다.
하지만 기본 설정으로 빌드하자 tsup은 index.mjs, index.js, index.d.ts를 만들었던 반면 tsdown은 index.mjs, index.cjs, index.d.mts, index.d.cts를 생성했습니다. 빌드는 종료 코드 0으로 성공했지만, 기존 package.json은 require 경로로 ./dist/index.js, 타입 경로로 ./dist/index.d.ts를 기대했습니다. 이 상태로 배포했다면 CJS 소비자와 TypeScript의 모듈 해석이 깨질 수 있었습니다.
저자는 tsdown의 기본 출력 확장자 선택을 버그로 보지는 않습니다. 패키지 형식과 출력 형식에 맞춰 모듈을 모호하지 않게 구분하려는 동작입니다. 다만 기존 패키지에서 새 기본값을 적용하면 호환성 변경이 생길 수 있습니다. 이를 막기 위해 outExtensions를 지정했습니다. CJS 출력에는 JS 확장자 .js와 선언 확장자 .d.ts를, ESM 출력에는 .mjs와 .d.mts를 사용하도록 했습니다. 그 결과 기존 export 경로가 계속 유효했습니다.
배포할 아티팩트를 소비자 관점에서 검사했습니다
is-kit의 pnpm test:package는 빌드 뒤 npm pack으로 tarball을 만들고, 임시 프로젝트에 설치해 확인합니다. 소스 코드나 저장소 내부 빌드만 검사하는 대신 실제 사용자가 설치할 패키지를 검증하는 방식입니다. 마이그레이션 뒤에는 exports의 import·require·types 경로, 런타임 의존성 0개, ESM과 CJS의 export 83개 유지, 선언 배너, ESM import와 CJS require 실행, 패키지 설치 후 TypeScript 소비 여부를 확인했습니다. 항목은 모두 통과했습니다.
TypeScript 5.7부터 7.0까지 각 버전으로 만든 임시 소비자 프로젝트에서도 선언 파일을 정상적으로 찾고 사용했습니다. 저자는 이 검사가 각 TypeScript 버전의 선언 파일 생성 능력을 시험하는 것이 아니라, 사용자가 실제로 설치하는 결과물을 확인하는 테스트라고 구분합니다.
크기와 빌드 시간
빌드 결과의 JS와 선언 파일을 합친 크기는 tsup 116,503바이트에서 tsdown 144,007바이트로 23.6% 늘었습니다. ESM JavaScript는 15,787바이트에서 29,410바이트로 86.3%, CJS는 19,318바이트에서 31,155바이트로 61.3% 증가했습니다. 선언 파일은 한 형식 기준 40,699바이트에서 41,721바이트로 2.5% 늘었고, npm tarball은 37,226바이트에서 42,366바이트로 13.8% 커졌습니다. tsdown 출력에 주석과 영역 표시가 더 남았고, 압축은 tarball 크기 차이를 줄였지만 없애지는 못했습니다.
벽시계 기준 빌드 시간은 tsup 한 번 측정에서 1.50초, tsdown 세 번 측정에서 1.22초, 1.17초, 1.18초였습니다. 다만 tsup 측정값이 하나뿐이고 작은 단일 진입점 라이브러리에서는 선언 생성 시간이 큰 비중을 차지합니다. 저자는 이 결과만으로 의미 있는 성능 향상을 주장하기 어렵다고 설명합니다. tsup의 JavaScript 빌드는 약 22ms, 선언 생성은 약 707ms였으며 tsdown 전체 빌드는 약 717~755ms였습니다.
마이그레이션 전 확인할 점
이번 작업은 의존성, 설정, 작업 문서, 패키지 스모크 테스트 변경으로 끝났고 소스 코드는 바꾸지 않았습니다. 단일 진입점, 런타임 의존성 없음, 번들러 플러그인 없음, CSS 처리 없음, 복잡한 코드 분할 없음, 기존 패키지 테스트 보유가 작은 범위에 영향을 줬습니다. 플러그인, 특이한 진입점, CSS 처리, 정확한 소스맵, 생성형 export, 엄격한 크기 제한, 오래된 Node 빌드 환경이 있다면 작업 범위가 달라질 수 있습니다.
tsdown 0.23.0과 Rolldown 1.2.8에서 빌드 도구의 Node 요구사항은 ^22.18.0 || ^24.11.0 || >=26.0.0이었습니다. 저자의 CI는 Node 22.22.0이라 문제가 없었지만, 기여자나 CI가 Node 20 또는 더 낮은 Node 22 버전을 쓴다면 먼저 대응해야 합니다. 이 요구사항은 라이브러리 소비자가 아니라 빌드 환경에 적용됩니다.
저자는 이 마이그레이션을 모두에게 권하지 않습니다. is-kit에서는 패키지 계약을 유지했고 환경도 요구사항을 충족했으므로 적절한 변경이었다고 봅니다. 다만 결과물 크기는 늘었고 Node 요구사항도 높아졌습니다. tsdown이 더 빠르다는 이유만으로 옮기기보다, 배포 패키지를 직접 검사하고 크기와 빌드 환경까지 비교하라는 것이 글의 요지입니다.
dev.to 반응
- @shubhradev — 빌드 성공에서 멈추지 않고 패키지를 묶어 테스트한 접근이 정말 좋았습니다. 저장소 빌드가 완전히 통과해도 export, 런타임 진입점, 선언 파일 경로와 맞지 않는 패키지를 배포할 수 있습니다. 실제 tarball을 임시 소비자 프로젝트에서 테스트하면 저장소 빌드만 검사할 때보다 그 경계가 더 분명해집니다.
- @nyaomaru — 감사합니다! 😸 소비자 입장에서 실제로 무엇이 묶이고 설치되는지 확인하는 것이 제가 신경 쓴 부분이었습니다. 빌드가 통과하는지만 보는 것이 아니라 실제 패키지를 확인하는 방식입니다. 좋게 봐주셔서 기쁩니다! 😺
- @technogamerz — 또 표지 이미지 잘 뽑으셨네요 🔥
- @kyisaiah47 — 스모크 테스트에서는 임시 소비자 프로젝트에서 import와 require를 모두 확인하나요, 아니면 묶인 tarball만 살펴보나요?
원문: dev.to / 번역·요약: Trawling