
Next.js GitHub Actions CI/CD 최적화 전략
GitHub Actions의 캐시 격리와 restore-keys를 활용한 Next.js 프로젝트 빌드 최적화 전략을 정리합니다.
들어가며
빠른 배포와 안정적인 운영을 위해서는 CI/CD 환경이 필수적입니다.
CI/CD를 구현하는 방법으로는 Jenkins, GitLab CI, GitHub Actions 등 다양한 도구가 있으며, 각각의 특색을 이해하고 프로젝트 상황에 맞게 선택해야 합니다.
저는 Git Flow 전략(main / develop / feature 브랜치 구조) 을 기반으로 Next.js 프로젝트를 운영하면서 GitHub Actions를 활용한 CI/CD 환경을 구성했습니다.
이 글에서는 다음과 같은 흐름으로 내용을 정리하려고 합니다.
- 캐시 대상 선정 - 어떤 디렉터리를 캐시해야 설치 및 빌드 속도를 효과적으로 단축할 수 있는지
- GitHub Actions의 특징 - 특히 캐시 메커니즘과 관련된
cache isolation
,restore-keys
동작 방식 - 실제 코드 예시 - 워크플로우 YAML 구성과 캐시 적용 방법
- 마무리 - 적용하면서 배운 점과 캐시 관리 전략에 대한 정리
이를 통해 Next.js 프로젝트에서 GitHub Actions를 활용한 CI/CD를 구축할 때 고려해야 할 핵심 포인트를 공유하려고 합니다.
캐시 대상 선정
CI/CD에서 빌드 속도를 개선하려면 캐시 대상 선정을 올바르게 하는 것부터 시작해야 합니다. Next.js 프로젝트의 경우 크게 두 가지 캐시를 활용하는 것이 효과적입니다.
- 라이브러리 설치 관련 캐시
- Next.js 빌드 캐시
캐시 후보군 1 - 라이브러리 설치 관련 캐시
npm, Yarn, pnpm 등 패키지 매니저들은 버전에 따라 방식이 다르지만, 공통적으로 다음 단계를 거칩니다.
- 패키지 매니저 캐시 확인 및 다운로드
- 캐시에 있으면 fetch
- 없으면 registry에서 다운로드 후 캐시에 저장
- 실행 환경에 패키지 연결
- npm, Yarn Classic:
node_modules
생성 - Yarn Berry(PnP), pnpm: 자체 방식(PnP API, 심볼릭 링크 등)으로 의존성 연결
- 빌드 단계에서 패키지 활용
즉, node_modules
는 일부 툴/버전에서만 쓰이고, 최신 도구들은 다른 방식으로도 패키지를 연결합니다.
그래서 캐시 전략을 세울 때는 사용하는 패키지 매니저와 버전을 반드시 고려해야 합니다.
아래는 Yarn Berry(v2 이상) 환경에서 실제 프로젝트 설치 시 출력된 로그 일부입니다.
┌ Resolution step
└ Completed in 9s 327ms
┌ Fetch step
└ Completed in 1m 53s
┌ Link step
└ Completed in 37s 765ms
Done in 2m 40s
Resolution step
: 의존성 버전 확정Fetch step
: 캐시 또는 레지스트리에서 패키지를 가져오는 단계 → 가장 많은 시간이 소요됨Link step
: PnP 또는 node_modules로 패키지 연결
위 로그에서도 확인할 수 있듯이, 전체 설치 시간 중 대부분이 Fetch step에 소비됩니다.
따라서, 개인적인 판단으로는 라이브러리 설치 캐시를 구성할 때 node_modules
만 캐시하는 것은 비효율적입니다.
패키지 매니저 캐시 디렉터리를 반드시 포함해야 설치 속도 개선 효과를 극대화할 수 있습니다.
프로젝트 설정에 따라 다르겠지만, 일반적인 패키지 매니저별 권장 캐시 대상은 다음과 같습니다:
- npm
- node_modules
- .npm/_cacache
- Yarn Classic (v1)
- node_modules
- .yarn/cache
- Yarn Berry (v2 이상)
- PnP 모드: .yarn/cache
- nodeLinker=node-modules 모드: .yarn/cache + node_modules
- pnpm
- .pnpm-store (중앙 스토어)
캐시 후보군 2 - Next.js 빌드 캐시
Next.js는 빌드 속도 개선을 위해 .next/cache
디렉토리에 빌드 아티팩트를 저장합니다. 이 캐시는 재빌드 시 변경되지 않은 부분을 재사용하여 빌드 시간을 크게 단축시켜 줄 수 있습니다.
그러나 .next/cache
는 자동 정리되지 않으며, 빌드가 반복될수록 용량이 계속 증가하는 특징이 있습니다.
2025-09-11 기준으로도 이 디렉토리는 계속 누적되며 삭제되지 않습니다.
이러한 특성을 고려할 때, CI/CD 환경에서는 다음과 같은 전략이 필요합니다:
.next/cache
를 포함한 캐시 활용 → 빌드 시간 단축- 캐시 용량이 커질 경우 캐시 업로드/다운로드 자체가 병목 요소 발생 가능
- 따라서 주기적인 캐시 초기화 또는 선별적 캐시 유지 전략이 필요합니다
실사례: MUI 프로젝트에서 .next/cache
용량 증가 이슈
- MUI 프로젝트에서도
.next/cache
크기가 지속적으로 커지는 문제가 보고되었습니다. 해당 이슈에서는 약 7개월 동안 3GB까지 증가했다는 사례를 확인할 수 있습니다. (GitHub issue) - 이처럼 장기적으로 누적된 캐시는 CI/CD 과정에서 캐시 업로드/다운로드 시간이 병목이 되거나, 디스크 공간 부족 문제를 일으킬 수 있습니다.이프라인에 포함시키는 방법이 효과적입니다.
요약하면:
.next/cache
는 빌드 최적화에 도움되지만, 관리 없이는 무한히 커질 수 있으므로- 적절한 캐시 관리 전략 (초기화 또는 선별적 유지) 이 필수입니다.
GitHub Actions 캐시 특징
GitHub Actions는 CI/CD 워크플로우에서 캐시를 지원하여 빌드 속도를 개선할 수 있습니다.
하지만 캐시가 단순히 공유되는 것이 아니라, 특정한 규칙을 가지고 관리되기 때문에 이를 잘 이해하고 활용하는 것이 중요합니다.
Cache Isolation
GitHub Actions의 캐시는 브랜치(branch)와 태그(tag) 단위로 격리(isolation) 되어 있습니다.
즉, 각 브랜치나 태그는 독립적인 캐시 영역을 가지며, 기본 브랜치(default branch)와 자기 자신의 캐시 영역만 접근할 수 있습니다.
이 말은 곧 다음과 같은 의미를 가집니다.
- feature 브랜치에서 생성된 캐시는 develop이나 main 브랜치에서 접근할 수 없음
- 기본 브랜치에서 생성된 캐시는 다른 브랜치에서도 활용 가능
따라서 Git Flow 전략에서 효율적으로 캐시를 사용하려면, 기본 브랜치에서 캐시를 먼저 생성하고 다른 브랜치에서 이를 재활용하는 구조가 필요합니다.
참고: [GitHub Docs - Dependency caching]restore-key
GitHub Actions의 캐시는 키(key) 기반으로 탐색되며, 정확히 일치하는 키가 없을 경우 사용할 수 없게 됩니다.
이를 보완하기 위해 restore-keys
기능이 제공되며, 유사한 키를 순차적으로 탐색하여 부분 일치하는 캐시를 복원할 수 있습니다.
동작 방식은 다음과 같습니다.
- GitHub은 먼저 정확히 일치하는 캐시 키를 탐색
- 일치하는 키가 있으면 해당 캐시 사용
- 없을 경우,
restore-keys
에 지정된 키들을 최근 저장된 순서대로 탐색 - 부분적으로 일치하는 키가 발견되면 해당 캐시를 복원
예시:
- name: Cache dependencies
uses: actions/cache@v3
with:
path: node_modules
key: nextjs-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }}
restore-keys: |
nextjs-${{ hashFiles('yarn.lock') }}-
nextjs-
이 경우 GitHub은 캐시를 찾을 때 다음과 같은 순서로 탐색합니다.
- nextjs-abcdef-123456 (정확히 일치)
- nextjs-abcdef- 로 시작하는 캐시 (부분 일치)
- nextjs- 로 시작하는 캐시 (더 넓은 범위 부분 일치)
이 메커니즘을 잘 활용하면, 브랜치 간 캐시 미스를 줄이고 빌드 시간을 단축할 수 있습니다.
GitHub Actions 캐시 활용 방법 (실제 구성)
앞에서 설명한 캐시 메커니즘(cache isolation
, restore-keys
)을 실제 프로젝트에서 어떻게 적용했는지 정리합니다.
주요한 고려사항은 다음과 같았습니다:
- Cache isolation 문제: GitHub Actions에서는 브랜치별로 캐시가 격리되기 때문에, 빌드 과정에서 캐시를 생성하는 대신 이미 생성된 캐시를 불러오기만 하도록 구성했습니다.
- 독립적인 캐시 생성 워크플로우: 캐시 생성은 별도의 워크플로우에서 기본 브랜치(develop)에서만 실행되도록 분리했습니다. 이렇게 해야 다른 브랜치에서도 해당 캐시를 활용할 수 있습니다.
- restore-key 설정: 해시 키가 완전히 일치하지 않더라도 유사한 캐시를 최대한 활용할 수 있도록
restore-keys
를 활용했습니다.
PR 빌드 워크플로우 예시
Pull Request가 열리거나 업데이트될 때 실행되는 워크플로우입니다.
이 단계에서는 캐시를 생성하지 않고 restore만 수행하여 빌드 속도를 높이는 데 집중했습니다.
name: PR Build Test
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Restore Cache
uses: actions/cache/restore@v4
with:
path: |
.yarn/cache
.next/cache
node_modules
key: ${{ runner.os }}-nextjs-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('yarn.lock') }}-
${{ runner.os }}-nextjs-
- run: yarn install
- run: yarn build --no-lint
캐시 생성 워크플로우 예시
캐시 저장은 별도의 워크플로우에서 실행했습니다. 기본 브랜치에서 주기적으로 실행되며, 여기서 새 캐시를 생성해두면 이후 PR 빌드에서 활용할 수 있습니다.
여기서는 캐시를 불러오는(restore) 단계가 없는 것이 특징인데, 이는 Next.js의 .next/cache가 빌드를 반복할수록 점점 커지는 현상을 고려한 조치입니다. 즉, 새로운 캐시를 항상 새롭게 생성하고 저장해두는 방식으로, 캐시 용량 누적 문제를 예방했습니다.
name: Cache Persist
on:
workflow_run:
workflows: ["Main push"]
types: [completed]
jobs:
persist-cache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: yarn install
- run: yarn build
- name: Save cache
uses: actions/cache/save@v4
with:
path: |
.yarn/cache
.next/cache
node_modules
key: ${{ runner.os }}-nextjs-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }}
메인 브랜치 트리거 워크플로우
마지막으로, 기본 브랜치(develop) 에서만 캐시 생성이 수행되도록 트리거용 워크플로우를 추가했습니다. 이는 불필요한 캐시 생성 과정을 최소화하고, 정식 배포 과정 중에만 안정적으로 캐시를 생성하기 위한 장치입니다.
name: Main push
on:
push:
branches:
- main
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- run: echo "main push"
마무리
이번 글에서는 Next.js 프로젝트를 Git Flow 전략 하에서 운영하면서, GitHub Actions 기반 CI/CD 환경을 구성할 때 고려했던 캐시 전략을 정리했습니다.
핵심적으로 다룬 내용은 다음과 같습니다:
-
캐시 대상 선정
- 패키지 매니저 캐시 디렉터리 + node_modules
- Next.js의
.next/cache
디렉터리 - 각각의 특징과 관리 전략
-
GitHub Actions 캐시 특징
- 브랜치 단위로 격리되는
cache isolation
- 부분 일치를 허용하는
restore-keys
- 브랜치 단위로 격리되는
-
실제 워크플로우 구성
- PR 빌드: 캐시를 생성하지 않고 restore만 수행
- Cache Persist: 기본 브랜치에서만 캐시 생성 (Next.js 캐시 증가 문제 대응)
- Main push: 정식 배포 과정에서만 캐시 생성을 트리거
간략한 내용
- 캐시 전략은 “무엇을 캐시할 것인가”와 “언제 캐시를 생성할 것인가” 두 가지를 고민해야 합니다.
- GitHub Actions의
cache isolation
때문에 무조건적으로 캐시가 공유되지 않는다는 점을 고려해야 하며, - Next.js의
.next/cache
처럼 시간이 지남에 따라 용량이 계속 증가하는 캐시는 주기적인 초기화나 제한적인 관리가 필수적입니다. restore-keys
는 캐시 활용률을 높이는 좋은 장치지만, 남용할 경우 예상치 못한 오래된 캐시를 가져올 수도 있어 주의가 필요합니다.
추가 고려 사항
이 글에서는 주로 캐시 전략과 관련된 부분을 다루었지만, CI/CD 파이프라인을 최적화할 때는 다음과 같은 부분도 함께 고민해볼 만합니다.
-
병렬 처리
- 테스트, 빌드, 린트 등을 병렬로 실행하면 전체 파이프라인 시간을 크게 단축할 수 있습니다.
- 예:
jobs.<job_id>.strategy.matrix
나needs
를 활용한 병렬 실행
-
빌드 명령어 최적화
yarn build --no-lint
처럼 불필요한 검증 단계를 빌드에서 제외하거나,- 프로덕션 빌드와 프리뷰 빌드를 분리하여 환경에 맞는 빌드 옵션을 적용할 수 있습니다.
-
빌드 결과물 관리
- 빌드 산출물(예:
.next/standalone
, Docker 이미지 등)을 아티팩트로 저장해두면 이후 배포 과정에서 다시 빌드할 필요가 줄어듭니다. - 특히 멀티 리전 배포나 서버리스 환경에서는 아티팩트 관리 전략이 중요합니다.
- 빌드 산출물(예:
CI/CD 환경은 프로젝트 성격에 따라 얼마든지 달라질 수 있습니다.
하지만 본문에서 다룬 원칙과 사례들이 Next.js 프로젝트뿐 아니라 다른 프론트엔드 프로젝트의 캐시 전략을 고민하는 데에도 도움이 되길 바랍니다.