Git & GitHub

GitHub Actions npm ci 실패 해결: lockfile과 Node 버전 확인하기

2026.05.18·수정 2026.05.19·약 16분

이 글에서 정리하는 내용

GitHub Actions에서 npm ci가 실패하면 먼저 패키지 자체보다 package.json, package-lock.json, Node 버전, npm 버전, lockfile 경로가 서로 맞는지 확인해야 합니다. 로컬에서는 npm install로 넘어가던 문제가 CI에서는 더 엄격하게 드러나기 때문에, 에러 문구를 기준으로 원인을 나누어 보는 것이 빠릅니다.

내 증상이 이거면 여기부터 보세요

GitHub Actions npm ci 실패 원인을 lockfile, Node 버전, 캐시 경로로 나누어 진단하는 흐름

GitHub Actions 로그에서 npm ci가 빨간색으로 멈추면 의존성 설치가 전부 문제처럼 보입니다. 하지만 실제로는 “설치할 패키지를 못 찾았다”보다 “CI가 기준으로 삼을 의존성 상태가 로컬과 다르다”에 가까운 경우가 많습니다.

예를 들어 로컬에서는 npm install을 실행하면서 lockfile이 자동으로 갱신됩니다. 이미 만들어진 node_modules도 남아 있습니다. 반면 GitHub Actions runner는 매번 깨끗한 환경에서 시작합니다. 여기에 npm ci는 lockfile 기준 설치를 강하게 요구하므로, 로컬에서 눈에 띄지 않던 차이가 CI에서 바로 실패로 이어집니다.

에러 상황 먼저 볼 지점
package-lock.json이 없다는 오류 lockfile 생성 여부와 Git 커밋 여부
package.json과 lockfile이 맞지 않는다는 오류 로컬에서 npm install 후 lockfile 변경분 확인
특정 패키지의 engine 또는 빌드 오류 CI의 Node/npm 버전과 로컬 버전 비교
cache dependency path 관련 오류 monorepo의 lockfile 위치와 cache-dependency-path

처음에는 이 네 가지 중 어디에 가까운지만 구분해도 충분합니다. 바로 캐시를 삭제하거나 workflow 전체를 바꾸면 원인이 더 흐려질 수 있습니다. 특히 npm ci 실패와 npm run build 실패는 로그상 가까이 붙어 있어도 원인이 다를 수 있으니, 설치 단계에서 멈춘 것인지 빌드 단계까지 넘어간 것인지 먼저 확인합니다.

에러 문구별로 원인 좁히기

GitHub Actions의 로그는 길지만, npm ci 오류는 반복되는 문구가 비교적 뚜렷합니다. 아래처럼 문구를 기준으로 보면 어떤 파일을 수정해야 하는지 바로 좁힐 수 있습니다.

npm ERR! The `npm ci` command can only install with an existing package-lock.json

이 문구는 현재 npm ci를 실행한 위치에서 lockfile을 찾지 못했다는 뜻입니다. 실제로 lockfile이 없는 경우도 있고, lockfile은 있는데 workflow가 다른 폴더에서 실행되는 경우도 있습니다. 저장소 루트에 package-lock.json이 있는지, 앱이 하위 폴더에 있다면 working-directory가 맞는지 확인해야 합니다.

npm ERR! `npm ci` can only install packages when your package.json and package-lock.json are in sync

이 문구는 package.json에 적힌 의존성과 package-lock.json에 기록된 의존성 트리가 맞지 않는다는 뜻입니다. 보통 패키지를 추가하거나 삭제한 뒤 lockfile을 함께 커밋하지 않았을 때 발생합니다. 이 경우에는 workflow보다 로컬의 lockfile 상태를 먼저 바로잡아야 합니다.

The engine "node" is incompatible with this module

이 문구가 보인다면 lockfile 자체보다 Node 버전 차이를 먼저 봅니다. 로컬은 Node 20인데 CI가 Node 18로 실행되거나, 반대로 프로젝트는 오래된 Node 기준인데 CI가 최신 Node로 실행되면 일부 패키지에서 engine 조건이나 빌드 스크립트가 실패할 수 있습니다.

npm ci가 npm install과 다르게 동작하는 지점

npm install은 개발 중 의존성을 추가하거나 갱신할 때 사용하는 명령에 가깝습니다. 필요한 경우 package-lock.json을 만들거나 수정하면서 현재 package.json에 맞는 상태를 맞춥니다.

