Skip to Content
WebTypeScriptTypeScript 실무18. tsdown으로 라이브러리 빌드와 d.ts 생성

이번 편의 결과물: packages/ui, packages/shared-typesnpm run build 스크립트가 생기고, dist/에 ESM·CJS 번들과 index.d.ts가 생성됩니다. · 다루는 개념: tsdown 설정(entry, format, dts), exports 필드 자동 생성, 개발용 소스 참조(devExports)

17편까지 packages/uitsc -b가 만든 distmain·types 필드로 직접 가리켰습니다. 이 방식은 코드를 고칠 때마다 빌드를 다시 돌려야 하고, 배포용으로 쓰기엔 파일이 소스 파일 개수만큼 그대로 나뉘어 있어 비효율적입니다. 이번 편은 두 패키지에 tsdown을 붙여 번들링·선언 파일 생성·package.jsonexports 필드 관리를 한 번에 맡깁니다.

이 편에서 만드는 파일

bookshelf-ts/ ├── package.json ~ (devDependencies에 tsdown 추가, -w 설치) └── packages/ ├── shared-types/ │ ├── package.json ~ (scripts.build를 tsdown으로 교체) │ └── tsdown.config.ts + └── ui/ ├── package.json ~ (scripts.build를 tsdown으로 교체) └── tsdown.config.ts +

개념 정리

tsdown의 두 가지 d.ts 생성 경로

tsdown은 tsconfig.jsonisolatedDeclarations가 켜져 있으면 더 빠른 oxc 기반 경로를, 아니면 TypeScript 컴파일러를 통한 경로를 씁니다. bookshelf-ts는 아직 isolatedDeclarations를 켜지 않았으므로(모든 함수에 반환 타입을 명시해야 하는 제약이 큽니다) 기본 경로를 그대로 씁니다. dts: true만 지정하면 됩니다.

peerDependencies 자동 외부화

tsdown은 peerDependencies에 적힌 패키지를 번들에 포함하지 않고 import·require 구문으로 남겨둡니다. 17편에서 react, react-dompeerDependencies로 선언해 둔 덕분에, 별도 설정 없이도 packages/ui의 번들에 React 코드가 섞이지 않습니다.

exports 필드 자동 생성과 devExports

exports: true를 켜면 tsdown이 실제 출력 파일을 분석해 package.jsonexports(그리고 format이 두 개 이상이면 main·module·types까지)를 채워 넣습니다. 여기에 devExports: true를 더하면, 평소에는 exports가 소스 파일(src/index.ts)을 직접 가리키고 publishConfig.exports에만 빌드 결과(dist) 경로가 들어갑니다.

