본문 바로가기
빌드노트 검색

빌드노트

npm run에 붙인 옵션이 스크립트까지 가지 않는다

npm run 옵션이 자식 스크립트에서 보이지 않을 때 -- 앞뒤를 확인한다. npm 10.9.8에서 실제 argv와 npm 설정 환경 변수를 비교하고, 공백 값과 pre/post 스크립트에 전달되는 인자의 차이를 설명한다.

npm run으로 실행한 Node 스크립트에서 명령에 붙인 옵션이 보이지 않는다면, 그 옵션을 npm이 먼저 해석했는지 확인한다. 스크립트가 받을 옵션은 npm run 이름 -- 옵션처럼 -- 뒤에 둔다. 명령이 성공했다는 사실만으로 자식 프로세스가 원하는 인자를 받았다고 판단할 수는 없다.

외부 의존성이 없는 작은 패키지에서 --loglevel=warn의 위치만 바꿔 비교했다. -- 앞에서는 npm 설정이 됐고, 뒤에서는 Node 스크립트가 받은 문자열이 됐다. npm 공식 문서도 일반 위치 인자는 전달하지만, -로 시작하는 옵션을 넘기려면 --를 쓰라고 설명한다. npm run-script의 인자 전달 규칙 (새 탭에서 열림)

같은 옵션을 경계 앞뒤에 놓아 본다

아래 재현은 Windows 11, PowerShell 7.6.6, Node.js 22.23.2, npm 10.9.8에서 실행했다. Node와 npm이 설치된 빈 실습 폴더에 두 파일을 준비하면 된다. 설치할 의존성은 없다. PowerShell에서는 npm.cmd를 사용했으며, 그 안에서 npm 스크립트를 실행하는 셸은 Windows 기본값인 cmd.exe였다.

package.json에는 같은 진단 파일을 실행하는 본 스크립트와 앞뒤 스크립트를 둔다.

{
  "name": "npm-argv-boundary-lab",
  "version": "1.0.0",
  "private": true,
  "packageManager": "npm@10.9.8",
  "scripts": {
    "preprobe": "node argv.cjs",
    "probe": "node argv.cjs",
    "postprobe": "node argv.cjs"
  }
}

같은 폴더의 argv.cjs에는 다음을 저장한다. 옵션을 적용하는 프로그램이 아니라, 받은 인자와 npm 설정 환경 변수 하나를 출력하는 코드다.

console.log(JSON.stringify({
  event: process.env.npm_lifecycle_event,
  args: process.argv.slice(2),
  npmLoglevel: process.env.npm_config_loglevel ?? null
}));

파일을 실행하는 Node의 process.argv 앞 두 항목은 Node 실행 파일과 진입 파일의 경로다. 그래서 slice(2)로 그 뒤의 인자를 읽는다. 문자열 배열을 JSON으로 출력하면 공백을 포함한 값이 하나인지 둘인지도 구별할 수 있다. Node.js 22.23.2의 process.argv 설명 (새 탭에서 열림)

실습 폴더에서 버전을 확인한 뒤 두 명령을 실행한다. packageManager를 적은 것만으로 현재 실행 파일의 버전이 같다고 가정하지 않고, 실제 출력이 각각 v22.23.2, 10.9.8인지 확인한다.

node --version
npm.cmd --version
npm.cmd run probe --loglevel=warn
npm.cmd run probe -- --loglevel=warn

npm의 실행 안내와 앞뒤 스크립트 출력을 제외하고, 각 명령의 probe 행만 같은 순서로 옮기면 다음과 같았다. 두 명령 모두 종료 코드는 0이었다.

{"event":"probe","args":[],"npmLoglevel":"warn"}
{"event":"probe","args":["--loglevel=warn"],"npmLoglevel":null}

첫 명령에서는 npm이 --loglevel=warn을 자기 설정으로 읽었다. Node의 추가 인자는 빈 배열이었지만 npm_config_loglevel 환경 변수에는 warn이 있었다. 이 값을 코드에서 직접 읽을 수 있다는 사실과, 옵션 문자열을 argv로 받았다는 것은 다르다.

두 번째 명령의 독립된 --는 npm의 옵션 해석을 끝내는 경계다. 그 뒤의 --loglevel=warn은 probe에 전달됐으며, 경계로 쓴 -- 자체는 args에 없었다. 실제 도구에 적용할 때도 npm용 설정과 자식 도구용 옵션을 이 경계로 나눈다. 이 진단 파일은 전달된 문자열을 출력할 뿐, 로그 수준을 바꾸지는 않는다.

