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

GitHub ActionsCI/CDDevOps자동화배포테스트

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


GitHub Actions 개념

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

기본 워크플로우

파일 위치

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

첫 번째 워크플로우

# .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

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

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

수동 실행

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)

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

다른 워크플로우 호출

# 호출하는 쪽
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

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 빌드 & 푸시

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

매트릭스 빌드

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 설정

Secrets 사용

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

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 캐시

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

커스텀 캐시

- 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 캐시

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

재사용 가능한 워크플로우

정의 (Callable Workflow)

# .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

호출

# .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..."

유용한 액션들

# 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. 병렬 실행

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. 조건부 실행

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. 타임아웃 설정

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

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

비용 절약

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

비용 절약 팁

# 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가 가장 자연스러운 선택입니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.