coredot.today
CI/CD 완전 정복: 코드를 커밋하면 자동으로 배포되는 마법
블로그로 돌아가기
CI/CDDevOps자동화배포JenkinsGitHub Actions파이프라인

CI/CD 완전 정복: 코드를 커밋하면 자동으로 배포되는 마법

FTP로 파일 올리던 시절부터 GitHub Actions 자동 배포까지. CI/CD 파이프라인의 개념, 구조, 주요 도구 비교, 배포 전략, 그리고 Next.js 앱을 자동으로 배포하는 실전 예제까지 한 번에 정리한다.

코어닷투데이2026-04-0176

들어가며: 수동 배포의 공포

금요일 오후 5시 47분. 퇴근 13분 전이다.

팀장이 슬랙에 메시지를 남긴다. "이거 오늘 안에 반영해야 합니다." 코드 변경은 3줄이다. 간단하다. 하지만 배포 과정은 간단하지 않다.

  1. 로컬에서 빌드한다
  2. FTP 클라이언트를 연다 (FileZilla, 아 그 초록색 아이콘)
  3. 운영 서버에 접속한다
  4. 변경된 파일을 찾아서 업로드한다
  5. "혹시 다른 파일도 바뀌었나?" 기억을 더듬는다
  6. 브라우저에서 확인한다
  7. 캐시 때문에 안 바뀐 것 같다. 강력 새로고침을 누른다
  8. 바뀌었다. 아니, 잠깐 — 이 페이지는 왜 깨져 있지?

파일 하나를 빠뜨렸다. 다시 FTP를 열고, 파일을 찾고, 업로드하고, 확인한다. 시계는 6시 23분. 퇴근은 물 건너갔다.

이것이 2010년대 초반까지 많은 개발팀의 현실이었다. 그리고 이런 경험이 쌓이면서 업계에 하나의 불문율이 생겼다:

🚨
"금요일 오후에는 배포하지 마라." — 문제가 생기면 주말이 날아간다. 월요일 아침에 하자. 이건 농담이 아니라 실제로 수많은 팀이 지키는 규칙이었다.

이 글은 그 공포에서 벗어나는 방법에 대한 이야기다. 코드를 커밋하는 순간, 테스트가 자동으로 돌고, 빌드가 만들어지고, 운영 서버에 안전하게 배포되는 시스템 — CI/CD 파이프라인을 처음부터 끝까지 파헤친다.

FTP 수동 배포 vs 자동화된 CI/CD 파이프라인 비교 일러스트


1. CI(Continuous Integration): 통합의 고통을 없애다

"통합 지옥"이라는 단어가 있었다

2000년대 초반, 소프트웨어 개발은 이런 식이었다:

  • 개발자 A가 2주 동안 기능을 만든다
  • 개발자 B도 2주 동안 다른 기능을 만든다
  • 2주 후, 두 사람의 코드를 합친다(merge)
  • 충돌(conflict) 수백 개. 합치는 데만 3일이 걸린다

이것을 "통합 지옥(Integration Hell)"이라 불렀다. 각자 오래 작업할수록 합칠 때의 고통은 기하급수적으로 증가했다.

Martin Fowler의 정의

소프트웨어 엔지니어링의 거장 Martin Fowler는 2006년 CI를 이렇게 정의했다:

MARTIN FOWLER — CONTINUOUS INTEGRATION (2006)
"Continuous Integration is a software development practice where members of a team integrate their work frequently — usually each person integrates at least daily — leading to multiple integrations per day."

"지속적 통합이란 팀원들이 자신의 작업을 자주 통합하는 소프트웨어 개발 관행이다. 보통 하루에 최소 한 번 이상, 하루에 여러 번 통합한다."

핵심은 "자주(frequently)"다. 2주에 한 번이 아니라, 하루에 여러 번. 통합을 자주 하면 충돌이 작아지고, 작은 충돌은 해결하기 쉽다.

CI의 핵심 원칙

원칙 1 단일 소스 저장소(Single Source Repository) — 모든 코드는 하나의 버전 관리 시스템(Git)에 있어야 한다
원칙 2 자동화된 빌드(Automated Build) — 코드를 가져와서 실행 가능한 상태로 만드는 과정이 자동이어야 한다
원칙 3 자동화된 테스트(Self-Testing Build) — 빌드 과정에서 테스트가 자동으로 실행되어야 한다
원칙 4 매일 메인 브랜치에 커밋 — 장기 브랜치를 만들지 말고, 작은 단위로 자주 통합한다
원칙 5 빌드가 깨지면 즉시 수정 — 깨진 빌드를 방치하지 않는다. 최우선 과제로 고친다

