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

빌드노트

로컬 npm install은 되는데 CI의 npm ci는 실패하는 이유

package.json만 바꾼 입력에서 npm install은 lock을 갱신하며 성공했지만 npm ci는 실패했다. 실제 오류의 두 버전과 lock diff로 누락을 확인하고, 기존 lock 갱신과 npm 옵션 대조 중 필요한 조치를 고른다.

package.json의 의존성 버전을 바꾼 뒤 CI가 npm ci에서 멈췄다면, 로컬의 npm install이 lock 파일도 바꾸지 않았는지 확인한다. npm install은 요구 버전에 맞춰 package-lock.json을 갱신할 수 있지만, npm ci는 두 파일의 의존성이 맞지 않으면 고치지 않고 실패한다. 로컬에서 갱신된 lock을 함께 커밋하지 않았다면 CI는 다른 입력으로 설치하고 있다. npm ci 공식 문서 (새 탭에서 열림)

먼저 CI 로그의 불일치 패키지·버전과, CI가 받은 package.json·package-lock.json을 대조한다. 의도한 버전 변경이 lock에 빠졌다면 기존 lock을 갱신해 함께 전달한다. 두 파일이 맞는데 peer 의존성 충돌로 실패한다면 lock 생성 때와 CI의 npm 옵션을 비교한다. 오류 코드 하나만 보고 lock부터 지우지는 않는다.

같은 입력에서 install은 고쳤고 ci는 멈췄다

차이를 확인하려고 실제 서비스와 분리한 작은 패키지로 실험했다. 환경은 Windows 11, PowerShell 7.6.6, Node.js 22.23.2, npm 10.9.8이다. is-number를 정확한 버전 6.0.0으로 선언해 lock을 만들자 npm ci는 종료 코드 0으로 성공했다.

그다음 package.json의 버전만 7.0.0으로 바꾸고 lock은 그대로 두었다. 두 파일을 node_modules가 없는 새 폴더로 복사해 npm ci --ignore-scripts --no-audit --no-fund를 실행하자 종료 코드 1과 함께 다음 오류가 나왔다.

npm error code EUSAGE
npm error Invalid: lock file's is-number@6.0.0 does not satisfy is-number@7.0.0

요구한 것은 7.0.0인데 lock에 기록된 것은 6.0.0이라는 뜻이다. 실패 전후 lock의 내용도 같았다. 같은 불일치 파일의 다른 복사본에서 일반 npm install --ignore-scripts --no-audit --no-fund는 종료 코드 0으로 7.0.0을 설치했다. 대신 lock을 갱신했다. 이 비교에서 로컬 설치 성공은 두 파일이 원래부터 맞았다는 증거가 아니었다.

자기 프로젝트에서도 로컬 설치 후 파일만 보지 말고 실패한 CI가 받은 버전의 두 파일을 연다. 위 오류라면 package.json의 dependencies 또는 devDependencies에서 해당 선언을 찾는다. 이 실험의 lock은 packages[""].dependencies에 6.0.0이 남았고, packages["node_modules/is-number"].version도 6.0.0이었다. 오류에 나온 이름과 버전을 기준으로 읽으면 수정한 파일과 빠뜨린 파일을 구별할 수 있다.

버전 변경이 맞다면 기존 lock을 갱신한다

7.0.0으로 바꾸려던 것이 맞다면 기존 lock을 둔 채 다음 순서로 갱신하고 확인한다. Node/npm과 레지스트리 접근이 필요하며, 프로젝트의 package.json과 lock이 있는 폴더에서 실행한다. 아래는 Windows PowerShell용 npm.cmd 명령이다. CI와 같은 npm 버전·의존성 처리 옵션을 먼저 맞춘다.

npm.cmd install --package-lock-only --ignore-scripts --no-audit --no-fund
npm.cmd ci --ignore-scripts --no-audit --no-fund

첫 명령의 종료 코드가 0인지 확인하고 lock diff를 읽은 뒤 두 번째를 실행한다. PowerShell에서는 각 명령 직후 $LASTEXITCODE로 확인한다. --package-lock-only는 설치 폴더 대신 lock을 갱신하는 옵션이다. npm ci는 기존 node_modules를 지우고 설치하므로 실행 중인 앱의 설치 폴더를 대상으로 삼지 않는다. npm install의 lock 갱신 옵션 (새 탭에서 열림)