반대로 npm ci는 CI 환경에서 같은 의존성 트리를 재현하기 위한 명령입니다. lockfile이 없거나 package.json과 lockfile이 맞지 않으면 설치를 멈춥니다. 또한 기존 node_modules를 기준으로 이어서 설치하는 방식이 아니라, 깨끗한 설치 상태를 만드는 데 초점이 있습니다.

이 차이 때문에 로컬에서 npm install만 실행해 보고 “설치가 되니까 문제없다”고 판단하면 CI에서 다시 실패할 수 있습니다. CI에서 npm ci를 사용한다면 로컬에서도 한 번은 같은 명령으로 검증하는 편이 안전합니다.

npm install
npm ci
npm run build
git status

새 패키지를 추가했거나 버전을 바꿨다면 위 순서로 확인합니다. 여기서 package-lock.json이 수정되었다면 그 변경분까지 커밋해야 GitHub Actions가 같은 기준으로 설치할 수 있습니다. 반대로 lockfile 변경이 의도하지 않은 것이라면 npm 버전 차이 또는 패키지 버전 범위 변경이 있었는지 먼저 확인해야 합니다.

먼저 적용할 workflow 예시

단일 프로젝트라면 workflow를 복잡하게 만들기보다 설치와 빌드 단계를 명확히 나누는 것이 좋습니다. actions/checkout, actions/setup-node, npm ci, npm run build를 분리하면 어느 지점에서 실패했는지 로그를 읽기 쉬워집니다.

name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

여기서 node-version은 프로젝트 기준에 맞춰 정해야 합니다. 로컬에서 Node 20으로 개발하고 있는데 CI만 Node 18이나 22를 쓰면 설치 단계는 통과해도 빌드나 테스트에서 다른 결과가 나올 수 있습니다.

cache: npm은 설치 속도를 줄이기 위한 설정입니다. 문제 해결의 핵심은 아닙니다. lockfile이 맞지 않으면 캐시를 켜도 실패하고, 캐시를 꺼도 실패합니다. 캐시 설정은 lockfile과 Node 버전이 정리된 뒤에 점검해도 늦지 않습니다.

package-lock.json을 다시 맞추는 방법

lockfile 문제는 대체로 패키지를 추가하거나 버전을 바꾼 뒤 package-lock.json을 같이 커밋하지 않아서 생깁니다. 다른 사람이 이미 lockfile을 수정한 브랜치에 내 변경분을 합치면서 충돌이 난 뒤, 파일을 대충 정리했을 때도 같은 문제가 생길 수 있습니다.

먼저 로컬에서 CI와 비슷한 상태를 만들어 봅니다. macOS나 Linux 환경이라면 아래처럼 확인할 수 있습니다.

rm -rf node_modules
npm install
npm ci
npm run build

Windows PowerShell을 사용한다면 rm -rf 대신 아래 명령을 쓰면 됩니다.

Remove-Item -Recurse -Force node_modules
npm install
npm ci
npm run build

npm install을 실행하면 현재 package.json에 맞게 lockfile이 정리됩니다. 그다음 npm ci를 직접 실행해서 CI와 같은 방식으로 설치 가능한지 확인합니다. 이 단계가 로컬에서도 실패하면 GitHub Actions 문제가 아니라 저장소의 의존성 기준이 맞지 않는 것입니다.

git diff package.json package-lock.json

변경된 이유가 패키지 추가, 패키지 제거, 버전 변경처럼 명확하다면 lockfile 변경분을 함께 커밋합니다. 이유 없이 lockfile 전체가 크게 바뀌었다면 바로 커밋하지 말고 npm 버전 차이로 인한 포맷 변경인지 확인합니다. 팀 프로젝트에서는 이런 lockfile 전체 변경이 나중에 충돌을 크게 만들 수 있습니다.

git add package.json package-lock.json
git commit -m "Fix npm lockfile for CI"

package-lock.json은 단순한 자동 생성 파일이 아니라 의존성 재현을 위한 기준 파일입니다. 라이브러리 패키지가 아니라 일반적인 애플리케이션 프로젝트라면 저장소에 포함해 두는 것이 CI 안정성 측면에서 유리합니다.

Node/npm 버전 차이를 줄이는 방법

lockfile을 맞췄는데도 실패한다면 Node와 npm 버전을 확인합니다. 오래된 프로젝트를 새 runner에서 돌리거나, 로컬은 nvm으로 특정 버전을 쓰는데 workflow에는 버전이 명시되지 않은 경우가 흔합니다.

node -v
npm -v

