테마 전환

Next.js CI/CD 실전 가이드: GitHub Actions로 자동 테스트와 배포 구현하기

Easton editorial illustration: rendering-mode selector

터미널에 계속 올라오는 로그를 바라보며 손가락은 기계적으로 git pull && npm install && npm run build && pm2 restart를 입력하고 있었습니다. 오늘만 세 번째 배포였습니다. 오전에는 작은 버그를 고쳤고, 점심에는 API 하나를 최적화했으며, 이제는 스타일을 조금 수정했습니다. 서버가 세 대라서 같은 작업을 각 서버에서 한 번씩 해야 했습니다.

마지막 서버까지 끝내고 나니 시계는 벌써 일곱 시를 가리키고 있었습니다.

그날 저녁 집으로 돌아가는 길에 분명 더 나은 방법이 있을 거라고 생각했습니다. 코드를 수정할 때마다 서버에 접속해 같은 작업을 반복하고 싶지 않았고, 어느 서버의 재시작을 빼먹어 운영 환경에 문제가 생길까 걱정하고 싶지도 않았습니다. 테스트도 자주 잊었습니다. 코드를 고치면 빨리 배포하고 싶은 마음에 곧바로 운영에 반영했다가, 나중에 버그를 발견하고 모든 과정을 다시 거치곤 했습니다.

이후 CI/CD와 GitHub Actions를 접하면서 작업 흐름이 완전히 달라졌습니다. 이제는 코드를 GitHub에 push하기만 하면 나머지 테스트, 빌드, 배포가 모두 자동으로 끝납니다. 커피 한 잔을 마시는 사이 새 버전이 이미 서비스에 반영됩니다.

수동 배포 때문에 고생하고 있다면, 지금부터 GitHub Actions로 Next.js 자동 배포 흐름을 만드는 방법을 소개하겠습니다. 가장 기본적인 설정부터 완전한 실전 사례까지, 제가 겪었던 시행착오와 해결 방법도 모두 다룹니다.

CI/CD가 필요한 이유

수동 배포의 고통

처음 Next.js 프로젝트를 만들 때의 배포 과정은 이랬습니다. 로컬에서 코드를 작성하고 테스트한 다음 SSH로 서버에 로그인해 git pull로 코드를 받고, npm install로 의존성을 설치하고, npm run build로 빌드한 뒤 마지막으로 pm2 restart로 서비스를 재시작했습니다. 모든 일이 순조로워도 십여 분은 걸렸습니다.

하지만 순조롭지 않은 날이 많았습니다.

한번은 서버 세 대에 배포하면서 앞의 두 대는 성공했지만 세 번째 서버의 SSH 연결이 끊긴 것을 알아차리지 못했습니다. 다음 날 사용자가 사이트가 정상적으로 보일 때도 있고 이전 버전으로 보일 때도 있다고 알려 왔습니다. 로드 밸런서가 업데이트되지 않은 서버로 요청을 보낸 것이 원인이었습니다. 또 한 번은 코드 수정이 잘된 것이 기뻐 테스트도 실행하지 않은 채 바로 배포했습니다. 운영 반영 후 치명적인 버그를 발견해 긴급 롤백을 하느라 진땀을 뺐습니다.

더 까다로운 문제는 다중 서버 빌드였습니다. Next.js는 build할 때마다 새로운 build ID를 생성합니다. 서버 세 대에서 각각 빌드하면 ID도 서로 달라집니다. 로드 밸런서가 사용자의 요청을 다른 서버로 보내면 Next.js가 ID 변경을 감지해 하드 리프레시를 일으키고 모든 리소스를 다시 불러옵니다. 사용자 경험은 말할 필요도 없었습니다.

CI/CD가 해결하는 문제

간단히 말해 CI/CD는 이렇게 반복적이고 오류가 발생하기 쉬운 과정을 자동화합니다.

**CI(지속적 통합)**는 테스트를 담당합니다. 코드를 push할 때마다 단위 테스트, 타입 검사, 코드 규칙 검사 등을 자동으로 실행합니다. 테스트가 통과하지 못하면 해당 커밋은 배포되지 않고 오류가 표시되므로 문제를 먼저 수정해야 합니다. 덕분에 문제가 있는 코드가 운영 환경에 배포되는 일을 막을 수 있습니다.

