본문 바로가기

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

npm ci가 CI에서만 실패한다면 package-lock.json부터 보세요

반응형

환경  npm 11 문서 기준 · 2026-07-31 공식 문서 확인

로컬 npm install 은 조용히 락파일을 고쳐 주지만 npm ci 는 고치지 않고 멈춥니다. 같은 저장소에서 결과가 갈리는 이유와 되돌리는 순서를 공식 문서 기준으로 정리했습니다.

npm ci 는 락파일을 고치는 대신 멈춘다

npm 공식 문서는 npm ci 를 테스트 플랫폼이나 지속적 통합, 배포처럼 자동화된 환경에서 쓰기 위한 명령으로 소개합니다. 그리고 두 파일이 어긋났을 때의 동작을 분명히 적어 둡니다. 락파일의 의존성이 package.json 과 맞지 않으면 락파일을 갱신하는 대신 오류를 내고 종료한다는 것입니다.

같은 문서는 npm ci 가 package.json 이나 어떤 락파일에도 쓰지 않는다고 못박습니다. 설치가 사실상 동결되어 있다는 표현을 씁니다. 그래서 로컬의 npm install 과 CI 의 npm ci 는 같은 상황에서 정반대로 반응합니다. 한쪽은 조용히 락파일을 고쳐서 넘어가고, 다른 쪽은 고치지 않기 때문에 멈춥니다.

이 차이를 알고 나면 재실행이 왜 소용없는지가 설명됩니다. 실패는 네트워크나 캐시의 문제가 아니라 저장소에 커밋된 두 파일의 상태 자체이고, 그 상태가 바뀌지 않는 한 몇 번을 돌려도 같은 지점에서 끝납니다.

# CI 와 같은 조건을 로컬에서 먼저 재현한다
npm ci

# 어떤 npm 이 도는지도 같이 확인 (버전에 따라 메시지 문구가 다르다)
npm -v

# 커밋된 락파일이 실제로 있는지 확인 — 없으면 npm ci 는 시작조차 하지 않는다
git ls-files package-lock.json npm-shrinkwrap.json

문서는 npm ci 에 package-lock.json 이나 npm-shrinkwrap.json 이 반드시 있어야 한다고 적고 있습니다. 로컬에서 npm ci 가 같은 오류를 내면 원인이 CI 환경이 아니라는 뜻입니다.

재실행으로 풀리지 않는다면, 고쳐야 할 것은 파이프라인이 아니라 커밋된 두 파일입니다.

package.json 을 손으로 고치면 간극이 생긴다

npm install 문서는 두 파일의 관계를 이렇게 설명합니다. package.json 은 허용되는 버전 범위의 기준이고, 락파일은 그 범위 안에서 실제로 고정된 버전을 담습니다. 범위가 맞지 않으면 npm 이 package.json 을 만족하는 새 버전을 찾아 락파일을 갱신합니다.

문제는 이 갱신이 로컬에서만 일어난다는 점입니다. 에디터에서 버전 범위를 직접 타이핑한 뒤 npm install 을 돌리면 락파일도 함께 바뀌는데, 커밋할 때 package.json 만 담고 락파일을 빠뜨리면 저장소에는 어긋난 짝이 남습니다. 이 상태가 CI 로 넘어가 npm ci 에서 드러납니다.

되돌리는 방법은 락파일을 다시 맞춘 뒤 함께 커밋하는 것입니다. node_modules 를 건드리지 않고 락파일만 정리하고 싶다면 package-lock-only 옵션이 있습니다. 문서는 이 옵션이 켜지면 해당 작업이 node_modules 를 무시하고 package-lock.json 만 사용한다고 설명합니다.

# node_modules 는 그대로 두고 락파일만 다시 맞춘다
npm install --package-lock-only

# 무엇이 달라졌는지 눈으로 확인한 뒤에 커밋한다
git diff --stat package.json package-lock.json

# 두 파일은 항상 같은 커밋에 담는다
git add package.json package-lock.json
git commit -m "chore: package-lock.json 을 package.json 에 맞춰 갱신"

락파일만 갱신하고 diff 를 확인한 뒤 두 파일을 한 커밋에 담습니다. 따로 커밋하면 그 사이의 커밋에서 CI 가 깨집니다.

