이번 편의 결과물: packages/ui가 npm pack으로 만든 tarball에 번들과 d.ts가 정확히 포함된 것을 확인하고, 실제 배포 명령과 스코프 이름 충돌을 피하는 방법을 정리합니다. · 다루는 개념: package.json 배포 필드(files, publishConfig), semver 규칙, npm pack·npm publish --dry-run, 모노레포에서 workspace: 프로토콜이 배포에 미치는 영향
18편에서 packages/ui는 tsdown으로 dist에 번들과 선언 파일을 만들고, devExports 덕분에 개발 중에는 소스를, 배포 시에는 dist를 가리키는 exports를 갖추게 됐습니다. 이번 편은 실제로 npm에 올리기 전 마지막 단계, 즉 배포 필드를 확정하고 npm pack으로 결과물을 검증하는 절차를 다룹니다. 이 편에서는 실제로 배포하지 않습니다.
이 편에서 만드는 파일
bookshelf-ts/
└── packages/
└── ui/
└── package.json ~ (version, description, license, files, publishConfig.access 확정)개념 정리
스코프 패키지명 충돌 주의
@bookshelf/ui의 bookshelf는 이 과목에서 쓰는 예시 스코프입니다. npm의 스코프(@ 뒤 이름)는 실제로 소유한 npm 사용자명이거나, npm에 만들어 둔 조직 이름이어야 합니다. 다른 사람이 이미 쓰고 있는 스코프에는 배포할 수 없습니다. 실제로 배포하려면 다음을 먼저 확인합니다.
npm view @bookshelf/ui실행 결과가 404면 그 이름은 비어 있다는 뜻입니다. 다만 스코프 자체를 내가 소유했는지는 별개로 확인해야 합니다.- 자신의 npm 계정 사용자명이
chulsoo92라면@chulsoo92/ui처럼 개인 스코프를 쓰는 것이 가장 안전합니다. - 스코프 없는 이름(
bookshelf-ui처럼)은 전역에서 단 하나만 존재할 수 있어 충돌 가능성이 훨씬 큽니다. 실무에서는 거의 항상 스코프를 씁니다.
package.json 배포 필드
| 필드 | 역할 |
|---|---|
version | semver(주.부.수) 규칙. 호환 안 되는 변경은 주, 기능 추가는 부, 버그 수정은 수를 올림 |
files | tarball에 포함할 경로 목록. 지정하지 않은 파일·폴더는 제외됨(package.json, README는 항상 포함) |
sideEffects | false면 번들러가 안전하게 사용하지 않는 코드를 제거할 수 있음을 알림 |
publishConfig.access | 스코프 패키지의 공개 범위. public을 명시하지 않으면 유료 비공개로 시도되어 무료 계정에서 배포가 실패함 |
0.1.0처럼 주 버전이 0인 동안은 API가 아직 불안정하다는 뜻으로 통용됩니다. 실사용자에게 배포할 준비가 되면 1.0.0으로 올립니다.
workspace 프로토콜과 배포 도구의 관계
packages/ui의 dependencies에는 "@bookshelf/shared-types": "workspace:*"가 들어 있습니다. pnpm publish·pnpm pack은 패키징 직전에 이 문자열을 실제 설치 가능한 버전 범위로 자동 치환합니다. 반면 npm pack·npm publish는 pnpm 워크스페이스를 알지 못해 workspace:*를 그대로 tarball에 남깁니다. 이 상태로 배포하면, 모노레포 밖에서 설치하는 사람은 workspace:*를 해석하지 못해 설치가 실패합니다.
실습
1. packages/ui/package.json 배포 필드 확정
// packages/ui/package.json (18편 이후 상태에 배포 필드 추가)
{
"name": "@bookshelf/ui",
"version": "0.1.0",
"description": "bookshelf-ts 재사용 UI 컴포넌트",
"license": "MIT",
"type": "module",
"files": ["dist"],
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./src/index.ts"
},
"publishConfig": {
"access": "public",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
},
"peerDependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0"
},
"dependencies": {
"@bookshelf/shared-types": "workspace:*"
}
}exports, publishConfig.exports는 18편에서 tsdown이 자동으로 채워둔 값을 그대로 둡니다. 이번 편에서 새로 추가하는 것은 description, license, files, sideEffects, publishConfig.access입니다.
2. npm pack으로 1차 검증
cd packages/ui
npm pack --dry-runnpm notice
npm notice 📦 @bookshelf/ui@0.1.0
npm notice === Tarball Contents ===
npm notice 1.2kB package.json
npm notice 0.4kB dist/index.d.ts
npm notice 0.3kB dist/index.d.cts
npm notice 2.1kB dist/index.js
npm notice 2.3kB dist/index.cjs
npm notice === Tarball Details ===
npm notice name: @bookshelf/ui
npm notice version: 0.1.0files: ["dist"] 덕분에 src나 tsdown.config.ts는 포함되지 않고, dist 아래 번들과 선언 파일만 들어갑니다. 여기서 package.json을 열어 dependencies를 확인하면 "@bookshelf/shared-types": "workspace:*"가 그대로 남아 있는 것도 보입니다.
3. pnpm pack으로 workspace 프로토콜 치환 확인
pnpm pack --pack-destination /tmptar -xOf /tmp/bookshelf-ui-0.1.0.tgz package/package.json | grep shared-types"@bookshelf/shared-types": "^0.1.0"같은 파일인데 pnpm pack으로 만들면 workspace:*가 실제 버전 범위(^0.1.0)로 바뀌어 있습니다. 모노레포 안 패키지를 배포할 때는 npm pack·npm publish가 아니라 pnpm pack·pnpm publish를 쓰는 것이 안전한 이유입니다.
4. 최종 드라이런
pnpm publish --access public --dry-run이 명령은 실제로 레지스트리에 아무것도 올리지 않고, 배포됐을 때와 동일한 검증·패키징 과정만 수행합니다. 출력 마지막의 tarball 목록이 2단계에서 본 것과 같은지 확인합니다.
확인
npm pack --dry-run결과에dist/index.js,dist/index.cjs,dist/index.d.ts,dist/index.d.cts가 모두 보입니다.src폴더나tsdown.config.ts는 tarball 목록에 없습니다.pnpm pack으로 만든 tarball 안package.json에서workspace:*가 실제 버전으로 바뀐 것을 확인합니다.pnpm publish --access public --dry-run이 오류 없이 끝납니다.
직접 해보기
npm view react version을 실행해 실제 배포된 패키지 정보를 조회하는 형식을 확인하고, 아직 존재하지 않는 이름(npm view @bookshelf/ui)과 결과가 어떻게 다른지 비교해 보세요.package.json의version을0.2.0으로 올려야 하는 변경(기존 컴포넌트의 동작은 그대로 두고Pagination처럼 새 컴포넌트만 추가하는 경우)과1.0.0으로 올려야 하는 변경(기존BookCard의 props 이름을 바꾸는 경우)을 구분해 적어 보세요.
정답 보기
새 컴포넌트 추가는 기존 사용자의 코드를 깨뜨리지 않는 기능 추가이므로 부 버전을 올립니다(0.1.0 → 0.2.0). BookCard의 props 이름을 바꾸는 것은 이미 이 컴포넌트를 쓰던 코드가 깨지는 호환성 파괴 변경이므로 주 버전을 올립니다. 아직 0.x인 상태에서는 관례상 부 버전을 올리는 것으로 파괴적 변경을 표시하기도 하지만, 1.0.0 이후에는 반드시 주 버전을 올려야 합니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 배포 시 비공개 패키지 등록 요금 안내와 함께 실패 | 스코프 패키지인데 --access public을 지정하지 않음 | publishConfig.access를 public으로 명시하거나 배포 명령에 --access public 추가 |
| 모노레포 밖에서 설치한 사람이 의존성 설치 실패를 겪음 | npm publish로 직접 배포해 workspace:*가 실제 버전으로 안 바뀜 | pnpm publish로 배포해 workspace 프로토콜을 자동 치환 |
| tarball에 src, 테스트 파일까지 포함됨 | files 필드를 지정하지 않음 | files: ["dist"]처럼 배포할 폴더만 명시 |
| 이미 있는 이름이라 배포가 거부됨 | 스코프를 내가 소유하지 않은 이름으로 정함 | 개인 npm 사용자명 스코프(@본인아이디/이름)로 바꾸거나 조직을 새로 만듦 |