Gitlab CI 환경에서의 Next.js Standalone 배포 및 캐시 최적화
회사에서 Next.js 프로젝트를 운영하면서, 번들 크기가 무려 1.7GB에 달하고, 모노레포 환경에서 배포 시간이 15분 이상 걸리는 비효율적인 상황이 있었습니다. 이로 인해 빌드 및 배포 효율을 극대화할 수 있는 구조가 필요하다고 느꼈고, 다양한 캐시 전략과 배포 방식을 적용하게 되었습니다. 그 결과, 번들 크기는 1.7GB에서 300MB로 대폭 감소했고, 배포 파이프라인도 캐시를 적극 활용할 경우 5분 이내로 단축되었습니다.
이 글에서는 Next.js standalone 빌드, next/cache, S3 static 서빙, 증분 빌드, 그리고 GitLab CI에서의 캐시 전략까지 실제 적용 사례와 함께, 전체 CI/CD 파이프라인의 흐름을 상세히 정리합니다.
전체 CI/CD 파이프라인 흐름
- PR: 개발자가 코드의 PR을 생성하면, 해당 PR 기준으로 CI/CD 파이프라인이 실행됩니다.
- 캐시 복원: 빌드 시작 전 S3에서 .next/cache를 복원하고, node_modules는 GitLab CI의 로컬 디스크 캐시를 활용합니다.
- 이미지 빌드: Next.js standalone 빌드 및 Docker 이미지 생성
- 정적 파일 업로드: 빌드된 .next/static을 S3에 업로드
- ECR에 이미지 push: 빌드된 이미지를 ECR에 업로드
- 배포: Elastic Beanstalk에서 ECR 이미지를 pull하여 서비스에 반영
- 캐시 저장: 빌드 후 .next/cache를 S3에 저장하여 다음 빌드에 활용
- 머지: CI/CD가 성공적으로 완료되면 PR을 머지합니다.
이런 구조를 통해, 대용량 번들/느린 배포 문제를 효과적으로 해결할 수 있었습니다.
전체 배포 구조
- ECR: Next.js standalone 이미지를 빌드하여 ECR에 업로드합니다.
- Elastic Beanstalk: ECR 이미지를 pull하여 서비스 운영이 가능합니다.
- S3: 정적 파일(.next/static)과 빌드 캐시(.next/cache)를 STATIC_S3_BUCKET에 업로드/다운로드합니다.
- GitLab CI: node_modules 캐시를 적극적으로 활용하여 빌드 속도가 최적화됩니다.
1. Next.js Standalone 빌드와 서빙
Next.js 13 이상에서는 output: 'standalone' 옵션을 통해 Node.js 런타임에서 실행 가능한 최소 파일만 추출할 수 있습니다.
이 방식은 컨테이너 이미지 크기를 줄이고, 배포 환경에 Node.js만 있으면 바로 실행이 가능하여 매우 효율적입니다.
// next.config.js
module.exports = {
output: "standalone",
assetPrefix: process.env.NEXT_PUBLIC_ASSET_PREFIX, // 예: https://**STATIC_S3_BUCKET**.s3.**AWS_REGION**.amazonaws.com/**STATIC_S3_PREFIX**/**BRANCH_NAME**/**SERVICE**/_next/static/
};
빌드 시에는 다음과 같이 standalone 디렉토리가 생성됩니다.
dist/**SERVICE_PATH**/.next/standalone/
여기에는 서버 실행에 필요한 최소 파일과 server.js가 포함됩니다.
실행은 아래처럼 단순하게 가능합니다.
node standalone/**SERVICE_PATH**/server.js
2. TypeScript incremental 빌드와 캐시 활용
빌드 속도를 높이기 위해 TypeScript의 incremental 옵션을 사용합니다.
증분 빌드는 이전 빌드 정보를 활용해 변경된 부분만 다시 컴파일하므로, 캐시와 함께 사용할 때 빌드 시간이 더욱 단축됩니다.
// tsconfig.json
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./.tsbuildinfo"
// ...기타 옵션
}
}
빌드를 실행하면 프로젝트 루트(혹은 지정한 경로)에 .tsbuildinfo 파일이 생성됩니다. 이 파일에는 이전 컴파일 결과와 의존성 정보가 저장되어, 다음 빌드 시 변경된 파일만 빠르게 다시 빌드할 수 있습니다.
- .tsbuildinfo: 증분 빌드 정보를 담고 있는 파일로, 변경된 부분만 다시 컴파일할 수 있게 도와줍니다.
- dist/: 실제로 컴파일된 js, d.ts 등 빌드 산출물이 저장되는 디렉토리입니다.
GitLab CI에서 .next/cache를 적극적으로 활용하기 때문에, 증분 빌드의 효과가 극대화됩니다. TypeScript incremental 빌드와 Next.js의 빌드 캐시가 결합되어, 전체 빌드 시간이 크게 단축됩니다.
3. 정적 파일 S3 업로드 및 서빙
Next.js의 정적 파일(.next/static)은 S3에 업로드하여 서빙합니다.
assetPrefix를 S3 주소(또는 S3와 연동된 CDN 주소)로 지정하면, 클라이언트가 정적 리소스를 S3(또는 CDN)에서 직접 받아가므로 서버 부하가 줄고, CDN을 연동할 경우 전 세계 어디서나 빠른 응답이 가능합니다.
// next.config.js
module.exports = {
assetPrefix: process.env.NEXT_PUBLIC_ASSET_PREFIX, // 예: https://**STATIC_S3_BUCKET**.s3.**AWS_REGION**.amazonaws.com/**STATIC_S3_PREFIX**/**BRANCH_NAME**/**SERVICE**/_next/static/
};
CI에서는 빌드 후 아래처럼 S3에 업로드합니다.
aws s3 sync dist/**SERVICE_PATH**/.next/static s3://**STATIC_S3_BUCKET**/**STATIC_S3_PREFIX**/**BRANCH_NAME**/**SERVICE**/_next/static/ --delete
4. Next.js 빌드 캐시(.next/cache) 활용
빌드 캐시를 적극적으로 활용하면, 빌드 속도가 크게 단축됩니다.
.next/cache를 S3에 저장하고, 빌드 시작 시 S3에서 받아와 복원합니다.
캐시 복원
aws s3 sync s3://**STATIC_S3_BUCKET**/nextjs-cache/**BRANCH_NAME**/ dist/**SERVICE_PATH**/.next/cache/ || true
캐시 저장
aws s3 sync dist/**SERVICE_PATH**/.next/cache/ s3://**STATIC_S3_BUCKET**/nextjs-cache/**BRANCH_NAME**/ --delete
(추가) GitLab 로컬 디스크 캐시
S3 외에도, GitLab CI의 로컬 디스크 캐시를 활용하면 네트워크 비용 없이 더 빠른 캐시 복원이 가능합니다.
cache:
key:
files:
- package-lock.json
# 필요시 주요 소스 파일, 환경변수 등 추가
paths:
- node_modules/
.next/cache는 아래와 같이 S3를 통해서만 관리합니다.
# 캐시 복원
aws s3 sync s3://**STATIC_S3_BUCKET**/nextjs-cache/**BRANCH_NAME**/ dist/**SERVICE_PATH**/.next/cache/ || true
# 캐시 저장
aws s3 sync dist/**SERVICE_PATH**/.next/cache/ s3://**STATIC_S3_BUCKET**/nextjs-cache/**BRANCH_NAME**/ --delete
5. GitLab CI 파이프라인 구조 (예시)
아래는 실제 환경에 맞게 변수화한 GitLab CI의 주요 부분 예시입니다.
stages:
- build_service
- deploy_service
build_service:
# ...생략
before_script:
# S3에서 .next/cache 복원
- aws s3 sync s3://**STATIC_S3_BUCKET**/nextjs-cache/**BRANCH_NAME**/ dist/**SERVICE_PATH**/.next/cache/ || true
script:
# Docker build (standalone)
# 컨테이너에서 static, cache 추출
# S3에 static, cache 업로드
# (아래 실제 예시 참고)
- |
docker buildx build \
--platform=linux/amd64 \
--memory=4g --memory-swap=5g \
--build-arg BRANCH_NAME=**BRANCH_NAME** \
--build-arg BUILDKIT_PROGRESS=plain \
--build-arg SERVICE=**SERVICE** \
-t **AWS_ACCOUNT_ID**.dkr.ecr.**AWS_REGION**.amazonaws.com/**IMAGE_TAG**:**TAG_NAME** \
--push -f Dockerfile-**SERVICE** .
# 컨테이너에서 static, cache 추출
- |
CONTAINER_ID=$(docker create **AWS_ACCOUNT_ID**.dkr.ecr.**AWS_REGION**.amazonaws.com/**IMAGE_TAG**:**TAG_NAME**)
mkdir -p dist/**SERVICE_PATH**/.next
docker cp $CONTAINER_ID:/app/dist/**SERVICE_PATH**/.next/static dist/**SERVICE_PATH**/.next/
docker cp $CONTAINER_ID:/app/dist/**SERVICE_PATH**/.next/cache dist/**SERVICE_PATH**/.next/
docker rm $CONTAINER_ID
# S3에 static, cache 업로드
- aws s3 sync dist/**SERVICE_PATH**/.next/static s3://**STATIC_S3_BUCKET**/**STATIC_S3_PREFIX**/**BRANCH_NAME**/**SERVICE**/_next/static/ --delete
- aws s3 sync dist/**SERVICE_PATH**/.next/cache/ s3://**STATIC_S3_BUCKET**/nextjs-cache/**BRANCH_NAME**/ --delete
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
# ...생략
deploy_service:
# ...생략
script:
# S3에 static 업로드
# ECR 이미지로 Beanstalk 배포
6. Dockerfile에서 node_modules, 캐시, static 복사 동작 상세
Dockerfile에서는 node_modules를 COPY --link로 복사합니다. 이때 GitLab CI에서 node_modules 캐시가 있으면, 아래와 같이 동작합니다.
- GitLab CI cache에 node_modules가 있을 때: 캐시가 복원되어 워크스페이스에 node_modules가 이미 존재하므로, Dockerfile에서 COPY --link로 빠르게 컨테이너에 복사됩니다. 이때 파일 시스템의 하드링크를 활용해 복사 속도가 매우 빠릅니다.
- 캐시가 없을 때: node_modules가 없으므로, Dockerfile의 deps 스테이지에서 npm ci로 새로 설치합니다. 이후 COPY --link로 복사됩니다.
FROM node:22.14.0-alpine AS deps
WORKDIR /app
COPY --link package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,id=**CACHE_VERSION**-**SERVICE**-**BRANCH_NAME**-npm-cache \
npm ci --quiet
FROM node:22.14.0-alpine AS builder
WORKDIR /app
COPY --link --from=deps /app/node_modules ./node_modules
# ...
정리:
- GitLab CI에서 node_modules 캐시가 복원되면, Dockerfile에서 COPY --link로 빠르게 복사됨 (하드링크 방식, 속도 빠름)
- 캐시가 없으면 deps 스테이지에서 npm ci로 새로 설치 후 복사
- node_modules는 오직 GitLab CI cache로만 관리하며, S3에는 저장/복원하지 않음
7. GitLab CI에서 static(.next/static), next cache(.next/cache) 활용 구조
- .next/cache: S3에서 복원 → 빌드 후 S3에 저장 (증분 빌드/캐시 최적화)
- .next/static: 빌드 후 컨테이너에서 추출 → S3에 업로드 (정적 파일 서빙)
- node_modules: GitLab CI cache로만 관리, Dockerfile에서 COPY --link로 복사
실제 파이프라인 예시
- 캐시 복원 단계
- S3에서 .next/cache 복원: 빌드 속도 향상
- GitLab CI cache에서 node_modules 복원: npm install 생략 가능
- Docker 빌드 및 이미지 push
- docker buildx build --push로 ECR에 이미지 업로드
- 컨테이너에서 static, cache 추출
- docker cp로 .next/static, .next/cache 추출
- S3 업로드
- static: S3에 업로드 (assetPrefix로 서빙)
- cache: S3에 저장 (다음 빌드에 활용)
8. 결론 및 정리 (실제 파이프라인 기준)
- ECR 이미지 push는 docker buildx build --push로 수행
- node_modules는 GitLab CI cache가 있으면 COPY --link로 빠르게 복사, 없으면 새로 설치
- .next/cache는 S3에서만 복원/저장, .next/static은 빌드 후 S3에 업로드(복원은 안 함)
- 실제 파이프라인 흐름: 캐시 복원 → Docker 빌드/이미지 push → 컨테이너에서 static/cache 추출 → S3 업로드 → 배포