테마 전환

GitHub Actions 캐시 전략: CI/CD 파이프라인을 5배 빠르게 만드는 법

Easton editorial illustration: fault-isolation scanner

npm install에 3분 15초.

지난해 제가 맡았던 한 프로젝트의 CI 빌드 시간이었습니다. 코드를 푸시할 때마다 GitHub Actions 로그의 로딩 표시를 바라보며 초록색 체크 표시가 나타나기만 기다려야 했습니다. 솔직히 그때는 다른 창으로 넘어가 딴짓을 하곤 했습니다. 어차피 기다려야 했으니까요.

그 후 캐시를 추가하자 같은 빌드가 40초 만에 끝났습니다. 거의 5배 빨라진 셈입니다.

마법 같은 일은 아닙니다. GitHub Actions의 캐시 전략을 제대로 설정했을 뿐입니다. 이 글에서는 제가 겪은 시행착오와 직접 테스트한 데이터, 바로 복사해서 사용할 수 있는 설정 템플릿을 한데 정리합니다. CI 빌드를 기다리고 있다면 커피 몇 잔 마실 시간을 아낄 수 있을 겁니다.

1. 캐시 메커니즘의 핵심 개념

먼저 캐시가 어떻게 작동하는지 이해해야 합니다. 그렇지 않으면 설정 과정에서 실수하기 쉽습니다.

GitHub Actions의 캐시 메커니즘은 사실 간단합니다. 검색 → 복원 → 저장의 세 단계입니다. key를 정의하면 GitHub가 모든 캐시에서 일치하는 항목을 찾습니다. 찾으면 작업 디렉터리에 바로 복원하고, 찾지 못하면 작업이 끝난 뒤 새 캐시를 저장합니다.

다만 반드시 알아야 할 제한이 몇 가지 있습니다.

제한 항목수치
저장소당 캐시 상한10 GB
개별 캐시 파일 상한5 GB(실제로는 1GB를 넘으면 문제가 생기기 쉬움)
캐시 보존 기간7일 동안 접근하지 않으면 삭제
전체 동시 업로드 제한캐시 최대 5개 동시 업로드

10GB 제한에 걸린 사례를 본 적이 있습니다. 프로젝트 의존성이 너무 많아 캐시가 계속 커지다가 결국 새 캐시는 저장되지 않고 기존 캐시마저 정리되어, 빌드할 때마다 콜드 스타트가 발생했습니다.

또 하나 혼동하기 쉬운 점이 있습니다. Cache와 Artifact는 서로 다릅니다. Cache는 CI를 빠르게 하기 위한 것이고, Artifact는 빌드 결과물이나 테스트 보고서처럼 사람이 확인하고 장기간 보관할 파일을 위한 것입니다. Cache에는 10GB 제한이 있지만 Artifact에는 상한이 없습니다. 다만 저장소 스토리지를 사용합니다.

이 밖에 Docker Layer Cache도 있습니다. Docker 빌드 전용 캐시로, 일반 캐시와는 작동 방식이 조금 다릅니다. 뒤에서 별도로 살펴보겠습니다.

2. 캐시 키 설계 전략

캐시의 적중 여부는 key를 어떻게 설계하느냐에 달려 있습니다. 캐시 전략 전체에서 가장 중요한 부분입니다.

hashFiles()란 무엇인가요?

GitHub는 파일의 해시값을 계산하는 내장 함수 hashFiles()를 제공합니다. 보통 package-lock.json이나 yarn.lock에 사용합니다. 의존성이 바뀌지 않으면 해시도 그대로이므로 캐시를 적중시킬 수 있습니다.

key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

이 코드는 npm-Linux-a1b2c3d4e5f6... 같은 형태의 키를 생성합니다. package-lock.json이 바뀌지 않는 한 이 키도 바뀌지 않습니다.

restore-keys: 예비 방식

하지만 의존성은 언젠가 업데이트됩니다. 이때 restore-keys가 필요합니다. 일종의 폴백 매칭 메커니즘입니다.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-{{ runner.os }}-

먼저 전체 key가 일치하는지 확인합니다. 일치하는 캐시가 없으면 npm-Linux-로 시작하는 기존 캐시를 찾습니다. 정확히 일치하지 않더라도 node_modules에 필요한 패키지 대부분이 이미 있으므로 새 의존성만 증분 설치하면 됩니다.

세 가지 키 명명 패턴 비교

직접 테스트한 결과 다음 세 가지 패턴을 권장합니다.

간단한 패턴(소규모 프로젝트에 적합):

key: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}

버전 패턴(여러 Node 버전에 적합):

key: {{ runner.os }}-node{{ matrix.node-version }}-{{ hashFiles('**/package-lock.json') }}

다중 경로 패턴(monorepo에 적합):

