문법과 실제 nextjs 프로젝트에서의 사용 사례를 중심으로 핸드북 형식으로 요약 정리. 기본부터 고급까지

GitHub Actions 문법과 Next.js 실제 사용 사례 핸드북

GitHub Actions 핵심 문법 가이드

기본 YAML 구조

GitHub Actions는 YAML 문법을 사용하며, 워크플로우 파일은 반드시 .github/workflows 디렉터리에 .yml 또는 .yaml 확장자로 저장되어야 합니다1.

기본 구조:

name: 워크플로우 이름
on: [트리거 이벤트]
jobs:
  작업명:
    runs-on: 실행환경
    steps:
      - name: 단계 이름
        uses: actions/checkout@v4
      - name: 커스텀 명령어
        run: echo "Hello World"

워크플로우 메타데이터

name (선택사항) 워크플로우의 이름을 정의합니다. 생략하면 파일 경로가 표시됩니다1.

run-name (선택사항) 특정 워크플로우 실행의 이름을 동적으로 설정할 수 있습니다1.

name: 
run-name: ${{ inputs.environment }}에 배포 by @${{ github.actor }}

이벤트 트리거 (on)

단일 이벤트

on: push

복수 이벤트

on: [push, pull_request]

고급 이벤트 설정

on:
  push:
    branches: [main, develop]
    paths: ['src/**', '!src/test/**']
  pull_request:
    types: [opened, synchronize, reopened]
    branches: [main]
  schedule:
    - cron: '0 2 * * 1-5'  # 평일 오전 2시
  workflow_dispatch:
    inputs:
      environment:
        description: '배포 환경'
        required: true
        type: choice
        options: [dev, staging, prod]

작업 (Jobs) 문법

기본 작업 구조

jobs:
  build:
    name: 빌드 작업
    runs-on: ubuntu-latest
    timeout-minutes: 30
    environment: production
    
    steps:
      - uses: actions/checkout@v4
      - run: npm install

작업 의존성 (needs)

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test
      
  deploy:
    needs: [test]  # test가 성공해야 실행
    runs-on: ubuntu-latest
    steps:
      - run: npm run deploy

조건부 실행 (if)

jobs:
  deploy:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "메인 브랜치에서만 실행"

복잡한 조건식 처리:

jobs:
  conditional-job:
    if: >-
      github.event_name == 'pull_request' ||
      (
        github.event_name == 'push' &&
        contains(github.event.head_commit.message, '[deploy]')
      )

매트릭스 전략 (Matrix Strategy)

매트릭스 전략은 여러 환경에서 동일한 작업을 병렬로 실행할 수 있게 해주는 강력한 기능입니다23.

기본 매트릭스

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [16, 18, 20]
        # 총 9개 작업 (3 × 3) 실행
    
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

매트릭스 확장 및 제외

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node-version: [16, 18]
    include:
      # 특정 조합 추가
      - os: macos-latest
        node-version: 18
        experimental: true
    exclude:
      # 특정 조합 제외
      - os: windows-latest
        node-version: 16
  
  fail-fast: false  # 하나 실패해도 나머지 계속 실행
  max-parallel: 2   # 최대 2개까지 병렬 실행

동적 매트릭스

jobs:
  generate-matrix:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - id: set-matrix
        run: |
          echo "matrix=[\"app1\", \"app2\", \"app3\"]" >> $GITHUB_OUTPUT
  
  deploy:
    needs: generate-matrix
    strategy:
      matrix:
        app: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying ${{ matrix.app }}"

환경 변수와 Secrets

환경 변수 설정

env:
  GLOBAL_VAR: "전체 워크플로우 변수"
 
jobs:
  build:
    env:
      JOB_VAR: "작업 레벨 변수"
    
    steps:
      - name: 환경 변수 사용
        run: echo $GLOBAL_VAR $JOB_VAR
        env:
          STEP_VAR: "단계 레벨 변수"

Secrets 사용

jobs:
  deploy:
    steps:
      - name: API 호출
        run: curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}"
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

Secrets 보안 모범 사례:

  • 민감한 정보는 절대 하드코딩하지 않기4
  • ::add-mask::를 사용하여 로그에서 숨기기
  • 환경별 secrets 관리5
  • 정기적인 rotation 수행