앞의 불일치 입력을 새 폴더에 복사해 실행했을 때 lock 갱신은 종료 코드 0이었고 node_modules는 생기지 않았다. 이어 실행한 npm ci도 종료 코드 0으로 7.0.0을 설치했다. 이번에는 ci 전후 lock이 바뀌지 않았다.

실제 diff에서는 lock의 루트 의존성과 패키지 항목의 버전이 모두 6.0.0에서 7.0.0으로 바뀌었다. 다운로드 주소인 resolved, 무결성 값인 integrity, Node 요구 범위인 engines도 함께 바뀌었다. lock의 버전 한 줄만 손으로 고칠 일이 아니다. npm이 만든 diff에서 의도한 패키지와 필요한 하위 의존성 변경인지 확인한다. 관련 없는 패키지나 lock 형식이 크게 바뀌면 갱신에 사용한 npm 버전과 설정부터 다시 비교한다.

버전 변경 자체가 실수였다면 package.json을 의도한 값으로 되돌린다. 변경이 맞고 diff도 확인했다면 갱신한 package-lock.json을 package.json과 함께 커밋한다. lock은 다음 설치가 같은 의존성 트리를 사용하도록 소스와 함께 보관하는 파일이다. package-lock.json의 역할 (새 탭에서 열림)

이 명령의 --ignore-scripts는 설치 입력을 확인하는 동안 lifecycle script 실행을 막는다. --no-audit와 --no-fund는 부가 요청과 안내를 줄인다. 설치 성공 뒤 프로젝트에 필요한 빌드·테스트는 기존 절차로 확인해야 한다. 위 성공은 그 검사까지 마쳤다는 뜻이 아니다.

peer 충돌이면 생성 당시 옵션도 확인한다

ERESOLVE와 함께 peer 요구 범위가 나온다면 옵션 차이도 조사한다. peer 의존성은 패키지가 함께 쓸 다른 패키지의 호환 범위를 선언한 것이다. 별도 합성 입력에서 루트는 is-number: 7.0.0, 직접 만든 패키지의 peer 요구는 ^6.0.0으로 충돌하게 했다. --legacy-peer-deps로 만든 같은 lock에 기본 npm ci를 실행하자 다음 오류와 종료 코드 1이 나왔다.

npm error code ERESOLVE
npm error Found: is-number@7.0.0
npm error peer is-number@"^6.0.0" from ci-lock-peer-demo@1.0.0

같은 lock을 다른 빈 폴더에서 생성 때와 같은 --legacy-peer-deps 옵션으로 설치하자 종료 코드 0이었다. 두 실행 모두 lock은 바뀌지 않았다. 이 결과는 옵션 차이도 실패를 만들 수 있다는 관찰이며, 충돌한 버전들이 실제로 호환된다는 증거는 아니다.

자기 프로젝트에서는 로컬과 CI 각각의 설치 작업 디렉터리에서 아래 결과를 비교한다.

npm.cmd --version
npm.cmd config get legacy-peer-deps
npm.cmd config get install-links

실험에서는 순서대로 10.9.8, false, false였다. 별도 config get은 앞선 install 명령에만 붙였던 옵션을 기억하지 않는다. 값이 같아도 실제 설치 명령의 인자, 프로젝트 .npmrc, CI 환경 변수를 함께 읽어야 한다. 공식 문서도 --legacy-peer-deps나 --install-links처럼 의존성 트리 형태에 영향을 주는 옵션은 lock 생성과 ci에서 맞추도록 설명한다. npm ci의 옵션 일치 조건 (새 탭에서 열림)

오류를 없애려고 --legacy-peer-deps를 새로 붙이는 처방으로 받아들이지는 않는다. 이 옵션은 peer 요구를 무시한다. 기존 팀 정책이라면 CI와 같은 설정을 공유하고, 정책을 바꾸려면 호환되는 패키지 조합을 정한 뒤 그 조건으로 lock을 갱신한다. legacy-peer-deps의 제한 (새 탭에서 열림)

여기서 직접 확인한 것은 npm 10.9.8의 설치 비교다. 원격 CI·Linux·npm 11·--install-links에 따른 실패와 앱 실행 호환성은 검증하지 않았다. 인증·네트워크·스크립트 오류라면 이 글의 lock 불일치와 다른 원인을 찾아야 한다.

← 전체 글 보기