GitHub Actions로 프론트엔드 배포 자동화: PR 미리보기부터 프로덕션까지

프론트엔드

GitHub ActionsCI/CD배포 자동화VercelDevOps

이 글은 누구를 위한 것인가

  • PR마다 자동으로 미리보기 URL을 생성하고 싶은 팀
  • GitHub Actions 캐시로 빌드 시간을 줄이고 싶은 개발자
  • 타입체크, 테스트, 배포를 자동화해서 수동 배포를 없애고 싶은 팀

들어가며

"배포하는 거 잊었다"는 더 이상 변명이 안 된다. GitHub Actions로 PR 생성 시 자동 미리보기, main 머지 시 자동 프로덕션 배포, 실패 시 Slack 알림까지 완전히 자동화할 수 있다.

이 글은 bluefoxdev.kr의 GitHub Actions CI/CD 가이드 를 참고하여 작성했습니다.


1. CI/CD 파이프라인 설계

[워크플로우 트리거]

PR 생성/업데이트:
  1. 타입체크 (tsc --noEmit)
  2. 린트 (ESLint)
  3. 단위 테스트 (Vitest)
  4. 빌드 확인
  5. 미리보기 배포 (Vercel Preview)
  6. PR 댓글에 Preview URL 게시

main 머지:
  1. 위 모든 체크
  2. 프로덕션 배포
  3. Slack 배포 알림

[캐시 전략]
  npm/pnpm: package-lock.json 해시로 캐시
  Next.js 빌드: .next/cache 캐시
  효과: 첫 실행 5분 → 이후 1-2분

[보안]
  VERCEL_TOKEN: GitHub Secrets
  환경변수: Environment별 분리
  OIDC: AWS 자격증명 대신 사용 (비밀키 없음)

2. GitHub Actions 워크플로우

# .github/workflows/ci.yml
name: CI

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

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # 이전 실행 취소

jobs:
  # 타입체크, 린트, 테스트 병렬 실행
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'  # pnpm 캐시 자동 관리

      - name: 의존성 설치
        run: pnpm install --frozen-lockfile

      - name: 타입 체크
        run: pnpm type-check

      - name: 린트
        run: pnpm lint

      - name: 테스트
        run: pnpm test --coverage

      - name: 커버리지 업로드
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  # 빌드 (캐시 활용)
  build:
    runs-on: ubuntu-latest
    needs: quality
    steps:
      - uses: actions/checkout@v4

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

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

      - name: 의존성 설치
        run: pnpm install --frozen-lockfile

      # Next.js 빌드 캐시
      - name: Next.js 빌드 캐시 복원
        uses: actions/cache@v4
        with:
          path: |
            ${{ github.workspace }}/.next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-

      - name: 빌드
        run: pnpm build
        env:
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
# .github/workflows/preview.yml
name: PR 미리보기 배포

on:
  pull_request:
    branches: [main]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write  # PR 댓글 작성 권한
    
    steps:
      - uses: actions/checkout@v4

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

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

      - run: pnpm install --frozen-lockfile

      - name: Vercel 미리보기 배포
        id: deploy
        run: |
          npm i -g vercel@latest
          DEPLOY_URL=$(vercel --token ${{ secrets.VERCEL_TOKEN }} \
            --env NEXT_PUBLIC_API_URL=${{ vars.PREVIEW_API_URL }} \
            2>&1 | grep "https://" | tail -1)
          echo "url=$DEPLOY_URL" >> $GITHUB_OUTPUT
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

      - name: PR 댓글에 미리보기 URL 게시
        uses: actions/github-script@v7
        with:
          script: |
            const previewUrl = '${{ steps.deploy.outputs.url }}';
            const comment = `## 🚀 미리보기 배포 완료
            
            | 항목 | 값 |
            |------|-----|
            | 🔗 URL | [미리보기 열기](${previewUrl}) |
            | 📦 커밋 | ${{ github.sha }} |
            | ⏰ 배포 시각 | ${new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })} |
            `;
            
            // 기존 댓글 업데이트 또는 새 댓글 작성
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            
            const existingComment = comments.find(c => c.body?.includes('미리보기 배포 완료'));
            
            if (existingComment) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existingComment.id,
                body: comment,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body: comment,
              });
            }
# .github/workflows/deploy-prod.yml
name: 프로덕션 배포

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # 승인 필요 환경
    
    steps:
      - uses: actions/checkout@v4

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

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

      - run: pnpm install --frozen-lockfile

      - name: 프로덕션 배포
        run: |
          npm i -g vercel@latest
          vercel --prod --token ${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

      - name: Slack 배포 알림
        if: always()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
              "text": "${{ job.status == 'success' && '✅' || '❌' }} 프로덕션 배포 ${{ job.status }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*프로덕션 배포 ${{ job.status }}*\n커밋: <${{ github.event.head_commit.url }}|${{ github.event.head_commit.message }}>\n작성자: ${{ github.event.head_commit.author.name }}"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

마무리

프론트엔드 CI/CD의 핵심은 캐시와 병렬 실행이다. actions/cache로 Next.js 빌드 캐시를 보존하고, 타입체크/린트/테스트를 병렬로 실행하면 5분 파이프라인을 2분으로 단축할 수 있다. concurrency로 동일 브랜치의 이전 실행을 취소하면 불필요한 실행을 줄인다. PR 미리보기 URL은 디자이너와 QA팀이 개발자 없이도 확인할 수 있어 협업 효율이 크게 오른다.