로컬에서 위 명령으로 버전을 확인한 뒤 workflow의 node-version과 맞춥니다. 프로젝트 안에 .nvmrc를 두면 로컬과 CI가 같은 기준을 공유하기 쉽습니다.

20

.nvmrc를 사용한다면 workflow에서도 해당 파일을 기준으로 Node 버전을 읽게 만들 수 있습니다.

- name: Setup Node
  uses: actions/setup-node@v4
  with:
    node-version-file: .nvmrc
    cache: npm
    cache-dependency-path: package-lock.json

팀에서 npm 버전까지 고정하고 싶다면 package.jsonpackageManager 필드를 사용할 수 있습니다. 다만 기존 프로젝트에 새로 추가할 때는 로컬 설치 결과와 CI 결과가 바뀌지 않는지 먼저 확인해야 합니다.

{
  "packageManager": "npm@10.8.2",
  "engines": {
    "node": ">=20 <21"
  }
}

버전 고정은 문제를 줄이는 장치입니다. “내 PC에서는 되는데 Actions에서는 안 된다”는 상황이 반복된다면 workflow에 Node 버전을 명시하지 않았는지부터 확인합니다.

monorepo에서 자주 놓치는 경로 설정

monorepo에서는 package-lock.json이 저장소 루트에 없을 수 있습니다. 예를 들어 실제 앱은 apps/web에 있고 lockfile도 apps/web/package-lock.json에 있는데, workflow가 루트에서 npm ci를 실행하면 lockfile을 찾지 못합니다.

name: Web CI

on:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: apps/web

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: apps/web/package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

defaults.run.working-directoryrun 명령이 실행될 위치를 바꿉니다. 하지만 actions/setup-nodecache-dependency-path는 lockfile 경로를 별도로 알려줘야 합니다. 둘을 같은 설정으로 착각하면 설치 명령은 하위 폴더에서 실행되는데 캐시는 루트 lockfile을 찾는 식으로 엇갈릴 수 있습니다.

앱이 여러 개라면 더 주의해야 합니다. apps/admin, apps/web이 각각 다른 lockfile을 가진 구조인지, 루트 하나의 lockfile로 모든 workspace를 관리하는 구조인지 먼저 정해야 합니다. 이 기준이 섞이면 CI 설정도 계속 흔들립니다.

다음에 같은 문제를 줄이는 체크리스트

GitHub Actions npm ci 오류 해결 후 package-lock.json과 workflow 설정을 검증하는 흐름

한 번 고친 뒤에는 같은 문제가 다시 생기지 않도록 기준을 남겨두는 것이 중요합니다. 특히 여러 사람이 패키지를 추가하는 프로젝트에서는 lockfile 충돌과 버전 차이가 쉽게 누적됩니다.

  • 패키지를 추가하거나 제거한 뒤에는 package.jsonpackage-lock.json을 함께 확인합니다.
  • PR을 올리기 전에 로컬에서 npm ci를 한 번 실행합니다.
  • workflow에는 node-version 또는 node-version-file을 명시합니다.
  • monorepo라면 working-directorycache-dependency-path를 따로 확인합니다.
  • 캐시 오류가 나더라도 먼저 lockfile 위치와 Node 버전 기준을 확인합니다.
  • lockfile 전체가 크게 바뀌면 npm 버전 차이로 인한 변경인지 확인한 뒤 커밋합니다.

CI 설정은 한 번 통과했다고 끝나는 파일이 아닙니다. 의존성, Node 버전, 패키지 매니저 버전이 바뀔 때마다 같이 영향을 받습니다. 그래서 workflow를 너무 복잡하게 만들기보다 설치, 빌드, 테스트 단계를 분리하고 로그를 읽기 쉬운 상태로 유지하는 것이 좋습니다.

정리

GitHub Actions에서 npm ci가 실패하면 먼저 package-lock.json이 있는지, package.json과 맞는지 확인해야 합니다. 그다음 로컬과 CI의 Node/npm 버전을 비교하고, monorepo라면 lockfile 경로와 실행 위치를 따로 점검합니다.

대부분의 문제는 workflow를 크게 바꾸지 않아도 해결됩니다. package-lock.json을 함께 커밋하고, Node 버전을 명시하고, cache-dependency-path를 실제 lockfile 위치에 맞추면 CI 실패 원인을 상당히 줄일 수 있습니다.

처음부터 캐시나 runner 문제로 접근하면 시간이 길어질 수 있습니다. npm ci는 의존성 기준을 엄격하게 확인하는 명령이라는 점을 먼저 잡고 보면, GitHub Actions 로그에서 봐야 할 부분이 비교적 명확해집니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기