package.json 을 수정한 커밋에는 package-lock.json 이 함께 있어야 합니다.

로컬 npm 과 CI npm 이 다르면 락파일 포맷도 흔들린다

락파일 문서에는 lockfileVersion 값의 의미가 정리되어 있습니다. 1 은 npm v5 와 v6 이 쓰던 형식, 2 는 v7 과 v8 의 형식으로 1 과 하위 호환되며, 3 은 v9 이상이 쓰는 형식으로 npm v7 까지 하위 호환된다고 적혀 있습니다.

그래서 팀원마다 다른 npm 으로 락파일을 다시 만들면 버전 값과 파일 구조가 왔다 갔다 합니다. 의존성은 하나만 바꿨는데 diff 가 수천 줄로 늘어나고, 그 안에서 진짜 변경을 골라내기 어려워집니다. 어긋난 짝을 눈으로 잡아내기도 그만큼 힘들어집니다.

CI 에서 노드와 npm 버전을 고정하고, 로컬도 같은 버전을 쓰도록 맞추는 편이 낫습니다. 어떤 조합에서 어떤 오류가 나는지는 문서에 나열되어 있지 않으므로, 포맷이 갈리는 상황 자체를 만들지 않는 쪽이 확실합니다.

# 지금 커밋된 락파일이 어떤 포맷인지 본다
head -5 package-lock.json
#   "lockfileVersion": 3,   ← npm v9 이상이 만든 파일

# 저장소에 기대 버전을 명시해 둔다 (package.json)
#   "engines": { "node": ">=22", "npm": ">=11" }

# CI 도 같은 버전으로 고정한다 (GitHub Actions 예시)
#   - uses: actions/setup-node@v4
#     with: { node-version: 22, cache: npm }

락파일 첫 줄의 lockfileVersion 만 봐도 어떤 세대의 npm 이 만든 파일인지 알 수 있습니다.

설치가 동결돼 있다는 전제를 받아들이기

npm ci 는 node_modules 가 이미 있으면 설치를 시작하기 전에 그 디렉터리를 자동으로 제거한다고 문서에 적혀 있습니다. 로컬에 남아 있던 부산물이 결과에 섞이지 않는다는 뜻이라 CI 에는 유리하지만, 설치 시간이 매번 처음부터라는 뜻이기도 합니다.

또 npm ci 는 프로젝트 전체만 설치할 수 있고 개별 의존성을 추가하는 데는 쓸 수 없습니다. CI 스크립트에서 패키지 하나를 급히 끼워 넣는 식의 우회가 통하지 않는 이유입니다. 필요한 변경은 결국 저장소의 두 파일에 반영해서 커밋해야 합니다.

운영 설치라 개발 의존성을 빼는 경우도 마찬가지입니다. 문서는 omit 으로 제외한 의존성도 여전히 해석되어 락파일에 기록되며, 다만 디스크에 물리적으로 설치되지 않을 뿐이라고 설명합니다. 설치 대상에서 빠졌다고 해서 락파일에서도 사라지는 것은 아닙니다.

npm ci 의 실패는 대부분 CI 설정이 아니라 커밋 내용에 대한 신호입니다.

조치 후 확인할 것

  • package.json 을 고친 커밋에 package-lock.json 이 함께 들어 있는지 확인하세요. 빠져 있으면 그 커밋부터 CI 가 깨집니다.
  • 로컬에서 npm ci 를 직접 돌려 보세요. 같은 오류가 나면 원인은 CI 환경이 아니라 저장소 상태입니다.
  • package-lock.json 이 .gitignore 에 들어가 있지 않은지 보세요. 문서는 이 파일을 저장소에 커밋하도록 안내합니다.
  • 락파일 첫머리의 lockfileVersion 이 최근 커밋들에서 오르내리지 않는지 확인하세요. 흔들린다면 팀의 npm 버전이 갈려 있다는 뜻입니다.

확인한 문서

정리하면 순서는 단순합니다. 로컬에서 npm ci 로 재현하고, 락파일을 package.json 에 맞춰 다시 맞추고, 두 파일을 한 커밋에 담습니다. npm ci 가 아무것도 고쳐 주지 않는다는 점이 불편해 보이지만, 그 덕분에 어긋난 상태가 배포까지 흘러가지 않고 설치 단계에서 멈춥니다.

반응형