레이블이 GitHub Actions인 게시물을 표시합니다. 모든 게시물 표시
레이블이 GitHub Actions인 게시물을 표시합니다. 모든 게시물 표시

토요일

GitHub Actions Node.js 빌드 실패 완전 정리 — npm ci, 캐시, 의존성 에러 해결

🔍 검색 키워드: GitHub Actions npm ci 실패, npm ci can only install packages when your package.json and package-lock.json are in sync, npm ci package-lock.json 없음, EBADENGINE GitHub Actions, setup-node cache 설정, npm ci vs npm install CI

요약: GitHub Actions에서 npm ci가 실패하는 가장 흔한 원인은 package.json과 package-lock.json이 서로 맞지 않거나, lock 파일이 저장소에 없거나, Node 버전이 engines 조건과 맞지 않는 경우입니다. 로컬에서 npm install로 lock 파일을 갱신해 함께 커밋하고, workflow의 Node 버전을 package.json과 맞추면 해결됩니다.

npm ci는 npm install과 무엇이 다른가요?

npm ci는 CI처럼 재현 가능한 설치가 필요한 환경을 위한 명령입니다. package-lock.json을 기준으로 의존성을 정확히 설치하고, lock과 package.json이 맞지 않으면 아예 실패합니다. npm install은 이런 불일치를 조용히 고치면서 lock 파일을 수정하는 점이 다릅니다. 그래서 로컬에서는 멀쩡한데 CI에서만 실패하는 일이 생깁니다.

직접 재현한 결과

아래 출력은 로컬에서 직접 실행한 결과이며 GitHub Actions 러너에서 실행한 것은 아닙니다. 같은 npm 명령이므로 에러 메시지는 동일하게 나타납니다. 레지스트리에서 패키지를 내려받지 않도록 로컬 경로 의존성(file:)으로 재현했습니다.

  • 환경: macOS 27.0.1, Node.js v20.20.2, npm 10.8.2

1) package.json과 lock 파일이 맞지 않는 경우

lock을 만든 뒤 package.json에 의존성을 추가하고 lock은 갱신하지 않은 상태입니다.

$ npm ci
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync. Please update your
npm error lock file with `npm install` before continuing.
npm error
npm error Missing: dep2@2.0.0 from lock file

마지막 줄의 Missing 항목이 어떤 패키지가 lock에 없는지 알려 줍니다.

2) lock 파일이 저장소에 없는 경우

$ npm ci
npm error code EUSAGE
npm error
npm error The `npm ci` command can only install with an existing package-lock.json or
npm error npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or
npm error later to generate a package-lock.json file, then try again.

.gitignore에 package-lock.json이 들어 있거나 커밋을 빠뜨린 경우입니다.

3) Node 버전이 engines 조건과 맞지 않는 경우

package.json에 engines.node가 >=22로 지정돼 있는데 Node 20으로 실행한 경우입니다. 엄격 모드(--engine-strict)를 켰을 때의 결과입니다.

$ npm ci --engine-strict
npm error code EBADENGINE
npm error engine Unsupported engine
npm error engine Not compatible with your version of node/npm: demo@1.0.0
npm error notsup Required: {"node":">=22"}

직접 확인한 결과, engine-strict 없이 실행하면 같은 상황에서 npm warn EBADENGINE 경고만 출력되고 설치는 종료 코드 0으로 계속됩니다. 이 경우 실패는 이후 단계(빌드·테스트)에서 나타나므로 원인을 놓치기 쉽습니다.

어떻게 해결하나요?

lock 파일을 갱신해서 커밋하기

npm install
git add package.json package-lock.json
git commit -m "chore: sync package-lock.json"

갱신 후 npm ci를 다시 실행하면 정상 설치됩니다. 직접 확인한 결과, 같은 프로젝트에서 npm install로 lock을 갱신한 뒤 npm ci가 dep, dep2를 모두 설치했습니다.

workflow의 Node 버전을 맞추기

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: npm
  - run: npm ci
  - run: npm test

node-version은 package.json의 engines나 .nvmrc와 같은 값을 쓰는 것이 안전합니다. 위 workflow는 일반적으로 쓰는 형태의 예시이며 이 글에서 실제 러너로 실행해 확인하지는 않았습니다.

setup-node의 cache 옵션과 lock 파일은 어떤 관계인가요?

actions/setup-node의 cache: npm 옵션은 lock 파일(package-lock.json 등)의 해시를 캐시 키로 사용합니다. 그래서 lock 파일이 없거나 하위 폴더에 있는 모노레포에서는 cache-dependency-path로 위치를 지정해야 합니다. 이 동작은 setup-node 공식 문서 기준 설명이며, 직접 재현하지는 않았습니다.

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