CI는 도구가 아니라 관행(practice)이다. Jenkins를 설치했다고 CI를 하는 것이 아니다. 팀원 모두가 하루에 여러 번 코드를 통합하고, 자동 빌드와 테스트로 그 통합이 안전한지 확인하는 것 — 그것이 CI다.


2. CD: Delivery와 Deployment, 한 글자 차이의 큰 차이

CD는 두 가지 의미로 쓰인다. Continuous DeliveryContinuous Deployment. 이름이 비슷해서 많은 사람이 혼동하지만, 의미는 명확히 다르다.

Continuous Delivery (지속적 전달)

코드 변경이 자동으로 빌드, 테스트를 거쳐 배포 가능한 상태(release-ready)가 되는 것.

실제 운영 환경에 배포하려면 사람이 버튼을 누른다. 자동으로 만들어진 배포 패키지를 검토하고, "이거 운영에 올려도 되겠다"고 판단한 후 수동으로 배포를 승인한다.

Continuous Deployment (지속적 배포)

코드 변경이 자동으로 빌드, 테스트를 거쳐 운영 환경까지 자동 배포되는 것.

사람의 개입이 없다. 테스트를 통과하면 바로 운영에 반영된다.

차이를 한눈에

CI → CD(Delivery) → CD(Deployment)
CI Continuous Integration 코드 통합 + 자동 빌드 + 자동 테스트
CD (Delivery) Continuous Delivery + 배포 가능 상태 자동 생성. 배포는 수동 승인
CD (Deployment) Continuous Deployment + 운영 배포까지 완전 자동. 사람 개입 없음
💡
어느 쪽을 선택해야 할까? 대부분의 팀은 Continuous Delivery부터 시작한다. 자동화된 테스트에 대한 신뢰가 충분히 쌓이면 Continuous Deployment로 전환한다. 금융, 의료 등 규제가 강한 산업에서는 감사(audit) 요구 때문에 수동 승인 단계가 필수인 경우가 많아 Continuous Delivery를 유지하기도 한다.

현실적으로, 많은 사람이 "CI/CD"라고 말할 때는 Continuous Delivery를 의미한다. Continuous Deployment는 더 높은 수준의 자동화 성숙도를 요구한다.


3. CI/CD 파이프라인의 구조

CI 파이프라인을 컨베이어 벨트와 품질 검사 로봇으로 표현한 일러스트

파이프라인(Pipeline)이란 코드가 개발자의 로컬 환경에서 운영 서버까지 도달하는 자동화된 경로를 말한다. 물이 파이프를 따라 흐르듯, 코드가 각 단계를 순서대로 통과한다.

4단계 핵심 구조

Source
Build
Test
Deploy

각 단계를 하나씩 파헤쳐 보자.

Stage 1: Source (소스)

파이프라인의 시작점이다. 트리거(trigger)라고도 한다.

  • Git push: 특정 브랜치(main, develop)에 코드가 push되면 파이프라인이 시작
  • Pull Request: PR이 생성되거나 업데이트되면 실행
  • 태그(Tag): v1.0.0 같은 릴리스 태그가 생성되면 실행
  • 스케줄: 매일 새벽 3시에 자동 실행 (nightly build)
  • 수동 트리거: 버튼을 눌러 수동으로 실행

가장 일반적인 패턴은 main 브랜치에 push되면 자동 실행이다.

Stage 2: Build (빌드)

소스 코드를 실행 가능한 형태로 변환하는 단계다.

2-1 의존성 설치npm install, pip install, gradle build 등으로 필요한 라이브러리를 설치한다
2-2 컴파일/트랜스파일 — TypeScript → JavaScript, Java → bytecode, Go → binary 등으로 변환한다
2-3 번들링/최적화 — 웹 앱이라면 Webpack/Turbopack 등으로 코드를 묶고, 압축하고, 트리 쉐이킹한다
2-4 아티팩트 생성 — 최종 결과물(Docker 이미지, JAR 파일, .next 빌드 결과물 등)을 만든다

빌드가 실패하면 파이프라인은 즉시 멈춘다. 타입 에러, 문법 오류, 의존성 충돌 등이 이 단계에서 잡힌다.

Stage 3: Test (테스트)

빌드된 코드가 제대로 동작하는지 검증하는 단계다. 이 단계에서 실행되는 테스트의 종류는 뒤에서 자세히 다룬다.

Stage 4: Deploy (배포)

테스트를 통과한 코드를 실제 환경에 반영하는 단계다.

보통 환경을 여러 단계로 나눈다:

DEV 개발 환경 — 개발자들이 자유롭게 테스트하는 공간. 불안정해도 괜찮다
STAGING 스테이징 환경 — 운영과 동일한 구성. QA 팀이 최종 검증하는 공간
PROD 운영 환경 — 실제 사용자가 접속하는 환경. 여기에 문제가 생기면 매출에 직결된다

Continuous Delivery에서는 STAGING까지 자동이고, PROD 배포는 수동 승인 후 실행된다. Continuous Deployment에서는 PROD까지 전부 자동이다.


4. 주요 CI/CD 도구 비교

시장에는 수많은 CI/CD 도구가 있다. 각각의 특성과 적합한 상황을 비교해 보자.

Jenkins

2011년에 등장한 오픈소스 CI/CD 서버. Java로 작성되었고, 1,800개 이상의 플러그인이 있다. 가장 오래되고 가장 널리 쓰이는 CI/CD 도구다.

hljs language-groovy
// Jenkinsfile (Declarative Pipeline)
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'npm install'
                sh 'npm run build'
            }
        }
        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
        stage('Deploy') {
            steps {
                sh 'docker build -t my-app .'
                sh 'docker push my-app:latest'
            }
        }
    }
}

장점: 극도의 유연성, 온프레미스 설치 가능, 거대한 플러그인 생태계 단점: 설정이 복잡하다. Jenkins 서버 자체를 관리해야 한다. "Jenkins 관리자"라는 역할이 따로 필요할 정도.

GitHub Actions

2019년에 출시된 GitHub 네이티브 CI/CD. GitHub 저장소와 완벽하게 통합된다. 설정 파일 하나로 파이프라인을 정의한다.

hljs language-yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build
      - run: npm test
      - name: Deploy to AWS
        run: aws s3 sync ./out s3://my-bucket
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

장점: GitHub 사용자라면 별도 설정 없이 바로 사용. YAML 기반으로 직관적. 마켓플레이스에 수만 개의 재사용 가능한 Actions. 단점: GitHub에 종속. 대규모 프로젝트에서 비용이 높아질 수 있음. 무료 티어는 월 2,000분.

GitLab CI/CD

GitLab에 내장된 CI/CD. .gitlab-ci.yml 파일로 파이프라인을 정의한다.

hljs language-yaml
# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - .next/

test:
  stage: test
  script:
    - npm test

deploy:
  stage: deploy
  script:
    - docker build -t my-app .
    - docker push my-app:latest
  only:
    - main

장점: Git 저장소부터 CI/CD, 컨테이너 레지스트리, 모니터링까지 올인원. Self-hosted 가능. 단점: GitLab 생태계에 종속. UI가 복잡할 수 있음.

도구 비교표

도구호스팅설정 방식무료 티어난이도
JenkinsSelf-hostedJenkinsfile (Groovy)완전 무료 (오픈소스)높음
GitHub ActionsCloud (GitHub)YAML월 2,000분낮음
GitLab CICloud / Self-hostedYAML월 400분중간
CircleCICloud / Self-hostedYAML월 6,000 크레딧중간
AWS CodePipelineCloud (AWS)Console / CloudFormation월 1 파이프라인 무료높음
💡
2026년 현재 트렌드: 스타트업과 오픈소스 프로젝트는 GitHub Actions가 사실상 표준이다. 엔터프라이즈에서는 Jenkins와 GitLab CI가 여전히 강세. AWS에 올인한 팀은 CodePipeline + CodeBuild 조합을 쓴다. 처음 시작한다면 GitHub Actions부터 시작하는 것을 추천한다.

5. Artifacts, 환경 변수, 시크릿: 파이프라인의 핵심 개념들

파이프라인을 실제로 구성하려면 세 가지 개념을 반드시 알아야 한다.

Artifacts (아티팩트)

빌드 결과물을 의미한다. 파이프라인의 한 단계에서 생성되어 다음 단계로 전달되는 파일이다.

아티팩트의 흐름
Build 단계 npm run build → .next/ 폴더 생성
아티팩트 저장 .next/ 폴더를 아티팩트로 저장 → 다음 단계에서 사용 가능
Deploy 단계 저장된 아티팩트를 다운로드 → 서버에 배포

아티팩트의 예시:

  • 웹 앱: 빌드된 정적 파일 (.next/, dist/, build/)
  • 백엔드: 컴파일된 바이너리, JAR 파일, Docker 이미지
  • 모바일: APK, IPA 파일
  • 테스트 결과: 커버리지 리포트, 테스트 결과 HTML

Environment Variables (환경 변수)

파이프라인 실행 시 주입되는 설정값이다. 코드에 직접 쓰지 않고 환경 변수로 분리하면, 같은 코드를 다른 환경에서 다른 설정으로 실행할 수 있다.

환경별 환경 변수 예시
DEV 환경:
API_URL=https://dev-api.example.com
DB_HOST=dev-db.internal
LOG_LEVEL=debug

STAGING 환경:
API_URL=https://staging-api.example.com
DB_HOST=staging-db.internal
LOG_LEVEL=info

PROD 환경:
API_URL=https://api.example.com
DB_HOST=prod-db.internal
LOG_LEVEL=warn

같은 Docker 이미지를 세 환경에 배포하되, 환경 변수만 다르게 주입한다. 코드는 process.env.API_URL로 값을 읽기만 하면 된다.

Secrets (시크릿)

절대로 노출되면 안 되는 민감한 값이다. API 키, 데이터베이스 비밀번호, 인증서, 토큰 등이 여기에 해당한다.

⚠️
시크릿 관리의 황금률: 시크릿은 절대로 코드에 하드코딩하지 않는다. Git 저장소에 커밋하지 않는다. 로그에 출력하지 않는다. CI/CD 도구의 시크릿 관리 기능을 사용하거나, AWS Secrets Manager, HashiCorp Vault 같은 전용 서비스를 사용한다.

GitHub Actions에서 시크릿을 사용하는 예:

hljs language-yaml
# 저장소 Settings → Secrets에서 등록한 시크릿 사용
- name: Deploy
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  run: aws s3 sync ./out s3://my-bucket

${{ secrets.AWS_ACCESS_KEY_ID }}는 실행 시 실제 값으로 치환되지만, 로그에는 ***로 마스킹된다. 실수로 출력해도 값이 노출되지 않는다.


6. 파이프라인 속 테스트: 무엇을, 어떤 순서로

"자동 테스트 없는 CI/CD는 자동화된 재앙 배포 시스템이다." 테스트 없이 자동 배포만 하면, 버그를 더 빠르게 운영에 올릴 뿐이다. 파이프라인에서 실행되는 테스트의 종류를 알아보자.

테스트 피라미드

E2E 테스트 (느리고 비쌈, 적게)
통합 테스트 (중간)
유닛 테스트 (빠르고 쌈, 많이)

아래로 갈수록 많이, 위로 갈수록 적게 — 이것이 테스트 피라미드(Test Pyramid)의 원칙이다.

파이프라인에서 실행되는 테스트 종류

테스트 종류검증 대상실행 시간도구 예시
Linting코드 스타일, 문법 오류수 초ESLint, Prettier, Pylint
Type Check타입 안전성수 초 ~ 수십 초TypeScript tsc, mypy
Unit Test개별 함수/컴포넌트수 초 ~ 수 분Jest, Vitest, pytest
Integration Test모듈 간 상호작용, API수 분Supertest, Testcontainers
E2E Test사용자 시나리오 전체수 분 ~ 수십 분Playwright, Cypress
Security Scan취약한 의존성, 코드 취약점수 분Snyk, Trivy, npm audit

실행 순서의 원칙: 빠른 것부터

파이프라인에서 테스트는 빠르고 싼 것부터 실행한다. 린팅에서 오류가 발견되면 굳이 10분짜리 E2E 테스트를 기다릴 필요가 없다.

1단계 Lint + Type Check (수 초) — 코드 스타일 오류, 타입 에러를 즉시 잡는다
2단계 Unit Test (수 초 ~ 수 분) — 개별 함수가 올바르게 동작하는지 확인
3단계 Integration Test (수 분) — 모듈 간 연동이 정상인지 확인
4단계 Security Scan (수 분) — 취약한 라이브러리, 하드코딩된 시크릿 탐지
5단계 E2E Test (수 분 ~ 수십 분) — 실제 브라우저에서 사용자 시나리오 재현
💡
병렬화 팁: 린팅, 유닛 테스트, 보안 스캔은 서로 독립적이므로 병렬로 실행할 수 있다. GitHub Actions에서는 별도의 job으로 분리하면 동시에 돌릴 수 있다. 이렇게 하면 총 파이프라인 시간을 크게 단축할 수 있다.

7. 배포 전략: 안전하게 운영에 올리는 방법

금요일 오후 배포의 공포를 코믹 호러 장면으로 표현한 일러스트

코드가 모든 테스트를 통과했다. 이제 운영 서버에 올려야 한다. 하지만 어떻게 올리느냐에 따라 위험도가 완전히 달라진다.

Big Bang Deployment (빅뱅 배포)

가장 원시적인 방법. 기존 버전을 내리고, 새 버전을 올린다. 전환 중에는 서비스가 중단된다.

  • 장점: 단순하다
  • 단점: 다운타임이 발생한다. 문제가 생기면 롤백에도 다운타임이 필요하다
  • 적합한 상황: 내부 도구, 다운타임이 허용되는 서비스

Rolling Deployment (롤링 배포)

서버가 여러 대일 때, 한 대씩 순서대로 새 버전으로 교체한다.

Step 1 서버 A를 새 버전으로 교체. B, C는 기존 버전 유지. 트래픽은 B, C로 분산
Step 2 A가 정상 확인되면, 서버 B를 교체. 트래픽은 A(신), C(구)로 분산
Step 3 B 정상 확인 후, 서버 C를 교체. 모든 서버가 새 버전으로 전환 완료
  • 장점: 다운타임 없음. 점진적 전환
  • 단점: 전환 중에 구/신 버전이 공존. DB 스키마 변경 시 주의 필요
  • 적합한 상황: 일반적인 웹 서비스. Kubernetes의 기본 배포 전략

Blue-Green Deployment (블루-그린 배포)

동일한 환경 두 벌을 준비한다. Blue(현재 운영)와 Green(새 버전).

Blue-Green 배포 전환
Blue (현재 운영) v1.0 현재 모든 트래픽을 처리 중
Green (대기) v2.0 새 버전 배포 완료, 내부 테스트 중
Blue (대기) v1.0 롤백 대비 유지. 문제 시 즉시 전환
Green (운영 전환) v2.0 로드 밸런서가 트래픽을 Green으로 전환

Green에서 문제가 발견되면? 로드 밸런서를 Blue로 되돌리면 끝. 롤백이 수 초 만에 가능하다.

  • 장점: 즉각적인 롤백. 다운타임 제로
  • 단점: 인프라 비용이 2배. 두 환경 모두 관리해야 함
  • 적합한 상황: 롤백 속도가 중요한 서비스, 금융/커머스

Canary Deployment (카나리 배포)

광산에서 유독 가스를 감지하기 위해 카나리아 새를 먼저 보낸 것에서 유래했다. 소수의 사용자에게만 새 버전을 노출하고, 문제가 없으면 점진적으로 비율을 늘린다.

5% 전체 트래픽의 5%만 새 버전으로 라우팅. 에러율, 응답 시간, CPU 사용률 모니터링
25% 지표가 정상이면 25%로 확대. 30분간 관찰
50% 여전히 정상이면 50%로 확대. 1시간 관찰
100% 모든 지표 정상 확인 후 전체 전환 완료
  • 장점: 위험 최소화. 실제 사용자 트래픽으로 검증
  • 단점: 구현이 복잡. 트래픽 분할 로직 필요. 모니터링 시스템 필수
  • 적합한 상황: 대규모 사용자 서비스. Netflix, Google 등이 사용

전략 요약

전략다운타임롤백 속도비용복잡도
Big Bang있음느림낮음낮음
Rolling없음중간낮음중간
Blue-Green없음매우 빠름높음 (2배)중간
Canary없음빠름중간높음

8. 실전: Next.js 앱을 GitHub Actions로 자동 배포하기

이론은 충분하다. 실제로 Next.js 앱을 GitHub Actions + Docker + AWS에 자동 배포하는 파이프라인을 만들어 보자.

전체 흐름

git push
GitHub Actions
Build & Test
Docker Image
ECR Push
ECS Deploy

Step 1: Dockerfile 작성

먼저 Next.js 앱을 Docker 이미지로 만들기 위한 Dockerfile이 필요하다.