key: {{ runner.os }}-{{ hashFiles('**/package-lock.json', '**/yarn.lock') }}

캐시 적중 여부 확인 방법

actions/cachecache-hit 변수를 출력합니다.

- uses: actions/cache@v4
  id: cache-npm
  with:
    path: ~/.npm
    key: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}

- name: Check cache hit
  run: echo "Cache hit - {{ steps.cache-npm.outputs.cache-hit }}"

true는 정확히 일치했음을 뜻하고, false는 부분 일치 또는 완전한 miss를 뜻합니다. 이 변수에 따라 npm ci를 실행할지 결정할 수 있습니다.

- name: Install dependencies
  if: steps.cache-npm.outputs.cache-hit != 'true'
  run: npm ci

3. 실전 설정 예시

이론은 여기까지입니다. 이제 바로 코드를 살펴보겠습니다. 아래 설정은 모두 직접 테스트했으며 그대로 복사해 사용할 수 있습니다.

npm 캐시(setup-node 권장)

setup-node에는 이미 캐시 기능이 내장되어 있어 actions/cache를 직접 사용하는 것보다 간결합니다.

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # 또는 'yarn', 'pnpm'

한 줄 설정이면 충분합니다. 다만 node_modules처럼 다른 디렉터리를 캐시하려면 여전히 actions/cache를 사용해야 합니다.

- uses: actions/cache@v4
  with:
    path: node_modules
    key: {{ runner.os }}-nm-{{ hashFiles('**/package-lock.json') }}
    restore-keys: {{ runner.os }}-nm-

제 권장 사항: 특별한 요구 사항이 없다면 setup-node의 내장 캐시를 우선 사용하세요.

yarn과 pnpm

yarn의 캐시 디렉터리는 npm과 다릅니다.

- uses: actions/cache@v4
  with:
    path: |
      ~/.yarn/cache
      ~/.yarn/install-state.gz
    key: yarn-{{ runner.os }}-{{ hashFiles('**/yarn.lock') }}

pnpm은 조금 더 특수하며 전역 store를 사용합니다.

- uses: pnpm/action-setup@v4
  with:
    version: 9

- uses: actions/cache@v4
  with:
    path: ~/.pnpm-store
    key: pnpm-{{ runner.os }}-{{ hashFiles('**/pnpm-lock.yaml') }}

Python/pip 캐시

Python 프로젝트의 캐시 경로는 다음과 같습니다.

- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-{{ runner.os }}-{{ hashFiles('**/requirements.txt') }}
    restore-keys: pip-{{ runner.os }}-

Docker Layer Cache

Docker 빌드는 가장 많은 시간이 걸리는 작업 중 하나입니다. 다행히 BuildKit은 GitHub Actions 캐시 백엔드를 지원합니다.

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    cache-from: type=gha
    cache-to: type=gha,mode=max

type=gha는 GitHub Actions의 캐시 서비스에 Docker 레이어를 저장한다는 뜻입니다. 직접 테스트해 보니 5분 걸리던 이미지 빌드가 약 1분으로 줄었습니다.

Go 모듈 캐시

- uses: actions/cache@v4
  with:
    path: |
      ~/go/pkg/mod
      ~/.cache/go-build
    key: go-{{ runner.os }}-{{ hashFiles('**/go.sum') }}

Rust Cargo 캐시

- uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: cargo-{{ runner.os }}-{{ hashFiles('**/Cargo.lock') }}

Rust는 컴파일 시간이 길어 캐시로 많은 시간을 절약할 수 있습니다. 다만 target 디렉터리가 계속 커질 수 있으므로 정기적으로 정리하는 것이 좋습니다.

4. 성능 최적화와 모범 사례

직접 측정한 데이터와 경험한 문제를 정리했습니다. 같은 시행착오를 줄이는 데 도움이 되길 바랍니다.

성능 벤치마크 데이터

RunsOn의 테스트 보고서(2026년 1월 업데이트)에 따르면 캐시를 적절히 설정했을 때 다음과 같은 결과가 나왔습니다.

작업캐시 없음캐시 있음향상 폭
npm install3분40초약 5배
yarn install2분 30초35초약 4배
Docker build5분1분약 5배
pip install45초8초약 5배

캐시 적중률은 70~90%이며, 키 전략을 얼마나 잘 설계했는지에 따라 달라집니다.

흔한 실수

node_modules를 직접 캐시하지 마세요

처음에는 저도 그렇게 했다가 큰 문제를 겪었습니다.

# 이렇게 작성하지 마세요
path: node_modules

node_modules는 플랫폼에 따라 달라집니다. Linux에서 설치한 패키지는 Windows에서 실행할 때 문제가 생길 수 있습니다. 올바른 방법은 전역 캐시 디렉터리(~/.npm)를 캐시하고 npm ci가 직접 구성하도록 하는 것입니다.