주의할 점

  • npm ci는 node_modules를 먼저 삭제하고 설치합니다. 직접 확인한 결과, node_modules에 넣어 둔 임의의 파일이 npm ci 실행 후 사라졌습니다.
  • CI에서 npm install로 바꿔서 에러를 넘기면 lock이 조용히 바뀌어 빌드 재현성이 깨집니다. 원인을 고치는 쪽이 낫습니다.
  • 로컬과 CI의 npm 메이저 버전이 다르면 lock 파일 형식(lockfileVersion)이 달라질 수 있습니다. 팀에서 Node와 npm 버전을 통일하세요.

정리표

증상(에러 메시지)원인해결
EUSAGE ... are in sync / Missing: ... from lock filepackage.json과 lock 불일치npm install 후 lock 커밋
EUSAGE ... existing package-lock.jsonlock 파일 없음lock 생성·커밋, .gitignore 확인
EBADENGINE Unsupported engineNode 버전이 engines와 불일치setup-node의 node-version 수정

자주 묻는 질문(FAQ)

로컬에서는 되는데 GitHub Actions에서만 npm ci가 실패하는 이유는 무엇인가요?

로컬에서는 npm install이 lock 불일치를 조용히 고치지만, CI의 npm ci는 불일치를 오류로 처리하기 때문입니다. lock 파일을 갱신해서 커밋하면 해결됩니다.

CI에서 npm install을 써도 되나요?

동작은 하지만 권장하지 않습니다. 설치 결과가 lock과 달라질 수 있어 재현성이 떨어집니다. 일반적으로 CI에서는 npm ci를 쓰고, 실패하면 원인을 고치는 방식을 권합니다.

package-lock.json은 커밋해야 하나요?

애플리케이션 프로젝트라면 커밋하는 것이 일반적입니다. npm ci가 lock 파일을 필요로 하기 때문입니다. npm 공식 문서도 npm ci는 기존 lock 파일이 있어야 동작한다고 설명합니다.

EBADENGINE이 경고로만 나오고 설치가 되는데 문제가 있나요?

설치는 되지만 요구 Node 버전과 다른 환경에서 빌드·테스트가 예기치 않게 실패할 수 있습니다. workflow의 node-version을 engines 조건에 맞추는 것이 안전합니다.

참고 문서: npm Docs - npm ci · actions/setup-node

최종 업데이트: 2026-10-07 · 작성: 내멋스레 사는 이야기

화요일

GitHub Actions OOM 에러 해결: JavaScript heap out of memory 완전 정복

🔍 검색 키워드: GitHub Actions JavaScript heap out of memory, FATAL ERROR Reached heap limit Allocation failed, max-old-space-size GitHub Actions, NODE_OPTIONS 힙 메모리 늘리기, Node.js heap out of memory 빌드, GitHub Actions 빌드 메모리 부족

요약: "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory"는 Node.js 프로세스가 V8 힙 상한을 넘겨 메모리를 더 확보하지 못해 강제 종료될 때 나옵니다. workflow의 env에 NODE_OPTIONS=--max-old-space-size=값을 지정해 상한을 올리거나, 빌드 자체의 메모리 사용량(소스맵, 병렬 작업 등)을 줄여서 해결합니다.

JavaScript heap out of memory는 왜 발생하나요?

Node.js는 V8 엔진의 힙(old space) 메모리에 상한을 둡니다. 빌드 도구(webpack, tsc, 번들러 등)가 큰 프로젝트를 처리하다가 이 상한을 넘으면 가비지 컬렉션으로도 공간을 확보하지 못해 프로세스를 중단합니다. 로컬에서는 괜찮다가 GitHub Actions에서만 실패하는 경우는 보통 러너 환경과 Node 버전, 프로젝트 크기가 달라서입니다.

직접 재현한 결과

아래 출력은 로컬에서 직접 실행한 결과이며 GitHub Actions 러너에서 실행한 것은 아닙니다. 빌드 도구 대신, 배열을 계속 늘리는 간단한 스크립트로 힙 상한을 넘기는 상황을 만들었습니다. 빠르게 재현하려고 상한을 64MB로 낮췄습니다.

  • 환경: macOS 27.0.1, Node.js v20.20.2, RAM 24GB
// leak.js
const a = [];
while (true) a.push(new Array(1e5).fill('x'));
$ node --max-old-space-size=64 leak.js
... Mark-Compact 64.2 (98.1) -> 64.2 (98.1) MB, ...
    allocation failure; scavenge might not succeed
FATAL ERROR: Reached heap limit Allocation failed -
JavaScript heap out of memory

종료 코드: 134 (Abort trap: 6)

가비지 컬렉션(Mark-Compact)을 해도 사용량이 줄지 않다가 상한에 닿아 프로세스가 중단됐습니다. 종료 코드는 134였습니다.

힙 상한은 어떻게 확인하고 바꾸나요?