hljs language-dockerfile
# Multi-stage build로 이미지 크기 최적화
FROM node:20-alpine AS base

# Stage 1: 의존성 설치
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: 빌드
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: 프로덕션 이미지
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

# 보안: root가 아닌 사용자로 실행
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000

CMD ["node", "server.js"]

Multi-stage build를 사용해서 최종 이미지에는 빌드 도구와 devDependencies가 포함되지 않는다. 이미지 크기가 1GB에서 150MB로 줄어든다.

Step 2: GitHub Actions 워크플로우

hljs language-yaml
# .github/workflows/deploy.yml
name: Build and Deploy

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

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: my-nextjs-app
  ECS_CLUSTER: production
  ECS_SERVICE: nextjs-service

jobs:
  # Job 1: 린팅과 타입 체크 (빠른 피드백)
  lint:
    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 lint
      - run: npx tsc --noEmit

  # Job 2: 유닛 테스트
  test:
    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 test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  # Job 3: 보안 스캔
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm audit --audit-level=high
      - uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          severity: 'CRITICAL,HIGH'

  # Job 4: 빌드 & 배포 (lint, test, security 모두 통과 후)
  deploy:
    needs: [lint, test, security]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push Docker image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster $ECS_CLUSTER \
            --service $ECS_SERVICE \
            --force-new-deployment

워크플로우 해부

이 파이프라인의 구조를 분석해 보자.

GitHub Actions 워크플로우 구조
lint 병렬 실행 ESLint + TypeScript 타입 체크
test 병렬 실행 Jest 유닛 테스트 + 커버리지
security 병렬 실행 npm audit + Trivy 취약점 스캔
deploy needs: [lint, test, security] 3개 Job 모두 통과해야 실행. Docker 빌드 → ECR 푸시 → ECS 배포

핵심 포인트:

  • lint, test, security 3개 Job이 병렬로 실행된다. 총 파이프라인 시간은 가장 느린 Job의 시간과 같다
  • deploy Job은 needs: [lint, test, security]3개 모두 통과해야 실행된다
  • if: github.ref == 'refs/heads/main' 조건으로 main 브랜치에 push할 때만 배포가 실행된다
  • PR을 올리면 lint, test, security만 돌고 배포는 안 된다 — Continuous Delivery의 전형적인 패턴

핵심 수치

~2분 lint + test + security (병렬)
~3분 Docker 빌드 + ECR 푸시
~5분 커밋 → 운영 배포 완료

FTP로 파일 올리던 시절의 불안한 30분이, 자동화된 안전한 5분으로 바뀌었다.


9. 파이프라인 최적화: 더 빠르게, 더 효율적으로

파이프라인이 느리면 개발자들이 CI/CD를 꺼린다. "빌드 기다리느니 그냥 수동으로 올리자"가 되면 모든 노력이 물거품이 된다. 파이프라인을 빠르게 만드는 실전 기법들을 알아보자.

캐싱 (Caching)

가장 효과적인 최적화. node_modules를 매번 새로 설치하는 대신, 이전 빌드의 결과를 재사용한다.

hljs language-yaml
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # package-lock.json이 변경되지 않으면 캐시 사용

package-lock.json의 해시를 키로 사용해서, 의존성이 변경되지 않았으면 npm ci가 수 초 만에 끝난다.

병렬 실행 (Parallelization)

독립적인 작업은 병렬로 실행한다. 위의 예시처럼 lint, test, security를 동시에 돌리면 총 시간이 줄어든다.

조건부 실행 (Conditional Execution)

변경된 파일에 따라 필요한 테스트만 실행한다.

hljs language-yaml
# 프론트엔드 파일이 변경됐을 때만 E2E 테스트 실행
- name: Check for frontend changes
  id: changes
  uses: dorny/paths-filter@v3
  with:
    filters: |
      frontend:
        - 'src/**'
        - 'public/**'

- name: Run E2E tests
  if: steps.changes.outputs.frontend == 'true'
  run: npx playwright test

README만 수정했는데 E2E 테스트를 10분간 돌리는 것은 자원 낭비다.

Docker 레이어 캐싱

Docker 빌드 시 변경되지 않은 레이어는 캐시에서 재사용한다. Dockerfile에서 변경이 적은 것을 위에, 변경이 잦은 것을 아래에 배치하는 것이 핵심이다.

