WebPiki
tutorial

GitHub Actions CI/CD — 워크플로우 작성부터 실전 배포까지

GitHub Actions로 CI/CD 파이프라인 구축하기. 워크플로우 문법, 실전 예제, 비용 절약 팁까지 정리.

코드를 push하면 테스트가 자동으로 돌고, main에 머지하면 프로덕션에 배포된다. CI/CD의 기본 개념인데, 이걸 직접 구축하려고 하면 Jenkins 같은 별도 서버를 세팅해야 했다. GitHub Actions는 이 과정을 GitHub 안에서 해결해준다.

리포지토리에 YAML 파일 하나 추가하면 끝. 별도 서버 없이, GitHub 계정만으로 CI/CD가 돌아간다.

워크플로우 기본 구조

.github/workflows/ 폴더에 YAML 파일을 넣으면 GitHub가 자동으로 인식한다. 가장 단순한 형태부터 보자.

name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

on이 트리거, jobs가 실제 작업, steps가 각 단계. 이 파일을 리포지토리에 커밋하는 순간부터 push나 PR이 올라올 때마다 테스트가 자동으로 돌아간다.

한 가지 주의할 점 — npm install이 아니라 npm ci를 쓴다. cipackage-lock.json 기준으로 정확한 버전을 설치하고, node_modules를 처음부터 새로 만든다. CI 환경에서는 재현성이 중요하니까 ci가 맞다.

트리거 — 언제 실행할 건지

on 필드에서 워크플로우가 실행되는 조건을 정한다. 자주 쓰는 패턴 몇 가지:

# 특정 경로가 변경됐을 때만 실행
on:
  push:
    paths:
      - 'src/**'
      - 'package.json'

# 스케줄 (크론) — 매일 새벽 3시 UTC
on:
  schedule:
    - cron: '0 3 * * *'

# 수동 실행 (workflow_dispatch)
on:
  workflow_dispatch:
    inputs:
      environment:
        description: '배포 환경'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

paths 필터는 모노레포에서 특히 유용하다. 프론트엔드 코드만 바꿨는데 백엔드 테스트까지 돌 필요는 없으니까. workflow_dispatch는 버튼 하나로 수동 실행할 수 있게 해주는데, 배포 타이밍을 직접 제어하고 싶을 때 쓴다.

잡(Job)과 스텝(Step)

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    needs: lint  # lint가 성공해야 실행
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: ./deploy.sh

needs로 의존 관계를 설정한다. 위 예시에서는 lint → test → deploy 순서로 실행된다. needs가 없는 잡들은 병렬로 돌아간다.

if 조건으로 특정 상황에서만 실행할 수도 있다. deploy 잡은 main 브랜치에 push될 때만 실행되게 했다. PR에서는 테스트만 돌리고 배포는 안 하는 거다.

시크릿과 환경 변수

API 키나 배포 토큰 같은 민감한 값은 리포지토리 Settings → Secrets에 등록하고, 워크플로우에서 참조한다.

steps:
  - name: Deploy
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
    run: |
      echo "Deploying with token..."
      ./deploy.sh

시크릿은 로그에 자동으로 마스킹된다. echo $API_KEY 해도 ***로 표시됨. 근데 시크릿을 base64 인코딩한다거나 문자열을 쪼개서 출력하면 마스킹이 안 될 수 있으니까, 시크릿 값을 직접 출력하는 코드는 아예 쓰지 않는 게 좋다.

환경(Environment)을 분리해서 관리할 수도 있다. production 환경에는 승인(approval)을 걸어두면 실수로 프로덕션에 배포하는 걸 방지할 수 있다.

캐싱 — 빌드 시간 단축

매번 npm ci로 패키지를 처음부터 설치하면 시간이 꽤 걸린다. 캐싱을 쓰면 이전 실행의 node_modules를 재사용할 수 있다.

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'  # 이 한 줄이면 된다
  - run: npm ci
  - run: npm test

actions/setup-node@v4cache: 'npm'만 추가하면 package-lock.json 기준으로 자동 캐싱된다. 캐시가 있으면 npm ci 시간이 몇 십 초에서 몇 초로 줄어든다.

Turbo나 nx 같은 빌드 도구를 쓰는 경우 빌드 캐시도 별도로 캐싱하면 효과가 크다.

- uses: actions/cache@v4
  with:
    path: .turbo
    key: turbo-${{ runner.os }}-${{ hashFiles('**/turbo.json') }}

실전 예제 — Next.js 프로젝트

실제 Next.js 프로젝트에서 쓸 법한 워크플로우 예시.

name: CI/CD

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

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type Check
        run: npx tsc --noEmit

      - name: Build
        run: npm run build

  preview:
    runs-on: ubuntu-latest
    needs: quality
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - 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 }}

  production:
    runs-on: ubuntu-latest
    needs: quality
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - 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'

PR이 올라오면 lint + 타입 체크 + 빌드를 돌리고, 통과하면 Vercel 프리뷰 배포. main에 머지되면 프로덕션 배포. Vercel을 안 쓴다면 preview/production 잡을 자기 배포 방식에 맞게 바꾸면 된다.

매트릭스 전략

여러 Node.js 버전이나 OS에서 테스트해야 할 때:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

이렇게 하면 2(OS) × 3(Node 버전) = 6개 조합이 병렬로 돌아간다. 라이브러리를 만들 때 호환성 테스트에 유용하고, 일반 웹 앱이라면 보통 하나의 조합만으로 충분하다.

비용과 제한

퍼블릭 리포지토리는 무료다. 시간 제한도 거의 없다. 오픈소스라면 걱정할 게 없다.

프라이빗 리포지토리는 월 2,000분이 무료로 제공된다 (Free 플랜 기준). 넘으면 분당 과금. 근데 2,000분이면 웬만한 개인 프로젝트에서는 넉넉하다. 빌드 시간이 5분짜리 워크플로우를 하루에 10번 돌려도 한 달에 1,500분.

비용을 줄이는 팁:

  • paths 필터로 불필요한 실행 줄이기
  • 캐싱 적극 활용
  • concurrency 설정으로 같은 브랜치의 이전 실행 자동 취소
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

이 설정이면 같은 브랜치에서 새 push가 오면 이전에 돌던 워크플로우가 자동으로 취소된다. PR에 push를 연속으로 할 때 불필요한 실행을 막아준다.

자주 하는 실수

시크릿을 fork한 리포지토리의 PR에서 접근하려는 것. 보안상 fork된 PR에서는 시크릿에 접근할 수 없다. 이건 의도된 동작이다. fork PR에서 시크릿이 필요한 작업을 하려면 pull_request_target 이벤트를 써야 하는데, 이것도 보안 위험이 있어서 신중하게 써야 한다.

워크플로우 파일에 문법 오류. YAML은 들여쓰기에 민감하다. 스페이스 하나 틀리면 워크플로우가 아예 실행이 안 된다. VS Code의 GitHub Actions 확장 프로그램을 쓰면 문법 검증을 해주니까 설치해두는 게 좋다.

actions/checkout을 빠뜨리는 것. 당연히 있을 거라고 생각하기 쉬운데, 명시적으로 써줘야 한다. 이게 없으면 코드가 아예 없는 상태에서 스텝이 실행된다.

GitHub Actions는 한번 세팅해두면 계속 돌아가는 인프라다. 처음에 YAML 작성하는 게 좀 번거롭지만, 수동으로 테스트 돌리고 배포하는 시간을 생각하면 초기 투자 대비 효과가 크다.

#GitHubActions#CI/CD#DevOps#자동화#GitHub

관련 글