**CD(지속적 배포)**는 빌드와 배포를 담당합니다. 테스트가 통과하면 자동으로 빌드를 시작하고, 빌드가 끝나면 서버에 자동 배포합니다. 전체 과정에서 직접 조작할 일은 없습니다.

지금 제 작업 흐름은 로컬 코드 작성 → GitHub 커밋 → 자동 테스트 → 자동 빌드 → 자동 배포입니다. 코드를 push한 뒤 운영 반영까지 약 5분이 걸리며 지켜보고 있을 필요도 없습니다. 물 한 잔을 마시고 돌아오면 새 버전이 이미 서비스 중입니다.

추적 가능하다는 장점도 있습니다. 어떤 커밋이 배포를 시작했는지, 테스트 결과는 어땠는지, 어느 서버에 배포했는지까지 매번 전체 로그가 남습니다. 문제가 생겨도 빠르게 원인을 찾을 수 있어 예전처럼 막막하지 않습니다.

GitHub Actions 기본 설정

먼저 작동 방식을 이해하기

GitHub Actions를 처음 접했을 때 workflow, job, step 같은 개념이 한꺼번에 보여 머리가 복잡했습니다. 하지만 실제로는 간단합니다.

**Workflow(워크플로)**는 완전한 자동화 흐름 한 세트입니다. 예를 들어 ‘테스트+빌드+배포’가 하나의 workflow입니다. 프로젝트의 .github/workflows 디렉터리에 있는 YAML 파일로 정의합니다.

**Job(작업)**은 workflow 안의 독립적인 작업 단위입니다. ‘테스트’ job과 ‘배포’ job을 각각 만들 수 있습니다. Job은 병렬로 실행하거나, 의존 관계를 설정해 순서대로 실행할 수 있습니다.

**Step(단계)**은 job 안에서 수행하는 구체적인 작업입니다. ‘코드 가져오기’, ‘의존성 설치’, ‘테스트 실행’ 등이 모두 step입니다.

트리거 조건도 유연합니다. main 브랜치에 push할 때마다 실행하거나, Pull Request를 만들 때만 실행할 수 있고, 매일 새벽에 자동 실행되는 예약 작업으로도 설정할 수 있습니다.

첫 번째 Workflow 만들기

프로젝트 루트에 .github/workflows 폴더를 만든 뒤 ci-cd.yml 파일을 새로 만듭니다. 가장 기본적인 설정은 다음과 같습니다.

name: CI/CD Pipeline

# 트리거 조건: main 브랜치에 push할 때 실행
on:
  push:
    branches: [main]

jobs:
  build:
    # 최신 Ubuntu 버전에서 실행
    runs-on: ubuntu-latest

    steps:
      # 첫 번째 단계: 코드 가져오기
      - uses: actions/checkout@v4

      # 두 번째 단계: Node.js 환경 설정
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      # 세 번째 단계: 의존성 설치
      - name: Install dependencies
        run: npm ci

      # 네 번째 단계: 프로젝트 빌드
      - name: Build
        run: npm run build

이 설정은 main 브랜치에 코드가 push될 때마다 Ubuntu 시스템에서 코드 가져오기, Node.js 설정, 의존성 설치, 빌드를 차례로 실행한다는 뜻입니다.

이 파일을 GitHub에 커밋한 뒤 저장소의 ‘Actions’ 탭으로 가면 workflow 실행 상태를 볼 수 있습니다. 처음 초록색 체크 표시를 봤을 때는 솔직히 꽤 뿌듯했습니다.

민감한 정보를 보호하는 Secrets 설정

서버에 배포하려면 서버 IP, SSH 키, API token 같은 민감한 정보가 필요합니다. 이런 정보는 절대 YAML 파일에 직접 쓰면 안 됩니다. GitHub에 커밋하는 순간 노출되기 때문입니다.

올바른 방법은 GitHub의 Secrets 기능을 사용하는 것입니다. 저장소의 Settings → Secrets and variables → Actions에서 ‘New repository secret’을 눌러 비밀 값을 추가합니다. 예를 들어 SERVER_HOST라는 secret을 만들고 서버 IP 주소를 값으로 넣습니다.

workflow에서는 ${{ secrets.SERVER_HOST }}로 참조합니다. GitHub가 이를 실제 값으로 자동 치환하며, 로그에서는 마스킹해 표시하므로 정보가 유출되지 않습니다.

자동 테스트 흐름 구축

테스트 환경 설정