DOCKERFILE 레이어 순서 원칙
COPY package.json → 의존성 정의 (자주 안 바뀜) ← 캐시 히트
RUN npm ci → 의존성 설치 (자주 안 바뀜) ← 캐시 히트
COPY . . → 소스 코드 전체 (자주 바뀜) ← 여기서부터 새로 빌드
RUN npm run build → 빌드 실행

package.json이 바뀌지 않았으면 npm ci까지 캐시에서 가져온다. 빌드 시간이 5분에서 1분으로 줄어들 수 있다.

최적화 효과

파이프라인 최적화 전후 비교
최적화 전
12분
캐싱 적용
7분
+ 병렬화
5분
+ 조건부
3분

10. CI/CD 문화: "아프면 더 자주 하라"

CI/CD는 도구가 아니라 문화다. 아무리 좋은 파이프라인을 만들어도 팀이 그것을 사용하지 않으면 의미가 없다.

"If it hurts, do it more frequently"

이것은 CI/CD 문화의 핵심 철학이다. 고통스러운 일은 더 자주 하라.

배포가 무섭다고 2주에 한 번 하면? 2주치 변경이 한꺼번에 올라가니까 더 무섭다. 하루에 10번 배포하면? 각 배포의 변경량이 작으니까 문제가 생겨도 원인을 찾기 쉽고, 롤백할 범위도 작다.

😰
문제: 배포가 무섭다
"배포할 때마다 뭔가 터진다. 차라리 안 하고 싶다."
🔁
해결: 더 자주 배포한다
배포 빈도를 높이면 각 배포의 변경 범위가 줄어든다. 자동 테스트가 안전망이 된다.
결과: 배포가 일상이 된다
하루에 여러 번 배포해도 두렵지 않다. "금요일 오후 배포 금지"가 사라진다.

DORA 메트릭: CI/CD 성숙도를 측정하는 4가지 지표

Google의 DORA(DevOps Research and Assessment) 팀이 6년간 수만 개 팀을 연구해서 도출한 소프트웨어 딜리버리 성과 지표다.

DORA 메트릭의미Elite 수준Low 수준
배포 빈도얼마나 자주 운영에 배포하는가하루 여러 번월 1회 미만
변경 리드 타임커밋 → 운영 반영까지 걸리는 시간1시간 미만1개월 이상
변경 실패율배포 중 장애를 일으키는 비율5% 미만46~60%
장애 복구 시간장애 발생 → 복구까지 걸리는 시간1시간 미만1주일 이상

이 4가지 지표는 서로 양의 상관관계를 가진다. 배포를 자주 하는 팀이 변경 실패율도 낮고, 복구도 빠르다. "자주 배포하면 위험하다"는 직관은 데이터로 반박된다. 자주 배포하는 팀이 오히려 더 안정적이다.

CI/CD 문화를 만드는 실천 사항

실천 1 깨진 빌드는 즉시 고친다 — CI가 빨간불이면 다른 모든 작업보다 우선. "나중에 고치겠다"는 빌드를 영원히 빨간 상태로 만든다
실천 2 작은 단위로 자주 커밋한다 — 1,000줄짜리 PR은 리뷰하기 어렵고 충돌 나기 쉽다. 100줄 이하의 작은 PR을 자주 올린다
실천 3 테스트를 작성한다 — 테스트 없는 CI/CD는 자동화된 재앙 배포. 새 기능에는 반드시 테스트를 함께 작성한다
실천 4 파이프라인을 빠르게 유지한다 — 10분 이상 걸리면 개발자들이 우회한다. 목표: 5분 이내
실천 5 모든 것을 코드로 관리한다 (Pipeline as Code) — 파이프라인 설정도 Git에 커밋한다. 리뷰 가능하고, 변경 이력이 남는다

11. 흔한 실수와 안티 패턴

CI/CD를 도입할 때 많은 팀이 빠지는 함정들이 있다. 미리 알고 피하자.

안티 패턴 1: "CI/CD 도구를 설치하면 CI/CD를 하는 것이다"

Jenkins를 설치했다고 CI를 하는 것이 아니다. 팀원들이 하루에 한 번도 메인 브랜치에 통합하지 않으면, 그건 Jenkins를 "자동 빌드 도구"로 쓰는 것이지 CI가 아니다.

안티 패턴 2: "테스트 없이 배포 자동화"

