본문 바로가기

복사금지 블로그 짜증나서 만든 개발문서

npm 배포 전에 어떤 파일이 올라가는지 미리 확인하는 방법

반응형

환경  npm 11.12.1 · Node.js 25.9.0 · 2026-08-07 확인

npm publish 를 누르고 나서야 빌드 산출물이 빠졌거나 .env 가 딸려 올라간 것을 알게 되는 일이 있습니다. 어떤 파일이 담기는지는 files 필드와 .npmignore, .gitignore 가 서로 덮어쓰며 정해져서 추론이 잘 맞지 않습니다. 올리기 전에 목록을 뽑아 확인하는 순서를 npm 11.12.1 재현 결과와 공식 문서로 정리했습니다.

무엇을 확인하려는 것이고, 무엇이 준비돼 있어야 하는가

목표는 하나입니다. npm publish 를 누르기 전에, 레지스트리로 올라갈 tarball 안의 파일 목록을 그대로 화면에 띄워 보는 것입니다. 이 목록은 npm pack 이 만들어 냅니다. 공식 npm-pack 문서는 이 명령이 패키지를 tarball 로 만들어 현재 디렉터리에 <name>-<version>.tgz 로 복사하고 파일 이름들을 표준 출력에 적는다고 설명합니다. 인자를 주지 않으면 현재 패키지 폴더를 대상으로 합니다.
npm-publish 문서는 무엇이 담기는지를 세 단계로 적어 둡니다. 기본적으로 모든 파일이 포함되고, package.json 에 files 목록이 있으면 거기 지정된 것만 포함되며, .gitignore 나 .npmignore 가 있으면 거기서 무시된 파일이 하위 디렉터리까지 포함해 빠집니다. 그리고 두 파일이 모두 있으면 .gitignore 는 무시되고 .npmignore 만 쓰인다고 못박습니다. 규칙이 셋인데 서로 덮어쓰는 관계라, 저장소마다 결과가 달라집니다.
실행 전에 두 가지만 맞춰 두면 됩니다. 첫째, 명령은 package.json 이 있는 패키지 루트에서 실행합니다. 둘째, 빌드가 필요한 패키지라면 빌드가 끝난 상태여야 합니다. 다만 dist 폴더를 손으로 미리 만들어 둘 필요는 없습니다. npm 11.12.1 에서 확인해 보면 --dry-run 을 붙여도 prepack 과 prepare 스크립트는 그대로 실행됩니다. 빌드를 그 자리에 걸어 둔 패키지라면 목록을 뽑는 동작만으로도 산출물이 새로 만들어집니다.

cd <패키지 루트>   # package.json 이 있는 자리
npm --version
# 11.12.1

# --dry-run 을 붙여도 prepack/prepare 는 실행된다
npm pack --dry-run
# > echo PREPACK-RAN
# PREPACK-RAN
# > echo PREPARE-RAN
# PREPARE-RAN
# npm notice total files: 4

# 대신 tgz 파일은 남지 않는다
ls *.tgz
# no matches found: *.tgz

드라이런은 목록만 출력하고 tarball 파일을 남기지 않습니다. 마지막 줄에 파일 이름이 찍히더라도 실제 파일은 없습니다.

확인하려는 것은 설정값이 아니라, 그 설정으로 만들어진 최종 파일 목록입니다.

먼저 목록을 뽑아 지금 상태를 본다

가장 먼저 할 일은 아무것도 고치지 않은 상태에서 npm pack --dry-run 을 한 번 돌리는 것입니다. 공식 문서는 --dry-run 을 npm 이 아무 변경도 하지 않고 무엇을 했을지만 보고하는 옵션이라고 설명합니다. 출력에는 파일별 크기와 함께 패키지 크기, 압축을 푼 크기, 총 파일 개수가 같이 나옵니다.
아래는 files 필드가 없고 .gitignore 에 node_modules 와 dist, .env 세 줄이 들어 있는 패키지에서 받은 실제 출력입니다. .env 는 목록에서 빠졌습니다. .gitignore 에 적혀 있으니 예상대로입니다. 그런데 같은 파일에 적혀 있는 dist 는 빠지지 않았고, 심지어 소스맵까지 함께 들어왔습니다. 이 차이가 왜 생기는지는 뒤에서 다시 다룹니다.
여기서 눈여겨볼 줄이 하나 더 있습니다. npm 11 은 .npmignore 가 없어 .gitignore 를 대신 쓰는 경우 gitignore-fallback 경고를 냅니다. 배포에 쓰이는 무시 규칙을 개발용 파일에 의존하고 있다는 뜻이므로, 이 경고가 보이면 다음 단계로 넘어갈 이유가 생긴 것입니다.

