배포 전 출국 심사 — Publint로 npm 패키지 실수를 원천 차단하는 법
npm 패키지의 exports, entry points, 모듈 포맷을 배포 전에 검증하는 Publint 도구 사용법.
여권에 이름을 틀리게 적고 공항에 갔다고 상상해보자. 체크인까지는 문제없지만, 출국 심사에서 걸린다. npm 패키지 배포도 비슷하다. package.json의 exports 필드 순서가 잘못되었거나, main과 module이 엇갈리거나, types 조건이 뒤에 빠져 TypeScript가 타입을 못 찾거나. 문제는 이런 실수가 로컬에서는 절대 드러나지 않는다는 것이다. 빌드도 되고 테스트도 통과하는데, npm publish 후에 사용자가 "import가 안 돼요"라고 이슈를 올린다. Publint는 출국 심사대처럼 배포 직전에 이런 실수를 잡아주는 도구다.
Publint — 패키지 메타데이터 린터
Publint은 package.json을 기반으로 선언된 엔트리 포인트와 실제 배포 파일이 일치하는지 검증하는 린터다. main, module, exports, types 필드를 순회하면서 Vite, Webpack, Rollup, Node.js 등 다양한 환경에서의 호환성을 확인한다.
npx publint@latest이 한 줄이면 현재 프로젝트의 패키징 문제를 즉시 확인할 수 있다. 다른 디렉토리의 패키지를 검사하고 싶으면 경로를 인자로 전달한다.
npx publint ./packages/my-lib[💡 잠깐! 이 용어는?]
exports 필드: package.json에서 패키지의 진입점(entry points)을 정의하는 최신 방식이다. 조건부 내보내기(import, require, types)를 지원해서 ESM/CJS/TypeScript 환경별로 다른 파일을 제공할 수 있다.
Publint이 잡아내는 네 가지 함정
1. types 조건의 순서 함정
exports 필드의 조건은 순서가 의미를 가진다. TypeScript가 타입을 올바르게 찾으려면 types가 반드시 첫 번째여야 한다. 뷔페에서 디저트를 먼저 집으면 메인 요리를 놓치는 것처럼, 잘못된 순서는 TypeScript가 타입 정보를 놓치게 만든다.
{
"exports": {
".": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
}
}{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
}
}Publint은 types 조건이 첫 번째가 아니면 경고를 표시한다.
2. module 필드만 있고 exports가 없는 경우
module 필드는 번들러 사이의 비공식 규약이다. Node.js는 이 필드를 인식하지 않는다. Publint은 module은 있는데 exports가 없으면, exports 사용을 권장한다.
{
"main": "./dist/index.cjs",
"module": "./dist/index.mjs"
}{
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}3. 엔트리 포인트가 실제 파일과 불일치
"main": "./dist/index.js"라고 선언했는데 실제 빌드 결과물에 dist/index.js가 없다면? 사용자는 설치 직후 import에서 바로 에러를 맞는다. Publint은 선언된 모든 경로가 실제로 디스크에 존재하는지 확인한다.
[💡 잠깐! 이 용어는?]
엔트리 포인트(Entry Point): 패키지를 import할 때 실제로 로드되는 파일이다. package.json의 main, module, exports 필드가 이 진입점을 정의한다. 잘못 설정하면 "Module not found" 에러가 발생한다.
4. exports 값의 상대 경로 생략
exports 필드의 값은 반드시 ./로 시작해야 한다. 사소해 보이지만, 이 한 글자 차이가 패키지 해석을 완전히 깨뜨릴 수 있다.
{
"exports": {
".": "dist/index.mjs"
}
}{
"exports": {
".": "./dist/index.mjs"
}
}검증 규칙 한눈에 보기
| 규칙 | 설명 | 심각도 |
|---|---|---|
| types 순서 | exports에서 types 조건이 첫 번째여야 함 | Warning |
| module vs exports | module 필드만 있으면 exports 권장 | Suggestion |
| 파일 존재 여부 | 선언된 엔트리 포인트가 디스크에 있어야 함 | Error |
| 상대 경로 | exports 값이 ./로 시작해야 함 | Error |
| ESM/CJS 포맷 일치 | .mjs는 ESM, .cjs는 CJS와 일치해야 함 | Warning |
| Dual Publishing | ESM과 CJS 동시 제공 시 일관성 확인 | Warning |
CI/CD 통합 — 자동화가 진짜 가치다
로컬에서 한 번 돌려보는 것도 좋지만, CI에서 매 PR마다 검증하거나 배포 직전에 강제하는 것이 훨씬 안전하다. 교통 법규를 알아도 사고는 나는 법이다. 과속 카메라가 있어야 속도를 줄이게 된다.
{
"scripts": {
"build": "tsup src/index.ts",
"lint:package": "publint",
"prepublishOnly": "npm run build && npm run lint:package"
},
"devDependencies": {
"publint": "^0.3.0"
}
}[💡 잠깐! 이 용어는?]
prepublishOnly: npm publish 실행 직전에 자동으로 실행되는 npm 라이프사이클 스크립트다. 빌드나 린트 같은 배포 전 검증을 여기에 넣으면, 깨진 패키지 배포를 원천 차단할 수 있다.
GitHub Actions에서는 이렇게 설정한다.
name: Validate Package
on: [push, pull_request]
jobs:
publint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx publint웹에서 바로 확인하기
CLI 없이도 publint.dev에서 패키지 이름을 검색하면 즉시 결과를 확인할 수 있다. 이미 npm에 배포된 패키지라면 이름만 입력하면 되고, pkg.pr.new 링크를 붙여넣어 PR 단계의 패키지도 검증할 수 있다.
유사 도구와의 비교
| 도구 | 범위 | 특징 |
|---|---|---|
| Publint | package.json 필드 + 파일 존재 검증 | 가볍고 빠름, 포맷/환경 호환성에 집중 |
npm pack --dry-run | 배포 대상 파일 목록 확인 | 포맷/호환성 검증 없음 |
arethetypeswrong | TypeScript 타입 해석 검증 | 타입에만 집중, 더 깊은 분석 |
| ESLint | 코드 품질 | 패키지 메타데이터 검증 아님 |
Publint과 arethetypeswrong를 함께 쓰면 패키지 배포 실수의 대부분을 사전에 잡아낼 수 있다.
마무리
npm 패키지 배포에서 가장 흔한 실수는 코드 버그가 아니라 메타데이터 설정 오류다. exports 필드 순서 하나, 상대 경로 표기 하나가 사용자의 import를 깨뜨린다. Publint은 이 문제를 빌드 타임에 잡아주는 간결하면서도 효과적인 도구다. prepublishOnly에 한 줄 추가하는 것만으로 "배포했는데 import 안 돼요" 이슈를 대부분 예방할 수 있다.
참고:
- Publint: https://publint.dev/
- Publint Rules: https://publint.dev/rules
- Are The Types Wrong: https://arethetypeswrong.github.io/
같은 카테고리 · Tooling
비슷한 주제의 최신 글
태그가 겹치는 글
공통 태그가 많을수록 위에 보인다