- Published on
GitHub Actions 캐시가 안먹을 때 9가지 원인
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
CI가 느려지는 가장 흔한 이유는 “캐시가 안 먹는다”입니다. GitHub Actions는 actions/cache(또는 setup-node/setup-python의 내장 캐시)를 통해 의존성 디렉터리나 빌드 산출물을 저장·복원할 수 있지만, 실제 운영에서는 Cache not found for input keys가 반복되거나, 분명 restore는 됐는데도 설치/빌드가 다시 도는 경우가 잦습니다.
이 글에서는 GitHub Actions 캐시가 안 먹는 9가지 원인을 “증상 → 진단 포인트 → 해결” 순서로 정리합니다. 마지막에는 바로 붙여 넣어 쓸 수 있는 권장 캐시 패턴 예제도 제공합니다. (캐시 무효화 사고 패턴은 Docker에서도 비슷하게 발생하니, 빌드 캐시 관점은 Docker 빌드 캐시가 무효화되는 원인 7가지도 함께 참고하면 좋습니다.)
1) 캐시 key가 매번 바뀐다(너무 자주 invalidation)
대표 증상
- 항상
Cache not found for input keys가 뜬다. - 매 실행마다 새로운 key가 찍힌다.
흔한 원인
- key에
github.run_id,github.run_number,github.sha등을 넣어 실행마다 다른 값이 포함됨 hashFiles('**/*')처럼 범위가 너무 넓어 사소한 파일 변경에도 hash가 바뀜
해결
- key는 “의존성 결정 요소”만 포함: OS, 런타임 버전, lockfile hash 정도
- 변경이 잦은 파일(README, 테스트 스냅샷, 빌드 아티팩트)을 hash 범위에서 제외
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: npm-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-node20-
npm-${{ runner.os }}-
2) restore-keys가 없거나 너무 구체적이다(부분 일치 복원 실패)
대표 증상
- lockfile이 조금만 바뀌면 캐시가 완전히 미스
- “거의 같은 환경”인데도 매번 처음부터 설치
진단 포인트
restore-keys가 비어 있거나,key와 동일한 수준으로 구체적
해결
- restore-keys는 점진적으로 덜 구체적인 prefix를 제공
- 예:
npm-OS-nodeVer-lockHash→npm-OS-nodeVer-→npm-OS-
restore-keys: |
npm-${{ runner.os }}-node20-
npm-${{ runner.os }}-
3) 캐시 path가 잘못됐거나(상대/절대/홈), 실제로는 비어 있다
대표 증상
Cache saved successfully인데 다음 실행에서 효과가 없다- restore는 되는데 설치 시간이 그대로
진단 포인트
- 캐시 대상 디렉터리가 실제로 생성되는지 확인
ls -al로 워크플로 내에서 경로 존재/용량 확인
해결
- 러너 OS별 경로 차이를 고려
- 패키지 매니저의 “다운로드 캐시”와 “설치 결과물”을 구분
- name: Inspect cache dirs
run: |
echo "$HOME"
ls -al ~/.npm || true
ls -al node_modules || true
- uses: actions/cache@v4
with:
path: |
~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
> 팁: Node는 ~/.npm(tarball 캐시)만 캐시해도 npm ci가 빨라지는 경우가 많고, node_modules까지 캐시하면 플랫폼/네이티브 모듈 이슈가 늘 수 있습니다.
4) 캐시 restore 시점이 늦다(설치/빌드 이후에 복원)
대표 증상
- 로그에 restore가 보이긴 하지만, 이미 의존성 설치가 끝난 뒤
- 캐시가 “먹은 것처럼 보이는데” 시간이 안 줄어듦
진단 포인트
actions/cachestep이npm ci,pip install,gradle build보다 아래에 위치
해결
- 설치/빌드 전에 restore가 이뤄지도록 step 순서를 조정
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
- run: pip install -r requirements.txt
5) 권한/이벤트 타입 문제: PR from fork에서는 저장이 안 된다
대표 증상
- 내부 브랜치 PR에서는 캐시가 쌓이는데, 포크 PR에서는 매번 미스
save cache단계가 스킵되거나 권한 관련 메시지
원인
- 보안상 포크 PR은 기본적으로
GITHUB_TOKEN권한이 제한됨 - 캐시 저장이 막히거나, 접근 범위가 달라짐
해결
- 포크 PR에서는 “restore만” 기대하고, 저장은
push/workflow_dispatch에서 수행 - 필요한 경우
pull_request_target을 신중히 사용(코드 인젝션 위험)
- uses: actions/cache@v4
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
6) 캐시 스코프(브랜치/키 네임스페이스) 착각
대표 증상
- main에서 만든 캐시가 feature 브랜치에서 안 보인다(혹은 반대)
- 같은 key를 쓰는데도 서로 못 찾음
설명
GitHub Actions 캐시는 **키뿐 아니라 스코프(브랜치/기본 브랜치/PR 등)**의 영향을 받습니다. 보통은 restore-keys로 완화되지만, “어디에서 생성된 캐시를 어디에서 재사용할지” 전략이 없으면 체감상 캐시가 계속 미스처럼 보입니다.
해결
- 기본 브랜치(main)에서 캐시를 잘 쌓고, 다른 브랜치에서는 restore-keys로 최대한 끌어오기
- 또는 브랜치명을 key에 넣되, 공통 prefix를 restore-keys로 제공
key: npm-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-main-
npm-${{ runner.os }}-
7) 캐시 크기/파일 수가 너무 커서 저장·복원이 비효율적이거나 실패
대표 증상
- 캐시 저장/복원 자체가 오래 걸려서 전체 시간이 줄지 않는다
- 간헐적으로 캐시 저장이 실패하거나 타임아웃
진단 포인트
- 캐시 step이 수 분 이상 걸림
node_modules,.gradle,target,dist등 거대한 디렉터리를 통째로 캐시
해결
- “다운로드 캐시” 위주로 캐시(예: npm/pip/gradle/cargo registry)
- 빌드 산출물은 캐시가 아니라 아티팩트로 분리(필요 시)
- 캐시 대상에서 불필요한 디렉터리 제외
# Gradle 예시: 의존성 캐시 위주
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
8) setup-* 내장 캐시와 actions/cache를 중복 사용해 충돌/헷갈림
대표 증상
setup-node에서cache: 'npm'을 켰는데도 별도actions/cache를 또 사용- 어떤 캐시가 적용되는지 로그가 혼란스럽고, 기대한 경로가 캐시되지 않음
해결
- 한 가지 방식으로 단순화
- Node:
actions/setup-node의 내장 캐시 사용 권장 - Python:
actions/setup-python+cache: 'pip' - 그 외 커스텀 경로는
actions/cache로 명시
- Node:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: package-lock.json
- run: npm ci
9) lockfile/의존성 결정 파일이 워크스페이스/모노레포에서 누락된다
대표 증상
- 모노레포에서 일부 패키지만 변경했는데도 캐시가 전혀 재사용되지 않음
- 반대로, 캐시가 재사용되는데 의존성 불일치로 빌드가 깨짐
진단 포인트
hashFiles()에 모노레포의 모든 lockfile이 포함되는지- pnpm/yarn workspaces의 경우 실제 의존성 결정 파일이 무엇인지 확인
해결
- 모노레포 구조에 맞춰 해시 대상을 정확히 지정
- pnpm은
pnpm-lock.yaml, yarn berry는.yarn/cache전략 등 도구별 베스트 프랙티스 적용
# 모노레포(여러 패키지)에서 모든 lockfile을 반영
key: node-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
node-${{ runner.os }}-
실전 권장 템플릿: “진단 로그 + 안전한 캐시 키”
아래 템플릿은 캐시가 안 먹을 때 원인을 빠르게 좁히기 위해 키/복원/디렉터리 상태를 함께 출력합니다.
name: ci
on:
push:
branches: [ main ]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Compute cache key inputs
run: |
echo "ref=${GITHUB_REF}"
echo "ref_name=${GITHUB_REF_NAME}"
echo "sha=${GITHUB_SHA}"
echo "lock_hash=${{ hashFiles('package-lock.json') }}"
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-node20-
npm-${{ runner.os }}-
- name: Inspect npm cache dir
run: |
ls -al ~/.npm || true
du -sh ~/.npm || true
- run: npm ci
- run: npm test
마무리: “캐시 미스”는 대부분 설계/스코프/경로 문제다
GitHub Actions 캐시는 마법이 아니라 키 설계(변경 빈도), 복원 범위(restore-keys), 경로 정확성, **이벤트 권한(PR/fork)**에 의해 거의 결정됩니다. 캐시가 안 먹는 상황을 만났다면,
- key가 너무 자주 바뀌는지, 2) restore-keys가 있는지, 3) 실제 캐시 경로가 맞는지, 4) step 순서가 올바른지부터 확인하세요.
추가로, CI/배포 파이프라인에서 권한 이슈로 예상치 못한 실패가 난다면 OIDC 쪽도 함께 점검해야 합니다. AWS 배포에서 토큰/권한이 꼬이는 케이스는 GitHub Actions OIDC AWS 배포 InvalidIdentityToken 해결에 정리해두었습니다.