위 null은 진단 코드가 환경 변수의 부재를 표시한 값이다. npm에 로그 수준이 없다는 뜻은 아니다. npm의 기본 로그 수준은 notice다. 실험은 사용자·전역 npm 설정을 빈 파일로 분리했으며, 자신의 환경에 이미 설정값이 있으면 npmLoglevel은 다를 수 있다. npm의 loglevel 설정과 기본값 (새 탭에서 열림)

공백 값과 pre/post 스크립트도 함께 확인한다

-- 앞에 있는 모든 값이 사라지는 것은 아니다. npm.cmd run probe report를 실행했을 때 probe의 args에는 ["report"]가 있었다. report는 옵션이 아닌 일반 위치 인자다. 다음은 그 위치 인자, npm 설정, 공백이 있는 자식 인자를 한 명령에 넣은 비교다.

npm.cmd run probe report --loglevel=warn -- --label "hello world"

종료 코드는 0이었고, npm의 안내행을 뺀 세 스크립트의 실제 출력은 다음과 같았다.

{"event":"preprobe","args":[],"npmLoglevel":"warn"}
{"event":"probe","args":["report","--label","hello world"],"npmLoglevel":"warn"}
{"event":"postprobe","args":[],"npmLoglevel":"warn"}

probe에는 report와 --label, 공백을 포함한 hello world가 들어갔다. preprobe와 postprobe도 실행됐지만 추가 인자는 빈 배열이었다. npm run probe에 붙인 인자는 probe에만 전달된다. 앞뒤 스크립트에서 같은 옵션을 받을 것으로 기대하면 안 된다. 세 스크립트에 공통으로 보인 warn은 이번 호출의 npm 설정 환경 변수다.

준비 작업인 preprobe에도 같은 입력이 필요하다면 자동 인자 전달에 기대지 말고, 본 작업에서 준비 함수를 호출하며 값을 넘기거나 각 단계가 읽을 공통 입력을 명시적으로 정한다. 본 스크립트가 받은 옵션이 자동으로 앞뒤 단계의 옵션이 되지는 않는다.

공백은 별도로 묶어야 한다. 같은 패키지에서 npm.cmd run probe -- --label hello world를 실행하자 본 스크립트의 args는 ["--label","hello","world"]였다. --는 npm과 스크립트 사이의 경계를 정하고, "hello world"의 따옴표는 이 실행 환경에서 공백을 포함한 값을 한 인자로 묶었다. 경계를 추가해도 따옴표를 생략한 값까지 합쳐 주지는 않는다.

내 스크립트에서는 argv를 기준으로 좁힌다

자기 프로젝트에서도 옵션을 읽는 Node 진입점에 잠시 process.argv.slice(2) 출력을 넣어 실제 명령과 대조할 수 있다. 앞의 진단처럼 배열 경계가 드러나는 형식으로 읽는다.

  • 기대한 옵션이 배열에 없다면 npm run 이름 -- 옵션의 경계를 확인한다. npm용 옵션은 앞에, 자식 스크립트용 옵션은 뒤에 둔다.
  • 옵션은 있지만 값이 여러 항목으로 갈라졌다면 호출한 셸의 인용 방식을 확인한다. 이 실습의 공백 값은 큰따옴표로 묶었다.
  • 본 스크립트에는 있고 pre/post에는 없다면 위 전달 규칙에 맞는 결과다. 필요한 공통 입력을 어느 단계에서 받을지 바꾼다.

예제의 event 행 자체가 없다면 먼저 해당 스크립트가 실행됐는지 확인한다. 실행한 스크립트의 args가 빈 경우와는 조사할 문제가 다르다. 진입점의 배열이 기대와 같은데도 동작이 다르면, 그때는 스크립트가 쓰는 옵션 파서와 다음 프로그램을 호출하는 코드를 확인한다. 이 실험의 진단 파일에는 옵션 파서도 다음 실행 단계도 없다. 전달 성공이 특정 빌드 도구의 옵션 지원까지 보장하지는 않는다.

직접 실행한 범위는 npm 10.9.8의 단일 패키지와 합성 인자다. 확인 당시 npm v10 웹 문서는 10.9.9로 표시되어 10.9.8 태그의 공식 run-script 문서 (새 탭에서 열림)에서도 인자와 pre/post 규칙을 대조했다. Linux·macOS, 다른 npm 버전, 사용자 지정 script-shell, 중첩 npm run, 실제 번들러의 옵션 처리는 실행해 확인하지 않았다.

← 전체 글 보기