GitHub Actions CI/CD 완벽 가이드 — 자동화 워크플로우 구축하기

발행 읽는 시간 약 2 분

  • GitHub Actions
  • CI/CD
  • DevOps
  • 자동화
  • 배포
  • 테스트

글 소개

GitHub Actions로 테스트, 빌드, 배포를 자동화하는 방법. 실전 워크플로우 예제와 최적화 팁을 다룹니다.

GitHub Actions는 GitHub에 내장된 CI/CD 플랫폼입니다. 코드 푸시부터 프로덕션 배포까지 모든 워크플로우를 자동화할 수 있습니다.


GitHub Actions 개념#

Diagram · Mermaid
개념설명
Workflow자동화 프로세스 전체 (.yml 파일)
Event워크플로우를 트리거하는 이벤트
Job같은 러너에서 실행되는 스텝 모음
Step개별 작업 단위 (액션 또는 스크립트)
Action재사용 가능한 작업 단위
Runner워크플로우를 실행하는 서버

기본 워크플로우#

파일 위치#

Text
my-repo/
└── .github/
    └── workflows/
        ├── ci.yml
        ├── deploy.yml
        └── release.yml

첫 번째 워크플로우#

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

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

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Run linter
        run: npm run lint

주요 트리거 (Events)#

push / pull_request#

YAML
on:
  push:
    branches:
      - main
      - 'release/**'
    paths:
      - 'src/**'
      - 'package.json'
    paths-ignore:
      - '**.md'
      - 'docs/**'

  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

수동 실행#

YAML
on:
  workflow_dispatch:
    inputs:
      environment:
        description: '배포 환경'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production
      debug:
        description: '디버그 모드'
        required: false
        type: boolean
        default: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ inputs.environment }}"
      - if: ${{ inputs.debug }}
        run: echo "Debug mode enabled"

스케줄 (Cron)#

YAML
on:
  schedule:
    # 매일 자정 (UTC)
    - cron: '0 0 * * *'
    # 매주 월요일 오전 9시 (KST = UTC+9)
    - cron: '0 0 * * 1'

다른 워크플로우 호출#

YAML
# 호출하는 쪽
on:
  workflow_run:
    workflows: ["Build"]
    types: [completed]
    branches: [main]

jobs:
  deploy:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build succeeded, deploying..."

실전 워크플로우#

Next.js CI/CD#

YAML
name: Next.js CI/CD

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

env:
  NODE_VERSION: '20'

jobs:
  # 1. 린트 & 타입 체크
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

  # 2. 테스트
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  # 3. 빌드
  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: build
          path: .next/
          retention-days: 7

  # 4. 배포 (main 브랜치만)
  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://myapp.com

    steps:
      - uses: actions/checkout@v4

      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: build
          path: .next/

      - 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'

Docker 빌드 & 푸시#

YAML
name: Docker Build

on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

매트릭스 빌드#

YAML
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [18, 20, 22]
        exclude:
          - os: windows-latest
            node: 18
        include:
          - os: ubuntu-latest
            node: 20
            coverage: true

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

      - run: npm ci
      - run: npm test

      - if: matrix.coverage
        run: npm run test:coverage

비밀 관리#

Secrets 설정#

Diagram · Mermaid

Secrets 사용#

YAML
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: |
          echo "Deploying with API key"
          # API_KEY와 DATABASE_URL 사용

Environment Variables#

YAML
env:
  # 워크플로우 레벨
  NODE_ENV: production

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      # Job 레벨
      CI: true

    steps:
      - name: Build
        env:
          # Step 레벨
          NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
        run: npm run build

캐싱 전략#

npm 캐시#

YAML
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # 자동 캐싱

커스텀 캐시#

YAML
- name: Cache Next.js
  uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.ts') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
      ${{ runner.os }}-nextjs-

Turborepo 캐시#

YAML
- name: Cache Turbo
  uses: actions/cache@v4
  with:
    path: .turbo
    key: ${{ runner.os }}-turbo-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-turbo-

재사용 가능한 워크플로우#

정의 (Callable Workflow)#

YAML
# .github/workflows/reusable-build.yml
name: Reusable Build

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
      environment:
        required: true
        type: string
    secrets:
      npm-token:
        required: true

jobs:
  build:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          registry-url: 'https://npm.pkg.github.com'

      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.npm-token }}

      - run: npm run build

호출#

YAML
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    uses: ./.github/workflows/reusable-build.yml
    with:
      node-version: '20'
      environment: production
    secrets:
      npm-token: ${{ secrets.NPM_TOKEN }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

유용한 액션들#

YAML
# PR 자동 라벨링
- uses: actions/labeler@v5
  with:
    repo-token: "${{ secrets.GITHUB_TOKEN }}"

# Slack 알림
- uses: slackapi/slack-github-action@v1
  with:
    channel-id: 'C123456'
    slack-message: "Build ${{ job.status }}: ${{ github.event.head_commit.message }}"
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

# PR 코멘트
- uses: marocchino/sticky-pull-request-comment@v2
  with:
    message: |
      ## 빌드 결과
      - 빌드: ✅
      - 테스트: ✅

# 자동 병합
- uses: peter-evans/enable-pull-request-automerge@v3
  with:
    pull-request-number: ${{ github.event.pull_request.number }}
    merge-method: squash

최적화 팁#

1. 병렬 실행#

YAML
jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]

  test:
    runs-on: ubuntu-latest
    steps: [...]

  build:
    needs: [lint, test]  # lint와 test는 병렬, build는 이후
    runs-on: ubuntu-latest
    steps: [...]

2. 조건부 실행#

YAML
steps:
  - name: Only on main
    if: github.ref == 'refs/heads/main'
    run: npm run deploy

  - name: Skip on bot commits
    if: "!contains(github.event.head_commit.author.name, 'bot')"
    run: npm test

  - name: On failure
    if: failure()
    run: echo "Previous step failed"

3. 타임아웃 설정#

YAML
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Long running test
        timeout-minutes: 5
        run: npm test

비용 절약#

Text
무료 한도 (Public 저장소: 무제한)
Private 저장소:
- GitHub Free: 2,000분/월
- GitHub Pro: 3,000분/월
- GitHub Team: 3,000분/월

비용 절약 팁#

YAML
# 1. 불필요한 실행 방지
on:
  push:
    paths-ignore:
      - '**.md'
      - 'docs/**'

# 2. 작은 러너 사용
jobs:
  test:
    runs-on: ubuntu-latest  # 가장 저렴

# 3. 캐시 적극 활용
- uses: actions/cache@v4

마치며#

GitHub Actions 핵심 포인트:

  1. 간단 시작: 기본 CI부터 시작해서 점진적으로 확장
  2. 캐싱 필수: 빌드 시간과 비용 절약
  3. 재사용: Reusable Workflows로 중복 제거
  4. 보안: Secrets와 Environments 적극 활용

GitHub 저장소라면 GitHub Actions가 가장 자연스러운 선택입니다.