npm ci 실패와 의존성 버전 충돌 해결 과정

제이·2026년 8월 7일

문제 상황

GitHub Actions CI 환경에서 npm ci 실행 시 의존성 설치 단계에서 실패하는 문제가 발생했다.

로컬에서는 npm install이 정상적으로 완료되고 테스트도 통과했지만, CI 환경에서는 lock file 검증 과정에서 의존성 충돌 오류가 발생했다.

처음에는 단순한 npm 캐시 문제나 package-lock.json 불일치 문제라고 생각했지만, 실제 원인은 서로 다른 패키지가 요구하는 하위 의존성 버전 충돌이었다.


1. 의존성 버전 충돌 분석

문제의 핵심은 magicast와 yaml 패키지 버전 충돌이었다.

magicast 충돌

prisma의 하위 의존성인 c12는 다음 조건을 요구했다.

magicast@^0.3.5

이는 0.3.x 버전만 허용한다는 의미이다.

하지만 테스트 환경의 @vitest/coverage-v8는:

magicast@0.5.x

를 요구하고 있었다.

npm은 npm install 과정에서 dedupe(중복 제거)를 수행하면서 하나의 버전으로 통합했고, 결과적으로 magicast@0.5.4가 선택되었다.

하지만 이는 c12가 선언한 semver 범위를 만족하지 않는 상태였다.

^0.3.5 ≠ 0.5.4

yaml 충돌

비슷한 문제가 yaml 패키지에서도 발생했다.

  • swagger-jsdoc

    • yaml@2.0.0-1 요구
  • vite 내부 의존성

    • yaml@^2.4.2 요구

npm install은 이 상태에서도 경고(ELSPROBLEMS)만 출력하고 진행되었지만,

npm ci는 package-lock.json이 모든 의존성 조건을 만족하는지 엄격하게 검증하기 때문에 실패했다.


2. 잘못된 해결 방법과 새로운 문제 발생

처음에는 overrides를 사용해 모든 패키지가 최신 yaml 버전을 사용하도록 강제했다.

{
  "overrides": {
    "yaml": "^2.4.2"
  }
}

하지만 이 방법은 새로운 런타임 오류를 발생시켰다.

Cannot set properties of undefined (setting 'keepCstNodes')

원인을 확인해보니 swagger-jsdoc@6.3.0이 특정 yaml 버전의 내부 API를 직접 참조하고 있었다.

즉, 단순히 최신 버전으로 강제 변경하면:

swagger-jsdoc
      ↓
yaml@2.0.0-1 API 기대

하지만 실제:
yaml@2.4.x 적용

      ↓

내부 API 변경으로 런타임 크래시

가 발생했다.


3. Scoped Override로 해결

문제 해결을 위해 override 범위를 전체가 아닌 필요한 패키지로 제한했다.

수정 전:

{
  "overrides": {
    "yaml": "^2.4.2"
  }
}

수정 후:

{
  "overrides": {
    "vite": {
      "yaml": "^2.4.2"
    }
  }
}

이렇게 변경하면서:

  • vite는 필요한 최신 yaml 버전 사용
  • swagger-jsdoc은 기존 yaml 버전 유지

하도록 의존성 트리를 분리했다.

결과적으로 런타임 오류 없이 테스트와 빌드가 정상 동작했다.


4. 로컬 성공과 실제 lock file 불일치 문제

scoped override 적용 후 로컬에서는 정상적으로 검증되었다.

하지만 커밋된 package-lock.json에는 변경된 nested override 결과가 제대로 반영되지 않았다.

원인은 기존 node_modules가 남아있는 상태에서 npm install을 실행했기 때문으로 추정된다.

기존 설치 트리를 기반으로 npm이 일부만 재계산하면서 실제 lock file과 환경 상태가 달라졌다.


최종 해결 과정

깨끗한 환경에서 다시 의존성을 생성했다.

rm -rf node_modules
rm package-lock.json

npm install
npm ci

이후 별도의 테스트 디렉토리에서:

  • package.json
  • package-lock.json

두 파일만 가지고 npm ci 실행을 검증했다.

검증 성공 후 새롭게 생성된 lock file을 커밋했다.


배운 점

1. npm install 성공 = lock file 정상은 아니다

npm install은 의존성 충돌이 있어도 경고 수준으로 넘어가는 경우가 있다.

하지만 CI 환경에서 사용하는:

npm ci

는 lock file을 기준으로 엄격하게 검증하기 때문에 숨겨진 문제가 드러날 수 있다.

따라서 배포 환경과 동일하게 npm ci 기준으로 검증해야 한다.


2. overrides는 신중하게 사용해야 한다

overrides는 강력하지만 전역 적용하면 기존 패키지가 기대하는 API를 깨뜨릴 수 있다.

특히 오래된 라이브러리가 특정 버전 내부 동작에 의존하는 경우:

{
  "overrides": {
    "package": "new-version"
  }
}

같은 방식은 위험하다.

가능하면:

{
  "overrides": {
    "specific-package": {
      "dependency": "version"
    }
  }
}

처럼 범위를 제한해야 한다.


3. node_modules가 있는 상태의 성공은 완전한 검증이 아니다

로컬 환경에서 성공했다고 해서 CI 환경에서도 동일하게 동작한다는 보장은 없다.

의존성 문제를 검증할 때는:

rm -rf node_modules
npm ci

처럼 깨끗한 환경에서 재현해야 한다.


4. 통합 테스트의 중요성

이번 문제는 새롭게 추가한 백엔드 통합 테스트 과정에서 발견되었다.

기존 단위 테스트는 app.ts를 직접 로드하지 않는 구조였기 때문에:

swagger.ts 로드 실패
        ↓
Express 앱 초기화 실패

같은 문제를 발견하지 못했다.

실제 실행 흐름과 가까운 테스트를 추가하면 의존성 문제나 초기화 오류를 빠르게 발견할 수 있다는 것을 배웠다.


결론

이번 장애는 단순한 npm 설치 오류가 아니라,

  • semver 범위를 무시한 dedupe
  • 잘못된 overrides 적용
  • stale node_modules 환경에서 생성된 lock file
  • 테스트 범위 부족

이 복합적으로 발생한 문제였다.

앞으로 의존성 문제를 해결할 때는 단순히 버전을 맞추는 것이 아니라,

  1. 어떤 패키지가 어떤 버전을 요구하는지 분석하고
  2. override 범위를 최소화하며
  3. 깨끗한 환경에서 npm ci로 재검증하는 과정

을 반드시 거쳐야겠다.

profile
배운 것을 기록합니다.

0개의 댓글