테스트에 대한 제 생각은 단순합니다. 자동화할 수 있다면 수동으로 하지 않습니다. 제가 설정한 테스트 흐름에는 코드 규칙 검사(ESLint), 타입 검사(TypeScript), 단위 테스트(Jest)가 포함되어 있어 대부분의 문제를 걸러낼 수 있습니다.

전체 테스트 job 설정은 다음과 같습니다.

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'  # npm 의존성을 캐시해 이후 빌드 가속

      - name: Install dependencies
        run: npm ci

      - name: Lint check
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Run tests
        run: npm run test -- --coverage

여기에는 작은 요령이 있습니다. cache: 'npm'은 npm 의존성을 자동으로 캐시하므로 두 번째 실행부터 모든 의존성을 다시 내려받지 않아도 됩니다. 몇 분을 아낄 수 있습니다.

테스트 흐름을 빠르게 만드는 몇 가지 방법

처음에는 테스트 흐름 한 번에 10분 넘게 걸려 커밋할 때마다 한참을 기다려야 했습니다. 이후 최적화를 거쳐 지금은 3분이면 끝납니다.

첫 번째 방법은 캐시입니다. 앞서 말한 npm 캐시뿐 아니라 Next.js 빌드 캐시도 저장할 수 있습니다.

- name: Cache Next.js build
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      .next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}

이렇게 하면 Next.js가 매번 모든 페이지를 새로 빌드할 필요가 없어져 상당히 빨라집니다.

두 번째 방법은 병렬 실행입니다. 테스트끼리 의존 관계가 없다면 여러 job으로 나눠 동시에 실행할 수 있습니다.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]  # lint만 실행

  test:
    runs-on: ubuntu-latest
    steps: [...]  # 단위 테스트만 실행

  type-check:
    runs-on: ubuntu-latest
    steps: [...]  # 타입 검사만 실행

세 job이 동시에 시작되므로 총소요 시간은 세 시간을 합친 값이 아니라 가장 느린 job의 실행 시간과 같습니다.

제가 겪은 시행착오

한번은 테스트를 설정했는데 로컬에서는 잘 실행되지만 GitHub Actions에서 계속 오류가 났습니다. 한참 디버깅한 끝에 GitHub Actions가 Pull Request 코드를 가져올 때 기본적으로 PR 브랜치를 대상 브랜치에 병합해 새로운 임시 커밋을 만든다는 사실을 알았습니다. 이 임시 커밋은 로컬에 존재하지 않으므로 예상치 못한 문제가 생길 수 있습니다.

해결 방법은 checkout에 다음 매개변수를 추가하는 것입니다.

- uses: actions/checkout@v4
  with:
    ref: ${{ github.head_ref }}  # PR 브랜치의 원본 코드 사용

또 다른 문제는 테스트 시간 초과였습니다. 일부 E2E 테스트는 오래 걸려 기본 제한 시간으로는 부족했습니다. job에서 제한 시간을 더 길게 설정할 수 있습니다.

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # 기본값은 6분

자동 배포 흐름 설정

Vercel에 배포하기: 가장 간단한 방법

프로젝트를 Vercel에서 호스팅한다면 거의 설정할 것이 없습니다. Vercel은 GitHub와 기본적으로 연동되므로 저장소를 Vercel 프로젝트에 연결하면 코드를 push할 때마다 자동 배포됩니다.

하지만 테스트가 통과한 뒤에만 배포하는 것처럼 더 세밀하게 제어하고 싶다면 Vercel 공식 GitHub Action을 사용할 수 있습니다.

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: test  # test job이 끝난 뒤 실행
    if: github.ref == 'refs/heads/main'  # main 브랜치에서만 배포

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'  # 운영 환경에 배포

Vercel 관리 화면에서 token을 생성하고 프로젝트 설정에서 org ID와 project ID를 찾아 GitHub Secrets에 추가해야 합니다.

Vercel은 각 PR마다 미리보기 환경을 자동 생성한다는 장점도 있습니다. PR을 올리면 임시 환경을 자동으로 배포하고 미리보기 링크를 제공하므로 변경 결과를 바로 확인할 수 있어 매우 편리합니다.

자체 서버에 배포하기: 더 유연하지만 조금 복잡한 방법

제 프로젝트는 더 많은 제어권을 얻기 위해 자체 서버에 배포합니다. 이 경우 SSH 연결을 설정하고 GitHub Actions가 SSH를 통해 서버에서 배포 명령을 실행하도록 해야 합니다.