OS 간 캐시에는 GNU tar + zstd를 사용하세요

기본 tar 형식은 macOS와 Windows에서 서로 달라 캐시 복원에 실패할 수 있습니다. 다음 설정을 추가하세요.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}
    enableCrossOsArchive: true

캐시 오염 문제

캐시에 문제가 있는 의존성이 저장되어 빌드가 계속 실패할 때가 있습니다. 해결 방법은 다음과 같습니다.

  1. 캐시 수동 삭제: GitHub 저장소의 Actions → Caches 페이지에서 삭제합니다.
  2. key 강제 변경: key에 접두사나 타임스탬프를 추가해 새로 생성합니다.
key: npm-v2-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

모범 사례 체크리스트

마지막으로 핵심 사항을 정리합니다. 설정하기 전에 하나씩 확인해 보세요.

  1. 공식 action의 내장 캐시를 우선 사용하세요(setup-node, setup-python).
  2. key에 hashFiles를 포함하세요. 그렇지 않으면 의존성이 업데이트된 뒤에도 오래된 캐시를 사용하게 됩니다.
  3. restore-keys를 작성하세요. 폴백 매칭이 위기 상황에서 도움이 됩니다.
  4. node_modules를 캐시하지 말고 전역 디렉터리를 캐시하세요.
  5. 오래된 캐시를 정기적으로 정리해 10GB 제한을 넘지 않도록 하세요.

5. 자주 묻는 질문

Q1: 캐시 적중률이 낮은 이유는 무엇인가요?

가장 흔한 원인은 key가 너무 자주 바뀌는 것입니다. 예를 들어 key에 타임스탬프나 브랜치 이름을 넣으면 push할 때마다 새로운 key가 생성됩니다. 해결 방법은 불필요한 변수를 빼고 runner.oshashFiles만 사용하는 것입니다.

또 다른 원인은 hashFiles가 매칭하지 않아야 할 파일까지 매칭하는 것입니다. 예를 들어 hashFiles('**/*.json')라고 작성하면 설정 파일 하나만 바뀌어도 캐시가 무효화됩니다. package-lock.json이나 yarn.lock만 매칭하도록 변경하세요.

Q2: 캐시 공간 한도를 초과하면 어떻게 해야 하나요?

10GB가 넉넉해 보여도 monorepo나 Docker 캐시는 쉽게 한도를 넘을 수 있습니다. 해결 방법은 다음과 같습니다.

  1. 정기 정리: GitHub Actions → Caches에서 오래된 캐시를 수동으로 삭제합니다.
  2. 캐시 분리: 의존성 유형마다 다른 key를 사용해 하나의 캐시에 모든 것을 저장하지 않습니다.
  3. self-hosted runners 사용: 10GB 제한이 없습니다.

Q3: self-hosted runners에는 특별한 설정이 필요한가요?

특별한 설정은 필요하지 않으며 캐시 메커니즘도 같습니다. 다만 self-hosted runners에는 캐시가 로컬에 저장되어 네트워크 전송 지연 없이 더 빠르게 복원된다는 장점이 있습니다. 단점은 캐시가 자동으로 정리되지 않아 정기적으로 삭제하는 스크립트를 직접 작성해야 한다는 것입니다.

Q4: 캐시를 강제로 업데이트하려면 어떻게 하나요?

key를 변경하세요. 접두사에 버전 번호를 추가하면 됩니다.

key: npm-v3-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

또는 기존 캐시를 삭제해 시스템이 새로 생성하도록 할 수 있습니다.

마무리

지금까지의 내용을 한 문장으로 요약하면 이렇습니다. 캐시를 잘 활용하면 CI를 5배 빠르게 만들 수 있습니다.

간단히 계산해 보겠습니다. 빌드할 때마다 2분을 절약하고 하루에 10번 실행한다면 한 달에 600분, 약 10시간을 아끼는 셈입니다. 글을 여러 편 쓸 수 있는 시간입니다.

GitHub Actions를 이제 막 시작했다면 setup-node의 내장 캐시부터 사용해 보세요. 설정 한 줄이면 충분합니다. 병목이 생겼을 때 더 복잡한 키 전략과 Docker Layer Cache를 살펴봐도 늦지 않습니다.

참고로 이 글은 GitHub Actions 실전 가이드 시리즈의 세 번째 글입니다. 앞서 CI 파이프라인 구축과 배포 전략도 다뤘으니 관심이 있다면 이전 글도 확인해 보세요.

다음에 코드를 푸시할 때는 빌드 시간을 꼭 살펴보세요. 3분을 40초로 줄일 수 있는지 직접 시험해 보면 알 수 있습니다.

