Waylog Blog
← 목록으로 돌아가기

GitHub Actions를 활용한 완벽한 CI/CD 파이프라인 구축 및 실무 최적화 가이드

DevOps

GitHub Actions CI/CD

GitHub Actions 혁명: 개발 패러다임을 바꾼 완벽한 자동화 환경

소프트웨어 개발 프로세스에서 **"빌드하고 배포하는 일"**은 개발의 본질이면서도, 동시에 가장 개발자를 지치게 만드는 노동이었습니다. 로컬 컴퓨터에서는 잘 돌아가던 코드가 운영 서버에만 올라가면 에러를 뿜어내고, 누군가 테스트 코드를 실행하지 않고 병합(Merge)해버려 전체 메인 브랜치가 마비되는 상황을 우리는 수없이 겪어왔습니다.

이러한 고통을 해결하기 위해 CI/CD(지속적 통합/지속적 배포)의 개념이 등장했고, Jenkins부터 Travis CI, CircleCI 등 수많은 도구들이 시장을 거쳐갔습니다. 하지만 현재 모던 웹 생태계의 패권은 단 하나의 도구가 완전히 장악했다고 해도 과언이 아닙니다. 바로 GitHub Actions입니다.

GitHub 안에서 코드를 호스팅하면서, 동시에 코드의 생명 주기를 통제하는 워크플로우를 소스코드와 동일하게 관리할 수 있다는 점은 개발 흐름의 단절을 완전히 없앴습니다. 이 글에서는 GitHub Actions의 기초 지식을 넘어, 실무 현장에서 즉시 적용할 수 있는 속도 최적화, 보안 아키텍처, 재사용 패턴, 그리고 Vercel/Docker 등과의 배포 통합 전략에 이르기까지 약 6,000자 분량으로 아주 상세히 짚어보고자 합니다.

1. GitHub Actions 아키텍처의 이해

GitHub Actions의 구조를 이해하는 것은 레고 블록을 조립하는 것과 같습니다. 네 가지 핵심 개념의 계층 구조를 이해해야 합니다.

  1. Workflow (워크플로우): 전체 자동화 프로세스의 최상위 개념입니다. .github/workflows/main.yml 처럼 YAML 파일로 정의되며, "언제 이 파이프라인이 실행될 것인가(Event)"를 결정합니다.
  2. Job (작업): 워크플로우 내에서 돌아가는 하나의 컨테이너 환경(예: Ubuntu 22.04 가상 머신)입니다. 여러 개의 Job은 기본적으로 병렬로 실행됩니다.
  3. Step (단계): Job 안에서 순차적으로 실행되는 단일 명령어나 작업 단위입니다.
  4. Action (액션): Step에서 재사용하기 위해 만들어진 패키지입니다. 누군가 미리 만들어놓은 스크립트 모음(actions/checkout, actions/setup-node 등)을 빌려 쓰는 것입니다.
# 기본 구조 예시: main.yml
name: Frontend CI Pipeline

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

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: 저장소 코드 체크아웃
        uses: actions/checkout@v4
        
      - name: Node.js 환경 세팅
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          
      - name: 의존성 설치
        run: npm ci
        
      - name: 린트  테스트
        run: npm run test

위의 기본적인 흐름만으로도 PR이 올라올 때마다 테스트를 강제하는 훌륭한 CI 환경이 완성됩니다. 하지만 프로젝트가 커지면 병목이 발생합니다. npm 패키지를 설치하는 데만 몇 분이 걸리기 때문입니다.

2. 병목 돌파: 캐싱(Caching) 전략 고도화

CI 환경은 매번 실행될 때마다 초기화된 가상 머신을 할당받습니다. 즉, 매번 처음부터 수백 메가바이트의 node_modules를 새로 다운로드해야 합니다. 이를 극복하는 핵심 기술이 바로 **캐싱(Caching)**입니다.

2.1 actions/setup-node의 내장 캐시 활용

최신 버전의 setup-node 액션은 내부적으로 캐싱 로직을 제공합니다.

      - name: Node.js 환경 세팅  의존성 캐시
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm' # npm, yarn, pnpm 등을 지원함

이 한 줄을 추가하는 것만으로 package-lock.json의 해시(Hash) 값을 기준으로 글로벌 npm 캐시 폴더를 GitHub 인프라에 저장하고 다음 빌드 때 불러옵니다. 초기 설치 시간을 절반 가까이 단축할 수 있습니다.

2.2 TurboRepo와 Next.js 빌드 캐싱

프론트엔드 모노레포 환경이나 Next.js 환경에서는 node_modules 외에도 .next/cache 같은 프레임워크 전용 빌드 캐시가 존재합니다. 이를 위해선 공식 actions/cache 액션을 직접 조합하여 사용해야 합니다.

      - name: Next.js 빌드 캐시 복원
        uses: actions/cache@v3
        with:
          path: |
            ~/.npm
            ${{ github.workspace }}/.next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

캐시 키(Key) 생성 시 파일 구조의 해시값을 조건으로 넣으면, 프론트엔드 코드가 변경되었을 때만 새로운 캐시를 생성하고 의존성이 동일하다면 완벽히 이전 캐시를 불러올 수 있습니다. 이 전략을 통해 Next.js 프로덕션 빌드 시간을 기존 5분에서 40초 대안으로 폭발적으로 감소시킬 수 있습니다.

3. 병렬 처리 아키텍처: Matrix Builds

테스트 코드가 1,000개가 넘어간다고 상상해보십시오. 단일 Job에서 이 모든 테스트를 돌리면 개발자의 생산성(대기 시간)은 급격히 하락합니다. GitHub Actions는 이를 위해 Matrix(매트릭스) 기능을 제공합니다.

Matrix는 서로 다른 환경(예: Node.js 18, 20, 22 버전을 동시 테스트)을 짤 때도 유용하지만, 테스트 파일을 노드(Node)별로 쪼개어(Sharding) 동시에 검증할 때 그 진가를 발휘합니다.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4] # 4대의 가상 머신을 띄움
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      
      # Jest의 --shard 옵션을 활용하여 전체 테스트의 1/4씩 분담 실행
      - name: Sharded Tests
        run: npx jest --shard=${{ matrix.shard }}/4

개발자가 PR을 올리면 동시에 4대의 서버가 실행되어 각각 25%의 테스트를 병렬로 처리합니다. 10분 걸리던 테스트가 2분 30초 만에 완료되는 기적을 경험할 수 있습니다.

4. OIDC (OpenID Connect): 보안의 혁신, Keyless 통신

웹 개발자들이 가장 많이 하는 실수 중 하나는 AWS ACCESS_KEY_IDSECRET_ACCESS_KEY 를 GitHub Secrets에 영구 저장하는 것입니다. 이 방식은 키가 유출될 위험이 있고, 주기적으로 키를 교체(Rotation)해야 하는 뼈아픈 유지보수 비용을 낳습니다.

최근의 모범 사례(Best Practice)는 OIDC를 이용한 키리스(Keyless) 인증입니다.
GitHub Actions가 AWS에게 "나는 내 레포지토리(waylog) 코드를 돌리고 있는 안전한 파이프라인이야"라는 OIDC 토큰을 건네면, AWS 내부 IAM Role 정책에서 이를 신뢰(Trust Policy)하고 임시(Temporary) 권한을 1시간 단위로만 부여해주는 방식입니다.

# OIDC를 사용하기 위해서는 반드시 권한 허용이 필요합니다.
permissions:
  id-token: write # OIDC JWT 토큰 발급 허용
  contents: read

jobs:
  deploy-to-s3:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: AWS 자격 증명 (Keyless)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsS3Role
          aws-region: ap-northeast-2
          
      # 이 아래부터는 S3 등 AWS 서비스에 비밀번호 없이 접근 가능!
      - name: 빌드 에셋 S3 업로드
        run: aws s3 sync ./out s3://my-waylog-bucket

AWS Access Key를 발급받지 않았기 때문에 탈취당할 위험이 0에 수렴합니다. 보안 측면에서 OIDC는 선택이 아니라 무조건 도입해야 할 필수 아키텍처입니다.

5. 코드 중복 제거: Reusable Workflows & Composite Actions

팀 내에 여러 개의 레포지토리가 있을 때, 모든 곳에 유사한 빌드/테스트 YAML 파일이 복사-붙여넣기 되어 있다면 심각한 기술 부채입니다. 이를 해결하기 위해 두 가지 추상화 전략을 사용합니다.

5.1 Reusable Workflows (재사용 워크플로우)

다른 워크플로우 전체(Job 단위)를 함수처럼 호출하는 기능입니다.

# 호출을 당하는 중앙 레포지토리 (central-shared/.github/workflows/reusable.yml)
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string

jobs:
  build:
    # ... 동일한 빌드 로직
# 호출하는 로컬 레포지토리
jobs:
  call-central-workflow:
    uses: my-org/central-shared/.github/workflows/reusable.yml@main
    with:
      node-version: 20

5.2 Composite Actions (복합 액션)

Job 단위가 아니라 Step단위의 반복을 모아서 하나의 커스텀 "Action"으로 패키징하는 기법입니다. setup-node, npm ci, lint 등 3~4개의 반복되는 작업을 묶어서 하나의 uses: ./my-custom-setup 으로 호출할 수 있게 만들어줍니다. 상황에 맞게 둘을 섞어 쓰면 수백 줄의 YAML파일을 20줄 미만으로 다이어트 시킬 수 있습니다.

6. 배포 자동화 통합 시나리오 (실무)

테스트와 린트(Linter)를 고도화했다면 최종 정착지는 CD(자동 배포)입니다.
Vercel이나 Cloudflare Pages같은 최신 플랫폼 기조는 자체적인 에코시스템(CI)을 갖고 있지만, 인하우스 서버나 복잡한 Docker 아키텍처로 넘어가면 GitHub Actions가 통제권을 가져야 합니다.

프론트엔드/백엔드 Docker 배포 파이프라인 예시:

  1. GitHub PR 생성됨: Linter, Unit Test, SonarQube 정적 코드 분석 실행. 결과 시각화(PR CommentBot이 댓글로 커버리지 결과 보고).
  2. 리뷰어가 승인 후 Main 병합:
  3. Main Push 감지 트리거 동작:
  4. 빌드 Job: 코드를 기반으로 Docker Image를 빌드합니다.(docker build)
  5. 푸시 Job: 빌드된 이미지를 Amazon ECR이나 GHCR(GitHub Container Registry)에 Push합니다. OIDC를 사용하여 권한을 통과합니다.
  6. 배포 Job (Deploy): k8s 클러스터의 Deployment yaml 이미지 해시를 업데이트하거나, 인스턴스에 SSH로 우회 접속하여 컨테이너 재시작(docker-compose up -d) 스크립트를 원격 실행합니다.
  7. 알림 전송: 배포 성공 여부를 Slack Webhook을 통해 지정된 개발팀 채널로 즉시 전송합니다.

이 모든 일련의 과정이 사람이 터미널을 열지 않고 오로지 git push라는 단순 행위에 의해 물방울처럼 연쇄 반응하여 프로덕션 서버의 갱신으로 이어집니다.

7. 결론: 개발 문화로서의 CI/CD

지금까지 기술적 최적화의 렌즈를 통해 GitHub Actions를 파헤쳐 보았습니다. 그러나 이 시스템이 주는 진정한 가치는 인프라 절약이나 속도가 아니라 **심리적 안전감(Psychological Safety)**에 있습니다.

"내가 이 코드를 변경해서 메인 서버가 죽으면 어떡하지?"라는 공포를, 테스트와 배포 자동화라는 견고한 성벽이 막아줍니다. 개발자는 비즈니스 로직 작성이라는 가장 가치 있는 일에만 에너지를 쏟아부을 수 있습니다.

이제 여러분의 레포지토리에 존재하는 .github/workflows 폴더를 단지 스크립트 모음 공간으로 생각하지 마십시오. 그 공간은 여러분의 코드를 살아 숨 쉬게 하고, 전 세계 사용자 무대로 안전하게 실어 나르는 가장 지적이고 정교하게 설계된 관제탑(Control Tower)입니다. 우아한 CI/CD의 세계를 마음껏 만끽하시길 바랍니다.