[week19] 프로젝트 : 오픈소스 기반의 웹 파이프라인 구축 (14) - 05/18

Kyulee·2026년 5월 18일

TIL 

목록 보기
90/93
post-thumbnail

이번 시간에는 프로젝트의 CI/CD 파이프라인 전체를 완성했습니다. 단위 테스트 코드 커버리지 측정부터 빌드·패키징, 스테이징 환경 배포, 인수 테스트까지 지속적 인도(CD)의 전 과정을 순서대로 진행했습니다.


CI/CD 파이프라인 설계

전체 파이프라인 흐름

지금까지 개별적으로 구성했던 요소들을 하나의 파이프라인으로 연결합니다.

코드 Push (main 브랜치)
  │
  ├─ 1. 단위 테스트 + 코드 커버리지 측정
  │
  ├─ 2. 빌드 및 패키징 (도커 이미지 생성 → ECR 푸시)
  │
  ├─ 3. 스테이징 환경 배포
  │
  ├─ 4. 인수 테스트 (E2E 테스트 자동 실행)
  │
  └─ 5. 프로덕션 배포 (인수 테스트 통과 시)

파이프라인 설계 원칙

  • 빠른 피드백 — 실패 시 최대한 이른 단계에서 중단하여 불필요한 리소스 낭비를 막습니다.
  • 한 번만 빌드 — 동일한 이미지가 스테이징과 프로덕션 모두에서 사용되어야 합니다.
  • 자동화 — 모든 단계는 사람의 개입 없이 자동으로 실행되어야 합니다.

단위 테스트 및 코드 커버리지

코드 커버리지란?

코드 커버리지(Code Coverage) 는 전체 코드 중 테스트가 실행된 코드의 비율을 나타내는 지표입니다. 커버리지가 높을수록 테스트가 코드의 더 많은 부분을 검증한다는 의미이지만, 100%가 목표는 아닙니다. 중요한 비즈니스 로직 위주로 의미 있는 커버리지를 확보하는 것이 핵심입니다.

FE 커버리지 측정 (Vitest)

// vitest.config.ts
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: ['node_modules/', 'src/main.tsx'],
      thresholds: {
        lines: 70,
        functions: 70,
        branches: 70,
      },
    },
  },
});
# 커버리지 리포트 생성
npm run test -- --coverage

BE 커버리지 측정 (Jest)

// jest.config.ts
export default {
  preset: 'ts-jest',
  collectCoverage: true,
  coverageReporters: ['text', 'html', 'lcov'],
  coverageThreshold: {
    global: {
      lines: 70,
      functions: 70,
      branches: 70,
    },
  },
  coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
};

GitHub Actions에 커버리지 적용

커버리지가 설정한 임계값(70%) 미만이면 파이프라인이 자동으로 실패하도록 설정합니다.

- name: FE 단위 테스트 및 커버리지
  run: |
    cd frontend
    npm ci
    npm run test -- --coverage
  # thresholds 미달 시 자동으로 exit code 1 반환 → 파이프라인 중단

- name: BE 단위 테스트 및 커버리지
  run: |
    cd backend
    npm ci
    npm test

빌드 및 패키징

빌드 버전 관리

어떤 버전이 어느 환경에 배포되어 있는지 추적하기 위해 Git 커밋 SHA 를 이미지 태그로 사용합니다.

- name: 도커 이미지 빌드 및 ECR 푸시
  env:
    ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
    IMAGE_TAG: ${{ github.sha }}
  run: |
    # BE 이미지 빌드
    docker build -t $ECR_REGISTRY/document-editor-backend:$IMAGE_TAG ./backend
    docker push $ECR_REGISTRY/document-editor-backend:$IMAGE_TAG

    # FE 이미지 빌드
    docker build -t $ECR_REGISTRY/document-editor-frontend:$IMAGE_TAG ./frontend
    docker push $ECR_REGISTRY/document-editor-frontend:$IMAGE_TAG

    # latest 태그도 함께 푸시
    docker tag $ECR_REGISTRY/document-editor-backend:$IMAGE_TAG \
               $ECR_REGISTRY/document-editor-backend:latest
    docker push $ECR_REGISTRY/document-editor-backend:latest

멀티 스테이지 빌드 최적화

이미지 크기를 최소화하기 위해 빌드 스테이지와 실행 스테이지를 분리합니다.

# BE Dockerfile (최적화 버전)
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
EXPOSE 8080
CMD ["node", "dist/index.js"]

스테이징 배포

스테이징 환경이란?

스테이징 환경 은 프로덕션 환경과 동일하게 구성된 테스트 환경입니다. 실제 서비스에 배포하기 전 최종 검증을 수행하는 단계로, 여기서 문제가 발견되면 프로덕션 배포를 차단합니다.

