Published on

GitHub Actions 캐시 안 먹을 때 키·경로·권한 7단계

Authors

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단계: 로그에서 restoresave를 분리해 읽기

가장 먼저 워크플로 로그에서 actions/cache 스텝의 메시지를 확인하세요.

  • 복원 실패: Cache not found for input keys:
  • 복원 성공: Cache restored from key:
  • 저장 스킵: Cache hit occurred on the primary key, not saving cache.
  • 저장 실패/미실행: post 단계가 안 돌거나, 권한/경로 문제

캐시가 “안 먹는다”는 보고를 받으면, 아래 두 질문을 먼저 답해야 합니다.

  1. 복원은 실패했는가? 성공했는가?
  2. 저장은 수행됐는가? 스킵/실패했는가?

이 두 개만 분리해도 원인의 70%가 정리됩니다.

2단계: key가 매번 바뀌는지(혹은 너무 고정인지) 점검

캐시 키 설계가 가장 흔한 원인입니다.

자주 하는 실수 1: 커밋 SHA를 키에 넣기

keygithub.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: 키가 너무 고정

반대로 keynpm-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단계(브랜치/스코프) 중 하나일 확률이 높습니다.

빠른 체크리스트(요약)

    1. 로그에서 restore 실패인지 save 실패인지 먼저 분리
    1. 키에 github.sha 같은 변동값이 들어가 miss를 만들고 있지 않은지
    1. path가 실제로 채워지는지(설치 이후 파일 존재/용량 확인)
    1. hashFiles 글롭이 제대로 매칭되는지(모노레포 주의)
    1. 포크 PR/이벤트 타입/권한으로 저장이 막히지 않는지
    1. 브랜치/스코프 정책으로 캐시가 공유되지 않는지
    1. 캐시가 너무 크거나 동시 저장 충돌이 없는지

CI 캐시는 한 번 잡히면 체감이 크지만, “키·경로·권한” 3요소 중 하나만 어긋나도 바로 무력화됩니다. 위 7단계를 순서대로 적용하면, 대부분의 cache miss는 10분 안에 원인 분리가 가능합니다.

추가로, 빌드/배포 파이프라인에서 인증/권한 이슈를 자주 겪는다면 GitLab CI Docker 로그인 실패 - 권한·토큰 해결처럼 “권한을 로그로 증명하는” 접근이 캐시 문제에도 그대로 통합니다.