- Published on
GitHub Actions 캐시가 안 먹을 때 터지는 7가지 함정
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
GitHub Actions에서 actions/cache를 붙였는데도 매번 Cache not found for input keys가 뜨면, 보통은 “캐시가 저장이 안 됐다”가 아니라 “저장됐는데 못 찾는다(키/경로/스코프 불일치)”에 가깝습니다. 특히 Node/pnpm, Python/pip, Gradle, Docker BuildKit처럼 캐시 레이어가 여러 겹인 도구들은, 캐시 키 설계와 저장 경로의 안정성이 조금만 흔들려도 hit가 급격히 떨어집니다.
이 글에서는 “캐시가 안 먹을 때 자주 터지는 7가지 함정”을 원인 → 증상 → 해결 순서로 정리합니다. 더 깊은 디버깅 흐름(로그로 원인 좁히기, 속도 개선 실전)은 GitHub Actions 캐시가 안 먹을 때 속도 3배 올린 실전도 함께 참고하면 좋습니다.
1) key에 휘발성 값(커밋 SHA/런 ID/시간)이 섞여 매번 미스
전형적인 실수
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ github.sha }}
github.sha는 매 커밋마다 바뀌므로 사실상 “캐시 비활성화”와 같습니다.
해결: 고정 + 변화의 기준을 “의존성 파일”로
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- key는 “정확히 동일한 의존성 조합”을 식별
- restore-keys는 “대략 비슷한 캐시”를 가져와서 설치 시간을 줄임
2) hashFiles 범위가 잘못되어 키가 불필요하게 자주 바뀜
증상
- lockfile이 안 바뀌었는데도 매번 cache miss
- 모노레포에서 어떤 패키지 변경이 전체 캐시를 날려버림
원인
key: node-${{ hashFiles('**/*') }}
**/*는 소스 코드 변경까지 키에 반영하므로 캐시가 거의 재사용되지 않습니다.
해결: “의존성에만” 반응하도록 범위를 좁히기
key: node-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
node-${{ runner.os }}-
패키지 매니저가 pnpm/yarn이라면 각각 pnpm-lock.yaml, yarn.lock로 맞추세요.
3) path가 실제로 쓰는 캐시 디렉터리가 아니라서 저장해도 효과가 없음
전형적인 착각
node_modules를 캐시하면 항상 빨라질 거라고 생각- 하지만 도구에 따라
node_modules는 재사용성이 낮거나(플랫폼/네이티브 모듈), 설치 과정에서 깨지기 쉬움
권장: “패키지 매니저 캐시”를 먼저 캐시
npm
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
pnpm (권장 패턴)
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Get pnpm store path
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
핵심은 **“도구가 실제로 읽는 캐시 위치”**를 잡는 것입니다.
4) OS/아키텍처/런타임 버전을 키에 포함하지 않아 ‘복구는 되지만 깨짐’
증상
- cache hit는 뜨는데 빌드가 실패
- 네이티브 바이너리 모듈(예:
sharp,esbuild)에서 런타임 오류
원인
ubuntu-latest(실제로는 22.04 → 24.04로 바뀔 수 있음)- Node 18 ↔ 20 전환
- x64 ↔ arm64 전환
키가 동일하면 서로 다른 환경의 산출물이 섞입니다.
해결: 환경 축을 키에 명시
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
가능하면 runner.arch도 넣어주세요.
key: npm-${{ runner.os }}-${{ runner.arch }}-node20-${{ hashFiles('package-lock.json') }}
5) restore 시점이 늦거나, 설치/빌드 전에 캐시가 복구되지 않음
증상
- 캐시 step은 있는데도 설치가 항상 풀로 진행
- 로그를 보면 install step 전에 restore가 없음
원인
캐시는 “사용하기 전에” 복구되어야 합니다. 예를 들어 pnpm store 경로를 얻기 전에 캐시를 복구하려고 하면 path가 비어 실패하거나, 반대로 install 이후에 restore하면 의미가 없습니다.
해결: restore → install 순서 고정
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Restore cache
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
- name: Install
run: npm ci
추가로, 여러 job으로 나뉘어 있다면 각 job은 별도의 VM에서 돌기 때문에, 캐시도 job마다 restore가 필요합니다.
6) 브랜치/PR 스코프 제약으로 “저장은 됐는데 다른 워크플로우에서 못 씀”
증상
- main에서 만든 캐시가 PR에서 안 보임
- 같은 repo인데도 workflow가 다르면 cache miss
배경
GitHub Actions 캐시는 기본적으로 브랜치/PR 및 접근 정책의 영향을 받습니다. (특히 fork PR, 권한 제한이 있는 리포지토리)
대응 전략
- PR과 main에서 **키 접두(prefix)**를 맞춰
restore-keys로 유사 캐시를 끌어오기 - fork PR은 보안상 제약이 많으므로, 캐시 전략을 “속도”보다 “안전” 쪽으로 조정
예시:
key: node-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
node-${{ runner.os }}-
이렇게 하면 PR에서 exact hit가 안 나도, OS 단위의 최근 캐시를 부분적으로 재사용할 확률이 올라갑니다.
7) 동시 실행/매트릭스에서 캐시 경합으로 저장이 스킵되거나 덮어쓰기 충돌
증상
- 어떤 run에서는 저장 로그가 있고, 어떤 run에서는 “already exists” 또는 저장이 스킵
- matrix(예: Node 18/20, OS별)에서 캐시가 불안정
원인
동일 key로 여러 job이 동시에 저장을 시도하면, 먼저 저장한 쪽만 성공하고 나머지는 실패/스킵될 수 있습니다. 또한 서로 다른 환경이 같은 key를 공유하면 내용이 섞입니다.
해결
- 매트릭스 축을 key에 포함
strategy:
matrix:
node: [18, 20]
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
- 캐시를 “공유”하고 싶다면 restore-keys로만 공유하고, 저장 key는 더 구체적으로
key: npm-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
실전 체크리스트: cache miss를 5분 안에 좁히는 로그 포인트
아래 4가지만 확인해도 대부분 원인이 드러납니다.
- restore step 로그에 key가 기대한 값인지
hashFiles()가 빈 문자열을 만들고 있지 않은지(경로 오타/checkout 누락)path가 실제로 존재하는지(설치 후 생성되는 캐시라면 저장은 되지만 restore 후 도구가 안 읽을 수 있음)- OS/Node/아키텍처/매트릭스 값이 key에 반영되는지
더 많은 디버깅 예시(캐시 hit/miss 로그 해석, key 설계 리팩터링, 속도 개선 전후 비교)는 GitHub Actions 캐시가 안 먹을 때 속도 3배 올린 실전에 정리해 두었습니다.
보너스: 캐시 문제를 ‘캐시 문제’로만 보지 말기
캐시는 본질적으로 “상태(state)”를 다루는 영역이라, 다른 캐시/상태 꼬임과 디버깅 방식이 유사합니다. 예를 들어 프런트엔드에서 빌드 캐시가 꼬여 결과물이 뒤섞이는 문제는 Next.js에서도 흔합니다. CI 캐시를 만지다 비슷한 감각을 느꼈다면 Next.js 14 App Router 캐시 꼬임 해결법처럼, “무엇이 캐시의 키이고 무엇이 캐시의 스코프인가”를 분리해서 보는 접근이 도움이 됩니다.
결론
GitHub Actions 캐시가 안 먹는 이유는 대개 7가지 범주로 수렴합니다.
- key에 휘발성 값이 섞였다
- hashFiles 범위가 과도하거나 잘못됐다
- path가 실제 캐시 위치가 아니다
- OS/런타임/아키텍처 축이 키에 없다
- restore 타이밍/잡 경계가 어긋났다
- 브랜치/PR 스코프 제약으로 재사용이 막혔다
- 동시 실행/매트릭스에서 경합이 난다
이 중 13번은 “cache miss”, 47번은 “cache hit인데도 깨짐/불안정”으로 나타나는 경우가 많습니다. 워크플로우를 고칠 때는 키 설계(정확도) → restore-keys(유연성) → path(효과) 순서로 점검하면 가장 빠르게 안정화됩니다.