지속적 통합(CI)에 테스트 통합
GitHub Actions에서 설치·빌드·테스트·커버리지 검사를 자동화하고 실패 증거를 남기는 CI 흐름을 구성합니다.
앞 글에서는 테스트 커버리지를 측정하고 이를 코드 품질 관리에 활용하는 방법을 알아보았습니다. 8장의 마지막인 이 글에서는 지속적 통합(CI, Continuous Integration)에 테스트를 통합하는 방법을 살펴봅니다.
지속적 통합은 팀의 모든 구성원이 작업한 코드를 자주 메인 브랜치에 병합하고, 병합된 코드를 자동으로 빌드하고 테스트하는 과정입니다. 테스트를 CI 파이프라인에 넣어 두면 사람이 잊어버려도 모든 변경이 같은 검사를 거치게 됩니다.
지속적 통합이란
지속적 통합은 개발자들이 작업한 코드를 하루에도 여러 번 Git 같은 공유 저장소에 병합하고, 병합된 코드에 대해 자동으로 빌드와 테스트를 실행하는 개발 방식입니다.
CI가 목표로 하는 것은 다음과 같습니다.
- 통합 문제의 조기 발견: 코드를 자주 합치고 테스트하므로, 합치는 과정에서 생기는 문제를 일찍 발견하고 고칠 수 있습니다.
- 회귀 버그 감소: 자동화된 테스트로 새 기능이 기존 기능에 미치는 부작용을 빠르게 찾아냅니다.
- 품질 유지: 늘 테스트를 통과하고 빌드할 수 있는 상태의 코드를 유지해 품질을 지속적으로 관리합니다.
- 배포 준비 상태 유지: 언제든 배포할 수 있는 안정된 코드를 유지하며, 이는 지속적 배포(CD)의 전제 조건입니다.
- 개발자 생산성 향상: 수동 테스트와 통합에 드는 시간을 줄여 더 중요한 작업에 집중할 수 있게 합니다.
CI 파이프라인은 보통 다음 단계로 이루어집니다.
- 코드 커밋: 개발자가 변경 사항을 GitHub, GitLab, Bitbucket 같은 버전 관리 시스템에 푸시하거나 풀 리퀘스트를 만듭니다.
- 빌드 트리거: 변경이 감지되면 Jenkins, GitHub Actions, GitLab CI, CircleCI 같은 CI 도구가 자동으로 작업을 시작합니다.
- 코드 가져오기: CI 서버가 최신 코드를 가져옵니다.
- 의존성 설치:
npm ci처럼 프로젝트에 필요한 패키지를 설치합니다. - 빌드: TypeScript를 JavaScript로 변환하는 것처럼 소스 코드를 실행할 수 있는 형태로 만듭니다.
- 테스트 실행: 단위 테스트, 통합 테스트, E2E 테스트 같은 자동화된 테스트를 실행합니다.
- 커버리지 측정: 테스트 커버리지를 측정하고 임계값을 채우는지 확인합니다.
- 결과 보고: 빌드와 테스트 결과를 개발자, 팀, Slack 같은 채널에 알리며, 실패하면 알림을 보냅니다.
- 결과물 생성: 빌드와 테스트가 성공하면 Docker 이미지나 압축된 실행 파일 같은 배포용 결과물(artifact)을 만듭니다.
GitHub Actions로 CI 구성하기
CI 도구는 다양하지만, 여기서는 GitHub 저장소에 코드를 푸시할 때 자동으로 파이프라인을 실행하는 GitHub Actions를 예로 들어 NestJS 테스트를 통합합니다. GitHub 계정이 있고, NestJS 프로젝트가 GitHub 저장소에 푸시되어 있어야 합니다.
GitHub 저장소의 .github/workflows 폴더 안에 확장자가 .yml이나 .yaml인 워크플로 파일을 만듭니다.
name: NestJS CI Pipeline # 워크플로 이름
on: # 워크플로가 실행될 조건
push: # main 브랜치에 코드가 푸시될 때
branches:
- main
pull_request: # main 브랜치로 풀 리퀘스트가 만들어지거나 갱신될 때
branches:
- main
jobs: # 실행할 작업들
build-and-test: # 작업 이름
permissions:
contents: read # checkout에 필요한 최소 읽기 권한
runs-on: ubuntu-latest # 작업을 실행할 GitHub 제공 가상 머신
strategy:
matrix: # 여러 Node.js 버전에서 같은 작업을 각각 실행
node-version: [24.x, 26.x] # NestJS 12의 Jest 실행 조건(24.9 이상)을 채우는 두 LTS
steps: # 작업 안에서 차례로 실행할 단계들
- name: Checkout Repository # 1. 저장소 코드 가져오기
uses: actions/checkout@v7
- name: Use Node.js ${{ matrix.node-version }} # 2. Node.js 환경 설정
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # npm 전역 다운로드 캐시 재사용
- name: Install Dependencies # 3. 의존성 설치
run: npm ci # package-lock.json 그대로 깨끗하게 설치
- name: Build Project # 4. 프로젝트 빌드
run: npm run build
- name: Run Unit Tests # 5. 단위 테스트 실행
run: npm run test
- name: Run E2E Tests # 6. E2E 테스트 실행 (DB가 필요하면 이 앞에 DB 준비 단계 추가)
run: npm run test:e2e
- name: Check Test Coverage # 7. 커버리지 확인 (임계값 미달 시 실패)
run: npm run test:cov
# 선택 사항: 커버리지 HTML 리포트를 결과물로 저장
- name: Upload Test Coverage Report
uses: actions/upload-artifact@v7
if: always() # 앞 단계의 실패와 상관없이 업로드를 시도
with:
name: test-coverage-report-${{ matrix.node-version }}
path: coverage/lcov-report # Jest 커버리지 HTML 리포트 경로
if-no-files-found: warn # 앞 단계에서 멈춰 리포트가 없으면 경고로 남김NestJS CI의 실행 게이트와 실패 증거
한 matrix job의 준비·install/build/test·coverage·실패 시 always 기존 증거 업로드로 job 상태가 변한다.
Nest · Configured CI workflow
Node 버전별 CI 실행 순서
각 Node 버전은 별도 job으로 실행됩니다. 한 job의 스텝은 순차 실행되며, 앞 검사가 실패하면 일반 스텝은 중단되고 증거 업로드를 시도합니다.
한 matrix job이 같은 순서와 권한으로 판정한다
트리거와 최소 권한
main의 push·pull request가 시작하고, job 토큰은contents: read만 갖습니다.런타임과 결정적 설치
Node
22.x·24.x에서 checkout@v7 setup-node@v7을 사용하고npm ci를 실행합니다. npm cache는 잠금 파일 해시를 키로 전역 다운로드 캐시를 재사용하며node_modules를 보존하지 않습니다.빌드와 빠른 로직 검사
npm run build와npm run test가 실패하면 뒤의 일반 스텝은 기본적으로 건너뜁니다.실제 흐름과 회귀 gate
npm run test:e2e뒤에npm run test:cov가 Jest 임계값을 판정합니다. 두 스크립트의 테스트 범위는 프로젝트 설정이 정합니다.조건부 증거 보존
if: always()인upload-artifact@v7이 버전별 고유 이름으로 실행되지만, coverage HTML 디렉터리가 실제로 생성된 경우에만 결과를 올릴 수 있습니다.
| 검사 | 통과 조건 | 현재 예시의 증거 경계 |
|---|---|---|
| Build | TypeScript 빌드 명령이 종료 코드 0 | Actions step log를 남기며 배포 이미지나 실행 파일은 만들지 않습니다. |
| Unit | npm run test가 전부 통과 | Jest 출력과 stack은 Actions log에 남습니다. |
| E2E | npm run test:e2e가 전부 통과 | 기본 log만 남습니다. 요청·응답이나 DB seed 파일은 reporter와 upload 경로를 따로 구성해야 합니다. |
| Coverage | test:cov가 합의한 threshold 이상 | 실행이 리포트를 만들었다면 Node 버전별 coverage HTML을 업로드합니다. |
컴파일 성공을 판정한다
- 통과 조건
- TypeScript 빌드 명령이 종료 코드 0
- 현재 예시의 증거 경계
- Actions step log를 남기며 배포 이미지나 실행 파일은 만들지 않습니다.
빠른 로직 실패를 먼저 잡는다
- 통과 조건
npm run test가 전부 통과- 현재 예시의 증거 경계
- Jest 출력과 stack은 Actions log에 남습니다.
핵심 HTTP 흐름을 실행한다
- 통과 조건
npm run test:e2e가 전부 통과- 현재 예시의 증거 경계
- 기본 log만 남습니다. 요청·응답이나 DB seed 파일은 reporter와 upload 경로를 따로 구성해야 합니다.
임계값과 HTML을 연결한다
- 통과 조건
test:cov가 합의한 threshold 이상- 현재 예시의 증거 경계
- 실행이 리포트를 만들었다면 Node 버전별 coverage HTML을 업로드합니다.
if: always()는 앞 단계가 실패해도 업로드를 시도하지만, 앞 단계가 만들지 못한 파일을 생성하지 않습니다. 로그·스크린샷·테스트 결과·seed가 필요한 팀은 각 도구가 파일을 쓰게 한 뒤 모든 경로를 명시적으로 업로드해야 합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- Node 22와 24는 별도 matrix job이다. 이 그림은 그중 한 job의 순서다. checkout과 런타임 준비 뒤 npm ci, build, unit, E2E, coverage를 성공한 경우에만 다음 일반 단계로 진행한다. 중간 실패는 남은 일반 단계를 건너뛰어 if always 업로드 시도로 이어지고, coverage 실패도 이미 생성된 파일이 있으면 업로드를 시도한다. 업로드는 기존 파일만 보존하므로 앞서 실패해 리포트가 없으면 warn이며 새 리포트를 생성하지 않는다. 점선은 실패 후의 증거 경로이고 성공으로 복구하는 경로가 아니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | NestJS CI의 실행 게이트와 실패 증거 |
| 58 | Node 22와 24는 별도 matrix job이다. 이 그림은 그중 한 job의 순서다. checkout과 런타임 준비 뒤 npm ci, build, unit, E2E, coverage를 성공한 경우에만 다음 일반 단계로 진행한다. 중간 실패는 남은 일반 단계를 건너뛰어 if always 업로드 시도로 이어지고, coverage 실패도 이미 생성된 파일이 있으면 업로드를 시도한다. 업로드는 기존 파일만 보존하므로 앞서 실패해 리포트가 없으면 warn이며 새 리포트를 생성하지 않는다. 점선은 실패 후의 증거 경로이고 성공으로 복구하는 경로가 아니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 한 matrix job 준비 Node 22 또는 24를 사용하는 하나의 job에서 checkout·runtime·npm ci를 실행합니다. | job: 독립 matrix · 설치: npm ci | 한 실행 환경의 입력이 준비됩니다. |
| 성공 gate 진행 build·unit·E2E·coverage는 앞 단계가 성공했을 때만 진행합니다. | 성공: 다음 일반 단계 · 실패: 나머지 생략 | 실패 지점이 다음 실행을 막습니다. |
| always 증거 업로드 성공·실패 여부와 관계없이 기존 파일의 업로드만 시도합니다. | 파일 있음: 보존 시도 · 없음: warn · 새 리포트 생성: 없음 | 실패 후 관측 자료만 보존합니다. |
NestJS CI의 실행 게이트와 실패 증거
한 matrix job의 준비·install/build/test·coverage·실패 시 always 기존 증거 업로드로 job 상태가 변한다.
Nest · Configured CI workflow
Node 버전별 CI 실행 순서
각 Node 버전은 별도 job으로 실행됩니다. 한 job의 스텝은 순차 실행되며, 앞 검사가 실패하면 일반 스텝은 중단되고 증거 업로드를 시도합니다.
한 matrix job이 같은 순서와 권한으로 판정한다
트리거와 최소 권한
main의 push·pull request가 시작하고, job 토큰은contents: read만 갖습니다.런타임과 결정적 설치
Node
22.x·24.x에서 checkout@v7 setup-node@v7을 사용하고npm ci를 실행합니다. npm cache는 잠금 파일 해시를 키로 전역 다운로드 캐시를 재사용하며node_modules를 보존하지 않습니다.빌드와 빠른 로직 검사
npm run build와npm run test가 실패하면 뒤의 일반 스텝은 기본적으로 건너뜁니다.실제 흐름과 회귀 gate
npm run test:e2e뒤에npm run test:cov가 Jest 임계값을 판정합니다. 두 스크립트의 테스트 범위는 프로젝트 설정이 정합니다.조건부 증거 보존
if: always()인upload-artifact@v7이 버전별 고유 이름으로 실행되지만, coverage HTML 디렉터리가 실제로 생성된 경우에만 결과를 올릴 수 있습니다.
| 검사 | 통과 조건 | 현재 예시의 증거 경계 |
|---|---|---|
| Build | TypeScript 빌드 명령이 종료 코드 0 | Actions step log를 남기며 배포 이미지나 실행 파일은 만들지 않습니다. |
| Unit | npm run test가 전부 통과 | Jest 출력과 stack은 Actions log에 남습니다. |
| E2E | npm run test:e2e가 전부 통과 | 기본 log만 남습니다. 요청·응답이나 DB seed 파일은 reporter와 upload 경로를 따로 구성해야 합니다. |
| Coverage | test:cov가 합의한 threshold 이상 | 실행이 리포트를 만들었다면 Node 버전별 coverage HTML을 업로드합니다. |
컴파일 성공을 판정한다
- 통과 조건
- TypeScript 빌드 명령이 종료 코드 0
- 현재 예시의 증거 경계
- Actions step log를 남기며 배포 이미지나 실행 파일은 만들지 않습니다.
빠른 로직 실패를 먼저 잡는다
- 통과 조건
npm run test가 전부 통과- 현재 예시의 증거 경계
- Jest 출력과 stack은 Actions log에 남습니다.
핵심 HTTP 흐름을 실행한다
- 통과 조건
npm run test:e2e가 전부 통과- 현재 예시의 증거 경계
- 기본 log만 남습니다. 요청·응답이나 DB seed 파일은 reporter와 upload 경로를 따로 구성해야 합니다.
임계값과 HTML을 연결한다
- 통과 조건
test:cov가 합의한 threshold 이상- 현재 예시의 증거 경계
- 실행이 리포트를 만들었다면 Node 버전별 coverage HTML을 업로드합니다.
if: always()는 앞 단계가 실패해도 업로드를 시도하지만, 앞 단계가 만들지 못한 파일을 생성하지 않습니다. 로그·스크린샷·테스트 결과·seed가 필요한 팀은 각 도구가 파일을 쓰게 한 뒤 모든 경로를 명시적으로 업로드해야 합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- Node 22와 24는 별도 matrix job이다. 이 그림은 그중 한 job의 순서다. checkout과 런타임 준비 뒤 npm ci, build, unit, E2E, coverage를 성공한 경우에만 다음 일반 단계로 진행한다. 중간 실패는 남은 일반 단계를 건너뛰어 if always 업로드 시도로 이어지고, coverage 실패도 이미 생성된 파일이 있으면 업로드를 시도한다. 업로드는 기존 파일만 보존하므로 앞서 실패해 리포트가 없으면 warn이며 새 리포트를 생성하지 않는다. 점선은 실패 후의 증거 경로이고 성공으로 복구하는 경로가 아니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | NestJS CI의 실행 게이트와 실패 증거 |
| 58 | Node 22와 24는 별도 matrix job이다. 이 그림은 그중 한 job의 순서다. checkout과 런타임 준비 뒤 npm ci, build, unit, E2E, coverage를 성공한 경우에만 다음 일반 단계로 진행한다. 중간 실패는 남은 일반 단계를 건너뛰어 if always 업로드 시도로 이어지고, coverage 실패도 이미 생성된 파일이 있으면 업로드를 시도한다. 업로드는 기존 파일만 보존하므로 앞서 실패해 리포트가 없으면 warn이며 새 리포트를 생성하지 않는다. 점선은 실패 후의 증거 경로이고 성공으로 복구하는 경로가 아니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 한 matrix job 준비 Node 22 또는 24를 사용하는 하나의 job에서 checkout·runtime·npm ci를 실행합니다. | job: 독립 matrix · 설치: npm ci | 한 실행 환경의 입력이 준비됩니다. |
| 성공 gate 진행 build·unit·E2E·coverage는 앞 단계가 성공했을 때만 진행합니다. | 성공: 다음 일반 단계 · 실패: 나머지 생략 | 실패 지점이 다음 실행을 막습니다. |
| always 증거 업로드 성공·실패 여부와 관계없이 기존 파일의 업로드만 시도합니다. | 파일 있음: 보존 시도 · 없음: warn · 새 리포트 생성: 없음 | 실패 후 관측 자료만 보존합니다. |
위 그림은 matrix의 Node 버전마다 만들어지는 job 하나 안에서 단계가 진행되는 순서를 보여 줍니다. checkout과 런타임 준비, npm ci 뒤에 빌드, 단위 테스트, E2E 테스트, 커버리지 검사가 차례로 실행되고, 앞 단계가 성공했을 때만 다음 일반 단계로 넘어갑니다. 중간에 실패하면 남은 일반 단계는 건너뛰고 if: always()가 붙은 업로드만 시도하며, 이 업로드는 이미 만들어진 파일만 저장할 뿐 새 리포트를 만들지는 않습니다. 그림은 이전 판의 Node 22·24 조합으로 그려져 있지만, NestJS 12에서는 Jest가 ESM 전용 패키지를 불러오려면 Node.js 24.9 이상이 필요하므로 이 글의 YAML은 24와 26을 씁니다. 순서와 실패 처리 방식은 그림과 같습니다.
워크플로 파일의 주요 부분은 다음과 같습니다.
name: GitHub Actions 화면에 표시될 워크플로 이름입니다.on: 워크플로가 언제 실행될지 정합니다. 여기서는main브랜치에push나pull_request가 발생할 때 실행됩니다.jobs: 실행할 작업을 하나 이상 정의합니다.permissions: contents: read: 작업이 쓰는 토큰의 권한을 checkout에 필요한 저장소 읽기로 제한합니다.runs-on: ubuntu-latest: 작업을 실행할 운영체제 환경입니다.strategy.matrix: 여러 Node.js 버전에서 같은 작업을 병렬로 실행해 호환성을 확인합니다. 1장에서 확인했듯 NestJS 12 애플리케이션 자체는 Node.js 20.19 이상에서 실행되지만, Jest로 테스트하려면 24.9 이상이 필요하므로 24와 26을 고릅니다.actions/checkout@v7: 저장소 코드를 워크플로 환경으로 가져옵니다.actions/setup-node@v7: 지정한 Node.js 버전을 설치합니다.cache: 'npm'은 lockfile의 해시를 키로 npm의 전역 다운로드 캐시를 재사용하며,node_modules를 보존하지는 않습니다.npm ci:package-lock.json에 적힌 정확한 버전의 의존성을 설치해 실행할 때마다 같은 조합을 보장합니다.npm run build:nest build로 애플리케이션을 빌드합니다.npm run test: 단위 테스트를 실행합니다.npm run test:e2e: E2E 테스트를 실행합니다. E2E 테스트에 실제 데이터베이스가 필요하다면, 이 단계 앞에 GitHub Actions의 서비스 컨테이너나docker compose up -d같은 단계를 추가해 데이터베이스를 띄워야 합니다.npm run test:cov: 커버리지를 측정하고jest.config.ts의 임계값을 채우는지 확인합니다. 임계값에 못 미치면 이 단계가 실패합니다.actions/upload-artifact@v7: matrix의 Node 버전을 이름에 넣어, 각 실행의 커버리지 HTML을 서로 다른 결과물로 저장합니다.if: always()는 앞 단계가 실패해도 업로드를 시도하게 하지만, 그 전에 커버리지 폴더가 만들어지지 않았다면 올릴 파일이 없습니다. 이 예시는if-no-files-found: warn으로 그 상황을 경고로 남기며, 단위·E2E 테스트의 실패 stack은 Actions 로그에 남습니다. 요청·응답 로그, 스크린샷, DB seed 같은 파일이 필요하다면 테스트 reporter가 파일을 만들게 하고 그 경로를 따로 지정해야 합니다.
matrix의 각 Node 버전은 별도 job으로 실행되지만, 한 job 안의 빌드, 단위 테스트, E2E 테스트, 커버리지 단계는 차례로 실행됩니다. 기본 설정에서는 한 단계가 실패하면 뒤의 일반 단계는 건너뛰고, if: always()가 붙은 업로드만 마지막에 시도됩니다. 또 test:cov는 같은 단위 테스트를 커버리지 모드로 다시 실행하므로 npm run test와 겹칩니다. 빠른 검사와 커버리지 검사를 별도 job으로 나눌지, test:cov 하나로 합칠지는 팀이 명시적으로 정해야 합니다.
.github/workflows/ci.yml을 만들어 GitHub 저장소에 푸시하면 GitHub Actions가 워크플로를 감지해 실행을 시작합니다. 저장소의 Actions 탭에서 실행 상태와 결과를 확인할 수 있습니다.
더 깊이: CI가 npm install 대신 npm ci를 쓰는 이유
npm 11 문서는 npm ci가 npm install과 다른 점을 다섯 가지로 정리합니다. 프로젝트에 package-lock.json이나 npm-shrinkwrap.json이 반드시 있어야 하고, lockfile의 의존성이 package.json과 맞지 않으면 lockfile을 고치는 대신 오류로 끝납니다. 패키지 하나를 골라 추가할 수 없고 프로젝트 전체만 설치하며, node_modules가 이미 있으면 설치 전에 지웁니다. 그리고 package.json과 lockfile에 아무것도 쓰지 않습니다.
이 성질들이 CI에 필요한 것들입니다. 누군가 package.json만 고치고 lockfile을 커밋하지 않았다면 npm install은 lockfile을 고쳐 가며 설치를 이어 가지만, npm ci는 그 불일치를 실패로 드러냅니다. 또 설치 전에 node_modules를 지우므로 이전 실행에서 남은 패키지가 결과에 섞이지 않습니다. 같은 이유로 node_modules를 캐시해도 npm ci가 지워 버리므로, actions/setup-node의 cache: 'npm'이 node_modules 대신 npm의 다운로드 캐시를 보존하는 방식과 잘 맞습니다.
CI 파이프라인 최적화와 고려사항
CI 파이프라인에 테스트를 통합할 때는 다음 사항을 고려해 최적화할 수 있습니다.
Nest · CI operating controls
CI 속도와 실패 재현의 균형
실행 비용을 줄이면서 같은 조건에서 실패를 설명할 수 있게 합니다. 아래 운영 선택은 팀이 필요에 따라 추가하는 기준이며, 앞의 최소 YAML에 모두 구현되어 있지는 않습니다.
빠른 검사를 앞세우되 전체 gate를 잃지 않는다
Fast fail. 빌드·단위 테스트처럼 싼 실패를 먼저 실행합니다.
Parallelism. Jest worker와 별도 job을 조절할 때 공유 DB·cache 충돌 여부를 함께 봅니다.
Cache scope. setup-node의 npm cache는 다운로드를 줄일 뿐 node_modules나 빌드 결과를 복원하지 않습니다.
Selection. --onlyChanged는 빠른 보조 신호로 쓰고, 병합 gate에는 합의한 전체 범위를 유지합니다.
테스트마다 상태와 신뢰 경계를 다시 세운다
Isolation. DB row, cache, 파일과 timer 상태가 테스트 사이에 새지 않게 정리합니다.
Database. SQLite, Testcontainers, 전용 test DB 중 선택한 저장소와 seed·cleanup 정책을 고정합니다.
Secrets. API key와 DB URL은 CI secret으로 주입하고 로그에 값을 출력하지 않습니다.
Real gates. lint·정적 분석·추가 typecheck는 실제 workflow step을 추가했을 때만 gate라고 부릅니다.
| 운영 결정 | 안전한 기준 | 남겨야 할 근거 |
|---|---|---|
| Fast fail | 설치·빌드·단위 테스트 실패 뒤의 비싼 일반 스텝을 멈춥니다. | 최초 실패 step, 명령, 종료 코드, Node 버전 |
| Retry | 알려진 네트워크·외부 sandbox 불안정에만 횟수와 대상을 제한합니다. | 첫 실패와 각 재시도 결과를 모두 보존 |
| Evidence | if: always()와 실제 생성 파일 경로를 함께 설정합니다. | coverage, reporter 결과, E2E log, 필요 시 screenshot·seed |
| Owner | 실패를 제품 코드, 테스트 코드, 실행 환경으로 분류하고 담당자를 정합니다. | 분류 이유, 재현 조건, 다음 조치 |
싼 실패에서 멈춘다
- 안전한 기준
- 설치·빌드·단위 테스트 실패 뒤의 비싼 일반 스텝을 멈춥니다.
- 남겨야 할 근거
- 최초 실패 step, 명령, 종료 코드, Node 버전
원인이 알려진 외부 불안정만 제한 재시도한다
- 안전한 기준
- 알려진 네트워크·외부 sandbox 불안정에만 횟수와 대상을 제한합니다.
- 남겨야 할 근거
- 첫 실패와 각 재시도 결과를 모두 보존
실제 파일 경로를 업로드한다
- 안전한 기준
if: always()와 실제 생성 파일 경로를 함께 설정합니다.- 남겨야 할 근거
- coverage, reporter 결과, E2E log, 필요 시 screenshot·seed
코드·테스트·환경으로 분류한다
- 안전한 기준
- 실패를 제품 코드, 테스트 코드, 실행 환경으로 분류하고 담당자를 정합니다.
- 남겨야 할 근거
- 분류 이유, 재현 조건, 다음 조치
재실행은 실패를 없애는 도구가 아니라 불안정 원인을 좁히는 도구입니다. 선택 실행·캐시·병렬화로 번 시간을 격리, 증거 보존, 원인 분류에 다시 투자해야 CI가 팀의 피드백 속도를 실제로 높입니다.
위 그림의 위쪽 두 칸은 피드백 속도를 높이는 방법과 실패를 재현할 수 있게 지키는 방법을 나란히 둡니다. 싼 실패를 먼저 잡고, 병렬화와 캐시, 변경 기반 실행으로 시간을 줄이되, 테스트마다 상태를 격리하고 비밀 값은 CI secret으로 주입합니다. 아래 표는 빠른 중단, 재시도, 기록 보존, 담당자 지정이라는 네 가지 운영 결정마다 안전한 기준과 남겨야 할 기록을 정리합니다. 이 그림의 내용은 팀이 필요에 따라 추가하는 기준이며, 앞의 최소 YAML에 모두 들어 있지는 않습니다.
- 병렬 실행: Jest의
--runInBand옵션을 빼거나maxWorkers설정을 조절해 테스트를 병렬로 실행합니다. 이때 공유 DB나 캐시가 충돌하지 않는지 함께 확인합니다. - 캐싱:
actions/setup-node의cache: 'npm'은 lockfile 해시를 기준으로 npm의 전역 다운로드 캐시만 재사용합니다.node_modules나 빌드 결과는 캐시하지 않으므로, 빌드 캐시가 필요하다면 입력 키와 복원 범위를 따로 설계합니다. - 테스트 격리: 각 테스트는 독립적으로 실행되어야 하며, 앞 테스트의 상태에 영향을 받지 않도록 환경을 격리합니다.
- 데이터베이스: E2E 목적에 맞는 DB를 격리해 준비합니다. SQLite를 쓰면 설치 비용은 줄지만, PostgreSQL이나 MySQL의 SQL 문법, 제약 조건, 잠금과 트랜잭션 동작까지 같다고 볼 수는 없습니다. 그 동작이 확인 대상이라면 실제로 쓰는 DB 엔진의 테스트 인스턴스가 필요하며, Testcontainers 같은 도구로 임시 컨테이너를 준비할 수 있습니다.
- 환경 변수: CI에서 필요한 환경 변수는 CI 도구의 secret 기능으로 안전하게 관리하고, 로그에 값을 출력하지 않습니다.
- 선택적 실행: Jest의
--onlyChanged같은 변경 기반 검사는 빠른 피드백을 위한 보조 job으로 쓸 수 있습니다. 간접 의존성의 회귀를 놓칠 수 있으므로, 메인 브랜치 병합 기준에는 팀이 합의한 전체 테스트를 유지합니다. - 알림: 빌드가 실패하면 Slack이나 이메일로 팀에 알려 문제를 빨리 해결하게 합니다.
- 린트와 포매터: NestJS 12 템플릿에는 린터로 oxlint가, 포매터로 Prettier가 들어 있습니다.
npm run lint단계를 워크플로에 추가하면 코드 스타일과 흔한 실수를 같은 기준으로 검사할 수 있습니다. 단계를 실제로 추가했을 때만 검사 항목이라고 부릅니다. - 정적 분석: SonarQube 같은 정적 분석 도구를 CI에 통합해 잠재적인 취약점이나 나쁜 코드 패턴을 자동으로 찾아냅니다.
Jest, Supertest, GitHub Actions를 함께 쓰면 팀은 변경을 더 자주 통합하고 버그를 일찍 발견하며, 안정된 애플리케이션을 꾸준히 내보낼 수 있습니다.
정리
- 지속적 통합은 코드를 자주 병합하고, 병합할 때마다 빌드와 테스트를 자동으로 실행해 문제를 일찍 찾는 방식입니다.
- GitHub Actions 워크플로는
.github/workflows폴더의 YAML 파일로 정의하며,npm ci, 빌드, 단위 테스트, E2E 테스트, 커버리지 검사를 차례로 실행합니다. - NestJS 12 프로젝트의 Jest 테스트는 Node.js 24.9 이상이 필요하므로, matrix에는 24와 26 같은 버전을 씁니다.
- 기본 설정에서는 한 단계가 실패하면 뒤의 일반 단계를 건너뛰고,
if: always()업로드는 이미 만들어진 파일만 저장합니다. - 병렬화·캐시·선택 실행으로 속도를 높이되, 테스트 격리와 실패 기록, 병합 기준의 전체 테스트는 지킵니다.
연습 문제
1. E2E가 실패한 뒤의 단계
새 runner에서 시작했고 기존 coverage 폴더는 없습니다. 설치, 빌드, 단위 테스트는 통과했지만 E2E 테스트가 실패했습니다. 이 YAML에서 커버리지 검사와 업로드는 각각 어떻게 될까요? 업로드 단계가 실행되었다는 이유로 E2E 성공이나 커버리지 통과를 기록해도 될까요?
해설 보기
기본 성공 조건을 쓰는 Check Test Coverage는 앞의 E2E 실패 때문에 건너뜁니다. if: always()가 있는 업로드는 시도되지만, 리포트가 만들어지지 않았으므로 if-no-files-found: warn에 따라 파일이 없다는 경고만 남기고 결과물은 만들지 못합니다.
E2E 실패의 stack과 실행 로그는 Actions 로그에서 확인합니다. 업로드를 시도했다고 테스트 결과가 성공으로 바뀌지 않으며, 실행하지 않은 커버리지 검사를 통과로 기록할 수도 없습니다.
2. matrix에 Node 22를 넣으면
누군가 호환성을 넓히려고 matrix를 [22.x, 24.x]로 바꾸었습니다. Node 22 job에서 빌드와 단위 테스트는 각각 어떻게 될까요?
해설 보기
1장에서 확인한 공식 문서의 조건에 따르면 NestJS 12 애플리케이션은 Node.js 20.19 이상(22.x 계열은 22.12 이상)에서 실행되므로, job에 22.12 이상의 22 계열 버전이 설치되면 npm run build의 컴파일은 통과할 수 있습니다. 하지만 Jest가 ESM 전용인 NestJS 12 패키지를 불러오려면 Node.js 24.9 이상이 필요하므로, npm run test에서 테스트가 실패하고 뒤의 단계는 건너뜁니다.
운영 환경을 Node 22에서 돌리는지 확인하고 싶다면 테스트 matrix가 아니라 빌드와 실행만 확인하는 별도 job을 두거나, 운영 환경을 테스트 조건에 맞춰 24 이상으로 올리는 편이 낫습니다.
3. 가끔 실패하는 E2E를 재시도로 덮으면
E2E 테스트 하나가 열 번에 한 번꼴로 실패합니다. 팀원이 E2E 단계를 실패하면 최대 세 번까지 다시 실행하도록 바꾸자고 제안했습니다. 위 그림의 기준으로 보면 무엇을 함께 정해야 할까요?
해설 보기
재시도는 원인이 알려진 네트워크나 외부 sandbox의 불안정에만, 횟수와 대상을 제한해서 써야 합니다. 원인을 모른 채 모든 E2E를 다시 실행하면 진짜 결함까지 성공으로 덮을 수 있습니다.
재시도를 쓰더라도 첫 실패와 각 재시도의 결과를 모두 기록으로 남겨야 하고, 그 실패가 제품 코드, 테스트 코드, 실행 환경 가운데 어디에서 왔는지 분류해 담당자와 다음 조치를 정해야 합니다. 재실행은 실패를 없애는 도구가 아니라 불안정의 원인을 좁히는 도구입니다.
이것으로 8장을 마칩니다. 8장에서는 단위 테스트와 모킹, E2E 테스트, 커버리지 측정, 그리고 이 모두를 CI 파이프라인에 연결하는 흐름을 이어 보았습니다. 다음 장의 캐싱 전략 (Redis, in-memory)부터는 애플리케이션의 성능을 높이는 방법을 다룹니다.