- Published on
GitHub Actions 캐시가 안 먹을 때 - cache-hit 0% 원인 정리
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 같은 워크플로를 돌리는데도 actions/cache가 매번 cache-hit: false(체감상 0%)로만 찍히면, 대부분은 “캐시가 저장되지 않았다”가 아니라 캐시 키가 매번 달라지거나(미스), 저장은 됐는데 복원 조건을 못 맞추거나(미복원), 저장 단계 자체가 실행되지 않는(미저장) 구조적 문제입니다.
이 글은 원인을 크게 키(key), 경로(path), 실행 조건, 권한/스코프, 플랫폼/런타임, 의존성 도구별 함정으로 나눠서, 로그에서 무엇을 확인해야 하는지와 바로 적용 가능한 수정 예제를 함께 정리합니다.
> 참고: 캐시 관련 오류가 권한 문제(403)로 보이면 토큰/권한부터 확인하세요. 관련 글: GitHub Actions 403 권한오류 해결 - GITHUB_TOKEN·OIDC
1) 먼저 확인: “복원”이 실패인지 “저장”이 안 된 건지
actions/cache는 보통 아래 두 가지 모드로 쓰입니다.
restore+save를 분리(권장: v4에서 가능)- 단일
uses: actions/cache@...로 restore/save를 한 번에 처리(구버전/간단 구성)
캐시가 안 먹는다고 느낄 때 가장 먼저 볼 것은 첫 실행에서 저장이 되었는지입니다.
체크리스트
- 첫 실행 로그에
Cache saved successfully가 있는가? post단계가 실행되었는가? (job이 중간에 실패하면 save가 안 될 수 있음)cache-hit는 “정확히 같은 primary key로 복원했는지”만 의미합니다.restore-keys로 부분 복원되면cache-hit: false지만 실제로는 캐시가 복원된 상태일 수 있습니다.
v4 권장 패턴(restore/save 분리)
- name: Restore cache
id: cache
uses: actions/cache/restore@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install
run: npm ci
- name: Save cache
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
이렇게 분리하면 “복원은 됐는데 저장이 안 됨” 문제를 더 명확히 관찰할 수 있습니다.
2) 원인 1: 캐시 키가 매번 달라진다(가장 흔함)
cache-hit 0%의 1순위 원인은 키에 변동 요소가 들어가는 것입니다.
흔한 실수
- 키에
${{ github.sha }}를 넣음 → 커밋마다 키가 바뀌니 매번 미스 - 키에
${{ github.run_id }}/${{ github.run_number }}포함 → 실행마다 미스 - 키에 날짜/시간을 포함하는 스크립트 결과 포함
나쁜 예
key: npm-${{ github.sha }}
좋은 예(락파일 기반)
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
hashFiles() 대상 경로가 잘못됐다
모노레포에서 package-lock.json이 루트가 아니라 apps/web/package-lock.json에 있는데 루트만 해시하면 항상 빈 해시가 나오거나(파일 미발견), 의도와 다른 키가 만들어집니다.
key: ${{ runner.os }}-npm-${{ hashFiles('apps/web/package-lock.json') }}
락파일이 매 실행마다 변경된다
npm install이 락파일을 업데이트pnpm install이pnpm-lock.yaml을 업데이트yarn이yarn.lock을 업데이트
CI에서는 가능하면 락파일을 변경하지 않는 설치 명령을 사용하세요.
- npm:
npm ci - pnpm:
pnpm install --frozen-lockfile - yarn:
yarn install --frozen-lockfile
3) 원인 2: path가 틀렸거나, 캐시할 게 없다
캐시는 “키”만 맞아도 “경로”가 잘못되면 의미가 없습니다.
상대경로/작업 디렉터리 함정
defaults.run.working-directory를 설정해두고,path: node_modules를 쓰면 의도한 위치가 아닐 수 있습니다.node_modules가 실제로는apps/web/node_modules에 생기는데 루트만 캐시함.
- uses: actions/cache/restore@v4
with:
path: |
apps/web/node_modules
~/.npm
key: ${{ runner.os }}-web-${{ hashFiles('apps/web/package-lock.json') }}
설치가 실패/스킵되어 디렉터리가 비어 있다
캐시는 지정한 path에 파일이 없으면 저장할 게 없습니다. 설치 단계가 조건문으로 스킵되거나, 이전 단계에서 실패했는지 확인하세요.
캐시하면 안 되는 것까지 캐시함
node_modules 자체를 캐시하면 플랫폼/네이티브 모듈 때문에 오히려 깨지는 경우가 있습니다. Node 생태계에서는 보통 아래 조합이 안정적입니다.
- npm:
~/.npm만 캐시 +npm ci - pnpm:
~/.pnpm-store캐시 - yarn:
.yarn/cache(Berry)
4) 원인 3: OS/아키텍처/런타임이 바뀌었다
키에 OS를 포함하지 않으면, Linux에서 저장한 캐시를 Windows/macOS에서 복원하려다 실패하거나(혹은 더 나쁘게는 잘못 복원)합니다.
key: ${{ runner.os }}-node${{ matrix.node }}-npm-${{ hashFiles('package-lock.json') }}
추가로 네이티브 바이너리 의존성이 있는 프로젝트라면 architecture까지 고려해야 합니다(특히 self-hosted, ARM64 등).
5) 원인 4: 브랜치/PR/포크 스코프 때문에 캐시가 안 보인다
GitHub Actions 캐시는 보안/격리 정책 때문에 “다른 컨텍스트의 캐시”를 마음대로 읽지 못합니다. 대표 케이스:
pull_request(포크)에서 base repo의 캐시를 못 읽음- 브랜치별로 캐시 접근이 제한되어 기대한 캐시가 안 나옴
해결 방향
- 포크 PR에서 캐시를 기대하지 말고, base 브랜치(main)에서 warm-up 후 PR은
restore-keys로만 부분 복원 기대 - 또는
pull_request_target로 전환(단, 보안상 매우 주의: 신뢰할 수 없는 코드 실행 위험)
6) 원인 5: 저장 단계가 실행되지 않는다(실패/취소/조건문)
actions/cache는 job이 끝날 때 post 단계에서 저장이 이뤄지는 패턴이 많습니다. 따라서 아래 상황이면 저장이 안 됩니다.
- 설치/테스트 단계에서 job이 실패하여 종료
if:조건 때문에 캐시 step 자체가 스킵continue-on-error/fail-fast조합으로 의도치 않게 조기 종료
팁: 실패해도 캐시는 저장하고 싶다
설치까지는 되었는데 테스트 실패로 저장이 안 되는 경우, save step을 분리하고 if: always()로 저장을 강제할 수 있습니다(단, 깨진 상태를 저장하지 않도록 조건을 더 엄격히 하세요).
- name: Save cache (even if tests fail)
if: always() && steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
7) 원인 6: 권한/토큰 문제로 캐시 API 호출이 막힌다
캐시는 내부적으로 GitHub의 cache 서비스 API를 호출합니다. 권한이 꼬이면 저장/복원 단계에서 403/404 유사 증상이 나타날 수 있습니다.
GITHUB_TOKEN권한이 제한됨- 조직 정책으로 Actions 권한 제한
permissions:를 너무 타이트하게 설정
예를 들어 아래처럼 permissions를 최소화하다가 캐시가 안 되는 경우가 있습니다.
permissions:
contents: read
# actions: read (조직/환경에 따라 필요할 수 있음)
권한 이슈가 의심되면 관련 글(GitHub Actions 403 권한오류 해결 - GITHUB_TOKEN·OIDC)의 체크리스트대로 토큰 스코프/조직 설정을 먼저 정리하세요.
8) 원인 7: restore-keys를 잘못 이해했다(“cache-hit false”지만 복원은 됨)
restore-keys는 prefix 매칭으로 “가장 가까운 캐시”를 가져옵니다. 이때 primary key가 정확히 일치하지 않으면 cache-hit는 false입니다.
즉, cache-hit: false가 항상 “캐시가 전혀 없음”을 의미하지 않습니다.
진단 방법
- 로그에서
Cache restored from key:문구 확인 steps.<id>.outputs.cache-primary-key/cache-matched-key확인(v4)
- name: Debug cache keys
run: |
echo "primary: ${{ steps.cache.outputs.cache-primary-key }}"
echo "matched: ${{ steps.cache.outputs.cache-matched-key }}"
9) 패키지 매니저별 베스트 프랙티스(안정성 중심)
npm
- 캐시 대상:
~/.npm - 설치:
npm ci
- uses: actions/cache/restore@v4
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
pnpm
- 캐시 대상:
~/.pnpm-store또는pnpm store path결과 - 설치:
pnpm install --frozen-lockfile
- name: Get pnpm store
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- uses: actions/cache/restore@v4
id: pnpm-cache
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- run: pnpm install --frozen-lockfile
Yarn Berry(2+)
- 캐시 대상:
.yarn/cache - 설치:
yarn install --immutable
- uses: actions/cache/restore@v4
id: yarn-cache
with:
path: .yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install --immutable
10) 실전 진단 순서(10분 컷)
1) 로그에서 “저장 성공”이 있었는지 확인
- 없다면: job 실패/취소/조건문/권한 문제부터
2) 키를 출력해 눈으로 비교
- name: Print cache key material
run: |
echo "os=${{ runner.os }}"
echo "lockHash=${{ hashFiles('package-lock.json') }}"
lockHash가 빈 값이면 파일 경로/checkout 문제
3) restore-keys로 복원은 되는지 확인
restore-keys: ${{ runner.os }}-npm-같은 prefix를 추가- 그래도 안 되면 path/스코프/권한을 의심
4) path가 실제로 존재하는지 확인
- name: Inspect paths
run: |
ls -la ~
ls -la ~/.npm || true
ls -la node_modules || true
11) 자주 묻는 함정 Q&A
Q1. cache-hit가 계속 false인데도 빌드가 빨라졌다
restore-keys로 부분 복원된 경우입니다. 정확히 같은 키가 아니면 cache-hit는 false지만, 복원은 되었을 수 있습니다. cache-matched-key를 출력해 확인하세요.
Q2. 캐시가 어느 순간부터 갑자기 안 먹는다
대개 아래 중 하나입니다.
- 락파일 포맷/경로 변경(모노레포 구조 변경)
- Node 버전/OS 매트릭스 추가로 키 설계가 깨짐
npm install로 바뀌면서 락파일이 매번 갱신
Q3. self-hosted runner에서만 캐시가 이상하다
self-hosted는 디스크 권한/홈 디렉터리/사용자 계정이 매번 다를 수 있습니다. ~가 가리키는 위치가 기대와 다른지부터 확인하세요.
12) 결론: “키 안정성” + “저장 보장” + “스코프 이해”
cache-hit 0%를 끝내려면 다음 3가지를 고정하면 됩니다.
- 키는 락파일 해시 기반으로 만들고 변동 요소(sha/run_id)를 넣지 않는다.
- 저장 단계가 실제로 실행되도록 restore/save를 분리하고, 실패/조건문에 의해 저장이 누락되지 않게 한다.
- 브랜치/PR/포크 스코프 때문에 캐시가 보이지 않을 수 있음을 이해하고, 기대치를 조정하거나 워크플로 이벤트를 재설계한다.
캐시가 권한 문제로 보이면 먼저 토큰/권한을 정리한 뒤 캐시를 재점검하세요: GitHub Actions 403 권한오류 해결 - GITHUB_TOKEN·OIDC