먼저 로컬에서 SSH 키 쌍을 생성합니다.

ssh-keygen -t ed25519 -C "github-actions"

공개 키는 서버의 ~/.ssh/authorized_keys에 추가하고, 비공개 키는 GitHub Secrets에 추가합니다. 예를 들어 이름은 SSH_PRIVATE_KEY로 지정합니다.

그다음 deploy job을 설정합니다.

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/my-nextjs-app
            git pull origin main
            npm install
            npm run build
            pm2 restart nextjs-app

이 설정은 SSH로 서버에 접속해 최신 코드를 가져오고, 의존성을 설치하고, 빌드한 다음 애플리케이션을 재시작합니다. 전체 과정이 완전히 자동화되므로 더 이상 직접 서버에 로그인할 필요가 없습니다.

다중 서버의 Build ID 불일치 문제 해결

저처럼 로드 밸런싱을 위해 여러 서버에 배포한다면 아주 중요한 점이 있습니다. 모든 서버의 build ID가 같아야 합니다. 그렇지 않으면 사용자가 사이트를 이용할 때 페이지가 계속 새로 고침됩니다.

한 곳에서 빌드를 완료한 뒤 그 결과물을 모든 서버에 배포하면 해결할 수 있습니다. 저는 다음과 같이 설정했습니다.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run build

      # 빌드 결과물을 묶어 업로드
      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: next-build
          path: |
            .next
            public

  deploy:
    runs-on: ubuntu-latest
    needs: build
    strategy:
      matrix:
        server: [server1, server2, server3]  # 여러 서버

    steps:
      # 빌드 결과물 다운로드
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: next-build

      # 해당 서버에 배포
      - name: Deploy to ${{ matrix.server }}
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets[format('{0}_HOST', matrix.server)] }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: ".next,public"
          target: "/var/www/my-nextjs-app"

이렇게 하면 한 번만 빌드하고 동일한 결과물을 서버 세 대에 배포하므로 build ID도 자연스럽게 일치합니다.

최적화와 모범 사례

빌드가 실패하면 어떻게 하나요?

자동화는 편리하지만 때로는 문제가 생깁니다. 빌드가 실패했다면 그 사실을 알아야 합니다. 코드는 커밋했는데 배포가 실패한 줄도 모른다면 곤란합니다.

저는 실패 알림을 설정해 두었습니다. 이메일이나 Slack 또는 DingTalk로 보낼 수 있습니다. Slack을 예로 들면 다음과 같습니다.

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      [...배포 단계...]

      # 배포에 실패하면 Slack 알림 전송
      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: '배포에 실패했습니다! 원인을 확인해 주세요.'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
          channel: '#deploy-notifications'

이렇게 하면 배포 실패 시 Slack으로 알림이 오므로 즉시 알 수 있습니다.

브랜치 전략: 개발 환경과 운영 환경 분리

실제 프로젝트에서는 환경마다 다른 브랜치를 사용합니다. develop 브랜치는 테스트 환경에, main 브랜치는 운영 환경에 배포합니다. 설정은 다음과 같습니다.

on:
  push:
    branches:
      - main      # 운영 환경
      - develop   # 테스트 환경

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - [...테스트 환경에 배포...]

  deploy-production:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - [...운영 환경에 배포...]

평소 개발 코드는 develop에 커밋해 테스트 환경에 자동 배포하고 검증합니다. 문제가 없음을 확인한 뒤 main에 병합하면 운영 환경에 자동 배포됩니다.

일부 팀에서는 운영 환경 배포 전에 수동 승인을 요구하기도 합니다. GitHub Actions는 이 기능을 지원합니다. job에 environment 설정을 추가하고 저장소 설정에서 승인자를 지정합니다.

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production  # 승인이 필요한 환경
    steps: [...]

이렇게 설정하면 운영 환경에 배포할 때마다 먼저 일시 중지되고, 승인자가 ‘동의’를 눌러야 계속 실행됩니다. 중요한 프로젝트라면 보호 장치를 한 겹 더 두는 것이 필요합니다.

롤백 메커니즘

테스트와 승인이 있어도 운영 환경에 문제가 생길 때가 있습니다. 이때는 이전 버전으로 빠르게 롤백해야 합니다.

간단한 방법은 배포할 때마다 tag를 붙이고 최근 몇 개 버전의 빌드 결과물을 보관하는 것입니다. 롤백이 필요하면 workflow를 수동으로 실행하면서 돌아갈 tag를 지정합니다.