npm pack --dry-run

# npm warn gitignore-fallback No .npmignore file found, using .gitignore
#   for file exclusion. Consider creating a .npmignore file to explicitly
#   control published files.
# npm notice package: acme-utils@1.4.0
# npm notice Tarball Contents
# npm notice 7B   README.md
# npm notice 18B  dist/index.js
# npm notice 18B  dist/index.js.map
# npm notice 76B  package.json
# npm notice 17B  src/index.js
# npm notice 5B   test/index.test.js
# npm notice total files: 6

.gitignore 에 .env 와 dist 가 함께 적혀 있는데도 한쪽만 빠졌습니다. 규칙을 읽는 것보다 목록을 보는 편이 빠른 이유입니다.

files 필드로 포함할 것을 직접 적는다

목록이 마음에 들지 않으면 무시 규칙을 손보는 대신 package.json 의 files 필드를 쓰는 편이 예측하기 쉽습니다. package.json 문서는 이 필드를 패키지가 의존성으로 설치될 때 포함할 항목을 기술하는 파일 패턴 배열이라고 설명합니다. 패턴 문법은 .gitignore 와 같지만 의미가 반대여서, 적힌 것이 제외가 아니라 포함 대상이 됩니다.
files 를 ["dist"] 하나로 두고 다시 목록을 뽑으면 결과가 네 개로 줄어듭니다. src 와 test 는 물론이고 .env 도 사라집니다. dist 외에 README.md 와 package.json 이 남는 것은 문서가 설정과 무관하게 항상 포함된다고 적어 둔 항목이기 때문입니다. 여기에는 확장자나 대소문자를 가리지 않는 README 와 LICENSE, main 필드가 가리키는 파일, bin 필드에 적힌 파일이 들어갑니다.
files 필드를 채우고 나면 앞서 본 gitignore-fallback 경고도 사라집니다. npm 11.12.1 에서 확인한 결과, 이 경고는 files 필드도 없고 .npmignore 도 없어서 .gitignore 로 되돌아간 경우에만 나왔습니다. 경고가 없어졌다는 것은 배포 대상이 개발용 파일이 아니라 package.json 에 적힌 목록으로 정해지고 있다는 뜻입니다.

{
  "name": "acme-utils",
  "version": "1.4.0",
  "main": "dist/index.js",
  "files": ["dist"]
}

// npm pack --dry-run --json 으로 뽑은 경로 목록
// ["README.md", "dist/index.js", "dist/index.js.map", "package.json"]

files 를 적으면 목록이 여섯에서 넷으로 줄어듭니다. README.md 와 package.json 은 적지 않아도 남습니다.

포함할 것을 적는 방식이 제외할 것을 적는 방식보다 결과를 읽기 쉽습니다.

.npmignore 를 하나 만들면 .gitignore 는 그 순간부터 읽히지 않는다

여기가 실제로 사고가 나는 지점입니다. 앞의 패키지에서 files 필드를 쓰지 않고, 대신 테스트 폴더만 빼려고 .npmignore 를 만들어 test 한 줄을 적었다고 해 보겠습니다. 의도는 test 를 추가로 제외하는 것이지만, 결과는 다릅니다. .npmignore 를 만든 순간 .gitignore 는 배포 대상 판정에서 통째로 빠지고, .gitignore 에만 적혀 있던 .env 가 tarball 안으로 들어옵니다.
npm-publish 문서가 두 파일이 함께 있으면 .gitignore 는 무시되고 .npmignore 만 쓰인다고 적은 것이 그대로 나타난 것입니다. 재현해 보면 .npmignore 를 만들기 전 목록에는 .env 가 없고 test/index.test.js 가 있었는데, 만든 뒤에는 test 가 빠지는 대신 .env 가 새로 들어옵니다. 한 파일을 빼려다 다른 파일을 들여보낸 셈입니다.
그래서 .npmignore 를 새로 도입할 때는 .gitignore 에 있던 제외 규칙 중 배포에도 필요한 줄을 모두 옮겨 적어야 합니다. 규칙이 두 파일에 나뉘어 있는 상태 자체가 위험하다면, 제외 목록을 관리하는 대신 앞 단계의 files 필드로 포함 목록만 관리하는 편이 낫습니다.

# .gitignore 에 node_modules, dist, .env 가 적혀 있는 상태
npm pack --dry-run --json
# ["README.md", "dist/index.js", "dist/index.js.map",
#  "package.json", "src/index.js", "test/index.test.js"]

# 테스트만 빼려고 .npmignore 를 만든다
echo 'test' > .npmignore

npm pack --dry-run --json
# [".env", "README.md", "dist/index.js", "dist/index.js.map",
#  "package.json", "src/index.js"]
#   ↑ test 는 빠졌지만 .env 가 들어왔다

