GitHub Actions CI/CD — 모노레포에서 효율적으로 설정하기
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 자체에서 각 작업의 시간을 보여준다. 병목을 파악할 수 있다.
최종 성과
최적화 전후:
- 설정 전: 30분 (모든 테스트를 순차적으로)
- 경로 필터 추가: 25분 (불필요한 테스트 스킵)
- 캐싱 추가: 18분 (의존성 재설치 제거)
- 병렬화: 8분 (작업 동시 실행)
- 최종 최적화: 5분 (모든 최적화 적용)
마무리
CI 속도는 개발자 경험의 핵심이다. 빠른 피드백 루프는 생산성을 크게 향상시킨다. 모노레포는 공유 코드의 이점이 있지만, CI 관리가 복잡해진다. 경로 필터, 캐싱, 병렬화로 충분히 극복할 수 있다.
우리 팀은 이제 PR을 올리고 커피를 마시다가 결과를 본다. 30분 기다리지 않아도 된다.