빌드노트
터미널에서는 없는 명령이 npm run에서는 실행된다
npm run에서만 도구를 찾는 이유를 로컬 .bin 검색 경로로 확인한다. 합성 패키지의 실제 실행 경로와 버전을 대조하고, 전역 설치를 추가하기 전에 프로젝트 도구를 선택하는 방법을 설명한다.
프로젝트의 npm 스크립트로는 도구가 실행되는데, 같은 폴더의 터미널에서 도구 이름만 입력하면 “명령을 찾을 수 없다”고 나올 때가 있다. 이 차이만 보고 전역 설치부터 할 필요는 없다. npm이 스크립트에 넘기는 PATH에 로컬 실행 파일 경로를 추가하기 때문일 수 있다.
PATH는 명령 이름으로 실행 파일을 찾을 때 살펴보는 디렉터리 목록이다. npm 공식 문서는 npm run이 기존 PATH에 node_modules/.bin을 추가하므로 로컬 의존성이 제공하는 명령을 이름만으로 쓸 수 있다고 설명한다. 이 환경은 스크립트에 전달된다. 이미 열려 있는 터미널의 검색 경로까지 바꿔 주는 것은 아니다. npm run-script 설명 (새 탭에서 열림)
같은 폴더에서 검색 결과를 비교했다
확인을 위해 외부 의존성이 없는 합성 패키지 두 개를 만들었다. 도구 패키지 cb-local-bin-tool의 버전은 1.2.3이고, bin에는 "cb-local-bin-probe": "cli.cjs"를 지정했다. 소비자 패키지는 이 도구를 file:../tool 개발 의존성으로 설치하고, probe 스크립트에서 cb-local-bin-probe를 실행했다.
여기서 패키지 이름과 명령 이름이 다르다는 점을 봐 두자. bin은 명령 이름을 패키지 안의 실행 파일에 연결하는 항목이다. 로컬 설치에서 그 실행 진입점이 놓이는 곳이 node_modules/.bin이다. Windows에서는 이번 설치로 .cmd 파일도 생성됐다. package.json의 bin (새 탭에서 열림), 로컬 실행 파일 위치 (새 탭에서 열림)
Windows 11, Node.js 22.23.2, npm 10.9.8에서 설치와 실행을 비교했다. 설치는 전용 캐시와 오프라인 옵션을 사용했고 전역 설치나 npx는 실행하지 않았다. 비교용 일반 cmd.exe와 npm에는 같은 짧은 기본 PATH를 전달했다. Node 디렉터리·System32·Windows만 넣었으며 사용자나 시스템의 PATH는 수정하지 않았다.
소비자 폴더는 같았지만 일반 셸의 PATH에는 소비자의 .bin이 없었고, npm 스크립트에서는 첫 항목이었다. 일반 셸의 명령 실행은 종료 코드 1과 함께 다음 오류를 냈다.
'cb-local-bin-probe' is not recognized as an internal or external command,
operable program or batch file.
반면 npm 스크립트 안의 where.exe cb-local-bin-probe는 소비자의 .bin 아래 파일들을 찾았다. 그중 .cmd 파일은 ..\cb-local-bin-tool\cli.cjs를 Node로 실행하는 내용이었다. npm run probe의 종료 코드는 0이었고, 도구가 직접 출력한 버전과 실행 경로도 로컬 패키지를 가리켰다. 아래는 긴 앞부분 경로를 생략한 관찰값이다.
toolVersion: 1.2.3
argv[1]: ...\consumer\node_modules\cb-local-bin-tool\cli.cjs
realBin: ...\tool\cli.cjs
Node의 process.argv[1]은 실행 진입점의 경로다. 여기서는 그 경로를 실제 파일로 해석한 값도 함께 출력했다. file: 의존성이 원본 도구 폴더로 연결되어 있어 두 경로의 모양이 달랐지만, 같은 cli.cjs였다. 단순히 “명령이 성공했다”보다 어떤 패키지가 실행됐는지 확인하는 데 도움이 됐다. Node.js의 process.argv (새 탭에서 열림)
일반 셸에서도 .\node_modules\.bin\cb-local-bin-probe.cmd처럼 경로를 지정하자 동일한 버전과 파일이 실행됐다. 파일이 없었던 것이 아니라, 이름만으로 찾는 경로에 없었던 것이다. npm 실행 뒤 새 일반 셸을 다시 열어 확인해도 기본 PATH는 그대로였다.
내 프로젝트에서는 검색 경로와 연결 대상을 본다
다음은 Windows PowerShell에서 확인하는 방법이다. Node와 npm을 실행할 수 있고, 해당 도구가 이미 로컬 의존성으로 설치된 프로젝트의 package.json 폴더에서 시작한다. 아래의 cb-local-bin-probe는 확인할 실제 명령 이름으로, cb-local-bin-tool은 그 명령을 제공하는 패키지 이름으로 바꾼다. 패키지의 bin 항목에서 두 이름의 관계를 확인할 수 있다.
우선 같은 폴더에 다음 내용으로 path-check.cjs를 만든다. 전체 환경 대신 검색 경로만 한 줄씩 출력한다.
const path = require('node:path');
const key = Object.keys(process.env).find(k => k.toLowerCase() === 'path');
console.log(process.env[key].split(path.delimiter).join('\n'));
기존 package.json의 scripts에 아래 항목을 합친다. 다른 스크립트는 유지하고, 이름이 겹치면 진단용 이름을 바꾼다.
{
"scripts": {
"bin-path": "node path-check.cjs",
"bin-where": "where.exe cb-local-bin-probe",
"probe": "cb-local-bin-probe"
}
}
다음을 각각 실행한다. 일반 셸의 where.exe가 찾지 못해도 뒤의 npm 스크립트와 비교한다.
node .\path-check.cjs
npm.cmd run --silent bin-path
where.exe cb-local-bin-probe
npm.cmd run --silent bin-where
Get-Content .\node_modules\.bin\cb-local-bin-probe.cmd
이번 실험에서는 첫 출력에 없던 소비자의 .bin이 두 번째 출력의 맨 앞에 나타났다. 일반 where.exe는 실패했고 npm 안의 검색은 그 디렉터리를 찾았다. 마지막 명령으로 읽은 .cmd에서 어느 패키지의 어느 파일을 호출하는지도 확인했다. 같은 결과라면 전역 설치를 추가하기보다 프로젝트 스크립트로 실행하면 된다.
설치된 패키지 버전은 다음처럼 읽을 수 있다. 첫 줄은 패키지 메타데이터를 읽는 것이며, 그 자체가 실행 파일 선택을 증명하지는 않는다. 앞에서 찾은 .cmd의 연결 대상과 함께 판단한다. 이 합성 도구는 실행 시에도 버전과 경로를 출력하므로 두 실행의 결과를 대조할 수 있었다. 실제 도구에서는 문서에 있는 버전 확인 옵션을 사용한다.
node -p "require('./node_modules/cb-local-bin-tool/package.json').version"
npm.cmd run probe
.\node_modules\.bin\cb-local-bin-probe.cmd
로컬 .cmd가 없다면 의존성이 실제로 설치되어 있는지, 명령 이름이 bin의 키와 맞는지부터 확인한다. npm 안의 검색이 다른 디렉터리를 가리킨다면 그곳의 도구를 실행하고 있을 수 있다. 이번 npm의 PATH에는 상위 폴더들의 .bin과 기존 기본 경로도 들어 있었다. 따라서 npm run의 성공만으로 바로 이 프로젝트의 도구 버전이 실행됐다고 확정하지 않고, 검색된 경로까지 확인한다.
이번 실행은 Windows의 기본 npm 스크립트 셸인 cmd.exe에서 확인했다. 첫 시도에서는 작업 환경의 긴 PATH를 상속한 상태에서 기본 명령 검색부터 실패해 비교에 쓰지 못했고, 위 비교에는 두 조건에 동일한 짧은 경로를 전달했다. Unix 계열의 기본 셸은 문서상 /bin/sh지만 이번에 실행하지 않았으며, .cmd를 읽고 실행하는 진단 명령도 Windows용이다. 실제 도구 두 버전이 충돌하는 환경은 재현하지 않았다.