본문 바로가기
웹개발

npm ci와 npm install 차이 — 속도가 아니라 락 파일을 대하는 태도다

by 꼼냥냥 2026. 9. 14.
728x90

npm ci와 npm install의 차이는 속도가 아니라 락 파일을 대하는 태도다. npm install은 필요하면 package-lock.json을 고쳐 쓰고, npm ci는 한 글자도 안 고치고 안 맞으면 멈춘다. 그래서 CI에서는 npm ci가 맞다. 커밋된 락 파일과 다른 트리가 설치되는 일이 구조적으로 불가능하기 때문이다. 대신 조용히 발목을 잡는 설정이 하나 있다. NODE_ENV=production이 먼저 잡혀 있으면 devDependencies가 설치되지 않는다. 빌드 도구가 거기 있으면 설치는 성공하고 빌드가 실패한다.

한 줄로 갈리는 지점

npm install은 "package.json을 만족하는 트리를 만든다"에 가깝고, npm ci는 "락 파일에 적힌 트리를 그대로 재현한다"에 가깝다.

그 차이가 다섯 가지 동작으로 나온다. npm 공식 문서가 명시한 내용이다.

  1. 락 파일이 반드시 있어야 한다. package-lock.json이나 npm-shrinkwrap.json이 없으면 npm ci는 동작하지 않는다.
  2. package.json과 락 파일이 안 맞으면 에러로 끝난다. 락 파일을 고쳐서 맞추지 않는다.
  3. node_modules가 있으면 먼저 지운다. 남아 있던 것 위에 덧설치하지 않는다.
  4. package.json도 락 파일도 절대 쓰지 않는다. 설치가 사실상 얼어 있다.
  5. 패키지 하나만 추가할 수 없다. 프로젝트 전체 단위로만 설치한다.

반대로 npm install은 node_modules나 package.json을 바꾸는 작업이면 락 파일을 자동으로 다시 생성한다. (문서)

왜 CI에서 npm install이 위험한가

락 파일의 목적은 어디서 설치하든 같은 트리가 나오게 하는 것이다. 팀원과 CI가 같은 의존성을 받게 하고, node_modules를 커밋하지 않고도 과거 상태로 돌아갈 수 있게 한다. 그래서 문서도 이 파일을 저장소에 커밋하라고 전제한다.

그런데 CI에서 npm install을 쓰면 이 전제가 흔들린다.

  • 누군가 package.json만 고치고 락 파일을 안 올렸다 → CI가 알아서 새 트리를 만들고 통과한다
  • 로컬과 CI에 설치된 트리가 달라진다 → "내 PC에서는 되는데"가 생긴다
  • CI가 락 파일을 고쳤는데 그건 커밋되지 않는다 → 다음 빌드도 또 새로 푼다. 고친 것과 반영된 것이 다른 구조는 이런 식으로 조용히 반복된다

npm ci라면 첫 번째 줄에서 멈춘다. 어긋남을 실패로 드러내는 것이 CI에 필요한 성질이다.

조용히 실패하는 설정 — NODE_ENV=production

이게 실제로 사람을 오래 붙잡는다.

npm ci에는 설치 트리에서 뺄 의존성 종류를 정하는 omit 설정이 있다. 기본값이 이렇다.

omit
  기본값: NODE_ENV=production 이면 'dev'
          그 외에는 비어 있음

환경변수 하나로 기본 동작이 바뀐다. CI 파이프라인 앞단에서 NODE_ENV=production을 전역으로 잡아 두면, npm ci가 devDependencies를 빼고 설치한다.

TypeScript·webpack·vite 같은 빌드 도구는 보통 devDependencies에 있다. 결과는 이렇게 된다.

npm ci          → 성공
npm run build   → 실패 (빌드 도구가 없다)

설치 단계는 초록불이고 빌드 단계에서 터진다. 에러 메시지도 "명령을 찾을 수 없다" 류라 설치를 의심하지 않게 된다.

해결은 둘 중 하나다.

  • NODE_ENV=production을 빌드가 끝난 뒤, 실행 단계에서만 설정한다
  • 빌드 단계에서 명시적으로 dev를 포함한다
npm ci --include=dev
npm run build

실행 이미지에서만 dev를 빼고 싶으면 빌드와 실행을 단계로 나눈다. 빌드 단계는 전부 설치하고, 실행 단계에서만 --omit=dev로 설치한다.

스크립트도 기본으로 돈다

ignore-scripts의 기본값은 false다. 즉 npm ci도 패키지의 설치 스크립트를 실행한다.

"ci는 락 파일대로만 까니까 안전하다"는 락 파일의 버전 이야기지, 그 버전이 설치 중에 무엇을 실행하는지와는 별개다. 의존성에 설치 스크립트가 있으면 CI에서 그대로 돈다.

로컬에서는 무엇을 쓰나

용도가 다르다.

상황 쓰는 것
의존성을 추가·삭제·업그레이드한다 npm install (락 파일이 갱신되고, 그걸 커밋한다)
클론 직후, 또는 브랜치를 옮겼다 npm ci (락 파일 그대로 깨끗하게)
CI·배포 빌드 npm ci

락 파일을 바꾸는 사람은 install, 락 파일을 믿는 사람은 ci로 나누면 헷갈리지 않는다.

npm ci가 node_modules를 지우고 시작한다는 점도 로컬에서는 장점이다. 브랜치를 오가다 쌓인 찌꺼기가 섞여 "어제는 됐는데"가 생기는 걸 막는다.

FAQ

npm ci가 락 파일과 package.json이 안 맞는다는 에러로 멈춰요

정상 동작이다. 누군가 package.json만 고치고 락 파일을 안 올린 것이다. 로컬에서 npm install로 락 파일을 갱신해 커밋하면 풀린다. CI에서 npm install로 바꿔 우회하면 문제를 덮는 것이다.

package-lock.json과 npm-shrinkwrap.json은 뭐가 다른가요

형식과 역할은 같다. 차이는 배포다. package-lock.json은 패키지로 배포되지 않고, npm-shrinkwrap.json은 배포에 포함된다. 둘 다 있으면 shrinkwrap이 우선한다. 애플리케이션이라면 package-lock.json이면 충분하다.

npm ci가 더 빠른가요

빠른 경우가 많지만 그게 핵심은 아니다. node_modules를 지우고 시작하므로 캐시가 없는 환경에서는 오히려 매번 전부 받는다. 고르는 기준은 속도가 아니라 락 파일을 고칠 권한을 줄 것이냐다.

확인한 것과 확인하지 못한 것

이 글의 동작은 npm 공식 문서(v11)에서 확인했다. npm ci의 다섯 가지 동작(락 파일 필수, 불일치 시 에러, node_modules 선삭제, 파일을 쓰지 않음, 개별 추가 불가), omit 기본값이 NODE_ENV=production일 때 dev가 되는 것, ignore-scripts 기본값 false, 락 파일이 커밋 대상이라는 것, 그리고 shrinkwrap과의 배포 차이가 그렇다.

확인하지 못한 것은 속도 수치다. 환경과 캐시에 따라 크게 달라서 "몇 배 빠르다" 같은 숫자는 넣지 않았다. 문서도 속도를 차이점으로 내세우지 않는다.

NODE_ENV는 npm 버전에 따라 동작이 달라져 왔다. 이 글은 v11 문서 기준이다. 오래된 npm을 쓴다면 해당 버전 문서에서 omit·production 설정을 따로 확인하는 편이 정확하다.

댓글