상황실제로 참조되는 파일
apps/web에서 pnpm 워크스페이스로 개발 중packages/ui/src/index.ts (빌드 없이 바로 반영)
npm pack·npm publish로 배포publishConfig.exportsdist/* (배포 직전 우선 적용)

pnpm·yarn의 pack·publish 명령은 publishConfigpackage.json에 병합한 뒤 패키징하므로, 개발 중에는 빌드 없이 즉시 반영되고 배포 시에는 자동으로 번들 파일을 쓰는 두 마리 토끼를 잡습니다. 17편에서 겪은 “고칠 때마다 dist를 다시 빌드해야 하는” 불편이 이렇게 해소됩니다.

실습

1. tsdown 설치

pnpm add -D tsdown -w

워크스페이스 루트(-w)에 한 번만 설치하면 모든 패키지의 tsdown 명령을 쓸 수 있습니다.

2. packages/ui/tsdown.config.ts 작성

// packages/ui/tsdown.config.ts import { defineConfig } from 'tsdown' export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, clean: true, sourcemap: true, exports: { devExports: true, }, })

format을 두 가지로 지정해 ESM(dist/index.js)과 CJS(dist/index.cjs)를 모두 만듭니다. apps/web은 ESM만 쓰지만, 이 패키지를 CommonJS 환경(예: 구버전 Node 스크립트)에서 쓰는 사람도 있을 수 있으므로 둘 다 지원합니다.

3. packages/shared-types/tsdown.config.ts 작성

// packages/shared-types/tsdown.config.ts import { defineConfig } from 'tsdown' export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, clean: true, exports: { devExports: true, }, })

zod 스키마는 타입이 아니라 실제 실행되는 값이므로, shared-typesui와 똑같이 실제 번들이 필요합니다.

4. 두 패키지의 package.json 갱신

// packages/ui/package.json (scripts 발췌) { "scripts": { "build": "tsdown", "dev": "tsdown --watch" } }
// packages/shared-types/package.json (scripts 발췌) { "scripts": { "build": "tsdown" } }

main·types 필드는 지우지 않아도 됩니다. 빌드를 한 번 실행하면 tsdown이 exports(그리고 devExports 규칙에 따른 publishConfig.exports)를 자동으로 써넣으면서 필요한 필드를 함께 정리합니다.

5. 빌드 실행과 결과 확인

pnpm --filter @bookshelf/shared-types build pnpm --filter @bookshelf/ui build
packages/ui/dist/ ├── index.js ├── index.js.map ├── index.cjs ├── index.cjs.map ├── index.d.ts └── index.d.cts

빌드가 끝나면 packages/ui/package.json을 다시 열어봅니다.

// packages/ui/package.json (빌드 후 자동 반영된 부분 발췌) { "exports": { ".": "./src/index.ts" }, "publishConfig": { "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } } } }

6. apps/web이 여전히 정상 동작하는지 확인

pnpm --filter @bookshelf/web dev

확인

  • packages/ui/dist, packages/shared-types/distindex.js, index.cjs, index.d.ts, index.d.cts가 모두 생성됩니다.
  • packages/ui/package.jsonexportspublishConfig.exports가 자동으로 채워집니다.
  • npm run devapps/web을 켜면 이전과 동일하게 렌더링되고, 이번에는 packages/ui의 소스를 고쳐도 별도 빌드 없이 곧바로 반영됩니다.
  • packages/ui/dist/index.cjs를 열어보면 react가 번들에 포함되지 않고 require("react") 형태로 남아 있습니다.

직접 해보기

  1. tsdown.config.tspublint: true를 추가하고 다시 빌드해, package.json 필드가 실제 출력 파일과 어긋나는 곳이 있는지 검사 결과를 읽어 보세요.
  2. packages/ui/tsconfig.jsonisolatedDeclarations: true를 켜고 빌드해 보세요. 반환 타입이 없는 함수가 있다면 오류 메시지가 어디를 가리키는지 확인합니다.

정답 보기

// packages/ui/tsdown.config.ts (publint 추가) import { defineConfig } from 'tsdown' export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, clean: true, sourcemap: true, publint: true, exports: { devExports: true, }, })

isolatedDeclarations를 켜면 DataTable 같은 제네릭 컴포넌트에서 반환 타입을 명시하지 않았다는 오류가 날 수 있습니다. 함수 시그니처에 : ReactNode 등 반환 타입을 직접 적어 해결합니다. 이 옵션은 파일마다 다른 파일을 보지 않고도 타입을 알 수 있게 강제해, tsdown이 더 빠른 경로로 선언 파일을 만들도록 돕습니다.

자주 하는 실수

증상원인고치는 법
소비하는 쪽에서 선언 파일을 찾을 수 없다는 오류tsdown.config.tsdts: true를 빠뜨림설정에 dts: true 추가 후 재빌드
번들 안에 React 코드가 통째로 들어감reactpeerDependencies가 아닌 일반 dependencies로 선언package.json에서 react, react-dompeerDependencies로 이동
빌드 결과에 예전 파일이 섞여 남음clean 옵션 없이 반복 빌드clean: true를 설정에 추가
exports 필드가 이상하게 채워짐entry에 없는 파일까지 내부적으로 참조entry를 실제 공개할 진입점(src/index.ts)만으로 좁히고 재빌드

확인 문제

문제 14지선다
tsdown 설정에서 dts를 참으로 설정하는 이유는
문제 24지선다
packages/ui의 번들 파일에 react 코드가 포함되지 않는 이유는
문제 34지선다
devExports를 켰을 때 개발 중(pnpm 워크스페이스 안)에 실제로 참조되는 파일은
문제 44지선다
publishConfig.exports가 실제로 적용되는 시점은

참고 자료

Last updated on