on:
  workflow_dispatch:  # 수동 실행
    inputs:
      tag:
        description: '롤백할 tag'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.inputs.tag }}  # 지정한 tag 사용

      - name: Deploy
        [...일반 배포 흐름...]

GitHub Actions 페이지에서 ‘Run workflow’를 클릭하고 tag를 입력하면 빠르게 롤백할 수 있습니다.

환경 변수 관리

Next.js 프로젝트에서는 API 주소나 데이터베이스 연결 문자열 같은 환경 변수가 자주 필요합니다. 이를 코드에 하드코딩하거나 Git 저장소에 커밋하면 안 됩니다.

모범 사례는 다음과 같습니다.

  1. 민감한 정보는 GitHub Secrets에 저장합니다.
  2. workflow에서 환경 변수로 주입합니다.
  3. Next.js가 빌드할 때 환경 변수를 읽습니다.
- name: Build
  run: npm run build
  env:
    NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
    DATABASE_URL: ${{ secrets.DATABASE_URL }}

이렇게 하면 환경마다 다른 환경 변수를 사용할 수 있으며 코드를 수정할 필요는 없습니다.

실전 사례: 전체 설정 예시

지금까지 설명한 테스트, 빌드, 배포의 전체 흐름을 담은 설정 예시를 살펴보겠습니다. 기본적으로 프로젝트에 바로 적용할 수 있습니다.

name: Next.js CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  # Job 1: 코드 검사와 테스트
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint check
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Run tests
        run: npm run test -- --coverage

  # Job 2: 빌드
  build:
    runs-on: ubuntu-latest
    needs: test  # 테스트 통과 후 빌드
    if: github.event_name == 'push'  # push할 때만 빌드하고 PR에서는 빌드하지 않음

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Cache dependencies and build
        uses: actions/cache@v3
        with:
          path: |
            ~/.npm
            .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}

      # 배포에 사용할 빌드 결과물 업로드
      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: next-build
          path: |
            .next
            public
            package.json

  # Job 3: 테스트 환경에 배포
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: next-build

      - name: Deploy to staging server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/staging
            pm2 stop nextjs-app || true
            rm -rf .next public
            pm2 start npm --name "nextjs-app" -- start
            pm2 save

      - name: Notify Slack
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: '테스트 환경 배포 ${{ job.status == "success" && "성공" || "실패" }}'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

  # Job 4: 운영 환경에 배포
  deploy-production:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production  # 수동 승인 필요

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: next-build

      - name: Deploy to production server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/production
            pm2 stop nextjs-app || true
            rm -rf .next public
            pm2 start npm --name "nextjs-app" -- start
            pm2 save

      - name: Notify Slack
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: '운영 환경 배포 ${{ job.status == "success" && "성공" || "실패" }}'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

이 설정에서는 PR에 대해 테스트만 실행하고 빌드와 배포는 하지 않습니다. develop 브랜치에 커밋하면 테스트 환경에 자동 배포되고, main 브랜치에 커밋하면 수동 승인 후 운영 환경에 배포됩니다. 배포할 때마다 Slack 알림을 보내므로 실패 여부도 즉시 알 수 있습니다.

실제로 사용할 때는 GitHub 저장소 설정에서 다음 Secrets를 구성해야 합니다.

  • API_URL: API 주소
  • STAGING_HOST / PROD_HOST: 테스트/운영 서버 주소
  • SERVER_USER: SSH 사용자 이름
  • SSH_PRIVATE_KEY: SSH 비공개 키
  • SLACK_WEBHOOK: Slack webhook 주소(선택 사항)

설정이 끝나면 전체 흐름이 작동합니다. 코드를 작성하고 커밋하기만 하면 나머지는 모두 자동으로 처리됩니다.

결론

수동 배포에서 자동화로의 전환은 제 개발 경험을 정말 크게 바꿨습니다. 코드를 수정할 때마다 서버에서 같은 명령을 반복해서 입력할 필요도 없고, 테스트 실행을 잊을까 걱정할 필요도 없으며, 터미널을 지켜보며 빌드가 끝나기를 기다릴 필요도 없습니다.