조건부 표현식과 함수

상태 확인 함수

steps:
  - name: 성공 시에만 실행
    if: success()
    run: echo "모든 이전 단계 성공"
    
  - name: 실패 시에도 실행
    if: always()
    run: echo "항상 실행"
    
  - name: 실패 시에만 실행
    if: failure()
    run: echo "이전 단계 실패"

컨텍스트 활용

jobs:
  info:
    runs-on: ubuntu-latest
    steps:
      - name: 컨텍스트 정보 출력
        run: |
          echo "이벤트: ${{ github.event_name }}"
          echo "브랜치: ${{ github.ref_name }}"
          echo "배우: ${{ github.actor }}"
          echo "러너 OS: ${{ runner.os }}"

액션 (Actions) 활용

공식 액션 사용

steps:
  - name: 체크아웃
    uses: actions/checkout@v4
    with:
      fetch-depth: 0
      
  - name: Node.js 설정
    uses: actions/setup-node@v4
    with:
      node-version: '18'
      cache: 'npm'
      registry-url: 'https://registry.npmjs.org'

버전 관리 모범 사례

steps:
  # 권장: 특정 커밋 SHA 사용 (최고 보안)
  - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3
  
  # 권장: 메이저 버전 태그 (편의성과 보안 균형)
  - uses: actions/checkout@v4
  
  # 주의: 특정 버전 태그
  - uses: actions/checkout@v4.2.0
  
  # 비권장: 브랜치 참조 (보안 위험)
  - uses: actions/checkout@main

고급 워크플로우 패턴

재사용 가능한 워크플로우 (Reusable Workflows)

재사용 가능한 워크플로우는 여러 저장소에서 공통 로직을 공유할 수 있게 해줍니다67.

재사용 가능한 워크플로우 정의

# .github/workflows/reusable-build.yml
name: 재사용 가능한 빌드
on:
  workflow_call:
    inputs:
      node-version:
        description: 'Node.js 버전'
        required: false
        type: string
        default: '18'
      environment:
        description: '배포 환경'
        required: true
        type: string
    secrets:
      NPM_TOKEN:
        description: 'NPM 토큰'
        required: true
    outputs:
      build-id:
        description: '빌드 ID'
        value: ${{ jobs.build.outputs.build-id }}
 
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      build-id: ${{ steps.build.outputs.id }}
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      
      - name: 빌드
        id: build
        run: |
          npm ci
          npm run build
          echo "id=$(date +%s)" >> $GITHUB_OUTPUT
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

재사용 가능한 워크플로우 호출

# .github/workflows/main.yml
name: 메인 워크플로우
on: [push]
 
jobs:
  call-reusable:
    uses: ./.github/workflows/reusable-build.yml
    with:
      node-version: '20'
      environment: 'production'
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
  
  deploy:
    needs: call-reusable
    runs-on: ubuntu-latest
    steps:
      - run: echo "빌드 ID: ${{ needs.call-reusable.outputs.build-id }}"

컴포지트 액션 (Composite Actions)

컴포지트 액션은 여러 단계를 하나의 액션으로 묶어 재사용성을 높입니다89.

컴포지트 액션 생성

# .github/actions/setup-node/action.yml
name: 'Node.js 환경 설정'
description: 'Node.js 설치 및 의존성 캐싱'
 
inputs:
  node-version:
    description: 'Node.js 버전'
    required: false
    default: '18'
  cache-dependency-path:
    description: '의존성 파일 경로'
    required: false
    default: 'package-lock.json'
 
outputs:
  cache-hit:
    description: '캐시 히트 여부'  
    value: ${{ steps.cache.outputs.cache-hit }}
 
runs:
  using: 'composite'
  steps:
    - name: Node.js 설정
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        
    - name: 의존성 캐시
      id: cache
      uses: actions/cache@v3
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles(inputs.cache-dependency-path) }}
        
    - name: 의존성 설치
      if: steps.cache.outputs.cache-hit != 'true'
      run: npm ci
      shell: bash

컴포지트 액션 사용

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-node
        with:
          node-version: '20'
      - run: npm run build

Next.js 프로젝트 실제 사용 사례

기본 CI 파이프라인