.npmignore 는 .gitignore 에 더해지는 규칙이 아니라, .gitignore 를 대체하는 규칙입니다.

.npmignore 를 만드는 일은 규칙을 추가하는 것이 아니라 기존 규칙 전체를 갈아 끼우는 일입니다.

main 이 가리키는 파일 주변은 무시 규칙을 비켜 간다

앞에서 넘긴 질문으로 돌아갑니다. .gitignore 에 dist 가 적혀 있는데도 dist/index.js 와 dist/index.js.map 이 목록에 남아 있었습니다. package.json 문서는 설정과 무관하게 항상 포함되는 항목에 main 필드가 가리키는 파일이 들어간다고 적어 두었습니다. 이 패키지의 main 이 dist/index.js 였습니다.
다만 관찰된 결과는 그 파일 하나에 그치지 않았습니다. main 을 dist/index.js 로 둔 채 목록을 뽑으면 같은 폴더의 index.js.map 까지 함께 들어오고, main 을 src/index.js 로 바꾸면 dist 폴더 전체가 .gitignore 대로 빠집니다. npm 11.12.1 에서 두 경우를 번갈아 실행해 확인한 결과입니다. 공식 문서에 폴더 단위 동작까지는 적혀 있지 않으므로, 버전에 따라 다를 수 있다고 보는 편이 안전합니다.
실무에서 의미는 분명합니다. 빌드 산출물 폴더를 .gitignore 에 넣어 두는 것은 흔한 구성인데, 그 폴더가 배포에서 빠질지 남을지는 main 이 어디를 가리키느냐에 따라 달라집니다. 저장소 설정만 보고 판단할 수 없고, 목록을 뽑아 봐야 알 수 있는 부분입니다.

# .gitignore: node_modules, dist   (files 필드 없음)

# main 이 dist 안을 가리킬 때
# "main": "dist/index.js"
npm pack --dry-run --json
# [".env", "README.md", "dist/index.js", "dist/index.js.map",
#  "package.json", "src/index.js", "test/index.test.js"]

# main 을 src 로 옮겼을 때
# "main": "src/index.js"
npm pack --dry-run --json
# [".env", "README.md", "package.json",
#  "src/index.js", "test/index.test.js"]
#   ↑ 같은 .gitignore 인데 dist 가 사라졌다

무시 규칙은 그대로 두고 main 만 바꿨을 때의 차이입니다. 배포 대상이 진입점 설정에 따라 흔들립니다.

확인은 tarball 을 직접 풀어 보고, 목록을 기계에 맡기는 것으로 끝낸다

목록이 원하는 모양이 되면 마지막으로 실제 tarball 을 한 번 만들어 봅니다. --dry-run 없이 npm pack 을 실행하면 <name>-<version>.tgz 파일이 생기고, tar -tf 로 내용을 그대로 볼 수 있습니다. 압축 안의 경로는 모두 package/ 아래에 놓입니다. 설치했을 때 node_modules 안에 펼쳐지는 모양이 이것입니다.
npm publish --dry-run 도 같은 목록을 보여 줍니다. 로그인이 필요하다는 경고가 함께 나오지만 드라이런이므로 레지스트리에는 아무것도 올라가지 않습니다. 배포 직전 마지막 확인으로는 이 명령이 실제 상황에 더 가깝습니다.
사람 눈으로 매번 목록을 훑는 대신, --json 출력을 받아 금지 목록에 걸리는 경로가 있으면 실패시키는 검사를 CI 에 걸어 두면 좋습니다. --json 은 일반 출력 대신 JSON 데이터를 내보내는 옵션으로, 배열의 첫 요소에 files 항목이 들어 있고 각 항목의 path 가 tarball 내부 경로입니다. 아래 검사를 배포 전 단계에 붙이면 .env 나 인증서 파일이 목록에 들어온 순간 파이프라인이 멈춥니다.

# 실제 tarball 을 만들어 내용을 확인한다
npm pack
# acme-utils-1.4.0.tgz
tar -tf acme-utils-1.4.0.tgz
# package/dist/index.js
# package/package.json
# package/dist/index.js.map
# package/README.md

# 배포 직전 확인 (레지스트리에는 아무것도 올라가지 않는다)
npm publish --dry-run
# npm notice total files: 4
# npm warn This command requires you to be logged in ... (dry-run)