GitHub Actions로 CI/CD 흐름을 설정하는 초기에는 익숙해지는 데 시간이 조금 필요할 수 있지만 충분히 가치가 있습니다. 한번 설정하면 계속 혜택을 얻습니다. 가장 단순한 설정부터 시작해 테스트와 빌드를 먼저 정상화한 다음 배포, 알림, 롤백 기능을 차례로 추가하면 됩니다.

솔직히 지금 돌아보면 예전에는 어떻게 수동 배포를 계속했는지 모르겠습니다. 아직도 수동으로 배포한다면 시간을 내어 자동화 흐름을 설정해 보길 권합니다. 코드를 push하고 커피 한 잔을 마신 뒤 돌아오면 새 버전이 서비스 중인 경험은 한번 맛보면 포기하기 어렵습니다.

지금 바로 시작해 보세요. Next.js 프로젝트에서 첫 workflow 파일을 만들고 GitHub Actions가 자동으로 실행되는 순간을 보면 제가 말한 뜻을 이해할 수 있을 것입니다.

Next.js CI/CD 전체 설정 절차

GitHub Actions workflow 생성부터 테스트, 빌드, 배포 설정까지의 전체 절차

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: GitHub Actions workflow 생성

    .github/workflows/deploy.yml을 만듭니다:
    ```yaml
    name: Deploy

    on:
    push:
    branches: [main]

    jobs:
    build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
    with:
    node-version: '18'
    - run: npm install
    - run: npm run build
    - run: npm test
    ```

    핵심 사항:
    • 트리거 조건: main 브랜치에 push
    • 실행 환경: ubuntu-latest
    • 단계: checkout → setup-node → install → build → test
  2. 2

    Step 2: 테스트 단계 설정

    테스트를 추가합니다:
    ```yaml
    - name: Run tests
    run: npm test

    - name: Type check
    run: npm run type-check

    - name: Lint
    run: npm run lint
    ```

    핵심 사항:
    • 테스트 실패 시 배포 차단
    • 타입 검사로 타입 안전성 보장
    • 코드 규칙 검사로 코드 품질 보장

    장점:
    • 문제가 있는 코드가 운영 환경에 배포되는 것을 방지
    • 자동 검사로 테스트 실행 누락 방지
  3. 3

    Step 3: 빌드 단계 설정

    빌드 설정:
    ```yaml
    - name: Build
    run: npm run build
    env:
    NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
    ```

    캐시 최적화:
    ```yaml
    - name: Cache dependencies
    uses: actions/cache@v3
    with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    ```

    핵심 사항:
    • 환경 변수 설정
    • 캐시를 사용해 빌드 가속
    • 빌드 출력 확인
  4. 4

    Step 4: 배포 단계 설정

    SSH 배포:
    ```yaml
    - name: Deploy to server
    uses: appleboy/ssh-action@master
    with:
    host: ${{ secrets.HOST }}
    username: ${{ secrets.USERNAME }}
    key: ${{ secrets.SSH_KEY }}
    script: |
    cd /path/to/app
    git pull
    npm install
    npm run build
    pm2 restart app
    ```

    다중 서버 배포:
    ```yaml
    - name: Deploy to servers
    uses: appleboy/ssh-action@master
    with:
    host: ${{ secrets.HOSTS }}
    username: ${{ secrets.USERNAME }}
    key: ${{ secrets.SSH_KEY }}
    script: |
    cd /path/to/app
    git pull
    npm install
    # GitHub Actions에서 빌드한 결과물 사용
    pm2 restart app
    ```

    핵심 사항:
    • SSH 키 인증 사용
    • 빌드 ID 통일(GitHub Actions에서 빌드)
    • 여러 서버의 빌드 ID 불일치 방지

FAQ

왜 CI/CD가 필요한가요?
수동 배포의 문제점:
• 코드를 수정할 때마다 서버에서 같은 작업 반복
• 서버 재시작을 빠뜨리기 쉬움
• 테스트 실행을 빠뜨리기 쉬움
• 다중 서버 배포에서 오류가 발생하기 쉬움
• 빌드 ID 불일치로 하드 리프레시 발생

CI/CD의 장점:
• 테스트, 빌드, 배포 자동화
• 코드를 push하면 자동으로 서비스 반영
• 수동 배포 오류 방지
• 빌드 ID 통일
• 개발 효율 향상

