- Published on
GitHub Actions 캐시 안 먹을 때 키·경로·권한 7단계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CI가 느려질 때 가장 먼저 손대는 게 GitHub Actions 캐시인데, 막상 cache hit이 안 뜨고 매번 새로 설치/빌드하는 경우가 많습니다. 이 문제는 대개 키가 바뀌었거나, 경로가 실제로 비어 있거나, 권한/브랜치 정책 때문에 저장이 막힌 케이스로 수렴합니다.
이 글은 캐시가 안 먹는 상황을 “감”이 아니라 로그와 설정으로 재현 가능하게 분리하는 7단계 점검 순서로 정리합니다. 각 단계는 독립적으로 원인을 좁혀가도록 구성했습니다.
관련해서 캐시/성능 최적화 맥락은 Next.js App Router 렌더링 폭주, RSC 캐시·revalidate로 TTFB 낮추기 글도 함께 보면 전체 파이프라인 관점에서 도움이 됩니다.
0. 먼저 확인할 것: actions/cache의 동작 원리
actions/cache는 크게 두 단계입니다.
- Restore(복원):
key또는restore-keys로 기존 캐시를 찾으면 워크스페이스에 풀어줍니다. - Save(저장): 잡이 끝날 때(정확히는 캐시 액션의 post 단계) 지정한
path를 압축해 저장합니다.
따라서 “캐시가 안 먹는다”는 말은 다음 중 하나입니다.
- 복원 단계에서 찾지 못함(키/브랜치/스코프 문제)
- 저장 단계에서 저장하지 못함(권한/이벤트/경로 비어 있음)
- 저장은 됐는데 다음 실행에서 키가 달라짐(키 설계 문제)
이제부터는 로그에서 원인을 갈라내는 순서대로 봅니다.
1단계: 로그에서 restore와 save를 분리해 읽기
가장 먼저 워크플로 로그에서 actions/cache 스텝의 메시지를 확인하세요.
- 복원 실패:
Cache not found for input keys: - 복원 성공:
Cache restored from key: - 저장 스킵:
Cache hit occurred on the primary key, not saving cache. - 저장 실패/미실행: post 단계가 안 돌거나, 권한/경로 문제
캐시가 “안 먹는다”는 보고를 받으면, 아래 두 질문을 먼저 답해야 합니다.
- 복원은 실패했는가? 성공했는가?
- 저장은 수행됐는가? 스킵/실패했는가?
이 두 개만 분리해도 원인의 70%가 정리됩니다.
2단계: key가 매번 바뀌는지(혹은 너무 고정인지) 점검
캐시 키 설계가 가장 흔한 원인입니다.
자주 하는 실수 1: 커밋 SHA를 키에 넣기
key에 github.sha를 넣으면 커밋마다 키가 바뀌어 사실상 매번 miss가 납니다.
잘못된 예:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ github.sha }}
권장 패턴: 락파일 해시 기반
의존성 캐시는 package-lock.json, pnpm-lock.yaml, yarn.lock 같은 락파일 해시가 안정적입니다.
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
key: “정확히 같은 의존성 세트”를 의미restore-keys: 락파일이 바뀌어도 OS 단위로 가장 가까운 캐시를 가져옴
자주 하는 실수 2: 키가 너무 고정
반대로 key를 npm-cache처럼 고정하면, 의존성이 바뀌어도 예전 캐시가 계속 복원되어 빌드 실패나 이상 동작을 유발할 수 있습니다.
3단계: path가 실제로 채워지는지 확인(가장 많이 놓침)
캐시 경로는 “존재”만으로는 부족하고, 실제로 파일이 생성되어 있어야 저장할 게 생깁니다.
대표적으로 Node 생태계에서 다음을 혼동합니다.
~/.npm: npm 다운로드 캐시(패키지 tarball)node_modules: 설치 결과물(용량 큼, OS/아키텍처 영향 큼)- pnpm:
~/.pnpm-store또는 프로젝트의.pnpm-store
경로가 비어 있는지 즉시 확인하는 디버그 스텝
- name: Debug cache paths
run: |
echo "HOME=$HOME"
ls -la ~/.npm || true
du -sh ~/.npm || true
만약 ~/.npm이 비어 있다면, 그 전에 npm ci 같은 설치가 실행되지 않았거나, 캐시 위치가 다른 설정을 쓰는 겁니다.
설치 후에 캐시를 저장하도록 순서 점검
actions/cache는 보통 설치 전에 restore를 하고, 설치 후에 post 단계에서 save합니다. 하지만 설치 전에 캐시 스텝이 있고 설치가 실패하면 저장까지 못 갑니다.
권장 순서:
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- run: npm ci
- run: npm test
4단계: 워크스페이스/모노레포에서 hashFiles가 0개 매칭되는지 확인
모노레포에서 흔한 함정은 hashFiles('**/package-lock.json')가 실제로는 매칭이 안 되어 빈 문자열처럼 동작하거나, 예상과 다른 파일이 잡히는 경우입니다.
디버깅은 간단합니다.
- name: Show lockfile hash
run: |
echo "hash=${{ hashFiles('**/package-lock.json') }}"
- 해시가 비어 있으면: 글롭이 틀렸거나 체크아웃 경로가 다름
- 해시가 예상과 다르면: 다른 패키지의 락파일이 섞여 들어옴
모노레포라면 패키지별로 키를 분리하는 게 안전합니다.
key: npm-${{ runner.os }}-${{ hashFiles('apps/web/package-lock.json') }}
5단계: 권한/이벤트 타입 때문에 “저장”이 막히는지 확인
캐시는 “복원”은 되는데 “저장”이 안 되는 케이스가 있습니다. 특히 pull_request 이벤트에서 포크 PR이면 권한이 제한될 수 있습니다.
포크 PR에서의 제한 포인트
- 리포지토리 보안 정책에 따라
GITHUB_TOKEN권한이 축소 - 워크플로가
pull_request로만 돌면 저장이 제한되는 구성이 있을 수 있음
실무적으로는 다음 전략을 씁니다.
- 포크 PR에서는 캐시 저장을 포기하고 복원만 시도
- 내부 브랜치(
push)에서만 캐시를 저장
예시:
- 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') }}
restore-keys: |
npm-${{ runner.os }}-
또한 워크플로 상단에 최소 권한을 명시하는 경우, 캐시 관련 동작에 필요한 권한이 부족하지 않은지 점검하세요.
permissions:
contents: read
일반적으로 캐시는 별도 권한을 명시하지 않아도 동작하지만, 조직 정책/보안 설정에 따라 예외가 생길 수 있어 “저장 단계가 실행됐는지”를 로그로 확인하는 게 중요합니다.
6단계: 브랜치/스코프 정책으로 캐시가 공유되지 않는지 확인
GitHub Actions 캐시는 “어디서 만든 캐시를 어디서 쓸 수 있나”에 스코프 규칙이 있습니다.
자주 겪는 증상:
main에서 만든 캐시가feature/*에서 안 보임- 태그 빌드에서 만든 캐시가 브랜치 빌드에서 안 보임
- 반대로
restore-keys를 넓게 잡았는데도 항상 miss
이때는 키 설계만 볼 게 아니라, 캐시가 생성된 워크플로/브랜치와 현재 실행 컨텍스트가 같은 계열인지 확인해야 합니다. 운영 팁은 다음과 같습니다.
- 기본 브랜치(
main)에서 주기적으로 캐시를 “따뜻하게” 만들기 restore-keys를 OS 단위로 넓혀서 최소한의 히트율 확보- 캐시를 공유하고 싶다면, 브랜치 전략과 워크플로 트리거를 함께 설계
CI 최적화는 결국 “캐시가 공유되는 경로”를 만드는 일이라, 앱 레벨 캐시 전략(예: Next.js RSC 캐시)과 함께 보시면 Next.js App Router 로딩 느림? RSC 캐시·prefetch 최적화도 연결됩니다.
7단계: 압축/용량/동시성으로 캐시 저장이 깨지는지 확인
키/경로/권한이 다 맞는데도 저장이 안 되면, 마지막으로 “캐시 자체가 너무 크거나”, “동시에 여러 잡이 같은 키로 저장하려다 충돌”하는 경우를 봅니다.
node_modules를 캐시할 때의 현실적인 문제
- 용량이 매우 큼
- 네이티브 모듈은 OS/아키텍처 영향
- 압축/업로드 시간이 설치 시간보다 길어지는 역전 현상
가능하면 node_modules 대신 패키지 매니저 캐시(~/.npm, pnpm store)만 캐시하고, 설치는 npm ci로 빠르게 재현하는 쪽이 안정적입니다.
동시성 충돌 완화
같은 키로 여러 워크플로가 동시에 저장하면, 하나만 성공하거나 예기치 않은 결과가 날 수 있습니다. 브랜치별/워크플로별로 키에 식별자를 추가해 충돌을 줄이세요.
key: npm-${{ runner.os }}-${{ github.workflow }}-${{ hashFiles('**/package-lock.json') }}
실전 템플릿: Node 프로젝트 캐시 “정상 동작” 기준선
아래 YAML은 디버그에 필요한 출력과, 키/경로/복원키 패턴을 포함한 기준선입니다.
name: ci
on:
push:
branches: [ main, develop ]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Print lock hash
run: echo "lock-hash=${{ hashFiles('**/package-lock.json') }}"
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- name: Install
run: npm ci
- name: Debug npm cache size
run: du -sh ~/.npm || true
- name: Test
run: npm test
이 템플릿으로도 Cache not found가 지속되면, 2단계(키)와 6단계(브랜치/스코프) 중 하나일 확률이 높습니다.
빠른 체크리스트(요약)
- 로그에서 restore 실패인지 save 실패인지 먼저 분리
- 키에
github.sha같은 변동값이 들어가 miss를 만들고 있지 않은지
- 키에
path가 실제로 채워지는지(설치 이후 파일 존재/용량 확인)
hashFiles글롭이 제대로 매칭되는지(모노레포 주의)
- 포크 PR/이벤트 타입/권한으로 저장이 막히지 않는지
- 브랜치/스코프 정책으로 캐시가 공유되지 않는지
- 캐시가 너무 크거나 동시 저장 충돌이 없는지
CI 캐시는 한 번 잡히면 체감이 크지만, “키·경로·권한” 3요소 중 하나만 어긋나도 바로 무력화됩니다. 위 7단계를 순서대로 적용하면, 대부분의 cache miss는 10분 안에 원인 분리가 가능합니다.
추가로, 빌드/배포 파이프라인에서 인증/권한 이슈를 자주 겪는다면 GitLab CI Docker 로그인 실패 - 권한·토큰 해결처럼 “권한을 로그로 증명하는” 접근이 캐시 문제에도 그대로 통합니다.