# CI 에 걸어 두는 검사
npm pack --dry-run --json | node -e '
const files = JSON.parse(require("fs").readFileSync(0, "utf8"))[0].files.map(f => f.path);
const bad = files.filter(p => /(^|\/)\.env|\.pem$|(^|\/)secrets\//.test(p));
if (bad.length) { console.error("배포 대상에 들어가면 안 되는 파일:", bad.join(", ")); process.exit(1); }
console.log("통과:", files.length + "개");
'
# 배포 대상에 들어가면 안 되는 파일: .env
# exit=1

검사가 걸리면 종료 코드 1 로 끝납니다. 배포 스크립트 앞에 두면 그 자리에서 멈춥니다.

목록을 사람이 매번 읽는 대신, 걸리면 멈추는 검사로 바꿔 두는 편이 오래갑니다.

이 절차에서 자주 어긋나는 곳

files 필드에 적으면 무엇이든 들어간다고 생각하는 경우가 있습니다. package.json 문서는 files 글로브로 되살릴 수 없는 예외를 따로 적어 두었습니다. .git 과 .npmrc, node_modules, 그리고 package-lock.json 을 비롯한 각종 잠금 파일입니다. 실제로 files 에 .npmrc 와 .env 를 함께 적어 목록을 뽑아 보면 .env 는 들어오고 .npmrc 는 들어오지 않습니다. 인증 토큰이 담기는 파일이 막혀 있다는 점은 다행이지만, 반대로 .env 처럼 이름만 비슷한 파일은 적힌 대로 그냥 들어옵니다.
잠금 파일이 빠지는 것도 자주 놀라는 지점입니다. package-lock.json 과 yarn.lock 은 기본 제외 목록에 있고 files 로도 되살릴 수 없습니다. 애플리케이션 저장소의 감각으로 잠금 파일이 함께 배포될 것이라 기대하면 어긋납니다.
빌드 없이 목록만 확인하다가 산출물이 빠진 채로 통과하는 경우도 있습니다. dist 를 files 에 적어 두었는데 그 자리에 파일이 없으면 목록에 아무것도 나타나지 않을 뿐, 오류가 나지는 않습니다. 목록을 볼 때는 들어가면 안 되는 것이 있는지만 보지 말고, 있어야 할 것이 개수만큼 있는지도 함께 봐야 합니다.
마지막으로 버전 차이입니다. 여기 적은 출력은 npm 11.12.1 에서 받은 것이고, gitignore-fallback 경고처럼 비교적 최근에 추가된 안내도 섞여 있습니다. 다른 버전에서는 경고 문구나 폴더 단위 동작이 다를 수 있습니다. 팀에서 쓰는 버전으로 한 번 돌려 보고 그 결과를 기준으로 삼는 것이 좋습니다.

# files 에 적어도 되살아나지 않는 파일이 있다
# "files": ["dist", ".npmrc", ".env"]
npm pack --dry-run --json
# [".env", "README.md", "dist/index.js", "dist/index.js.map", "package.json"]
#   ↑ .env 는 들어왔고 .npmrc 는 끝내 들어오지 않는다

# 있어야 할 것이 빠지지 않았는지도 개수로 확인한다
npm pack --dry-run --json | node -e '
const files = JSON.parse(require("fs").readFileSync(0, "utf8"))[0].files.map(f => f.path);
console.log(files.filter(p => p.startsWith("dist/")).length + "개의 산출물");
'

.npmrc 가 막히는 것은 문서에 적힌 동작입니다. 반대로 말하면 그 목록에 없는 파일은 적은 대로 들어갑니다.

조치 후 확인할 것

  • npm pack --dry-run 을 패키지 루트에서 돌려 파일 목록과 total files 개수를 눈으로 확인했는지 봅니다. 설정만 보고 판단하지 않습니다.
  • .npmignore 를 새로 만들었다면, .gitignore 에만 있던 제외 규칙 중 배포에도 필요한 줄을 모두 옮겨 적었는지 확인합니다.
  • 빌드 산출물 폴더가 목록에 있는지, 파일 개수가 빌드 결과와 맞는지 확인합니다. 비어 있어도 오류 없이 통과합니다.
  • npm pack 으로 실제 tgz 를 만들어 tar -tf 로 열어 보고, 확인이 끝나면 그 파일을 지웁니다.
  • --json 출력에서 .env·인증서·비밀 폴더 경로가 잡히면 실패하는 검사를 배포 스크립트 앞단에 걸어 둡니다.

확인한 문서

정리하면 순서는 간단합니다. 손대기 전에 목록을 한 번 뽑고, 포함할 것을 files 필드에 적고, 다시 뽑아 달라진 곳을 봅니다. 그다음 실제 tarball 을 풀어 확인하고, 같은 검사를 CI 에 남겨 둡니다. 무시 규칙을 머리로 계산하는 대신 목록을 눈으로 보는 것, 배포 사고를 막는 방법은 결국 그것 하나입니다.

반응형