GitHub Actions 캐시를 설정해 CI/CD 가속하기

GitHub Actions 캐시를 설정하여 npm install 빌드 시간을 3분에서 40초로 단축합니다.

⏱️ Estimated time: 10 min

  1. 1

    Step 1: 캐시 방식 선택

    프로젝트의 패키지 관리자에 따라 캐시 방식을 선택합니다.

    • npm 프로젝트: setup-node 내장 캐시 우선 사용
    • yarn/pnpm 프로젝트: 캐시 경로 설정
    • Docker 빌드: BuildKit의 gha 백엔드 사용
  2. 2

    Step 2: 캐시 키 설계

    hashFiles()로 잠금 파일을 기반으로 한 안정적인 키를 생성합니다.

    • 기본 패턴: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}
    • restore-keys를 예비 매칭에 활용
    • key에 타임스탬프나 브랜치 이름을 넣지 않기
  3. 3

    Step 3: 캐시 설정 추가

    workflow 파일에 캐시 단계를 추가합니다.

    • npm: actions/setup-node@v4를 사용하고 cache: 'npm' 설정
    • 사용자 지정 경로: actions/cache@v4 사용
    • Docker: cache-from과 cache-to 설정
  4. 4

    Step 4: 캐시 효과 확인

    캐시 적중 여부를 확인합니다.

    • cache-hit 출력 변수 확인(true이면 정확히 일치)
    • 빌드 시간 비교(4~5배 단축되어야 함)
    • Actions → Caches 페이지에서 캐시가 저장되었는지 확인
  5. 5

    Step 5: 캐시 정기 관리

    캐시 문제를 예방합니다.

    • 캐시 공간 사용량 모니터링(상한 10GB)
    • 오래된 캐시 정기 삭제
    • 캐시 오염 시 key 접두사를 변경해 강제로 재생성

FAQ

캐시 적중률이 30%에 불과한 이유는 무엇인가요?
대개 key 설계 문제입니다. key에 타임스탬프나 브랜치 이름처럼 자주 바뀌는 변수를 넣었는지 확인하고, runner.os와 hashFiles만 사용하도록 변경하세요. 또한 hashFiles 경로가 잠금 파일과 정확히 일치하는지 확인하고, 와일드카드로 너무 많은 파일을 매칭하지 않도록 주의하세요.
캐시가 10GB를 넘으면 어떻게 되나요?
GitHub가 공간을 확보하기 위해 가장 오래된 캐시를 자동으로 정리합니다. 다음 방법을 권장합니다.

• 의존성 유형별로 캐시 분리(npm, Docker, pip에 각각 별도 key 사용)
• Actions → Caches 페이지에서 불필요한 캐시를 정기적으로 수동 삭제
• monorepo 프로젝트는 저장소 분리 또는 self-hosted runners 사용 고려
서로 다른 브랜치에서 캐시를 공유할 수 있나요?
기본적으로 캐시는 현재 브랜치와 기본 브랜치(main/master) 사이에서만 공유됩니다. 브랜치 간에 공유하려면 key에서 브랜치 이름을 빼고 파일 기반 hash만 사용해야 합니다. restore-keys를 사용하면 다른 브랜치의 캐시를 매칭하는 데도 도움이 됩니다.
self-hosted runner의 캐시는 무엇이 다른가요?
캐시 메커니즘은 같지만 두 가지 차이가 있습니다. 캐시가 로컬에 있어 네트워크 지연 없이 더 빠르게 복원된다는 장점이 있습니다. 반면 10GB 제한은 없지만 자동으로 정리되지 않으므로 오래된 캐시를 주기적으로 삭제하는 스크립트를 직접 작성해야 합니다.
캐시 복원에 실패하면 빌드가 중단되나요?
아닙니다. 캐시는 선택적인 최적화이므로 복원 실패가 빌드에 영향을 주지 않습니다. GitHub Actions는 이후 단계를 계속 실행하며, 이번 빌드에서는 의존성을 다시 다운로드할 뿐입니다. 로그에 'Cache not found for key: xxx' 메시지가 표시되고, 다음 실행을 위해 새 캐시가 자동으로 저장됩니다.
캐시를 업데이트해야 하는 시점은 어떻게 판단하나요?
다음 세 가지 경우에 캐시를 업데이트해야 합니다.

• 의존성 버전 변경: hashFiles가 자동으로 처리하므로 수동 개입 불필요
• 캐시 오염: 빌드가 갑자기 실패하면 기존 캐시 삭제 필요
• 설정 변경: Node 버전 업그레이드 시 key에 버전 번호 추가 필요

대부분은 올바르게 설정한 뒤 수동으로 관리할 필요가 없습니다.

2분 읽기 · 게시일: 2026년 4월 7일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog