
이번 시간에는 프로젝트의 CI/CD 파이프라인 전체를 완성했습니다. 단위 테스트 코드 커버리지 측정부터 빌드·패키징, 스테이징 환경 배포, 인수 테스트까지 지속적 인도(CD)의 전 과정을 순서대로 진행했습니다.
지금까지 개별적으로 구성했던 요소들을 하나의 파이프라인으로 연결합니다.
코드 Push (main 브랜치)
│
├─ 1. 단위 테스트 + 코드 커버리지 측정
│
├─ 2. 빌드 및 패키징 (도커 이미지 생성 → ECR 푸시)
│
├─ 3. 스테이징 환경 배포
│
├─ 4. 인수 테스트 (E2E 테스트 자동 실행)
│
└─ 5. 프로덕션 배포 (인수 테스트 통과 시)
코드 커버리지(Code Coverage) 는 전체 코드 중 테스트가 실행된 코드의 비율을 나타내는 지표입니다. 커버리지가 높을수록 테스트가 코드의 더 많은 부분을 검증한다는 의미이지만, 100%가 목표는 아닙니다. 중요한 비즈니스 로직 위주로 의미 있는 커버리지를 확보하는 것이 핵심입니다.
// 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
// 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/'],
};
커버리지가 설정한 임계값(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
스테이징 배포가 완료되면 이전에 작성한 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