v8 모듈의 getHeapStatistics()로 현재 상한(heap_size_limit)을 확인할 수 있습니다. 직접 확인한 결과는 다음과 같습니다.

$ node -e "console.log(require('v8').getHeapStatistics().heap_size_limit/1024/1024)"
4144   # 이 컴퓨터(RAM 24GB, Node 20)의 기본값(MB)

$ NODE_OPTIONS=--max-old-space-size=2048 node -e "..."
2096   # NODE_OPTIONS로 지정한 값이 적용됨

두 가지를 확인했습니다. 첫째, NODE_OPTIONS로 지정한 값이 실제로 적용됩니다. 둘째, 이 컴퓨터는 기본 상한이 이미 약 4GB라서 --max-old-space-size=4096을 줘도 값이 4144로 같았습니다. 기본 상한은 Node 버전과 시스템 메모리에 따라 달라지므로, 숫자를 무작정 키우기 전에 현재 상한을 먼저 확인하세요. 러너의 기본 상한과 사용 가능한 메모리는 직접 확인하지 못했으며 GitHub 공식 문서의 러너 사양을 확인해야 합니다.

어떻게 해결하나요?

1) NODE_OPTIONS로 힙 상한 올리기

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      NODE_OPTIONS: --max-old-space-size=4096
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run build

위 workflow는 일반적인 형태의 예시이며 실제 러너로 실행해 확인하지는 않았습니다. 값은 러너의 실제 메모리보다 크게 잡으면 안 됩니다. 상한이 물리 메모리를 넘으면 힙 오류 대신 운영체제가 프로세스를 강제 종료할 수 있습니다.

2) 빌드 스크립트에만 적용하기

{
  "scripts": {
    "build": "NODE_OPTIONS=--max-old-space-size=4096 vite build"
  }
}

모든 Node 프로세스가 아니라 빌드 한 곳에만 적용하고 싶을 때 씁니다. Windows 러너를 쓴다면 cross-env 같은 도구가 필요합니다.

3) 메모리 사용량 자체를 줄이기

  • 프로덕션 소스맵 생성을 끄거나 줄입니다. 소스맵은 빌드 메모리를 많이 쓰는 대표적인 옵션입니다.
  • 병렬 처리 수를 줄입니다(예: 번들러의 worker 수, 테스트의 --maxWorkers).
  • 타입 검사와 번들을 한 프로세스에서 같이 돌리고 있다면 단계를 나눕니다.
  • 한 번에 처리하는 파일이 너무 크지 않은지 확인합니다.

주의할 점

  • 상한을 올리는 것은 증상을 미루는 방법일 수 있습니다. 메모리 누수가 원인이면 값을 키워도 시간이 조금 늦어질 뿐 다시 실패합니다.
  • GitHub Actions 로그에서 같은 단계가 "Killed"나 종료 코드 137로 끝나면 힙 오류가 아니라 운영체제 수준의 메모리 부족일 가능성이 있습니다. 이 경우는 위 재현과는 다른 증상이며 직접 재현하지는 않았습니다.
  • Node 버전이 바뀌면 기본 힙 상한이 달라질 수 있으므로, setup-node의 node-version을 고정하세요.

정리표

증상원인해결
FATAL ERROR ... heap out of memory (종료 코드 134)V8 힙 상한 초과NODE_OPTIONS=--max-old-space-size 지정
상한을 올려도 계속 실패메모리 누수 또는 과도한 사용소스맵·병렬 수·작업 분할 점검
Killed / 종료 코드 137OS 수준 메모리 부족(문서 기준)상한을 낮추거나 러너 사양 변경

자주 묻는 질문(FAQ)

--max-old-space-size는 단위가 무엇인가요?

메가바이트(MB)입니다. 직접 확인한 결과, 2048을 지정하면 heap_size_limit가 약 2096MB로 표시됩니다.

NODE_OPTIONS는 어디에 넣어야 하나요?

job 또는 step의 env에 넣거나 package.json의 스크립트 앞에 붙입니다. 모든 Node 프로세스에 적용되므로 필요한 범위로만 지정하는 것이 좋습니다.

로컬에서는 되는데 GitHub Actions에서만 실패하는 이유는 무엇인가요?

Node 버전, 시스템 메모리, 프로젝트 의존성 상태가 다를 수 있기 때문입니다. 힙 상한을 확인하는 명령을 workflow에 한 번 출력해 비교해 보세요.

상한을 크게 올리면 항상 해결되나요?

아닙니다. 누수가 원인이면 다시 실패하고, 값이 물리 메모리보다 크면 OS가 프로세스를 종료할 수 있습니다.

참고 문서: Node.js CLI options · GitHub-hosted runners

최종 업데이트: 2026-10-08 · 작성: 내멋스레 사는 이야기