AWS S3 + CloudFront로 Next.js 정적 사이트 배포하고 GitHub Actions 자동화하기

Next.js 기반 공개 사이트를 AWS에 올리면서, 비용은 낮게 유지하고 SEO와 운영 편의성은 챙기는 방향으로 배포 구성을 정리했다. 최종적으로는 castcanvaslab.com 본 사이트, www.castcanvaslab.com 리 다이렉트, GitHub Actions 자동 배포, robots.txt/sitemap.xml/OG 이미지까지 한 번에 마무리했다.


목표

  • Next.js 공개 사이트를 S3 + CloudFront + Route 53 + ACM 구조로 운영 가능한 상태로 배포하기
  • GitHub Actions를 통해 main 브랜치 푸시 시 자동 배포되도록 구성하기

핵심 구현 포인트

1) Next.js를 정적 export 기반으로 고정

이 프로젝트는 퍼블릭 소개 사이트라서 서버 렌더링보다 정적 배포가 더 적합했다. next.config.ts에서 output: 'export'를 사용하고, 빌드 결과를 out/으로 생성하도록 구성했다. 로컬 환경에서는 Turbopack 빌드가 멈추는 문제가 있어서 next build --webpack으로 빌드 스크립트를 고정했다.

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'export',
  trailingSlash: true,
  images: {
    unoptimized: true,
  },
};

export default nextConfig;

2) AWS 배포 구조를 역할별로 분리

본 사이트는 private S3 bucket + CloudFront OAC 조합으로 구성했고, www는 별도 S3 website redirect bucket과 별도 CloudFront distribution으로 구성했다. 이 방식으로 castcanvaslab.com은 실제 콘텐츠를 서빙하고, www.castcanvaslab.com은 apex로 리다이렉트되도록 만들었다.

  # GitHub Actions deploy workflow
  name: Deploy Site

  on:
    push:
      branches:
        - main
    workflow_dispatch:

  permissions:
    contents: read
    id-token: write

  jobs:
    deploy:
      runs-on: ubuntu-latest

      steps:
        - uses: actions/checkout@v4

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

        - uses: actions/setup-node@v4
          with:
            node-version: 22
            cache: pnpm

        - run: pnpm install --frozen-lockfile
        - run: pnpm lint
        - run: pnpm build

        - uses: aws-actions/configure-aws-credentials@v4
          with:
            role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
            aws-region: ap-northeast-2

        - run: aws s3 sync out/ s3://castcanvaslab.com --delete
        - run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"

———

트러블슈팅 / 고민 포인트

CloudFront 리다이렉트 배포에서 504 발생

  • 원인: www 리다이렉트용 CloudFront origin을 S3 website endpoint로 연결했는데, origin protocol policy가 HTTPS only로 설정되어 있었다. S3 website endpoint는 HTTPS origin으로 붙는 구조가 아니라 HTTP origin으로 접근해야 한다.
  • 해결: www용 CloudFront distribution의 origin type을 Other로 두고, S3 website endpoint를 사용한 뒤 Origin protocol policy를 HTTP only로 변경했다.

———

결과

———

다음 개선 아이디어

  • 실제 서비스 화면 기반의 OG 이미지로 교체해서 공유 미리보기 완성도 높이기
  • Search Console에서 sitemap 제출과 인덱싱 요청까지 완료해 검색 노출 속도 개선하기