⚠️
자동 테스트 없는 자동 배포 = 자동화된 재앙. 버그를 더 빠르게 운영에 올릴 뿐이다. CI/CD의 신뢰성은 테스트의 품질에 비례한다. 파이프라인을 구축하기 전에, 최소한의 테스트부터 작성하라.

안티 패턴 3: "장기 브랜치"

feature/payment-v2 브랜치를 3주 동안 키운 후 main에 머지한다. 충돌 100개. 이것은 Continuous Integration의 정반대다. 브랜치 수명은 짧을수록 좋다. 이상적으로는 하루 이내.

안티 패턴 4: "빨간 빌드 방치"

CI가 실패한 상태에서 새 커밋을 계속 쌓는다. "나중에 한꺼번에 고치겠다." 빌드가 계속 빨간 상태면 팀이 CI를 무시하게 된다. "Broken Windows" 이론 — 깨진 창문 하나를 방치하면 빌딩 전체가 망가진다.

안티 패턴 5: "시크릿을 코드에 넣기"

hljs language-yaml
# 절대 이렇게 하지 마세요
env:
  API_KEY: "sk-1234567890abcdef"  # Git에 커밋됨 = 전 세계에 공개됨

GitHub에 커밋된 시크릿은 몇 분 내에 봇에 의해 탐지된다. AWS 키가 노출되면 수 시간 만에 수천 달러의 요금이 발생할 수 있다.


12. CI/CD 시작하기: 단계별 로드맵

"CI/CD를 도입하고 싶은데, 어디서부터 시작하지?" 한 번에 완벽한 파이프라인을 만들 필요 없다. 단계적으로 성숙도를 높여가면 된다.

Level 0 수동 배포 — FTP, SSH 접속해서 직접 파일 복사. "내 컴퓨터에서는 됐는데..." 단계
Level 1 자동 빌드 — GitHub Actions로 push 시 자동 빌드. 빌드 실패를 즉시 알 수 있다. 여기서부터 CI 시작
Level 2 자동 테스트 — 유닛 테스트, 린팅을 파이프라인에 추가. PR을 올리면 자동으로 검증. 진짜 CI
Level 3 자동 배포 (Staging) — main 머지 시 스테이징 환경에 자동 배포. QA 후 수동으로 운영 배포. Continuous Delivery 달성
Level 4 자동 배포 (Production) — 테스트 통과 시 운영까지 자동 배포. 모니터링 + 자동 롤백. Continuous Deployment 달성
Level 5 고급 전략 — 카나리 배포, 피처 플래그, A/B 테스트, GitOps. 하루 수십 회 배포. Elite 팀

오늘 당장 할 수 있는 것: GitHub 저장소에 .github/workflows/ci.yml 파일을 만들고, npm run buildnpm run lint를 자동으로 실행하게 설정한다. 10분이면 된다. 그것이 Level 1이다.

hljs language-yaml
# 최소한의 CI 파이프라인 — 10분이면 만든다
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint
      - run: npm run build

이 15줄이 당신의 CI/CD 여정의 시작이다.


마치며: 배포는 이벤트가 아니라 일상이어야 한다

이 글에서 다룬 내용을 정리하자.

CI 자주 통합 + 자동 빌드 + 자동 테스트
CD Delivery(수동 승인) vs Deployment(완전 자동)
5분 커밋에서 배포까지 목표 시간
문화 CI/CD는 도구가 아니라 팀의 관행

FTP로 파일을 올리던 시절, 배포는 이벤트였다. 날을 잡고, 체크리스트를 만들고, 긴장하고, 기도했다. "금요일 오후에는 배포하지 마라"는 규칙이 필요했다.

CI/CD가 정착된 팀에서 배포는 일상이다. 코드를 커밋하면 테스트가 돌고, 빌드가 만들어지고, 스테이징에 올라간다. 확인하고 승인하면 운영에 반영된다. 금요일이든 월요일이든, 오전이든 오후든 상관없다. 파이프라인이 안전을 보장한다.

"아프면 더 자주 하라." 배포가 무서우면 더 자주 배포하라. 통합이 어려우면 더 자주 통합하라. 테스트가 귀찮으면 더 자주 테스트하라. 빈도를 높이면 각각의 크기가 작아지고, 작아지면 쉬워지고, 쉬워지면 무섭지 않다.

오늘 GitHub Actions YAML 파일 15줄을 작성하는 것으로 시작하자. 그것이 "금요일 오후 배포 공포"에서 벗어나는 첫걸음이다.