package.json scripts와 npm run 정리
Node 프로젝트를 열어보면 package.json 안에 scripts라는 항목이 자주 보인다.
Next.js 프로젝트에서는 npm run dev를 실행하고, 테스트가 있는 프로젝트에서는 npm test를 실행한다.
처음에는 이 명령어들이 npm에 원래 들어있는 기능처럼 보이지만, 대부분은 프로젝트의 package.json에 적힌 스크립트를 실행하는 방식이다.
npm run은 package.json의 scripts 객체에 있는 명령어를 실행한다.
프로젝트마다 dev, build, lint, test가 다르게 동작하는 이유도 각 프로젝트의 scripts 내용이 다르기 때문이다.
scripts의 역할
scripts는 프로젝트에서 자주 쓰는 명령어에 이름을 붙여두는 공간이다.
긴 명령어를 매번 직접 치지 않고, 짧은 이름으로 실행할 수 있게 해준다.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint .",
"test": "vitest run"
}
}이 설정이 있으면 아래 명령어를 실행할 수 있다.
npm run dev
npm run build
npm run lint
npm testscripts는 npm이 프로젝트별로 실행할 명령어 목록을 읽는 약속이라고 보면 된다.
팀원이 같은 저장소를 내려받으면 같은 이름의 명령어를 사용할 수 있어서 협업에도 편하다.
npm run dev의 의미
npm run dev는 npm에 내장된 개발 서버 명령어가 아니다.package.json의 scripts.dev에 적힌 명령어를 실행하는 것이다.
{
"scripts": {
"dev": "next dev"
}
}이 경우 npm run dev는 실제로 next dev를 실행한다.
Vite 프로젝트라면 dev 값이 vite일 수 있고, 다른 프레임워크라면 전혀 다른 명령어일 수 있다.
그래서 프로젝트에서 어떤 명령어가 실행되는지 궁금하면 먼저 package.json의 scripts를 보면 된다.
README보다 scripts가 더 정확한 경우도 많다.
npm start와 npm run start
start script가 정의되어 있으면 npm start와 npm run start는 보통 같은 start script를 실행한다고 이해하면 된다.
{
"scripts": {
"start": "next start"
}
}npm start
npm run start둘 다 next start를 실행한다.
다만 npm start는 npm CLI에 있는 별도 명령이고, npm 문서 기준으로 루트에 server.js가 있는데 start script가 없으면 node server.js가 기본값으로 사용될 수 있다.
일반적인 프론트엔드 프로젝트에서는 scripts.start를 명시해두는 경우가 많다.
test, stop, restart도 비슷하게 짧은 명령어가 있다.
그래서 npm test는 보통 npm run test와 같은 식으로 이해할 수 있다.
node_modules/.bin을 생략하는 이유
프로젝트에 설치한 CLI 도구는 보통 node_modules/.bin 아래에 실행 파일을 만든다.
예를 들어 eslint, prettier, vitest, next 같은 도구가 여기에 연결될 수 있다.
직접 실행하려면 아래처럼 써야 할 것 같지만, scripts 안에서는 보통 이렇게 쓰지 않는다.
{
"scripts": {
"lint": "node_modules/.bin/eslint ."
}
}npm은 script를 실행할 때 node_modules/.bin을 PATH에 추가한다.
그래서 scripts 안에서는 아래처럼 짧게 쓸 수 있다.
{
"scripts": {
"lint": "eslint ."
}
}이 덕분에 전역 설치에 기대지 않고 프로젝트에 설치된 버전의 도구를 실행할 수 있다.
패키지가 dependencies에 있는지 devDependencies에 있는지 헷갈린다면 dependencies와 devDependencies 차이를 같이 보면 좋다.
인자 전달 방식
script에 추가 인자를 넘길 때는 --를 사용한다.-- 뒤에 있는 값은 npm이 해석하지 않고, 실행되는 script 쪽으로 넘긴다.
{
"scripts": {
"test": "vitest run"
}
}npm run test -- --watch이 명령어는 vitest run --watch처럼 동작한다.--watch가 npm 옵션이 아니라 vitest 옵션이라는 점을 명확히 하기 위해 --를 끼워 넣는 것이다.
자주 쓰는 옵션이라면 script 이름을 따로 만드는 편이 더 읽기 좋다.
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}이렇게 두면 npm run test:watch처럼 의미 있는 이름으로 실행할 수 있다.
pre와 post script
npm은 같은 이름 앞에 pre와 post를 붙인 script를 특별하게 처리한다.
예를 들어 build 앞뒤로 실행할 명령어를 만들 수 있다.
{
"scripts": {
"prebuild": "npm run lint",
"build": "next build",
"postbuild": "node scripts/check-output.js"
}
}npm run build를 실행하면 prebuild, build, postbuild 순서로 실행된다.
빌드 전에 lint를 돌리거나, 빌드 후 산출물을 검사하는 용도로 쓸 수 있다.
다만 너무 많은 동작을 숨겨두면 명령어 하나가 무겁고 예측하기 어려워진다.
초보자에게는 build, lint, test처럼 직접 실행할 수 있는 script를 분리해두는 구성이 더 읽기 좋다.
CI에서 쓰는 scripts
CI에서는 사람이 직접 명령어를 기억하지 않아도 되도록 scripts를 조합해서 사용한다.
예를 들어 아래처럼 검증 명령어를 모아둘 수 있다.
{
"scripts": {
"lint": "eslint .",
"test": "vitest run",
"build": "next build",
"check": "npm run lint && npm test && npm run build"
}
}CI 설정에서는 npm run check만 실행하면 된다.
프로젝트 내부의 실제 검사 흐름이 바뀌어도 CI 파일을 크게 바꾸지 않아도 된다.
의존성을 설치하는 단계에서는 npm install보다 npm ci를 쓰는 경우가 많다.
이 차이는 npm ci란?에 정리해두었다.
설치 옵션을 프로젝트에 고정해야 한다면 .npmrc 파일이란?도 함께 연결해서 보면 좋다.
정리
scripts는 프로젝트 명령어에 이름을 붙여두는 공간이다.npm run dev는 scripts.dev에 적힌 명령어를 실행한다.npm start와 npm test는 자주 쓰는 script를 짧게 실행하는 명령어로 이해할 수 있다.
npm은 script 실행 시 node_modules/.bin을 PATH에 넣어준다.
그래서 eslint, next, vitest 같은 로컬 CLI를 scripts 안에서 바로 사용할 수 있다.
추가 인자를 넘길 때는 npm run script -- 인자 형태를 사용한다.
명령어가 길어지거나 자주 반복된다면 새로운 script 이름을 만들어두는 편이 좋다.
참고 자료








