- Published on
GitHub Actions 캐시가 안 먹을 때 - cache-hit 0% 정리
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 같은 워크플로를 돌리는데도 cache-hit 이 계속 false 로 찍히고, 매번 의존성 설치가 처음부터 다시 되는 경우가 있습니다. 특히 Node.js npm ci 나 pnpm install 같은 단계가 길어지면 CI 시간이 체감으로 폭증합니다.
이 글은 GitHub Actions 캐시가 “왜 안 먹는지”를 cache-hit 0% 관점에서 원인별로 분해하고, 바로 적용 가능한 해결 패턴(키 설계, 경로 검증, restore-keys 전략, 디버깅 방법)을 코드와 함께 정리합니다.
참고: 인증/권한 이슈로 워크플로 자체가 흔들리는 경우는 캐시보다 먼저 잡아야 합니다. OIDC 기반 assume-role 문제가 섞여 있다면 GitHub Actions OIDC assume-role 실패 원인별 해결 도 함께 확인하세요.
1) 먼저 확인할 것: actions/cache 가 “정상 동작” 하고 있는지
캐시가 안 먹는 문제는 크게 두 종류입니다.
- 저장은 되는데 다음 실행에서 복원이 안 됨(키 불일치, 스코프 문제)
- 저장 자체가 안 됨(경로 문제, 권한/용량/타이밍 문제)
가장 먼저 워크플로 로그에서 아래를 확인하세요.
Cache not found for input keys가 뜨는지Cache restored from key:가 뜨는지- Job 끝에서
Cache saved successfully가 뜨는지
actions/cache 는 기본적으로 “복원 단계” 와 “저장 단계” 가 분리되어 있고, 저장은 대개 Job 종료 시점에 수행됩니다. 중간에 Job 이 실패하거나 강제 종료되면 저장이 되지 않습니다.
2) 원인 1: 캐시 키가 매번 바뀐다(가장 흔함)
2-1. 락파일이 계속 변한다
키에 hashFiles('**/package-lock.json') 를 넣어두었는데, CI 과정에서 락파일이 생성/수정되면 매번 키가 바뀝니다.
npm install은 락파일을 갱신할 수 있음npm ci는 락파일을 “그대로” 사용(권장)pnpm도 lockfile 버전이나 설정에 따라 변동 가능
해결
- CI에서는 가능한
npm ci를 사용 - 락파일을 생성하는 단계가 캐시 복원 이후에 실행되도록 정렬
예시:
- uses: actions/checkout@v4
- uses: actions/cache@v4
id: cache
with:
path: |
~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
2-2. 키에 너무 많은 변수를 섞는다
아래처럼 커밋 SHA, run id, timestamp 등을 키에 넣으면 캐시는 사실상 매번 미스가 납니다.
${{ github.sha }}${{ github.run_id }}${{ github.run_number }}
해결
- 키는 “의존성 그래프가 바뀌는 조건” 에만 반응하도록 최소화
- 브랜치별 격리가 필요하면
github.ref_name정도까지만 고려
권장 패턴:
key: ${{ runner.os }}-node20-npm-${{ hashFiles('**/package-lock.json') }}
3) 원인 2: path 가 잘못되었거나, 실제로는 비어 있다
캐시는 “경로에 있는 파일” 을 저장합니다. 경로가 틀리면 저장할 게 없고, 복원도 당연히 안 됩니다.
자주 틀리는 케이스:
node_modules를 캐시하려고 했는데 실제 설치는 다른 디렉터리에 됨(모노레포)~/.npm대신~/.cache/npm를 써야 하는 환경- Windows 런너에서
~확장이 기대대로 안 됨
3-1. 경로를 출력해서 검증하기
캐시 단계 직전에 실제 경로를 찍어보면 원인이 빨리 드러납니다.
- name: Debug cache paths
shell: bash
run: |
echo "HOME=$HOME"
ls -la "$HOME" || true
ls -la "$HOME/.npm" || true
du -sh "$HOME/.npm" || true
3-2. Node 의존성 캐시는 setup-node 내장 캐시도 고려
actions/setup-node@v4 는 cache: 'npm' 같은 내장 캐시를 제공합니다. 직접 actions/cache 를 쓰는 것보다 실수가 줄어드는 경우가 많습니다.
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
모노레포라면:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: |
package-lock.json
apps/web/package-lock.json
packages/shared/package-lock.json
4) 원인 3: 브랜치/PR 스코프 때문에 캐시가 “보이지” 않는다
GitHub Actions 캐시는 기본적으로 키가 같아도 “어떤 이벤트에서 생성되었는지” 에 따라 접근이 제한될 수 있습니다.
대표적으로:
pull_request이벤트에서 fork PR 은 캐시 저장/복원이 제한될 수 있음- 기본 브랜치에서 만든 캐시가 PR 브랜치에서 기대대로 복원되지 않는다고 느끼는 케이스
해결 전략
- PR 에서는
restore-keys로 기본 브랜치 캐시를 “부분 매칭” 하게 설계 - fork PR 은 보안상 제약이 있으므로 캐시에 과도하게 의존하지 않도록 설계
예시:
- uses: actions/cache@v4
id: cache
with:
path: |
~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
restore-keys 는 완전 동일 키가 없어도 prefix 매칭으로 “가장 가까운 캐시” 를 찾아옵니다. 의존성 설치가 조금 바뀌어도 체감 속도가 좋아지는 이유가 여기 있습니다.
5) 원인 4: Job 이 끝나기 전에 실패해서 저장이 안 된다
캐시는 Job 종료 시점에 업로드되는 구조라서, npm test 나 build 가 실패하면 캐시 저장 단계까지 도달하지 못합니다.
해결
- 의존성 캐시는 설치 직후 저장되는 게 아니라는 점을 이해하고, “첫 성공 run” 을 만들어야 함
- 실패가 잦은 단계 전에도 캐시가 저장되게 하려면, 의존성 설치가 끝난 뒤에 캐시 저장이 수행되도록 워크플로를 단순화하거나, 실패 원인을 먼저 줄이기
실무적으로는 “캐시 미스가 원인이 아니라, 빌드 실패가 먼저” 인 경우가 많습니다. 시스템 전체가 불안정할 때의 접근법은 systemd 서비스 재시작 루프, 10분 디버깅 처럼 원인 분해 체크리스트 방식이 잘 통합니다.
6) 원인 5: OS, 아키텍처, 런너가 달라서 캐시가 분리된다
키에 ${{ runner.os }} 를 넣어두면 Linux, Windows, macOS 캐시가 분리됩니다. 의도한 분리라면 문제 없지만, 매트릭스 전략에서 아래 상황이 자주 발생합니다.
- 로컬은 Linux, CI 는 macOS 로 바뀜
ubuntu-latest가 업그레이드되며 환경이 바뀜- self-hosted 런너와 GitHub-hosted 런너를 섞음
해결
- OS 별 분리가 필요 없다면 키에서 OS 를 제거(단, 바이너리 캐시라면 위험)
ubuntu-22.04처럼 런너를 고정
예시:
runs-on: ubuntu-22.04
7) 원인 6: 캐시하려는 대상이 “재현 불가능” 하거나 “너무 크다”
7-1. node_modules 캐시는 종종 역효과
node_modules 는 파일 수가 많고, OS/Node 버전/네이티브 모듈에 민감해서 캐시 복원에 시간이 더 걸리거나 깨지기 쉽습니다.
권장:
~/.npm(다운로드 캐시)~/.pnpm-store~/.cache/yarn
pnpm 예시:
- uses: actions/cache@v4
with:
path: |
~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- run: pnpm install --frozen-lockfile
7-2. 캐시 업로드/다운로드가 병목
캐시는 네트워크 전송이므로, 압축/해제 및 전송 시간이 커지면 “캐시가 있어도 느린” 상황이 됩니다. 특히 대형 모노레포에서 빌드 산출물까지 캐시하려다 보면 역전이 자주 발생합니다.
이때는 빌드 파이프라인을 캐시로만 해결하려 하지 말고, 빌드 자체를 줄이는 튜닝이 더 효과적일 수 있습니다. Next.js 빌드/서빙 성능 이슈가 섞여 있다면 Next.js 14 App Router TTFB 폭증 잡는 RSC 튜닝 같은 접근이 더 큰 시간을 절약합니다.
8) 원인 7: 모노레포에서 hashFiles 패턴이 엉뚱한 파일을 잡는다
hashFiles('**/package-lock.json') 는 리포 전체의 모든 락파일을 해시합니다. 모노레포에서 일부 패키지만 바뀌어도 전체 키가 바뀌어 캐시가 자주 무효화됩니다.
해결
- 실제로 설치에 영향을 주는 락파일만 포함
- 워크스페이스별로 Job 을 나눠 키를 분리
예시:
key: ${{ runner.os }}-web-npm-${{ hashFiles('apps/web/package-lock.json') }}
9) 원인 8: 워크플로 파일 변경으로 키가 달라진다(간접 요인)
직접 키에 워크플로 경로를 넣지 않았는데도, 아래처럼 “설치되는 툴 버전” 이 바뀌면 캐시가 사실상 무효가 될 수 있습니다.
- Node 버전 변경
- pnpm 버전 변경
- Python 버전 변경
해결
- 키에 런타임 버전을 명시적으로 포함
key: ${{ runner.os }}-node20-npm-${{ hashFiles('**/package-lock.json') }}
매트릭스라면:
strategy:
matrix:
node: [18, 20]
# ...
key: ${{ runner.os }}-node${{ matrix.node }}-npm-${{ hashFiles('**/package-lock.json') }}
10) 실전 디버깅 체크리스트(로그만으로 끝내기)
아래 순서로 보면 대부분 10분 안에 결론이 납니다.
- 캐시 복원 로그에
Cache restored from key가 있는가 - 없다면
key가 매번 바뀌는지 확인(특히 락파일 해시) restore-keys를 넣어도 복원이 안 되는가path가 실제로 존재하고 용량이 있는지ls,du로 확인- Job 이 성공해서 저장까지 갔는지
Cache saved successfully확인 - 이벤트가
pull_request이고 fork 인지 확인(스코프/권한 제약) - OS/Node/패키지매니저 버전이 바뀌지 않았는지 확인
디버그용으로 키 재현 값을 출력하면 훨씬 빠릅니다.
- name: Print cache key inputs
shell: bash
run: |
echo "ref=${GITHUB_REF}"
echo "sha=${GITHUB_SHA}"
echo "os=${RUNNER_OS}"
echo "lockfile hash follows from actions/cache logs"
11) 추천 워크플로 템플릿(Node.js 기준)
npm 기준으로 “안 깨지고, 캐시 히트율이 높은” 형태입니다.
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm test
actions/cache 를 직접 써야 한다면:
- uses: actions/cache@v4
id: cache
with:
path: |
~/.npm
key: ${{ runner.os }}-node20-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node20-npm-
- run: npm ci
마무리: cache-hit 0% 는 “키와 경로” 문제로 수렴한다
GitHub Actions 캐시가 안 먹는 문제는 복잡해 보이지만, 실제로는 다음 두 축으로 대부분 설명됩니다.
- 키가 안정적인가(락파일, 버전, 불필요 변수)
- 저장할 경로가 맞는가(존재, 용량, OS 차이)
여기에 PR 스코프 제약과 Job 실패로 인한 저장 누락까지 점검하면, cache-hit 를 의미 있게 끌어올릴 수 있습니다. 캐시가 정상화되면 CI 시간은 체감상 가장 먼저 개선되는 영역이니, 위 체크리스트대로 하나씩 제거해 보세요.