실제 사례:
• 서버 세 대 중 앞의 두 대는 배포에 성공했지만 세 번째 서버의 SSH 연결이 끊긴 것을 놓침
• 로드 밸런서가 업데이트되지 않은 서버로 요청을 보내 사용자가 이전 버전을 봄
• 코드 수정 후 서둘러 배포하느라 테스트를 잊고, 운영 반영 후 치명적인 버그를 발견함

해결 방법: GitHub Actions로 전체 과정을 자동화합니다.
GitHub Actions는 어떻게 설정하나요?
.github/workflows/deploy.yml을 만듭니다:
```yaml
name: Deploy

on:
push:
branches: [main]

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run build
- run: npm test
```

핵심 사항:
• 트리거 조건: main 브랜치에 push
• 실행 환경: ubuntu-latest
• 단계: checkout → setup-node → install → build → test

Secrets 설정:
• GitHub 저장소의 Settings → Secrets에서 설정
• HOST, USERNAME, SSH_KEY 등
• SSH 배포에 사용

권장 사항: 가장 단순한 설정부터 시작해 테스트와 빌드를 먼저 실행한 뒤 배포를 차례로 추가합니다.
다중 서버 배포에서 빌드 ID 불일치를 어떻게 방지하나요?
문제: 각 서버에서 따로 빌드하면 ID가 달라져 로드 밸런싱 중 하드 리프레시가 발생합니다.

해결 방법: 빌드 ID를 통일합니다.

GitHub Actions에서 빌드합니다:
```yaml
- name: Build
run: npm run build

- name: Deploy to servers
uses: appleboy/ssh-action@master
with:
script: |
cd /path/to/app
git pull
# GitHub Actions에서 빌드한 결과물 사용
pm2 restart app
```

또는 빌드 결과물을 사용합니다:
```yaml
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: .next

- name: Deploy to servers
uses: appleboy/ssh-action@master
with:
script: |
# 빌드 결과물 다운로드
# 서버에 배포
```

핵심 사항:
• GitHub Actions에서 한 번만 빌드
• 빌드 결과물을 여러 서버에 배포
• 각 서버에서 별도로 빌드하지 않음

장점:
• 빌드 ID 일치
• 하드 리프레시 방지
• 사용자 경험 개선
테스트 단계는 어떻게 설정하나요?
테스트를 추가합니다:
```yaml
- name: Run tests
run: npm test

- name: Type check
run: npm run type-check

- name: Lint
run: npm run lint
```

핵심 사항:
• 테스트 실패 시 배포 차단
• 타입 검사로 타입 안전성 보장
• 코드 규칙 검사로 코드 품질 보장

장점:
• 문제가 있는 코드가 운영 환경에 배포되는 것을 방지
• 자동 검사로 테스트 실행 누락 방지
• 코드 품질 향상

권장 사항:
• 가장 단순한 테스트부터 시작
• 테스트 커버리지를 점진적으로 확대
• 테스트 품질을 지속적으로 개선
배포 알림은 어떻게 설정하나요?
Slack 알림:
```yaml
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Deployment completed'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
```

이메일 알림:
```yaml
- name: Send email
uses: dawidd6/action-send-mail@v3
with:
to: [email protected]
subject: 'Deployment completed'
body: 'Deployment to production completed successfully'
```

핵심 사항:
• 배포 성공과 실패 모두 알림
• 버전, 시간 등 배포 정보 포함
• 팀에 신속하게 알림

권장 사항:
• 실시간 Slack 알림 설정
• 백업용 이메일 알림 설정
• 배포 상세 정보 포함
CI/CD 모범 사례는 무엇인가요?
점진적 도입:
1. 테스트와 빌드를 먼저 정상화합니다.
2. 이후 배포를 추가합니다.
3. 마지막으로 알림과 롤백 기능을 추가합니다.

한 번에 완성하려 하지 말고 가장 기본적인 흐름부터 실행한 뒤 단계적으로 개선합니다.

지속적인 개선:
• 배포 후 매번 로그 확인
• 실제 상황에 맞게 설정 조정
• 빌드 시간 최적화
• 테스트 커버리지 향상

핵심 지표:
• 빌드 시간
• 테스트 통과율
• 배포 성공률
• 롤백 횟수

권장 사항:
• 가장 단순한 설정부터 시작
• 지속적인 개선
• 팀 협업

기억하세요. CI/CD는 일회성 작업이 아니라 계속 이어지는 과정입니다.

5분 읽기 · 게시일: 2025년 12월 20일 · 수정일: 2026년 9월 8일

댓글

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

Easton BlogEaston Blog