- Published on
GitHub Actions 캐시 미스? 키·경로 함정 9가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 PR인데도 매번 의존성을 다시 받거나, 동일 커밋을 재실행해도 캐시가 안 맞는 경험은 GitHub Actions에서 흔합니다. 문제는 대개 actions/cache 자체가 아니라 키(key) 설계, 경로(path) 선택, 잡(job) 간 워크스페이스 차이, 락파일/매니페스트의 해시 범위 같은 디테일에서 발생합니다.
이 글은 캐시 미스의 원인을 “재현 가능한 체크리스트”로 정리하고, Node.js와 Python을 중심으로 바로 붙여 넣을 수 있는 YAML 예시를 제공합니다.
참고로 배포 파이프라인에서 병렬 실행이 캐시를 더 어지럽히는 경우가 많습니다. 동시 실행 제어가 필요하다면 GitHub Actions 동시 실행 막힘 해결 - concurrency·cancel-in-progress도 함께 보세요.
캐시가 동작하는 방식 3줄 요약
- 캐시는
key가 완전 일치하면cache-hit: true로 복원됩니다. key가 불일치해도restore-keys접두사(prefix)로 부분 일치 복원이 가능합니다.path는 “존재하는 디렉터리/파일”만 의미가 있고, 실행 환경/작업 디렉터리가 바뀌면 같은 문자열이어도 실제 경로가 달라질 수 있습니다.
함정 1) key에 매번 바뀌는 값(런 번호, SHA)을 넣는다
가장 흔한 실수입니다. github.run_number, github.run_id, github.sha를 키에 넣으면 실행할 때마다 새 캐시를 만들게 됩니다. 캐시는 “빌드 결과물 아카이브”가 아니라 “재사용 가능한 의존성/툴체인”에 가깝기 때문에, 키는 의존성 그래프가 바뀔 때만 변경되도록 설계해야 합니다.
나쁜 예
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ github.sha }}
좋은 예
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
함정 2) hashFiles 패턴이 실제 파일을 못 찾는다
hashFiles가 빈 문자열을 반환하면(매칭 파일 없음) 키가 예상과 달라지고, 결과적으로 캐시가 계속 미스 납니다. 특히 모노레포에서 락파일이 하위 폴더에 있거나, pnpm-lock.yaml을 쓰는데 package-lock.json을 해시하는 식의 불일치가 잦습니다.
점검 방법: 해시 결과를 로그로 남기기
- name: Debug hash
run: |
echo "lock hash = ${{ hashFiles('**/pnpm-lock.yaml') }}"
모노레포에서 의도적으로 범위를 좁히기
- uses: actions/cache@v4
with:
path: |
~/.pnpm-store
key: pnpm-${{ runner.os }}-${{ hashFiles('apps/web/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
함정 3) 캐시할 path가 매 실행마다 달라진다
대표적으로 다음 케이스가 있습니다.
- 패키지 매니저가 버전/설정에 따라 저장소 위치를 바꿈
~확장이 기대와 다름(특히 Windows)- 작업 디렉터리를
defaults.run.working-directory로 바꿨는데 상대 경로를 그대로 사용
Node.js 예시: npm 캐시 위치를 고정/확인
- name: Show npm cache dir
run: npm config get cache
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
Python 예시: pip 캐시 위치를 직접 조회
- name: Show pip cache dir
run: python -m pip cache dir
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-
함정 4) actions/setup-*의 내장 캐시와 actions/cache를 중복 사용한다
예를 들어 actions/setup-node에는 cache: npm 옵션이 있고, setup-python도 cache: pip를 지원합니다. 여기에 추가로 actions/cache를 또 붙이면 “무조건 망한다”까지는 아니지만, 다음 문제가 생기기 쉽습니다.
- 어떤 캐시가 실제로 히트했는지 추적이 어려움
- 서로 다른 키/경로로 캐시가 분산되어 히트율이 떨어짐
- 저장 시점이 꼬여서 기대한 캐시가 덮이거나 누락됨
권장: 하나만 선택
setup-node 내장 캐시 사용 예:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- run: npm ci
모노레포에서 내장 캐시의 cache-dependency-path를 지정하지 않으면 미스가 잦으니 꼭 맞춰주세요.
함정 5) OS/아키텍처를 키에 넣지 않아 교차 오염이 난다
리눅스에서 만든 캐시를 macOS/Windows에서 복원하면 바이너리 애드온, 파일 권한, 경로 구분자 차이로 문제가 생깁니다. 캐시 미스가 아니라 “복원은 됐는데 빌드가 깨지는” 형태로 나타나기도 합니다.
키에 최소한 OS는 포함
key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
네이티브 바이너리가 민감한 프로젝트라면 아키텍처까지 분리하는 것도 좋습니다.
key: deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}
함정 6) restore-keys를 너무 구체적으로 써서 “부분 복원”이 안 된다
restore-keys는 “접두사 매칭”입니다. 그런데 restore-keys를 키와 거의 동일하게 써버리면 사실상 의미가 없어집니다.
나쁜 예
restore-keys: |
npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
좋은 예: 접두사를 넉넉히
restore-keys: |
npm-${{ runner.os }}-
npm-
이렇게 하면 락파일이 조금 바뀌어도 “가까운 캐시”를 가져와서 다운로드량을 크게 줄일 수 있습니다.
함정 7) 캐시 대상에 “빌드 산출물”을 넣어 오히려 느려진다
캐시는 저장/복원에 압축과 업로드/다운로드가 포함됩니다. dist, build, target 같은 산출물을 무작정 캐시하면 다음 문제가 생깁니다.
- 캐시 크기가 커져서 저장/복원 자체가 느려짐
- 캐시가 자주 무효화되어 히트율이 떨어짐
- 산출물이 환경에 종속되면 재사용이 불가능
권장: 의존성/툴체인 위주로
- Node:
~/.npm,~/.pnpm-store,~/.cache/yarn - Python:
~/.cache/pip, Poetry의 다운로드 캐시 - Gradle/Maven:
~/.gradle/caches,~/.m2/repository
산출물 캐시는 “정말로 빌드 시간이 길고, 산출물이 재현 가능하며, 키를 엄격히 설계할 자신이 있을 때”만 고려하세요.
함정 8) 경로에 와일드카드/상대경로를 섞어 실제로는 아무것도 캐시되지 않는다
path는 글로브가 일부 동작하긴 하지만, 기대와 다르게 확장되거나(특히 숨김 폴더), 작업 디렉터리에 따라 빈 경로가 될 수 있습니다. 캐시 스텝 로그에서 “Paths: …”를 꼭 확인해야 합니다.
안전한 패턴: 절대경로/홈 기준
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.local/share/pnpm
key: toolcache-${{ runner.os }}-${{ hashFiles('**/requirements.txt', '**/pnpm-lock.yaml') }}
그리고 캐시 스텝 직전에 디렉터리가 실제로 존재하는지 만드는 것도 도움이 됩니다.
- name: Ensure cache dirs
run: |
mkdir -p ~/.cache/pip
함정 9) 잡 간 캐시 공유를 기대하지만, 실제로는 “저장 타이밍”이 안 맞는다
캐시는 “복원”과 “저장”이 같은 잡 안에서 일어납니다. 예를 들어 test 잡과 lint 잡이 동시에 돌고 둘 다 같은 키로 저장하려 하면, 둘 중 하나만 저장되거나 마지막에 끝난 잡이 저장을 시도하다가 noop가 되는 등 기대와 다르게 보일 수 있습니다.
이 문제는 캐시 미스처럼 보이기도 하고, “어제는 됐는데 오늘은 안 됨” 같은 플래키한 현상으로 나타납니다.
해결 전략
- 동시 실행을 제한해 캐시 경합을 줄이기
- 저장은 한 잡에서만 하도록 분리(예:
build잡에서만) - 키를 잡별로 분리하거나, 읽기 전용 복원만 하도록 설계
동시 실행 제어가 필요하다면 GitHub Actions 동시 실행 막힘 해결 - concurrency·cancel-in-progress를 참고해 concurrency 그룹을 잡아두는 것이 효과적입니다.
실전 예시 1) Node.js npm 캐시: 히트율 높게, 키 안정적으로
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'
- 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
핵심은 github.sha 같은 변동 값을 빼고, restore-keys로 “근접 캐시”를 허용하는 것입니다.
실전 예시 2) Python pip 캐시: requirements 해시 + 부분 복원
name: ci
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Ensure pip cache dir
run: mkdir -p ~/.cache/pip
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-py311-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-py311-
pip-${{ runner.os }}-
- run: python -m pip install -r requirements.txt
- run: pytest -q
Python은 인터프리터 버전에 따라 휠 호환성이 갈릴 수 있어 py311 같은 축을 키에 넣는 편이 안전합니다.
캐시 미스 디버깅 체크리스트
- 캐시 스텝 로그에서
cache-hit값 확인 key가 의도대로 고정되는지(락파일 변경 시에만 바뀌는지)hashFiles가 실제 파일을 매칭하는지(빈 값이면 위험)path가 실제로 존재하고, 실행 환경에서 동일한 위치인지- OS/아키텍처/런타임 버전 축이 키에 반영됐는지
restore-keys가 충분히 넓은 접두사인지- 동시 실행으로 저장 경합이 발생하지 않는지
마무리
GitHub Actions 캐시는 “한 번 맞추면 계속 이득”이지만, 처음 설계가 어긋나면 영원히 미스가 납니다. 특히 키를 너무 자주 바뀌게 만들거나, hashFiles가 아무 파일도 못 찾는 상태로 방치하는 경우가 많습니다.
위 9가지 함정을 하나씩 제거하면서, 키는 안정적으로, 경로는 실제로 존재하는 디렉터리로, restore-keys는 접두사 기반으로 넉넉히 잡아보세요. 캐시 히트가 올라가면 CI 시간과 네트워크 비용이 눈에 띄게 줄어듭니다.
추가로 AWS 배포 파이프라인에서 인증/권한 이슈로 재시도가 잦아 CI가 느려지는 경우도 많습니다. 배포 단계까지 포함한 안정화가 필요하면 GitHub Actions OIDC로 AWS 배포 실패 해결 가이드도 함께 보면 좋습니다.