- Published on
GitHub Actions 캐시 무효화로 빌드 느림 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 로컬에서는 빠른데 GitHub Actions만 유독 느려질 때, 가장 먼저 의심해야 하는 축이 cache invalidation 입니다. 캐시가 너무 자주 무효화되면 매번 의존성 설치와 빌드가 풀로 돌아가고, 반대로 캐시가 너무 넓게 유지되면 오래된 산출물이 꼬여서 재빌드나 실패를 유발합니다. 이 글은 “캐시가 무효화되어 빌드가 느려지는” 케이스를 중심으로, 원인 진단부터 key 설계, 캐시 범위 최적화, 안전한 무효화 전략까지 실전 레시피로 정리합니다.
특히 actions/cache는 동작이 단순해 보이지만, key와 restore-keys 설계를 조금만 잘못해도 캐시가 매번 새로 생성되거나(=무효화), 반대로 엉뚱한 캐시가 복원되어 빌드가 흔들립니다.
관련해서 캐시 미스 자체를 더 깊게 디버깅하는 방법은 아래 글도 함께 보면 좋습니다.
1) “캐시 무효화”가 빌드를 느리게 만드는 전형적인 패턴
패턴 A: key에 변동값을 넣어 매번 새 캐시 생성
가장 흔한 실수는 key에 github.run_id, github.run_number, 커밋 SHA 전체 같은 항상 변하는 값을 넣는 것입니다. 이렇게 하면 캐시는 사실상 매 실행마다 새로 저장되고, 복원은 거의 실패합니다.
- 증상:
Cache not found for input keys가 자주 보임 - 결과:
npm ci또는pnpm install이 매번 풀로 실행
패턴 B: key가 너무 민감해서 사소한 변경에도 캐시가 깨짐
hashFiles 대상에 불필요한 파일(예: README.md, package.json 외의 자주 변하는 설정 파일, 빌드 산출물)을 포함하면, 의존성과 무관한 변경에도 캐시가 무효화됩니다.
- 증상: 문서 수정만 했는데도 의존성 캐시가 새로 만들어짐
- 결과: 불필요한 설치 및 빌드 시간 증가
패턴 C: 캐시 범위를 잘못 잡아 “복원은 되는데 느린” 상태
예를 들어 Node 생태계에서 node_modules를 통째로 캐시하면, 압축 및 업로드/다운로드 비용이 커져서 오히려 느려질 수 있습니다. 특히 대형 모노레포에서는 캐시 크기가 커지며 병목이 됩니다.
- 증상: 캐시 복원 로그는 보이는데 전체 시간은 줄지 않음
- 결과: 네트워크 I/O가 설치 시간과 맞먹거나 더 커짐
패턴 D: 잘못된 캐시로 인해 재빌드가 폭증
Next.js, Turbo, Nx 등 빌드 캐시가 꼬이면 “캐시 복원은 성공했는데 빌드는 매번 풀로” 같은 현상이 생깁니다.
- 증상:
next build가 항상 오래 걸림, 증분 빌드가 안 됨 - 결과: 캐시가 문제를 해결하는 게 아니라 문제를 만드는 상태
2) 먼저 확인할 로그 포인트: 캐시가 왜 무효화되나
워크플로 로그에서 다음을 우선 확인하세요.
actions/cache단계에서Cache restored from key:가 찍히는지Cache not found for input keys:가 나오는지- 복원된 키가 기대한 키인지(접두사만 맞고 다른 캐시가 잡히는지)
- 캐시 저장 단계에서
Cache saved successfully가 매번 발생하는지
캐시가 “매번 저장”된다면, 대개 key가 매번 달라지고 있다는 뜻입니다. 반대로 “복원은 되는데 느리다”면 캐시 대상이 너무 크거나, 복원 후에도 설치/빌드가 재실행되는 이유가 있는지 봐야 합니다.
3) 핵심 원칙: key는 “의존성 변화”에만 반응하게 설계
캐시 무효화로 인한 빌드 느림을 줄이려면, key를 아래 원칙으로 재설계합니다.
- OS, 런타임 버전(예: Node 20) 등 호환성 축은 포함
- 의존성 잠금 파일(예:
package-lock.json,pnpm-lock.yaml,yarn.lock)의 해시를 포함 - 소스 파일 변경, 커밋 SHA, run id 같은 값은 넣지 않기
- 모노레포라면 워크스페이스별 lockfile 전략을 고려
Node 예시: pnpm 스토어 캐시(권장 패턴)
node_modules 대신 pnpm 스토어를 캐시하면 크기가 상대적으로 안정적이고, 설치는 링크 기반으로 빨라집니다.
name: ci
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Enable corepack
run: corepack enable
- name: Get pnpm store path
id: pnpm-store
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Restore pnpm store cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-node20-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-node20-
- name: Install
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
포인트는 key가 pnpm-lock.yaml에만 반응한다는 점입니다. 소스 변경으로는 캐시가 무효화되지 않으니, PR에서 코드만 바뀌는 경우 설치 단계가 안정적으로 빨라집니다.
npm 예시: ~/.npm 캐시
npm도 node_modules보다 npm 캐시 디렉터리(~/.npm) 캐시가 보통 더 낫습니다.
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Restore npm cache
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node20-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-node20-
- run: npm ci
4) “너무 자주 무효화”를 막는 hashFiles 범위 조정
hashFiles는 편하지만, 글로브 패턴이 과하면 캐시가 자주 깨집니다.
- 좋은 예:
hashFiles('**/pnpm-lock.yaml') - 주의:
hashFiles('**/*')같은 과도한 패턴 - 모노레포 주의: 루트 lockfile 하나로 충분한데 패키지별 lockfile까지 다 포함하면 변경 민감도가 커짐
권장 체크리스트:
- lockfile이 단일 루트에만 있으면 경로를 좁히기:
hashFiles('pnpm-lock.yaml') - lockfile이 여러 개면 “정말 필요한 것만” 포함
- 문서나 CI 설정 변경으로 캐시가 깨진다면
hashFiles대상이 잘못된 것
5) “복원은 되는데 느림”을 해결하는 캐시 대상 재선정
캐시로 시간을 줄이려면, 캐시의 전송비용이 절감되는 시간보다 작아야 합니다. 즉 “캐시 다운로드/업로드가 너무 비싸면” 역효과가 납니다.
권장 우선순위
- 패키지 매니저 다운로드 캐시(
~/.npm, pnpm store, yarn cache) - 빌드 툴 캐시(예: Next.js
.next/cache, Turbo.turbo, Nx.nx/cache) - 정말 필요한 경우에만
node_modules(대개 비권장)
Next.js 예시: .next/cache만 캐시하기
Next.js 전체 .next를 캐시하면 산출물까지 포함되어 충돌/비대화가 생기기 쉽습니다. 보통은 .next/cache만 잡는 게 안전합니다.
- name: Restore Next.js build cache
uses: actions/cache@v4
with:
path: .next/cache
key: next-cache-${{ runner.os }}-node20-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
next-cache-${{ runner.os }}-node20-
- run: pnpm build
여기서도 핵심은 key를 소스 해시로 잡지 않는 것입니다. 소스가 바뀌면 Next가 알아서 필요한 부분을 재빌드하고, 캐시는 “툴 내부 최적화 데이터”로 남겨두는 쪽이 대개 효율적입니다.
6) 의도적으로 캐시를 무효화하는 안전한 방법
캐시가 오염되었거나(예: 잘못된 바이너리, 깨진 lockfile 상태), 대규모 업그레이드(예: Node 18에서 20으로 점프)로 캐시를 갈아엎어야 할 때가 있습니다. 이때 “무효화를 위한 스위치”를 설계해두면 운영이 편합니다.
방법 A: CACHE_VERSION 접두사로 강제 무효화
워크플로에 버전 문자열을 두고, 필요할 때만 올립니다.
env:
CACHE_VERSION: v3
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ env.CACHE_VERSION }}-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ env.CACHE_VERSION }}-${{ runner.os }}-node20-
CACHE_VERSION만 v4로 바꾸면 전체 캐시가 새로 생성됩니다. “캐시 삭제 권한”이 없거나, 리포지토리 캐시 정리 정책과 무관하게 즉시 효과가 필요할 때 유용합니다.
방법 B: 런타임/플랫폼 변경을 키에 반영
네이티브 모듈이 있는 프로젝트는 runner.os, Node 버전을 키에 반드시 포함하세요. 그렇지 않으면 리눅스에서 만든 캐시가 다른 환경에 복원되어 설치가 꼬이고, 결국 재설치로 느려집니다.
- 예:
...-${{ runner.os }}-node20-...
방법 C: 복원은 넓게, 저장은 좁게
restore-keys는 접두사 매칭이라 “가장 근접한 캐시”를 찾아줍니다. 다만 저장은 정확한 key로만 되므로, 아래처럼 설계하면 lockfile이 바뀌었을 때도 이전 캐시를 seed로 활용할 수 있습니다.
key: pnpm-store-${{ runner.os }}-node20-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-node20-
이 전략은 “완전 미스”를 줄여 네트워크 다운로드를 줄이는 데 효과적입니다.
7) 캐시가 느림을 만드는 숨은 원인: 동시성, 권한, 분기
PR과 push가 서로 다른 캐시를 만드는 경우
기본적으로 캐시는 브랜치/이벤트 조건에 따라 접근성이 달라질 수 있습니다. 특히 포크 PR은 보안 정책 때문에 캐시 저장이 제한되거나 기대와 다르게 동작할 수 있습니다.
- 증상:
push에서는 빠른데 PR에서는 느림 - 대응: PR 워크플로에서 캐시 저장이 가능한지, 동일 키로 복원되는지 확인
이 이슈는 key 문제처럼 보이지만 실제로는 권한/이벤트 정책인 경우가 많습니다. 위에 링크한 “권한” 글을 함께 확인하세요.
매트릭스 잡이 서로 캐시를 덮어쓰는 경우
Node 버전 매트릭스, OS 매트릭스에서 동일 key를 쓰면 서로 다른 환경 캐시가 충돌합니다. 반드시 OS, 런타임 버전, 아키텍처를 포함하세요.
8) 빠르게 적용하는 개선 플랜(현업 체크리스트)
actions/cache로그에서 복원 성공률부터 확인key에서 변동값 제거:run_id,sha, 날짜 등 제거hashFiles범위를 lockfile로 최소화node_modules캐시를 쓰고 있다면 패키지 매니저 캐시로 전환- 빌드 캐시는 툴 전용 캐시 디렉터리만 선택: 예를 들어 Next는
.next/cache - 강제 무효화가 필요하면
CACHE_VERSION도입 - PR과 push, 포크 PR의 캐시 정책 차이를 분리 진단
9) 결론: 캐시 무효화는 “설계 문제”로 해결된다
GitHub Actions 빌드가 느려지는 문제는 단순히 캐시를 켜고 끄는 문제가 아니라, key가 무엇에 반응하는지(=무효화 조건), 캐시 대상이 무엇인지(=전송 비용), 그리고 이벤트/권한/매트릭스 조건이 어떻게 결합되는지의 문제입니다.
- 캐시 무효화가 잦다면
key를 lockfile 중심으로 재설계 - 복원은 되는데 느리다면 캐시 대상을 더 작고 효과적인 디렉터리로 교체
- 캐시 오염이 의심되면
CACHE_VERSION으로 안전하게 리셋
위 원칙대로 정리하면 “PR 하나 올릴 때마다 5~10분씩 느려지는” 류의 CI 병목을 꽤 높은 확률로 제거할 수 있습니다.