GitHub Actions CI/CD — 모노레포에서 효율적으로 설정하기

게시일: 2025년 6월 13일 · 15분 읽기

CI가 30분이면 PR 리뷰가 안 된다

모노레포로 전환했을 때 가장 큰 문제가 뭐였을까? CI 시간이 엄청 늘어난 거다. 처음엔 PR을 올려도 30분을 기다려야 테스트 결과를 볼 수 있었다. 그 동안 다른 PR을 리뷰한다? 맥락 전환이 너무 많다.

캐싱과 병렬화로 15분으로 줄였다. 방법을 공유하겠다.

모노레포의 기본 구조

먼저 우리 모노레포의 구조를 보자:

<?xml version="1.0"?>
monorepo/
  apps/
    web/           # Next.js
    api/           # Node.js Express
    admin/         # React
  packages/
    ui/            # 공유 UI 컴포넌트
    utils/         # 공유 유틸리티
    types/         # 공유 타입 정의
  .github/
    workflows/
      ci.yml

웹, API, 관리자 페이지가 있고, 공유 패키지가 3개다.

경로 기반 트리거 설정

첫 번째 최적화: 관련 있는 부분만 빌드한다.

<?xml version="1.0"?>
name: CI

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

jobs:
  # Web 앱 테스트 (web 또는 공유 패키지 변경 시에만)
  test-web:
    runs-on: ubuntu-latest
    if: |
      contains(github.event.pull_request.title, '[web]') ||
      contains(github.event.pull_request.title, '[shared]') ||
      github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup

      - name: Test Web
        run: pnpm --filter web test

  # API 테스트
  test-api:
    runs-on: ubuntu-latest
    if: |
      contains(github.event.pull_request.title, '[api]') ||
      contains(github.event.pull_request.title, '[shared]') ||
      github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup

      - name: Test API
        run: pnpm --filter api test

  # 관리자 페이지
  test-admin:
    runs-on: ubuntu-latest
    if: |
      contains(github.event.pull_request.title, '[admin]') ||
      contains(github.event.pull_request.title, '[shared]') ||
      github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup

      - name: Test Admin
        run: pnpm --filter admin test

더 정교한 방식은 경로 필터를 쓰는 것이다:

<?xml version="1.0"?>
name: CI

on:
  pull_request:
    paths:
      - 'apps/web/**'
      - 'packages/**'
      - 'pnpm-lock.yaml'
      - '.github/workflows/**'

jobs:
  test-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup
      - run: pnpm --filter web test

이렇게 하면 web 앱 변화가 없고 다른 부분만 변했으면 이 작업은 실행되지 않는다.

캐싱 전략

캐싱만으로도 시간을 절반 줄일 수 있다.

<?xml version="1.0"?>
name: Setup Node and Cache

on:
  workflow_call:

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

cache: 'pnpm' 이 줄이 핵심이다. GitHub Actions가 자동으로 pnpm 캐시를 관리한다.

더 정교한 캐싱:

<?xml version="1.0"?>
- name: Cache pnpm
  uses: actions/cache@v3
  with:
    path: ~/.pnpm-store
    key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-

- name: Cache node_modules
  uses: actions/cache@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-node-

- name: Cache build artifacts
  uses: actions/cache@v3
  with:
    path: |
      apps/*/dist
      packages/*/dist
    key: ${{ runner.os }}-build-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-build-

병렬 빌드

병렬화로 더 많은 시간을 절약한다.

<?xml version="1.0"?>
name: CI

on:
  pull_request:
    branches: [main, develop]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # 린트, 타입 체크는 빨리 끝남
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup
      - run: pnpm lint

  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup
      - run: pnpm type-check

  # 병렬로 각 앱 빌드
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [web, api, admin]
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup
      - run: pnpm --filter ${{ matrix.app }} build

  # 테스트도 병렬
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [web, api, admin]
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup
      - run: pnpm --filter ${{ matrix.app }} test

  # 모든 작업 완료 후 최종 확인
  all-checks:
    runs-on: ubuntu-latest
    if: always()
    needs: [lint, type-check, build, test]
    steps:
      - name: Check if all checks passed
        if: |
          needs.lint.result == 'failure' ||
          needs.type-check.result == 'failure' ||
          needs.build.result == 'failure' ||
          needs.test.result == 'failure'
        run: exit 1

이렇게 하면 lint, type-check, build, test가 모두 동시에 실행된다. 따로 실행하면 각 5분씩 20분인데, 병렬로 하면 5분만 걸린다.

Matrix 빌드로 여러 환경 테스트

여러 Node 버전에서 테스트하고 싶을 때:

<?xml version="1.0"?>
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16, 18, 20]
        app: [web, api]
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter ${{ matrix.app }} test

이렇게 하면 총 6개의 조합 (3개 Node 버전 × 2개 앱)이 병렬로 테스트된다.

실전: 자동화된 배포**

테스트 통과 후 자동 배포:

<?xml version="1.0"?>
name: Deploy

on:
  push:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup
      - run: pnpm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ./.github/actions/setup
      - run: pnpm build
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: dist/

  deploy-dev:
    if: github.ref == 'refs/heads/develop'
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v3
        with:
          name: build-artifacts
      - name: Deploy to staging
        run: |
          # 배포 스크립트
          ./scripts/deploy.sh staging
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

  deploy-prod:
    if: github.ref == 'refs/heads/main'
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v3
        with:
          name: build-artifacts
      - name: Deploy to production
        run: |
          ./scripts/deploy.sh production
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

성능 모니터링

CI 시간을 추적하자:

<?xml version="1.0"?>
- name: Report job status
  if: always()
  run: |
    echo "Job duration: ${{ job.duration }}"
    echo "Total workflow time: $(date +%s) - $GITHUB_WORKFLOW_START"

# GitHub에서 제공하는 정보
- name: Print job summary
  run: |
    echo "Build Status: ${{ job.status }}"
    echo "Build Number: ${{ github.run_number }}"
    echo "Build Attempt: ${{ github.run_attempt }}"

GitHub Actions 자체에서 각 작업의 시간을 보여준다. 병목을 파악할 수 있다.

최종 성과

최적화 전후:

마무리

CI 속도는 개발자 경험의 핵심이다. 빠른 피드백 루프는 생산성을 크게 향상시킨다. 모노레포는 공유 코드의 이점이 있지만, CI 관리가 복잡해진다. 경로 필터, 캐싱, 병렬화로 충분히 극복할 수 있다.

우리 팀은 이제 PR을 올리고 커피를 마시다가 결과를 본다. 30분 기다리지 않아도 된다.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.