이미지 로딩 중...
AI Generated
2025. 11. 5. · 8 Views
GitHub Actions 테스트 전략 완벽 가이드
CI/CD 파이프라인에서 효과적인 테스트 자동화를 구축하는 방법을 배워봅니다. 단위 테스트부터 E2E 테스트까지, 실무에서 바로 적용 가능한 GitHub Actions 테스트 전략을 소개합니다.
목차
- GitHub Actions 기본 테스트 워크플로우
- 매트릭스 전략으로 다중 환경 테스트
- 테스트 커버리지 리포트 자동화
- E2E 테스트 자동화 with Playwright
- 병렬 테스트 실행으로 속도 최적화
- 조건부 테스트 실행으로 리소스 절약
- 테스트 결과 리포트 및 시각화
- 캐싱으로 테스트 속도 향상
- 플레이키 테스트 재시도 전략
- 보안 취약점 스캔 통합
1. GitHub Actions 기본 테스트 워크플로우
시작하며
여러분이 코드를 푸시할 때마다 수동으로 테스트를 돌려야 한다면 얼마나 번거로울까요? 팀원들이 각자 다른 환경에서 테스트를 실행하다 보니 "내 컴퓨터에서는 잘 되는데요?"라는 말을 자주 듣게 되는 상황 말이죠.
이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 테스트가 자동화되지 않으면 휴먼 에러가 발생하기 쉽고, 코드 품질을 일관되게 유지하기 어렵습니다.
특히 여러 명이 협업하는 프로젝트에서는 이런 문제가 더욱 심각해집니다. 바로 이럴 때 필요한 것이 GitHub Actions 테스트 워크플로우입니다.
코드를 푸시하는 순간 자동으로 테스트가 실행되고, 결과를 즉시 확인할 수 있어 개발 생산성이 크게 향상됩니다.
개요
간단히 말해서, 이 개념은 Git 이벤트(push, pull request 등)가 발생할 때 자동으로 테스트를 실행하는 자동화 시스템입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 매번 코드 변경 시 수동으로 테스트를 실행하는 것은 시간 낭비이며 실수하기 쉽습니다.
GitHub Actions를 사용하면 코드가 메인 브랜치에 병합되기 전에 자동으로 모든 테스트를 실행하여 버그를 조기에 발견할 수 있습니다. 예를 들어, 새로운 기능을 개발한 개발자가 PR을 올리면 자동으로 전체 테스트 스위트가 실행되어 기존 기능을 망가뜨리지 않았는지 확인할 수 있습니다.
기존에는 Jenkins나 Travis CI 같은 별도의 CI 서버를 구축하고 관리해야 했다면, 이제는 GitHub 저장소에 YAML 파일 하나만 추가하면 바로 CI/CD를 시작할 수 있습니다. 이 개념의 핵심 특징은 첫째, GitHub에 완전히 통합되어 있어 별도 설정이 최소화된다는 점, 둘째, YAML 기반의 선언적 구문으로 누구나 쉽게 이해하고 수정할 수 있다는 점, 셋째, 다양한 운영체제와 Node.js 버전에서 동시에 테스트할 수 있는 매트릭스 빌드를 지원한다는 점입니다.
이러한 특징들이 팀의 코드 품질을 높이고 배포 안정성을 크게 향상시켜줍니다.
코드 예제
name: Run Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Node.js 환경 설정
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
# 의존성 설치
- name: Install dependencies
run: npm ci
# 테스트 실행
- name: Run tests
run: npm test
설명
이것이 하는 일: 이 워크플로우는 코드가 main이나 develop 브랜치에 푸시되거나 PR이 생성될 때 자동으로 테스트 환경을 구축하고 테스트를 실행합니다. 첫 번째로, on 섹션에서 워크플로우가 언제 실행될지를 정의합니다.
push 이벤트는 특정 브랜치에 코드가 푸시될 때, pull_request 이벤트는 PR이 생성되거나 업데이트될 때 트리거됩니다. 이렇게 하는 이유는 메인 브랜치의 코드 품질을 보호하고, PR 단계에서 미리 문제를 발견하기 위함입니다.
그 다음으로, jobs 섹션에서 실제 작업을 정의합니다. runs-on: ubuntu-latest는 Ubuntu 최신 버전의 가상 머신에서 테스트를 실행하겠다는 의미입니다.
steps 안에서는 순차적으로 작업이 진행되는데, 먼저 actions/checkout으로 코드를 가져오고, actions/setup-node로 Node.js 환경을 설정합니다. cache: 'npm' 옵션은 npm 의존성을 캐싱하여 다음 실행 시 설치 시간을 단축시킵니다.
세 번째 단계에서 npm ci 명령으로 의존성을 설치합니다. npm install 대신 npm ci를 사용하는 이유는 package-lock.json을 정확히 따르고, 더 빠르고 신뢰할 수 있는 설치를 보장하기 때문입니다.
마지막으로 npm test가 실행되면서 package.json에 정의된 테스트 스크립트가 동작하여 최종적으로 테스트 결과를 생성합니다. 여러분이 이 코드를 사용하면 코드 푸시 후 몇 분 안에 테스트 결과를 자동으로 확인할 수 있습니다.
PR에서는 merge 버튼 옆에 테스트 통과 여부가 표시되어 리뷰어가 코드 품질을 즉시 판단할 수 있고, 팀 전체의 개발 속도와 코드 신뢰성이 향상됩니다.
실전 팁
💡 npm ci 대신 npm install을 사용하면 package-lock.json이 업데이트될 수 있어 예상치 못한 의존성 변경이 발생할 수 있습니다. CI 환경에서는 항상 npm ci를 사용하세요. 💡 cache: 'npm' 옵션을 추가하면 의존성 설치 시간이 30초에서 5초로 대폭 단축됩니다. 큰 프로젝트일수록 효과가 크니 꼭 활용하세요. 💡 테스트가 실패하면 PR을 병합할 수 없도록 브랜치 보호 규칙을 설정하세요. Settings > Branches > Branch protection rules에서 "Require status checks to pass"를 활성화하면 됩니다. 💡 워크플로우 파일은 .github/workflows/ 디렉토리에 저장해야 하며, 파일명은 test.yml 또는 ci.yml 같이 목적을 명확히 나타내는 이름을 사용하는 것이 좋습니다.
2. 매트릭스 전략으로 다중 환경 테스트
시작하며
여러분의 라이브러리나 애플리케이션이 Node.js 16, 18, 20 모두에서 잘 동작하는지 확인하고 싶은데, 각 버전마다 수동으로 테스트를 돌려야 한다면 얼마나 비효율적일까요? 게다가 Windows, macOS, Linux 세 운영체제에서도 모두 테스트해야 한다면 경우의 수는 9가지나 됩니다.
이런 문제는 오픈소스 라이브러리나 크로스 플랫폼 애플리케이션을 개발할 때 필수적으로 마주치게 됩니다. 모든 환경에서 수동으로 테스트하는 것은 현실적으로 불가능하며, 특정 환경에서만 발생하는 버그를 놓치기 쉽습니다.
바로 이럴 때 필요한 것이 GitHub Actions의 매트릭스 전략입니다. 여러 버전과 운영체제의 조합을 자동으로 생성하고 병렬로 테스트하여, 모든 환경에서의 호환성을 한 번에 검증할 수 있습니다.
개요
간단히 말해서, 매트릭스 전략은 여러 변수의 조합으로 작업을 자동으로 생성하고 병렬 실행하는 기능입니다. 실무에서 이 기능이 왜 필요한지 설명하자면, 사용자들은 다양한 환경에서 여러분의 코드를 실행합니다.
Node.js 버전도 다르고, 운영체제도 다르며, 때로는 Python이나 Java 같은 다른 런타임 버전도 고려해야 합니다. 매트릭스 전략을 사용하면 이 모든 조합을 자동으로 테스트할 수 있어, 특정 환경에서만 발생하는 버그를 조기에 발견할 수 있습니다.
예를 들어, Windows에서만 발생하는 경로 구분자 문제나 Node.js 16에서만 나타나는 호환성 문제를 배포 전에 미리 잡아낼 수 있습니다. 기존에는 각 환경별로 별도의 워크플로우 파일을 만들거나 수동으로 테스트해야 했다면, 이제는 matrix 키워드 하나로 모든 조합을 자동 생성할 수 있습니다.
핵심 특징은 첫째, 변수 조합을 자동으로 생성하여 중복 코드를 제거한다는 점, 둘째, 모든 테스트가 병렬로 실행되어 전체 실행 시간이 단일 테스트 시간과 거의 동일하다는 점, 셋째, include/exclude 옵션으로 특정 조합만 선택하거나 제외할 수 있다는 점입니다. 이러한 특징들이 크로스 플랫폼 호환성 검증을 매우 효율적으로 만들어줍니다.
코드 예제
name: Matrix Testing
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
# 운영체제와 Node.js 버전의 조합 생성
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
# 3 x 3 = 9개의 작업이 자동 생성됨
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
# 테스트 결과에 환경 정보 표시
- name: Display test environment
run: |
echo "OS: ${{ matrix.os }}"
echo "Node: ${{ matrix.node-version }}"
설명
이것이 하는 일: 이 워크플로우는 3개의 운영체제와 3개의 Node.js 버전, 총 9가지 환경 조합에서 동시에 테스트를 실행하여 크로스 플랫폼 호환성을 검증합니다. 첫 번째로, strategy.matrix 섹션에서 테스트할 변수들을 정의합니다.
os와 node-version 배열에 각각 3개의 값을 지정하면, GitHub Actions가 자동으로 이들의 모든 조합(3×3=9)을 생성합니다. 이렇게 하는 이유는 한 번의 설정으로 모든 환경을 커버할 수 있고, 새로운 버전을 추가할 때도 배열에 값만 추가하면 되기 때문입니다.
그 다음으로, runs-on과 node-version 설정에서 ${{ matrix.os }}와 ${{ matrix.node-version }} 같은 매트릭스 변수를 사용합니다. 각 작업은 서로 다른 조합의 값을 받아 독립적으로 실행됩니다.
예를 들어, 첫 번째 작업은 ubuntu-latest + Node 16, 두 번째는 ubuntu-latest + Node 18 식으로 실행되며, 이들은 모두 병렬로 동작하여 시간을 절약합니다. 세 번째로, 각 작업에서는 동일한 steps가 실행되지만 환경만 다릅니다.
npm ci로 의존성을 설치하고 npm test로 테스트를 실행하는 과정은 같지만, 운영체제와 Node.js 버전이 달라 각 환경에서의 호환성을 검증할 수 있습니다. 마지막 step에서는 어떤 환경에서 테스트가 실행되었는지 로그에 출력하여 디버깅을 쉽게 만듭니다.
여러분이 이 코드를 사용하면 한 번의 푸시로 9가지 환경에서 테스트 결과를 얻을 수 있으며, PR 페이지에서 각 환경별 테스트 통과 여부를 한눈에 확인할 수 있습니다. 특정 환경에서만 실패한 경우 해당 로그만 확인하여 빠르게 문제를 해결할 수 있어, 크로스 플랫폼 개발의 생산성이 크게 향상됩니다.
실전 팁
💡 fail-fast: false 옵션을 strategy에 추가하면 하나의 조합이 실패해도 나머지 테스트가 계속 실행됩니다. 기본값은 true로, 하나만 실패해도 모든 테스트가 중단되니 주의하세요. 💡 include를 사용하면 특정 조합만 추가할 수 있습니다. 예: include: - os: ubuntu-latest, node-version: 21, experimental: true 형태로 최신 버전만 별도로 테스트할 수 있습니다. 💡 exclude로 특정 조합을 제외할 수 있습니다. 예를 들어 Windows + Node 16 조합이 의미 없다면 exclude: - os: windows-latest, node-version: 16으로 제외하세요. 💡 GitHub Actions는 동시 실행 작업 수에 제한이 있습니다(무료 계정은 20개). 매트릭스가 너무 크면 일부 작업이 대기하게 되니, 꼭 필요한 조합만 선택하세요. 💡 매트릭스 변수는 steps의 name이나 run 명령에서도 사용할 수 있어, 환경별로 다른 명령을 실행하거나 조건부 실행도 가능합니다.
3. 테스트 커버리지 리포트 자동화
시작하며
여러분이 테스트를 작성했지만 실제로 코드의 몇 퍼센트가 테스트되고 있는지 모른다면, 어디에 테스트를 추가해야 할지 감이 오지 않겠죠? 팀 리뷰에서 "이 기능은 테스트가 있나요?"라는 질문에 정확히 답하기 어려운 상황이 자주 발생합니다.
이런 문제는 테스트 품질을 객관적으로 측정하지 못할 때 생깁니다. 코드 커버리지가 낮은 부분은 버그가 숨어있을 가능성이 높지만, 수동으로 확인하기는 매우 번거롭습니다.
특히 PR 리뷰 시 새로운 코드에 대한 테스트 커버리지를 확인하는 것은 더욱 어렵습니다. 바로 이럴 때 필요한 것이 자동화된 테스트 커버리지 리포팅입니다.
매번 테스트 실행 시 커버리지를 자동으로 측정하고, PR에 코멘트로 결과를 표시하여 코드 품질을 지속적으로 모니터링할 수 있습니다.
개요
간단히 말해서, 테스트 커버리지 자동화는 코드 실행 경로 중 테스트로 검증된 비율을 자동으로 측정하고 시각화하는 시스템입니다. 실무에서 이 기능이 필수인 이유는, 커버리지 수치가 코드 품질의 객관적 지표가 되기 때문입니다.
80% 이상의 커버리지를 유지하는 팀은 버그 발생률이 현저히 낮다는 연구 결과도 있습니다. GitHub Actions로 커버리지를 자동화하면 매 PR마다 커버리지 변화를 추적할 수 있어, 새로운 코드가 테스트 없이 추가되는 것을 방지할 수 있습니다.
예를 들어, 새로운 API 엔드포인트를 추가했는데 테스트가 없다면 커버리지가 떨어지면서 PR에 경고가 표시됩니다. 기존에는 개발자가 로컬에서 커버리지를 확인하고 스크린샷을 찍어 공유해야 했다면, 이제는 자동으로 PR 코멘트에 상세한 리포트가 올라옵니다.
핵심 특징은 첫째, Istanbul/NYC 같은 도구로 정확한 라인/브랜치/함수 커버리지를 측정한다는 점, 둘째, Codecov나 Coveralls 같은 서비스와 통합하여 시각적인 리포트를 제공한다는 점, 셋째, 커버리지 임계값을 설정하여 기준 미달 시 빌드를 실패시킬 수 있다는 점입니다. 이러한 특징들이 팀의 테스트 문화를 강화하고 장기적인 코드 품질을 보장해줍니다.
코드 예제
name: Test Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
# 커버리지 포함하여 테스트 실행
- name: Run tests with coverage
run: npm test -- --coverage
# Codecov에 커버리지 리포트 업로드
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_ci_if_error: true
# PR에 커버리지 코멘트 추가
- name: Comment coverage on PR
uses: romeovs/lcov-reporter-action@v0.3.1
with:
lcov-file: ./coverage/lcov.info
github-token: ${{ secrets.GITHUB_TOKEN }}
설명
이것이 하는 일: 이 워크플로우는 테스트를 실행하면서 코드 커버리지를 측정하고, 그 결과를 외부 서비스에 업로드하며, PR에 시각적인 리포트를 자동으로 추가합니다. 첫 번째로, npm test -- --coverage 명령으로 Jest(또는 다른 테스트 프레임워크)를 실행하면서 커버리지 데이터를 수집합니다.
--coverage 플래그는 코드의 각 라인이 실행되었는지 추적하고, coverage/ 디렉토리에 lcov.info 파일을 생성합니다. 이 파일에는 파일별, 함수별, 라인별 커버리지 정보가 상세히 담겨있습니다.
그 다음으로, codecov/codecov-action을 사용하여 커버리지 데이터를 Codecov 서비스에 업로드합니다. CODECOV_TOKEN은 GitHub Secrets에 저장된 인증 토큰으로, Codecov 웹사이트에서 발급받아 설정해야 합니다.
fail_ci_if_error: true 옵션은 업로드가 실패하면 워크플로우 전체를 실패시켜, 커버리지 누락을 방지합니다. Codecov는 업로드된 데이터를 기반으로 트렌드 그래프와 파일별 상세 리포트를 제공합니다.
세 번째로, lcov-reporter-action을 사용하여 PR에 직접 커버리지 코멘트를 추가합니다. 이 액션은 lcov.info 파일을 파싱하여 전체 커버리지 퍼센트, 변경된 파일의 커버리지, 커버되지 않은 라인 등을 표 형태로 예쁘게 정리하여 PR 코멘트로 작성합니다.
GITHUB_TOKEN은 자동으로 제공되는 토큰으로 별도 설정이 필요 없습니다. 여러분이 이 코드를 사용하면 모든 PR에서 커버리지 변화를 즉시 확인할 수 있습니다.
"이 PR로 커버리지가 85%에서 87%로 올라갔네요!"처럼 구체적인 피드백을 받을 수 있고, 리뷰어는 테스트가 충분한지 객관적으로 판단할 수 있습니다. 또한 Codecov 대시보드에서 시간에 따른 커버리지 변화를 추적하여 장기적인 품질 트렌드를 파악할 수 있습니다.
실전 팁
💡 package.json의 jest 설정에 coverageThreshold를 추가하면 커버리지가 기준 이하일 때 테스트를 실패시킬 수 있습니다. 예: "coverageThreshold": {"global": {"branches": 80, "functions": 80, "lines": 80}} 💡 Codecov 대신 무료 대안인 Coveralls를 사용할 수도 있습니다. coverallsapp/github-action을 사용하면 설정이 더 간단합니다. 💡 coverage/ 디렉토리는 .gitignore에 추가하여 커밋되지 않도록 하세요. 커버리지 데이터는 로컬에서만 필요하고 저장소에는 불필요합니다. 💡 --coverage 플래그는 테스트 실행 시간을 20-30% 증가시킵니다. 로컬 개발 시에는 제외하고, CI에서만 사용하는 것이 좋습니다. 💡 HTML 리포트도 생성하려면 coverageReporters: ['lcov', 'html']을 설정하세요. 로컬에서 coverage/index.html을 열면 시각적으로 커버되지 않은 부분을 확인할 수 있습니다.
4. E2E 테스트 자동화 with Playwright
시작하며
여러분의 웹 애플리케이션이 실제 브라우저에서 잘 동작하는지 어떻게 확인하시나요? 단위 테스트는 통과했지만 실제로 사용자가 버튼을 클릭하면 아무 반응이 없거나, 특정 브라우저에서만 레이아웃이 깨지는 경험을 해보셨을 겁니다.
이런 문제는 단위 테스트만으로는 잡아낼 수 없는 통합 이슈입니다. 실제 사용자 시나리오를 수동으로 매번 테스트하는 것은 시간이 오래 걸리고, 회귀 버그(이전에 작동하던 기능이 망가지는 것)를 놓치기 쉽습니다.
특히 Chrome, Firefox, Safari 등 여러 브라우저에서 테스트하려면 더욱 복잡해집니다. 바로 이럴 때 필요한 것이 Playwright를 사용한 E2E(End-to-End) 테스트 자동화입니다.
실제 브라우저를 자동으로 조작하여 사용자 행동을 시뮬레이션하고, 여러 브라우저에서 동시에 테스트하여 크로스 브라우저 호환성을 보장할 수 있습니다.
개요
간단히 말해서, E2E 테스트 자동화는 실제 브라우저 환경에서 사용자의 전체 사용 흐름을 자동으로 검증하는 시스템입니다. 실무에서 이 기능이 중요한 이유는, 단위 테스트나 통합 테스트로는 발견할 수 없는 UI/UX 버그를 잡아내기 때문입니다.
로그인 → 상품 검색 → 장바구니 추가 → 결제 같은 실제 사용자 여정을 자동으로 테스트하면, 어느 단계에서 문제가 발생하는지 정확히 파악할 수 있습니다. Playwright는 Chrome, Firefox, Safari 모두를 지원하여 한 번의 테스트 코드로 모든 브라우저를 검증할 수 있습니다.
예를 들어, Safari에서만 발생하는 날짜 입력 필드 버그를 배포 전에 미리 발견할 수 있습니다. 기존에는 Selenium이나 Puppeteer를 사용했지만 설정이 복잡하고 느렸다면, Playwright는 자동 대기, 자동 재시도, 네트워크 모킹 등 현대적인 기능을 기본 제공하여 더 안정적인 테스트를 작성할 수 있습니다.
핵심 특징은 첫째, 병렬 실행으로 수백 개의 테스트도 빠르게 실행된다는 점, 둘째, 스크린샷과 비디오 녹화로 실패 원인을 쉽게 디버깅할 수 있다는 점, 셋째, 헤드리스 모드로 CI 환경에서 효율적으로 실행된다는 점입니다. 이러한 특징들이 프론트엔드 품질을 크게 향상시키고 사용자 경험을 보장해줍니다.
코드 예제
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
# Playwright 브라우저 설치
- name: Install Playwright browsers
run: npx playwright install --with-deps
# 애플리케이션 빌드
- name: Build application
run: npm run build
# E2E 테스트 실행 (크로스 브라우저)
- name: Run Playwright tests
run: npx playwright test
# 실패 시 스크린샷과 비디오 업로드
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
설명
이것이 하는 일: 이 워크플로우는 애플리케이션을 빌드하고, Playwright를 사용하여 여러 브라우저에서 실제 사용자 시나리오를 자동으로 테스트하며, 실패 시 디버깅을 위한 자료를 수집합니다. 첫 번째로, npx playwright install --with-deps 명령으로 테스트에 필요한 브라우저(Chrome, Firefox, Safari)와 시스템 의존성을 설치합니다.
--with-deps 옵션은 Linux 시스템 라이브러리(libgdk, libgtk 등)도 함께 설치하여 Ubuntu 환경에서 브라우저가 정상 작동하도록 보장합니다. 이 과정은 처음에는 시간이 걸리지만, GitHub Actions 캐싱을 활용하면 다음 실행부터는 빨라집니다.
그 다음으로, npm run build로 애플리케이션을 프로덕션 빌드합니다. E2E 테스트는 실제 배포될 코드를 테스트해야 의미가 있기 때문에, 개발 모드가 아닌 빌드된 결과물을 대상으로 테스트합니다.
그런 다음 npx playwright test가 실행되면서 playwright.config.ts에 정의된 모든 테스트가 병렬로 실행됩니다. Playwright는 기본적으로 Chromium, Firefox, WebKit(Safari) 세 브라우저에서 모든 테스트를 실행합니다.
세 번째로, if: failure() 조건이 붙은 아티팩트 업로드 단계는 테스트가 실패했을 때만 실행됩니다. Playwright는 테스트 실패 시 자동으로 스크린샷을 찍고, 비디오를 녹화하며, HTML 리포트를 생성하여 playwright-report/ 디렉토리에 저장합니다.
actions/upload-artifact는 이 디렉토리를 GitHub에 업로드하여, 나중에 다운로드하여 어떤 화면에서 어떤 이유로 실패했는지 시각적으로 확인할 수 있게 해줍니다. 여러분이 이 코드를 사용하면 매 푸시마다 실제 브라우저에서 애플리케이션이 정상 작동하는지 자동으로 확인할 수 있습니다.
로그인 흐름, 폼 제출, 페이지 네비게이션 등 중요한 사용자 시나리오가 망가지지 않았는지 즉시 알 수 있고, 만약 실패하면 스크린샷과 비디오를 보며 정확히 어디서 문제가 발생했는지 파악할 수 있어 디버깅 시간이 크게 단축됩니다.
실전 팁
💡 Playwright 브라우저는 용량이 크므로(약 500MB), actions/cache를 사용하여 캐싱하면 설치 시간을 2분에서 10초로 줄일 수 있습니다. ~/.cache/ms-playwright를 캐시 대상으로 지정하세요. 💡 E2E 테스트는 실행 시간이 길어질 수 있으므로, shard 기능으로 분할 실행하세요. playwright test --shard=1/3 형태로 테스트를 3등분하여 병렬 실행하면 전체 시간을 1/3로 줄일 수 있습니다. 💡 retries: 2 옵션을 playwright.config.ts에 설정하면 네트워크 지연 등으로 인한 불안정한 테스트(flaky test)의 영향을 줄일 수 있습니다. 💡 테스트가 성공해도 trace를 저장하려면 trace: 'on'으로 설정하세요. trace 파일을 Playwright Inspector로 열면 모든 액션을 단계별로 재생하며 디버깅할 수 있습니다. 💡 환경 변수로 BASE_URL을 설정하면 로컬, 스테이징, 프로덕션 등 다른 환경에서도 동일한 테스트를 실행할 수 있습니다.
5. 병렬 테스트 실행으로 속도 최적화
시작하며
여러분의 테스트 스위트가 점점 커져서 전체 실행에 15분이 걸린다면, 개발 속도가 크게 느려지겠죠? PR을 올릴 때마다 15분을 기다려야 한다면 개발자들은 테스트를 건너뛰고 싶은 유혹을 느낄 수 있습니다.
이런 문제는 테스트가 순차적으로 실행되기 때문에 발생합니다. 수백 개의 테스트가 하나씩 차례로 실행되면 시간이 오래 걸릴 수밖에 없습니다.
특히 통합 테스트나 E2E 테스트처럼 시간이 오래 걸리는 테스트가 많으면 문제가 더 심각해집니다. 바로 이럴 때 필요한 것이 병렬 테스트 실행 전략입니다.
테스트를 여러 그룹으로 나누어 동시에 실행하면, 전체 실행 시간을 대폭 단축시킬 수 있습니다. GitHub Actions의 매트릭스와 샤딩 기능을 활용하면 쉽게 구현할 수 있습니다.
개요
간단히 말해서, 병렬 테스트 실행은 테스트 스위트를 여러 부분으로 나누어 동시에 실행하여 전체 시간을 단축하는 기법입니다. 실무에서 이 전략이 필수인 이유는, 빠른 피드백이 개발 생산성의 핵심이기 때문입니다.
15분 걸리던 테스트를 3분으로 줄이면 개발자들이 더 자주 테스트를 실행하게 되고, 버그를 조기에 발견할 확률이 높아집니다. Jest, Pytest, Playwright 등 대부분의 테스트 프레임워크는 병렬 실행을 지원하며, GitHub Actions는 여러 러너를 동시에 실행할 수 있어 이상적인 조합입니다.
예를 들어, 1000개의 테스트를 5개의 그룹으로 나누어 병렬 실행하면 이론적으로 5배 빠른 결과를 얻을 수 있습니다. 기존에는 하나의 CI 작업에서 모든 테스트를 순차 실행했다면, 이제는 여러 작업으로 분산하여 GitHub의 컴퓨팅 자원을 최대한 활용할 수 있습니다.
핵심 특징은 첫째, 테스트 파일을 자동으로 균등하게 분배하여 각 러너의 부하를 비슷하게 맞춘다는 점, 둘째, 독립적인 러너에서 실행되어 테스트 간 간섭이 없다는 점, 셋째, 하나의 샤드가 실패해도 다른 샤드의 결과를 확인할 수 있다는 점입니다. 이러한 특징들이 대규모 테스트 스위트를 효율적으로 관리하게 해줍니다.
코드 예제
name: Parallel Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
# 테스트를 4개 그룹으로 분할
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
# Jest 병렬 실행 (4개 샤드 중 현재 샤드만)
- name: Run tests (shard ${{ matrix.shard }}/4)
run: npx jest --shard=${{ matrix.shard }}/4
# 각 샤드의 커버리지 업로드
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
flags: shard-${{ matrix.shard }}
설명
이것이 하는 일: 이 워크플로우는 전체 테스트를 4개의 균등한 그룹으로 자동 분할하고, 각 그룹을 독립적인 러너에서 동시에 실행하여 테스트 시간을 대폭 줄입니다. 첫 번째로, strategy.matrix.shard: [1, 2, 3, 4] 설정으로 4개의 작업을 생성합니다.
각 작업은 동일한 코드베이스에서 시작하지만 서로 다른 샤드 번호를 받습니다. GitHub Actions는 이 4개의 작업을 동시에 시작하여(사용 가능한 러너가 충분하다면), 각각 독립적으로 진행됩니다.
이렇게 하는 이유는 한 러너에서 모든 테스트를 실행하는 것보다 여러 러너에 분산하는 것이 훨씬 빠르기 때문입니다. 그 다음으로, npx jest --shard=${{ matrix.shard }}/4 명령이 핵심입니다.
Jest는 --shard 옵션을 받으면 모든 테스트 파일을 해싱하여 4개 그룹으로 균등하게 나눈 후, 현재 샤드에 해당하는 테스트만 실행합니다. 예를 들어, --shard=1/4는 전체의 25%에 해당하는 첫 번째 그룹만 실행합니다.
Jest는 내부적으로 각 테스트 파일의 실행 시간을 추정하여 각 샤드의 총 실행 시간이 비슷하도록 스마트하게 분배합니다. 세 번째로, 각 샤드는 독립적으로 커버리지 데이터를 생성하고, flags 옵션으로 샤드별로 구분하여 Codecov에 업로드합니다.
Codecov는 여러 샤드의 커버리지를 자동으로 병합하여 전체 커버리지를 계산합니다. 만약 샤드 2번이 실패하더라도 1, 3, 4번의 결과는 확인할 수 있어, 어느 부분에서 문제가 발생했는지 빠르게 파악할 수 있습니다.
여러분이 이 코드를 사용하면 테스트 시간을 극적으로 줄일 수 있습니다. 12분 걸리던 테스트가 3-4분으로 단축되어 개발 사이클이 빨라지고, 개발자들이 더 자주 테스트를 실행하게 되어 코드 품질이 향상됩니다.
샤드 수는 테스트 스위트 크기에 맞게 조정할 수 있으며, 일반적으로 2-8개 사이가 적당합니다.
실전 팁
💡 샤드 수는 테스트 스위트 크기와 실행 시간에 맞게 조정하세요. 전체 테스트가 5분 미만이면 샤딩이 오히려 오버헤드를 만들 수 있고, 20분 이상이면 8개 이상의 샤드를 고려하세요. 💡 Playwright의 경우 --shard=${{ matrix.shard }}/${{ strategy.job-total }} 형태로 사용하면 매트릭스 크기를 자동으로 감지합니다. 💡 각 샤드의 실행 시간이 크게 다르다면 테스트 파일 크기가 불균형하다는 신호입니다. 큰 테스트 파일을 여러 파일로 분리하면 샤딩 효율이 올라갑니다. 💡 fail-fast: false를 설정하여 하나의 샤드가 실패해도 다른 샤드가 계속 실행되도록 하세요. 그래야 전체 실패 범위를 파악할 수 있습니다. 💡 GitHub Actions 무료 티어는 동시 실행 작업 수에 제한이 있으니, 너무 많은 샤드를 만들면 대기 시간이 발생할 수 있습니다. 실제 병렬 실행 가능한 수를 고려하세요.
6. 조건부 테스트 실행으로 리소스 절약
시작하며
여러분이 README.md만 수정했는데도 매번 전체 테스트 스위트가 돌아간다면 시간과 리소스가 낭비되겠죠? GitHub Actions의 무료 사용 시간은 한정되어 있고, 불필요한 테스트로 인해 정작 중요한 코드 변경 시 빌드가 대기하는 상황이 발생할 수 있습니다.
이런 문제는 모든 변경사항을 동일하게 취급할 때 생깁니다. 문서 수정, 설정 파일 변경, 실제 코드 수정은 각각 다른 수준의 테스트가 필요하지만, 일반적인 CI 설정은 이를 구분하지 못합니다.
이로 인해 CI 시간이 불필요하게 길어지고 비용이 증가합니다. 바로 이럴 때 필요한 것이 조건부 테스트 실행 전략입니다.
변경된 파일의 경로나 타입을 감지하여 필요한 테스트만 선택적으로 실행하면, 리소스를 절약하면서도 코드 품질을 유지할 수 있습니다.
개요
간단히 말해서, 조건부 테스트 실행은 변경된 파일을 분석하여 관련된 테스트만 선택적으로 실행하는 최적화 기법입니다. 실무에서 이 전략이 효과적인 이유는, 모든 변경이 전체 테스트를 필요로 하지는 않기 때문입니다.
마크다운 파일만 수정했다면 E2E 테스트를 건너뛸 수 있고, 프론트엔드만 변경했다면 백엔드 테스트는 불필요합니다. GitHub Actions는 path 필터와 조건문을 제공하여 이런 로직을 쉽게 구현할 수 있습니다.
예를 들어, docs/ 디렉토리만 변경된 PR에서는 린트 체크만 실행하고 전체 테스트는 건너뛸 수 있습니다. 기존에는 모든 푸시에 대해 똑같은 워크플로우가 실행되었다면, 이제는 변경 내용에 맞는 최소한의 검증만 수행할 수 있습니다.
핵심 특징은 첫째, path 필터로 특정 디렉토리나 파일 패턴이 변경되었을 때만 트리거한다는 점, 둘째, if 조건문으로 런타임에 동적으로 단계를 건너뛸 수 있다는 점, 셋째, dorny/paths-filter 같은 액션으로 더 복잡한 조건을 구현할 수 있다는 점입니다. 이러한 특징들이 CI 비용을 절감하고 실행 시간을 최적화해줍니다.
코드 예제
name: Conditional Tests
on:
push:
# 코드 파일이 변경될 때만 실행
paths:
- 'src/**'
- 'test/**'
- 'package.json'
- 'package-lock.json'
# 문서만 변경 시 건너뛰기
paths-ignore:
- '**.md'
- 'docs/**'
jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
# 변경된 파일 감지
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'src/frontend/**'
- 'src/components/**'
backend:
- 'src/api/**'
- 'src/server/**'
frontend-tests:
needs: changes
if: ${{ needs.changes.outputs.frontend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run test:frontend
backend-tests:
needs: changes
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run test:backend
설명
이것이 하는 일: 이 워크플로우는 어떤 파일이 변경되었는지 분석하고, 변경된 부분과 관련된 테스트만 실행하여 불필요한 리소스 사용을 방지합니다. 첫 번째로, on.push.paths와 paths-ignore 설정으로 워크플로우 트리거 자체를 제어합니다.
paths에는 src/, test/ 등 실제 코드와 의존성 파일이 포함되어 있어, 이 파일들이 변경될 때만 워크플로우가 시작됩니다. 반대로 paths-ignore에는 .md 파일과 docs/ 디렉토리가 지정되어 있어, 문서만 수정하면 아예 워크플로우가 실행되지 않습니다.
이렇게 하면 README 오타 수정 같은 경우 CI 시간을 전혀 사용하지 않습니다. 그 다음으로, changes 작업에서 dorny/paths-filter 액션을 사용하여 더 세밀한 변경 감지를 수행합니다.
이 액션은 filters 설정에 정의된 경로 패턴과 실제 변경된 파일을 비교하여, frontend와 backend 각각 true/false 값을 출력합니다. 예를 들어, src/components/Button.tsx만 수정되었다면 frontend는 true, backend는 false가 됩니다.
이 출력값은 outputs로 다른 작업에 전달됩니다. 세 번째로, frontend-tests와 backend-tests 작업은 needs: changes로 changes 작업이 완료될 때까지 기다린 후, if 조건문으로 실행 여부를 결정합니다.
frontend-tests는 frontend가 true일 때만, backend-tests는 backend가 true일 때만 실행됩니다. 만약 프론트엔드만 변경되었다면 backend-tests는 아예 건너뛰어지고, 그만큼 CI 시간과 비용이 절약됩니다.
여러분이 이 코드를 사용하면 평균 CI 시간을 30-50% 줄일 수 있습니다. 특히 모노레포나 풀스택 프로젝트에서 효과가 큽니다.
프론트엔드 개발자가 UI만 수정했을 때 백엔드 테스트가 돌지 않아 피드백이 더 빨라지고, GitHub Actions 무료 사용 시간도 절약할 수 있어 일석이조입니다.
실전 팁
💡 paths-ignore보다 paths가 우선순위가 높습니다. 두 조건이 충돌하면 paths를 먼저 평가하니, 로직을 명확히 설계하세요. 💡 pull_request 이벤트에서는 base 브랜치와의 차이만 감지합니다. 따라서 feature 브랜치에서 여러 번 커밋해도 전체 변경사항을 기준으로 필터링됩니다. 💡 paths-filter 액션의 list-files: shell 옵션을 사용하면 변경된 파일 목록을 스크립트로 전달하여 더 복잡한 로직을 구현할 수 있습니다. 💡 브랜치 보호 규칙에서 required checks를 설정할 때, 조건부로 건너뛸 수 있는 작업은 필수로 지정하지 마세요. 그렇지 않으면 건너뛰어진 작업이 "pending" 상태로 남아 PR을 병합할 수 없습니다. 💡 모든 작업을 건너뛰더라도 최소한 하나의 검증 작업(예: lint)은 항상 실행되도록 설정하여, PR이 완전히 검증 없이 병합되는 것을 방지하세요.
7. 테스트 결과 리포트 및 시각화
시작하며
여러분이 수백 개의 테스트를 실행했는데 어떤 테스트가 실패했는지 찾기 위해 수천 줄의 로그를 스크롤해야 한다면 얼마나 답답할까요? 특히 여러 브라우저나 환경에서 테스트가 실패하면 각각의 로그를 일일이 확인하는 것은 정말 비효율적입니다.
이런 문제는 테스트 결과가 텍스트 로그로만 제공될 때 발생합니다. 어떤 테스트가 자주 실패하는지, 실패율 추세가 어떤지, 특정 환경에서만 문제가 있는지 등을 한눈에 파악하기 어렵습니다.
리뷰어나 팀원들도 테스트 결과를 이해하기 어려워 커뮤니케이션 비용이 증가합니다. 바로 이럴 때 필요한 것이 테스트 결과 리포트 자동화입니다.
테스트 결과를 HTML 리포트나 PR 코멘트로 시각화하고, GitHub의 Checks API를 활용하여 실패한 테스트를 바로 확인할 수 있게 만들면 디버깅 효율이 크게 향상됩니다.
개요
간단히 말해서, 테스트 결과 리포팅은 실행 결과를 사람이 이해하기 쉬운 형태로 변환하고 시각화하여 제공하는 시스템입니다. 실무에서 이 기능이 중요한 이유는, 빠른 문제 파악이 빠른 해결로 이어지기 때문입니다.
Jest, Playwright, Mocha 등의 테스트 러너는 JUnit XML이나 JSON 형태로 결과를 출력할 수 있고, 이를 파싱하여 예쁜 HTML 리포트나 PR 코멘트로 변환할 수 있습니다. GitHub Actions는 Checks API를 통해 PR에 테스트 결과를 직접 표시하여, 어떤 테스트가 어떤 이유로 실패했는지 클릭 한 번으로 확인할 수 있게 해줍니다.
예를 들어, E2E 테스트가 실패하면 해당 위치의 스크린샷이 자동으로 PR에 추가되어 리뷰어가 즉시 문제를 파악할 수 있습니다. 기존에는 "로그 확인해주세요"라는 코멘트만 남겼다면, 이제는 "로그인 버튼 클릭 시 타임아웃 - 스크린샷 첨부"처럼 구체적이고 시각적인 피드백을 제공할 수 있습니다.
핵심 특징은 첫째, JUnit XML, HTML, JSON 등 다양한 포맷으로 리포트를 생성할 수 있다는 점, 둘째, actions/upload-artifact로 리포트를 저장하고 나중에 다운로드할 수 있다는 점, 셋째, 써드파티 액션으로 PR에 자동으로 코멘트를 추가할 수 있다는 점입니다. 이러한 특징들이 테스트 실패 시 디버깅 시간을 크게 단축시켜줍니다.
코드 예제
name: Test Reports
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
# Jest 테스트 실행 및 JUnit 리포트 생성
- name: Run tests with reporters
run: |
npm test -- \
--coverage \
--reporters=default \
--reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_DIR: ./reports
JEST_JUNIT_OUTPUT_NAME: junit.xml
# 테스트 결과 발행 (PR에 표시)
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: ./reports/junit.xml
check_name: Test Results
# HTML 리포트를 아티팩트로 업로드
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v3
with:
name: test-report
path: coverage/
설명
이것이 하는 일: 이 워크플로우는 테스트를 실행하면서 다양한 형식의 리포트를 생성하고, GitHub PR에 시각적으로 표시하며, 상세한 HTML 리포트를 다운로드 가능하게 만듭니다. 첫 번째로, npm test 명령에 여러 리포터를 지정합니다.
--reporters=default는 콘솔에 기본 출력을 유지하고, --reporters=jest-junit은 JUnit XML 형식의 리포트를 추가로 생성합니다. 환경 변수 JEST_JUNIT_OUTPUT_DIR과 JEST_JUNIT_OUTPUT_NAME으로 리포트 파일이 ./reports/junit.xml에 저장되도록 설정합니다.
이 XML 파일에는 각 테스트의 이름, 실행 시간, 성공/실패 여부, 실패 메시지 등이 구조화되어 저장됩니다. 그 다음으로, EnricoMi/publish-unit-test-result-action을 사용하여 JUnit XML을 파싱하고 결과를 GitHub Checks API로 발행합니다.
if: always()는 테스트가 실패해도 이 단계를 실행하도록 보장합니다(기본적으로는 이전 단계가 실패하면 다음 단계를 건너뜁니다). 이 액션은 PR의 "Checks" 탭에 "Test Results"라는 이름으로 결과를 표시하며, 몇 개의 테스트가 성공했고 몇 개가 실패했는지, 실행 시간은 얼마나 걸렸는지 등을 요약하여 보여줍니다.
실패한 테스트를 클릭하면 상세한 에러 메시지를 바로 확인할 수 있습니다. 세 번째로, actions/upload-artifact로 coverage/ 디렉토리 전체를 아티팩트로 업로드합니다.
이 디렉토리에는 Jest가 생성한 HTML 커버리지 리포트가 포함되어 있어, 워크플로우 실행 페이지에서 다운로드하여 브라우저로 열면 어느 라인이 커버되지 않았는지 색깔로 표시된 소스 코드를 확인할 수 있습니다. if: always()로 테스트 실패 시에도 리포트를 업로드하여 디버깅에 활용할 수 있게 합니다.
여러분이 이 코드를 사용하면 PR 페이지에서 클릭 몇 번으로 모든 테스트 결과를 확인할 수 있습니다. "500개 테스트 중 2개 실패 - 로그인 테스트 타임아웃, 결제 테스트 assertion 실패" 같은 요약을 즉시 볼 수 있고, 팀원들과 구체적인 문제에 대해 소통할 수 있어 협업 효율이 크게 향상됩니다.
실전 팁
💡 jest-junit 외에도 jest-html-reporter를 추가하면 더 예쁜 HTML 리포트를 생성할 수 있습니다. 이미지와 차트가 포함되어 비개발자도 이해하기 쉽습니다. 💡 publish-unit-test-result-action의 comment_mode: always 옵션을 설정하면 PR에 직접 코멘트로도 결과를 남깁니다. Checks 탭을 확인하지 않는 팀원들에게 유용합니다. 💡 실패한 테스트의 스택 트레이스가 너무 길면 가독성이 떨어집니다. Jest의 --verbose=false 옵션으로 출력을 간결하게 만들 수 있습니다. 💡 GitHub Pages를 활용하면 커버리지 리포트를 웹에서 직접 볼 수 있습니다. peaceiris/actions-gh-pages 액션으로 coverage/ 디렉토리를 gh-pages 브랜치에 배포하세요. 💡 Playwright의 경우 HTML 리포트가 기본 제공되며, playwright-report/ 디렉토리를 업로드하면 테스트 실행 영상과 스크린샷이 모두 포함된 인터랙티브 리포트를 확인할 수 있습니다.
8. 캐싱으로 테스트 속도 향상
시작하며
여러분의 CI 파이프라인에서 매번 npm install이 2-3분씩 걸린다면, 실제 테스트 시간보다 의존성 설치 시간이 더 길 수도 있습니다. 특히 node_modules가 수백 MB에 달하는 대규모 프로젝트에서는 이 시간이 더욱 길어집니다.
이런 문제는 매 실행마다 모든 의존성을 처음부터 다시 다운로드하기 때문에 발생합니다. package-lock.json이 변경되지 않았는데도 같은 패키지를 반복해서 다운로드하는 것은 명백한 낭비입니다.
네트워크 상태에 따라 설치 시간이 들쭉날쭉하여 CI 안정성도 떨어집니다. 바로 이럴 때 필요한 것이 의존성 캐싱 전략입니다.
GitHub Actions의 캐시 기능을 활용하여 node_modules나 패키지 매니저 캐시를 저장하고 재사용하면, 설치 시간을 10배 이상 단축시킬 수 있습니다.
개요
간단히 말해서, 의존성 캐싱은 이전 실행에서 다운로드한 패키지를 저장해두고 재사용하여 중복 다운로드를 방지하는 최적화 기법입니다. 실무에서 이 전략이 필수인 이유는, CI 시간의 상당 부분이 의존성 설치에 소비되기 때문입니다.
GitHub Actions는 actions/cache와 setup-node의 내장 캐싱 기능을 제공하여 이를 쉽게 구현할 수 있습니다. package-lock.json(또는 yarn.lock, pnpm-lock.yaml)의 해시를 캐시 키로 사용하면, 의존성이 변경되지 않은 한 캐시를 재사용하고, 변경되었을 때만 새로 다운로드합니다.
예를 들어, 100개의 커밋 중 1-2개만 의존성을 변경한다면, 나머지 98%의 실행에서는 캐시를 사용하여 시간을 절약할 수 있습니다. 기존에는 매번 npm install에 2분이 걸렸다면, 캐싱을 활용하면 10-15초로 단축됩니다.
핵심 특징은 첫째, setup-node의 cache 옵션만으로 자동 캐싱이 가능하다는 점, 둘째, 캐시 키에 lock 파일 해시를 사용하여 정확한 무효화가 이루어진다는 점, 셋째, restore-keys로 부분 일치 캐시도 활용할 수 있다는 점입니다. 이러한 특징들이 CI 비용과 시간을 크게 절감해줍니다.
코드 예제
name: Cached Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Node.js 설정 및 자동 npm 캐싱
- name: Setup Node.js with cache
uses: actions/setup-node@v3
with:
node-version: '18'
# package-lock.json 기반 자동 캐싱
cache: 'npm'
# 캐시 히트 시 수 초 만에 완료
- name: Install dependencies
run: npm ci
# Playwright 브라우저 수동 캐싱 (용량이 큼)
- name: Cache Playwright browsers
uses: actions/cache@v3
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
# 캐시 미스 시에만 브라우저 설치
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
- run: npm test
설명
이것이 하는 일: 이 워크플로우는 npm 패키지와 Playwright 브라우저를 캐싱하여, package-lock.json이 변경되지 않은 한 이전에 다운로드한 파일을 재사용합니다. 첫 번째로, actions/setup-node의 cache: 'npm' 옵션만으로 자동 캐싱이 활성화됩니다.
이 옵션은 내부적으로 package-lock.json의 해시를 계산하여 캐시 키를 생성하고, 이전 실행에서 저장된 npm 캐시 디렉토리(~/.npm)를 복원합니다. npm ci 명령은 이 캐시를 활용하여 패키지를 다운로드하는 대신 캐시에서 복사하므로, 설치 시간이 2분에서 10-15초로 단축됩니다.
의존성이 변경되면 package-lock.json의 해시가 달라져 캐시가 무효화되고 새로 다운로드됩니다. 그 다음으로, Playwright 브라우저는 용량이 크고(약 500MB) setup-node의 자동 캐싱에 포함되지 않으므로, actions/cache로 수동 캐싱합니다.
path: ~/.cache/ms-playwright는 Playwright가 브라우저 바이너리를 저장하는 디렉토리이고, key는 runner.os(운영체제)와 package-lock.json 해시를 조합하여 생성합니다. 이렇게 하면 운영체제가 다르거나 Playwright 버전이 바뀔 때만 캐시가 무효화됩니다.
id: playwright-cache로 이 단계에 식별자를 부여하여 캐시 히트 여부를 확인할 수 있습니다. 세 번째로, if: steps.playwright-cache.outputs.cache-hit != 'true' 조건으로 캐시가 없을 때만 브라우저를 설치합니다.
cache-hit 출력은 캐시가 성공적으로 복원되었으면 'true', 그렇지 않으면 'false'입니다. 캐시 히트 시 이 단계는 건너뛰어지고, 미스 시에만 npx playwright install --with-deps가 실행되어 브라우저를 다운로드합니다.
이를 통해 매번 500MB를 다운로드하는 대신, 첫 실행 후에는 수 초 만에 브라우저를 사용할 수 있습니다. 여러분이 이 코드를 사용하면 CI 실행 시간이 평균 50% 이상 단축됩니다.
특히 의존성이 자주 바뀌지 않는 성숙한 프로젝트에서는 90% 이상의 실행에서 캐시를 활용할 수 있어, 피드백 속도가 크게 빨라지고 GitHub Actions 사용 시간도 절약됩니다.
실전 팁
💡 yarn을 사용한다면 cache: 'yarn', pnpm은 cache: 'pnpm'으로 변경하면 됩니다. setup-node가 자동으로 적절한 lock 파일을 감지합니다. 💡 actions/cache의 restore-keys 옵션을 사용하면 부분 일치 캐시도 활용할 수 있습니다. 예: restore-keys: playwright-${{ runner.os }}- 형태로 OS만 일치해도 이전 캐시를 사용합니다. 💡 GitHub Actions의 캐시 용량은 저장소당 10GB로 제한됩니다. 오래된 캐시는 자동으로 삭제되지만, 너무 많은 캐시 키를 생성하면 유용한 캐시가 밀려날 수 있으니 주의하세요. 💡 캐시 키에 버전 번호를 추가하면 강제로 캐시를 무효화할 수 있습니다. 예: key: v1-playwright-... 형태로 만들고, 문제 발생 시 v2로 올리면 됩니다. 💡 npm ci 대신 npm install을 사용하면 package-lock.json이 업데이트될 수 있어 캐싱이 무의미해집니다. 항상 npm ci를 사용하세요.
9. 플레이키 테스트 재시도 전략
시작하며
여러분의 E2E 테스트가 가끔 이유 없이 실패했다가 재실행하면 통과하는 경험이 있나요? 네트워크 지연, 애니메이션 타이밍, 비동기 로딩 등으로 인해 동일한 테스트가 때로는 성공하고 때로는 실패하는 "플레이키 테스트"는 개발자를 매우 좌절시킵니다.
이런 문제는 외부 요인에 의존하는 테스트에서 필연적으로 발생합니다. 플레이키 테스트를 방치하면 팀원들이 테스트 결과를 신뢰하지 않게 되고, "그냥 재실행해보면 통과할 거야"라는 나쁜 문화가 생깁니다.
진짜 버그인지 플레이키 테스트인지 구분하기 어려워 생산성이 크게 떨어집니다. 바로 이럴 때 필요한 것이 자동 재시도 전략입니다.
테스트 실패 시 자동으로 몇 번 더 재시도하여 일시적인 실패를 걸러내고, 진짜 버그만 리포트하면 신뢰성을 높일 수 있습니다. 동시에 플레이키 테스트를 추적하여 근본 원인을 해결할 수 있습니다.
개요
간단히 말해서, 플레이키 테스트 재시도 전략은 실패한 테스트를 자동으로 여러 번 재실행하여 일시적 실패와 진짜 버그를 구분하는 기법입니다. 실무에서 이 전략이 필요한 이유는, 완벽하게 안정적인 E2E 테스트를 만드는 것은 거의 불가능하기 때문입니다.
실제 브라우저, 네트워크, 서버 응답 시간 등 제어할 수 없는 변수가 많아 가끔 타임아웃이나 타이밍 이슈가 발생합니다. Playwright와 Jest 모두 재시도 기능을 제공하며, GitHub Actions에서도 재시도 로직을 구현할 수 있습니다.
예를 들어, 테스트가 3번 중 1번이라도 성공하면 통과로 간주하되, 플레이키하게 동작한 테스트는 별도로 리포트하여 개선할 수 있습니다. 기존에는 플레이키 테스트로 인해 전체 빌드가 실패하고 수동으로 재실행해야 했다면, 이제는 자동으로 재시도하여 대부분의 경우 성공합니다.
핵심 특징은 첫째, 테스트 프레임워크 수준에서 재시도를 설정할 수 있다는 점, 둘째, GitHub Actions의 workflow-level 재시도도 가능하다는 점, 셋째, 재시도 횟수와 성공 기준을 세밀하게 조정할 수 있다는 점입니다. 이러한 특징들이 CI 안정성을 높이고 개발자 경험을 개선해줍니다.
코드 예제
name: Flaky Test Retry
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
# Jest 테스트 재시도 설정
- name: Run unit tests
run: npm test
# Jest 설정에서 jest.retryTimes(2) 사용
# Playwright 테스트 재시도 (최대 2회)
- name: Run E2E tests
run: npx playwright test --retries=2
# 특정 단계 실패 시 전체 재시도
- name: Retry failed jobs
if: failure()
uses: nick-invision/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
command: npx playwright test --retries=0
# 플레이키 테스트 리포트 생성
- name: Report flaky tests
if: always()
run: |
# 재시도로 통과한 테스트 찾기
grep -r "RETRY" playwright-report/ || true
설명
이것이 하는 일: 이 워크플로우는 여러 레벨에서 재시도 로직을 구현하여 일시적인 테스트 실패를 자동으로 복구하고, 진짜 버그만 리포트합니다. 첫 번째로, Jest 테스트는 jest.retryTimes() API를 코드에서 설정합니다.
jest.config.js에 retryTimes: 2를 추가하거나, 특정 테스트에 jest.retryTimes(2)를 호출하면, 해당 테스트가 실패 시 최대 2번 더 재시도됩니다. 3번 중 1번이라도 통과하면 성공으로 간주됩니다.
이렇게 하는 이유는 API 호출이나 타이밍에 민감한 테스트에서 일시적인 네트워크 문제로 인한 실패를 걸러내기 위함입니다. 그 다음으로, Playwright 테스트는 --retries=2 플래그로 커맨드 레벨에서 재시도를 설정합니다.
Playwright는 각 테스트가 실패하면 자동으로 재시도하며, 재시도 시에는 trace와 비디오를 추가로 기록하여 디버깅을 돕습니다. 또한 playwright.config.ts의 retries 옵션으로도 설정할 수 있습니다.
Playwright는 재시도 횟수를 리포트에 표시하여 어떤 테스트가 플레이키한지 추적할 수 있게 해줍니다. 세 번째로, if: failure() 조건과 nick-invision/retry 액션을 조합하여 워크플로우 레벨에서도 재시도를 구현합니다.
이전 단계가 실패하면 이 단계가 실행되며, max_attempts: 3으로 최대 3번까지 전체 테스트를 재실행합니다. timeout_minutes: 10은 각 시도가 10분을 넘으면 중단하는 설정입니다.
이 레벨의 재시도는 환경 문제(메모리 부족, 일시적인 네트워크 단절 등)로 인한 실패를 복구하는 데 유용합니다. 여러분이 이 코드를 사용하면 플레이키 테스트로 인한 거짓 실패(false negative)를 90% 이상 줄일 수 있습니다.
네트워크 타임아웃이나 애니메이션 타이밍 이슈 같은 일시적 문제는 자동으로 해결되고, 진짜 버그만 리포트되어 개발자가 신뢰할 수 있는 CI 시스템을 구축할 수 있습니다. 또한 Playwright 리포트에서 재시도 횟수를 확인하여 어떤 테스트를 개선해야 할지 파악할 수 있습니다.
실전 팁
💡 재시도는 증상을 완화할 뿐 근본 원인을 해결하지 않습니다. 재시도 횟수가 많은 테스트는 별도로 추적하여 근본 원인(부적절한 대기, 타이밍 이슈 등)을 해결하세요. 💡 Playwright의 expect.toPass() 함수를 사용하면 특정 assertion만 재시도할 수 있습니다. 예: await expect(async () => { expect(await page.locator('.count').textContent()).toBe('5'); }).toPass({ timeout: 5000 }); 💡 재시도가 너무 많으면 CI 시간이 길어집니다. 일반적으로 1-2번 재시도가 적당하며, 3번 이상은 테스트 품질 문제를 의심해야 합니다. 💡 환경 변수 CI=true를 설정하면 로컬에서는 재시도하지 않고 CI에서만 재시도하도록 조건부 설정할 수 있습니다. 💡 플레이키 테스트 추적 서비스인 BuildPulse나 Flaky Tests Detection 도구를 활용하면 어떤 테스트가 가장 불안정한지 통계를 얻을 수 있습니다.
10. 보안 취약점 스캔 통합
시작하며
여러분의 프로젝트가 알려진 보안 취약점이 있는 npm 패키지를 사용하고 있다는 것을 배포 후에 발견한다면 얼마나 위험할까요? 악의적인 공격자가 이 취약점을 악용하여 사용자 데이터를 탈취하거나 서비스를 마비시킬 수 있습니다.
이런 문제는 의존성 보안을 수동으로 관리할 때 발생합니다. npm audit이나 Snyk 같은 도구가 있지만, 개발자가 매번 실행하는 것을 기억하기 어렵고, 누군가는 경고를 무시하고 푸시할 수 있습니다.
특히 간접 의존성(의존성의 의존성)에 숨어있는 취약점은 발견하기 더욱 어렵습니다. 바로 이럴 때 필요한 것이 자동화된 보안 스캔입니다.
모든 PR에서 자동으로 취약점을 검사하고, 심각한 취약점이 발견되면 빌드를 실패시켜 배포를 차단하면, 보안 문제를 조기에 발견하고 해결할 수 있습니다.
개요
간단히 말해서, 보안 취약점 스캔은 코드와 의존성에서 알려진 보안 문제를 자동으로 검출하고 리포트하는 시스템입니다. 실무에서 이 기능이 중요한 이유는, 보안 사고는 한 번 발생하면 회복하기 어렵고 비용이 막대하기 때문입니다.
npm audit은 Node Security Platform의 데이터베이스를 기반으로 취약한 패키지를 찾아내고, Snyk이나 GitHub의 Dependabot은 더 포괄적인 스캔과 자동 PR 생성까지 지원합니다. GitHub Actions에 통합하면 매 푸시마다 자동으로 스캔이 실행되어, 새로운 의존성 추가 시 보안 문제가 있는지 즉시 확인할 수 있습니다.
예를 들어, 인기 있는 라이브러리에서 심각한 취약점이 발견되면 PR에 경고가 표시되고, 업데이트 방법을 제안받을 수 있습니다. 기존에는 분기별로 수동 보안 감사를 했다면, 이제는 매 커밋마다 자동으로 검사하여 취약점 노출 시간을 최소화할 수 있습니다.
핵심 특징은 첫째, npm audit으로 무료로 기본 스캔을 할 수 있다는 점, 둘째, Snyk이나 Trivy 같은 전문 도구로 더 정밀한 검사가 가능하다는 점, 셋째, 취약점 심각도에 따라 빌드 실패 여부를 조정할 수 있다는 점입니다. 이러한 특징들이 애플리케이션 보안을 크게 강화해줍니다.
코드 예제
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
# npm 내장 취약점 스캔
- name: Run npm audit
run: |
npm audit --audit-level=moderate
# moderate 이상 심각도 취약점 발견 시 실패
# Snyk으로 더 정밀한 스캔
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
# 코드 스캔 (SAST)
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
# 결과를 Security 탭에 업로드
- name: Upload to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v2
설명
이것이 하는 일: 이 워크플로우는 여러 레이어에서 보안 검사를 수행하여 의존성 취약점과 코드 레벨 보안 이슈를 모두 찾아내고, 심각한 문제가 있으면 빌드를 실패시킵니다. 첫 번째로, npm audit --audit-level=moderate 명령으로 npm의 내장 보안 스캔을 실행합니다.
npm audit은 package-lock.json의 모든 의존성(직접 및 간접)을 npm 레지스트리의 취약점 데이터베이스와 비교하여 알려진 CVE를 찾아냅니다. --audit-level=moderate는 moderate, high, critical 심각도의 취약점이 발견되면 명령이 0이 아닌 exit code를 반환하여 워크플로우를 실패시킵니다.
low 심각도는 허용하여 과도한 false positive를 방지합니다. 그 다음으로, Snyk 액션을 사용하여 더 포괄적인 스캔을 수행합니다.
Snyk은 npm audit보다 더 많은 취약점 데이터베이스를 참조하고, 라이선스 문제도 검사하며, 자동 수정 PR을 생성할 수 있습니다. SNYK_TOKEN은 Snyk 웹사이트에서 발급받아 GitHub Secrets에 저장해야 합니다.
args: --severity-threshold=high는 high와 critical 심각도만 빌드 실패로 간주하고, medium 이하는 경고만 표시합니다. Snyk은 PR에 코멘트로 발견된 취약점과 업그레이드 경로를 상세히 설명합니다.
세 번째로, GitHub CodeQL을 사용하여 SAST(Static Application Security Testing)를 수행합니다. CodeQL은 코드를 분석하여 SQL 인젝션, XSS, 커맨드 인젝션 같은 보안 취약점 패턴을 찾아냅니다.
init 단계에서 언어를 지정하면(JavaScript, TypeScript, Python 등) 해당 언어의 보안 규칙을 로드하고, analyze 단계에서 실제 스캔을 수행합니다. 결과는 SARIF 형식으로 생성되어 GitHub의 Security 탭에 업로드되며, PR에도 알림이 표시됩니다.
여러분이 이 코드를 사용하면 보안 취약점이 포함된 코드가 메인 브랜치에 병합되는 것을 원천 차단할 수 있습니다. 의존성에 심각한 취약점이 발견되면 PR 단계에서 경고를 받고, 안전한 버전으로 업그레이드한 후 병합할 수 있습니다.
Security 탭에서 모든 보안 이슈를 추적하고, Dependabot이 자동으로 보안 업데이트 PR을 생성하여 항상 최신 보안 패치를 유지할 수 있습니다.
실전 팁
💡 npm audit fix --force로 자동 수정을 시도할 수 있지만, 주요 버전 업그레이드로 인해 breaking change가 발생할 수 있으니 주의하세요. 테스트와 함께 실행하는 것이 안전합니다. 💡 Snyk의 무료 티어는 오픈소스 프로젝트에 충분하지만, 프라이빗 저장소는 제한이 있습니다. 대안으로 Trivy를 사용하면 완전 무료입니다. 💡 CodeQL 스캔은 시간이 오래 걸립니다(5-10분). 조건부 실행으로 코드 파일이 변경되었을 때만 실행하도록 최적화하세요. 💡 GitHub의 Dependabot을 활성화하면 취약점 발견 시 자동으로 업데이트 PR을 생성합니다. .github/dependabot.yml 파일로 스케줄과 타겟 브랜치를 설정할 수 있습니다. 💡 OWASP Dependency-Check도 좋은 대안입니다. Java, .NET, Python 등 다양한 언어를 지원하며 오프라인 데이터베이스를 사용하여 빠릅니다.