name: Next.js CI
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
env:
  NODE_VERSION: '20'
 
jobs:
  code-quality:
    name: 코드 품질 검사
    runs-on: ubuntu-latest
    
    steps:
      - name: 체크아웃
        uses: actions/checkout@v4
      
      - name: Node.js 설정    
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: 의존성 설치
        run: npm ci
      
      - name: TypeScript 타입 검사
        run: npx tsc --noEmit
      
      - name: ESLint 검사
        run: npm run lint
      
      - name: Prettier 포맷 검사
        run: npm run format:check
  
  test:
    name: 테스트 실행
    runs-on: ubuntu-latest
    needs: code-quality
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - run: npm ci
      
      - name: 단위 테스트
        run: npm test -- --coverage --watchAll=false
      
      - name: 테스트 커버리지 업로드
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
  
  build:
    name: 빌드 검증
    runs-on: ubuntu-latest
    needs: [code-quality, test]
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - run: npm ci
      
      - name: Next.js 빌드
        run: npm run build
        env:
          NEXT_TELEMETRY_DISABLED: 1
      
      - name: 빌드 아티팩트 저장
        uses: actions/upload-artifact@v3
        with:
          name: build-files
          path: .next/
          retention-days: 1

ESLint 및 Prettier 통합

package.json 스크립트 설정:

{
  "scripts": {
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
    "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "type-check": "tsc --noEmit"
  }
}

GitHub Actions 워크플로우:

name: 코드 품질
on: [push, pull_request]
 
jobs:
  quality-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      
      - name: ESLint 실행
        run: |
          npm run lint 2>&1 | tee eslint-report.txt
          exit ${PIPESTATUS[^0]}
      
      - name: Prettier 검사
        run: npm run format:check
      
      - name: TypeScript 검사  
        run: npm run type-check
      
      - name: ESLint 결과 코멘트
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const eslintOutput = fs.readFileSync('eslint-report.txt', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## ESLint 검사 실패\n\`\`\`\n${eslintOutput}\n\`\`\``
            });

매트릭스를 활용한 다중 환경 테스트

name: 크로스 플랫폼 테스트
on: [push, pull_request]
 