스테이징 네임스페이스 분리

쿠버네티스 네임스페이스 를 활용해 스테이징과 프로덕션 환경을 논리적으로 분리합니다.

# 네임스페이스 생성
kubectl create namespace staging
kubectl create namespace production

스테이징 배포 스크립트

- name: 스테이징 환경에 배포
  env:
    IMAGE_TAG: ${{ github.sha }}
  run: |
    # kubeconfig 업데이트
    aws eks update-kubeconfig --name document-editor

    # 스테이징 네임스페이스에 이미지 업데이트
    kubectl set image deployment/backend \
      backend=$ECR_REGISTRY/document-editor-backend:$IMAGE_TAG \
      -n staging

    kubectl set image deployment/frontend \
      frontend=$ECR_REGISTRY/document-editor-frontend:$IMAGE_TAG \
      -n staging

    # 배포 완료까지 대기
    kubectl rollout status deployment/backend -n staging --timeout=300s
    kubectl rollout status deployment/frontend -n staging --timeout=300s

스테이징 환경 변수 관리

스테이징과 프로덕션은 데이터베이스 등 일부 설정이 다르기 때문에, 환경별로 별도의 Secret을 관리합니다.

# 스테이징 시크릿
kubectl create secret generic app-secret \
  --from-literal=db-host=<스테이징 RDS 엔드포인트> \
  --from-literal=jwt-secret=<스테이징 JWT 시크릿> \
  -n staging

# 프로덕션 시크릿
kubectl create secret generic app-secret \
  --from-literal=db-host=<프로덕션 RDS 엔드포인트> \
  --from-literal=jwt-secret=<프로덕션 JWT 시크릿> \
  -n production

인수 테스트

스테이징 환경에서 E2E 테스트 자동 실행

스테이징 배포가 완료되면 이전에 작성한 Selenium E2E 테스트를 스테이징 URL로 자동 실행합니다.

- name: 인수 테스트 실행
  env:
    BASE_URL: https://staging.document-editor.example.com
  run: |
    pip install selenium pytest webdriver-manager
    pytest tests/e2e/ -v --tb=short

인수 테스트 시나리오

# tests/e2e/test_acceptance.py

class TestUserJourney:
    """핵심 사용자 여정 인수 테스트"""

    def test_signup_and_login(self, driver, base_url):
        """회원가입 후 로그인까지의 전체 흐름"""
        # 회원가입
        signup_page = SignupPage(driver, base_url)
        signup_page.signup("newuser@test.com", "Password123!")

        # 로그인 페이지로 이동 확인
        assert "login" in driver.current_url

        # 로그인
        login_page = LoginPage(driver, base_url)
        login_page.login("newuser@test.com", "Password123!")

        # 홈 화면으로 이동 확인
        assert driver.current_url == f"{base_url}/"

    def test_create_and_edit_document(self, driver, base_url):
        """문서 생성 및 편집 전체 흐름"""
        login(driver, base_url)

        # 새 문서 생성
        editor_page = EditorPage(driver, base_url)
        editor_page.create_new_document()
        editor_page.set_title("인수 테스트 문서")
        editor_page.set_content("# 테스트\n\n인수 테스트 내용입니다.")

        # 저장 확인
        editor_page.save()
        assert editor_page.is_saved()

        # 문서 목록에서 확인
        home_page = HomePage(driver, base_url)
        home_page.navigate()
        assert "인수 테스트 문서" in home_page.get_document_titles()

인수 테스트 실패 시 처리

인수 테스트가 실패하면 스테이징 환경을 이전 버전으로 자동 롤백하고 슬랙 등으로 알림을 발송합니다.

- name: 인수 테스트 실패 시 롤백
  if: failure()
  run: |
    kubectl rollout undo deployment/backend -n staging
    kubectl rollout undo deployment/frontend -n staging

- name: 슬랙 알림 발송
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "❌ 인수 테스트 실패 — 스테이징 환경을 롤백했습니다.\n빌드: ${{ github.sha }}"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

인수 테스트 통과 후 프로덕션 배포

인수 테스트를 모두 통과하면 동일한 이미지를 프로덕션 네임스페이스에 배포합니다.

- name: 프로덕션 배포
  if: success()
  env:
    IMAGE_TAG: ${{ github.sha }}
  run: |
    kubectl set image deployment/backend \
      backend=$ECR_REGISTRY/document-editor-backend:$IMAGE_TAG \
      -n production

    kubectl rollout status deployment/backend -n production --timeout=300s
profile
안녕하세요 매일의 배움을 기록으로 자산화하는 개발자 이규현입니다 😊

0개의 댓글