jobs:
  test-matrix:
    name: 테스트 (${{ matrix.os }}, Node ${{ matrix.node }})
    runs-on: ${{ matrix.os }}
    
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [18, 20]
        include:
          - os: ubuntu-latest
            node: 21
            experimental: true
        exclude:
          - os: windows-latest
            node: 18  # Windows에서 Node 18 제외
    
    continue-on-error: ${{ matrix.experimental == true }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Node.js ${{ matrix.node }} 설정
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'
      
      - name: 의존성 설치
        run: npm ci
      
      - name: 테스트 실행
        run: npm test
      
      - name: 빌드 테스트
        run: npm run build
        env:
          NODE_ENV: production

스테이징/프로덕션 배포 파이프라인

name: 배포 파이프라인
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      environment:
        description: '배포 환경'
        required: true
        type: choice
        options: [staging, production]
 
jobs:
  deploy:
    name: ${{ inputs.environment || 'staging' }} 배포
    runs-on: ubuntu-latest
    environment: 
      name: ${{ inputs.environment || 'staging' }}
      url: ${{ steps.deploy.outputs.url }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: 환경 설정
        id: config
        run: |
          if [[ "${{ inputs.environment ]]; then
            echo "domain=app.example.com" >> $GITHUB_OUTPUT
            echo "next_public_api_url=https://api.example.com" >> $GITHUB_OUTPUT
          else
            echo "domain=staging.example.com" >> $GITHUB_OUTPUT  
            echo "next_public_api_url=https://staging-api.example.com" >> $GITHUB_OUTPUT
          fi
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      
      - name: 환경별 빌드
        run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ steps.config.outputs.next_public_api_url }}
          NODE_ENV: production
      
      - name: Vercel 배포
        id: deploy
        run: |
          npx vercel --token ${{ secrets.VERCEL_TOKEN }} \
            --scope ${{ secrets.VERCEL_ORG_ID }} \
            --prod=${{ inputs.environment == 'production' && 'true' || 'false' }} \
            --yes > deployment-url.txt
          
          url=$(cat deployment-url.txt)
          echo "url=$url" >> $GITHUB_OUTPUT
      
      - name: 배포 알림
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: |
            ${{ inputs.environment || 'staging' }} 환경 배포 완료!
            🚀 URL: ${{ steps.deploy.outputs.url }}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

GitHub Pages 정적 배포

name: GitHub Pages 배포
on:
  push:
    branches: [main]
  workflow_dispatch:
 
permissions:
  contents: read
  pages: write
  id-token: write
 
concurrency:
  group: "pages"
  cancel-in-progress: false
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: 체크아웃
        uses: actions/checkout@v4
      
      - name: Node.js 설정
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: 의존성 설치
        run: npm ci
      
      - name: Next.js 설정 확인
        run: |
          cat > next.config.js << 'EOF'
          /** @type {import('next').NextConfig} */
          const nextConfig = {
            output: 'export',
            basePath: process.env.NODE_ENV === 'production' ? '/repository-name' : '',
            assetPrefix: process.env.NODE_ENV === 'production' ? '/repository-name/' : '',
            images: {
              unoptimized: true
            },
            trailingSlash: true
          }
          module.exports = nextConfig
          EOF
      
      - name: 정적 빌드
        run: npm run build
        env:
          NODE_ENV: production
      
      - name: Pages 아티팩트 업로드
        uses: actions/upload-pages-artifact@v2
        with:
          path: ./out
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    
    steps:
      - name: GitHub Pages 배포
        id: deployment
        uses: actions/deploy-pages@v2

성능 모니터링 및 최적화

name: 성능 모니터링
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  performance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      - run: npm run build
      
      - name: Bundle Analyzer
        run: |
          npm install -g @next/bundle-analyzer
          ANALYZE=true npm run build
      
      - name: Lighthouse CI
        uses: treosh/lighthouse-ci-action@v10
        with:
          configPath: './.lighthouserc.json'
          uploadArtifacts: true
          temporaryPublicStorage: true
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
      
      - name: 성능 리포트 코멘트
        uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        with:
          script: |
            const fs = require('fs');
            const lighthouse = JSON.parse(fs.readFileSync('./lhci_reports/manifest.json'));
            const summary = lighthouse[^0].summary;
            
            const comment = `## 🚀 성능 리포트
            
            | 메트릭 | 점수 |
            |--------|------|
            | Performance | ${summary.performance} |
            | Accessibility | ${summary.accessibility} |
            | Best Practices | ${summary['best-practices']} |
            | SEO | ${summary.seo} |
            `;
            
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: comment
            });

보안 검사 및 의존성 관리

name: 보안 검사
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # 매주 월요일 오전 2시
 
jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: npm audit
        run: |
          npm audit --audit-level=high
          npm audit --output=json > audit-results.json
        continue-on-error: true
      
      - name: Snyk 보안 테스트
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high
      
      - name: CodeQL 분석
        uses: github/codeql-action/init@v2
        with:
          languages: javascript
      
      - name: CodeQL 분석 수행
        uses: github/codeql-action/analyze@v2
      
      - name: 의존성 검토
        uses: actions/dependency-review-action@v3
        if: github.event_name == 'pull_request'

이 핸드북을 통해 GitHub Actions의 핵심 문법부터 Next.js 프로젝트의 실제 CI/CD 구축까지 체계적으로 학습할 수 있습니다. 특히 매트릭스 전략, 재사용 가능한 워크플로우, 컴포지트 액션 등의 고급 기능을 활용하면 더욱 효율적이고 유지보수 가능한 파이프라인을 구축할 수 있습니다.

Footnotes

  1. https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 2 3

  2. https://www.geeksforgeeks.org/git/the-matrix-strategy-in-github-actions/

  3. https://graphite.dev/guides/github-actions-matrix

  4. https://docs.github.com/actions/security-guides/using-secrets-in-github-actions

  5. https://docs.github.com/en/actions/concepts/security/secrets

  6. https://resources.github.com/learn/pathways/automation/intermediate/create-reusable-workflows-in-github-actions/

  7. https://thearchivelog.dev/article/how-to-reduce-duplication-in-workflows/

  8. https://earthly.dev/blog/composite-actions-github/

  9. https://deku.posstree.com/ko/github_actions/composite-action/