GitHub Actions로 테스트 자동화하기




5편까지 4가지 테스트 유형을 모두 작성하는 방법을 배웠다. 하지만 테스트를 로컬에서만 돌리면 결국 “깜빡하고 안 돌리는 날”이 온다. 바쁠 때, 급한 핫픽스를 배포할 때, “이 정도는 괜찮겠지” 싶을 때. 그리고 그날 버그가 터진다.

테스트의 진짜 힘은 자동화에서 나온다. 코드를 push하거나 PR을 올릴 때마다 모든 테스트가 자동으로 실행되고, 실패하면 머지가 차단되는 환경을 만들어야 한다. 이번 편에서는 GitHub Actions를 사용해서 이 파이프라인을 구축한다.

CI/CD란

본격적으로 들어가기 전에 용어를 정리하자.

CI(Continuous Integration)는 코드 변경을 자주 통합하고, 통합할 때마다 자동으로 빌드와 테스트를 실행하는 것이다. “코드를 합치기 전에 문제가 없는지 자동으로 확인한다”는 의미다.

CD(Continuous Deployment 또는 Continuous Delivery)는 CI를 통과한 코드를 자동으로 배포하는 것이다. Vercel을 쓰고 있다면 main 브랜치에 머지하는 순간 자동 배포되므로, 이미 CD를 하고 있는 셈이다.

이번 편에서 구축하는 것은 CI 파이프라인이다. push/PR 시 테스트를 자동 실행하고, 실패하면 알려주는 것이다.

GitHub Actions란

GitHub 저장소에 코드를 push하거나 PR을 올리면, GitHub 서버에서 자동으로 원하는 작업을 실행해주는 기능이다. 별도의 서버를 준비할 필요 없이, YAML 파일 하나만 작성하면 된다. 무료 플랜에서도 월 2,000분까지 사용할 수 있어서 개인 프로젝트나 소규모 팀에게 충분하다.

기본 구조 이해하기

GitHub Actions의 YAML 파일은 세 가지 개념으로 구성된다.

Workflow는 전체 자동화 프로세스다. .github/workflows/ 폴더 안의 YAML 파일 하나가 워크플로우 하나다.

Job은 워크플로우 안의 독립적인 작업 단위다. 각 Job은 별도의 서버에서 실행되므로, 서로 영향을 주지 않는다.

Step은 Job 안의 개별 실행 단계다. 명령어 하나하나가 Step이다.

Workflow (ci.yml)
  └── Job (lint-and-test)
        ├── Step: 코드 가져오기
        ├── Step: Node.js 설치
        ├── Step: 패키지 설치
        ├── Step: 린트 검사
        └── Step: 테스트 실행
  └── Job (e2e)
        ├── Step: 코드 가져오기
        ├── Step: Playwright 설치
        └── Step: E2E 테스트 실행

Step 1: 폴더와 파일 생성

프로젝트 루트에 .github/workflows/ 폴더를 만들고 YAML 파일을 생성한다.

mkdir -p .github/workflows
touch .github/workflows/ci.yml
my-project/
├── .github/
│   └── workflows/
│       └── ci.yml    ← 이 파일을 만든다
├── src/
├── e2e/
├── package.json
└── ...

Step 2: 기본 워크플로우 작성

먼저 정적 테스트와 단위/통합 테스트만 포함한 기본 버전부터 작성하자.

# .github/workflows/ci.yml

name: CI

# 언제 실행할지
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest

    steps:
      # 저장소 코드를 가져온다
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      # Node.js를 설치한다
      - name: Node.js 설치
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      # 의존성을 설치한다
      - name: 패키지 설치
        run: npm ci

      # 정적 테스트
      - name: 린트 검사
        run: npm run lint

      - name: 타입 검사
        run: npx tsc --noEmit

      # 단위 + 통합 테스트
      - name: 테스트 실행
        run: npm run test:ci

각 항목을 살펴보자.

on 블록은 트리거를 정의한다. main 브랜치에 push하거나 main으로 향하는 PR이 올라올 때 실행된다.

runs-on: ubuntu-latest는 GitHub이 제공하는 리눅스 서버에서 실행하겠다는 의미다. 매 실행마다 깨끗한 서버가 할당되므로, 이전 실행의 잔여물이 남아있을 걱정이 없다.

cache: 'npm'은 node_modules를 캐싱해서 다음 실행 때 설치 시간을 줄여준다. 이 한 줄로 1~2분을 아낄 수 있다.

npm cinpm install과 비슷하지만, package-lock.json을 기준으로 정확한 버전을 설치하고 기존 node_modules를 삭제한 뒤 새로 설치한다. CI 환경에서는 항상 npm ci를 쓰는 것이 원칙이다.

Step 3: E2E 테스트 추가

E2E 테스트는 브라우저를 설치해야 하고 실행 시간이 길기 때문에, 별도의 Job으로 분리하는 것이 좋다.

name: CI

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

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      - name: Node.js 설치
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: 패키지 설치
        run: npm ci

      - name: 린트 검사
        run: npm run lint

      - name: 타입 검사
        run: npx tsc --noEmit

      - name: 테스트 실행
        run: npm run test:ci

  e2e:
    runs-on: ubuntu-latest
    needs: lint-and-test
    steps:
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      - name: Node.js 설치
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: 패키지 설치
        run: npm ci

      - name: Playwright 브라우저 설치
        run: npx playwright install --with-deps

      - name: 빌드
        run: npm run build

      - name: E2E 테스트 실행
        run: npx playwright test

      # 실패 시 디버깅용 아티팩트 업로드
      - name: 테스트 결과 업로드
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: |
            playwright-report/
            test-results/
          retention-days: 7

여기서 핵심은 needs: lint-and-test다. 이 설정으로 lint-and-test Job이 통과해야만 e2e Job이 실행된다. 린트나 단위 테스트에서 이미 실패했다면 E2E를 돌릴 필요가 없으므로, 시간과 비용을 절약할 수 있다.

E2E에서 npm run build를 먼저 실행하는 이유는, 프로덕션 빌드 기준으로 테스트해야 실제 배포 환경과 동일한 조건이 되기 때문이다. 개발 서버(npm run dev)로 테스트하면 빌드 시에만 발생하는 문제를 놓칠 수 있다.

if: failure() 조건이 붙은 마지막 Step은 테스트가 실패했을 때만 실행된다. Playwright의 리포트와 스크린샷이 GitHub Actions의 Artifacts 탭에 업로드되어, 실패 원인을 분석할 수 있다. retention-days: 7로 7일 후 자동 삭제되도록 설정해서 저장 공간을 관리한다.

Step 4: push하면 끝

파일을 커밋하고 push하면 GitHub이 알아서 실행한다.

git add .github/workflows/ci.yml
git commit -m "CI 파이프라인 설정"
git push

GitHub 저장소의 Actions 탭에서 실행 상태를 확인할 수 있다. 초록색 체크가 뜨면 모든 테스트가 통과한 것이고, 빨간색 X가 뜨면 어딘가에서 실패한 것이다. 클릭하면 어떤 Step에서 실패했는지 로그를 볼 수 있다.

PR을 올리면 PR 페이지 하단에도 테스트 통과 여부가 표시된다. 코드 리뷰어는 테스트가 통과했는지 한눈에 확인할 수 있다.

Step 5: PR 머지 차단 설정

테스트가 실패해도 머지가 가능하면 안전망에 구멍이 생긴다. 테스트를 통과해야만 머지할 수 있도록 설정하자.

GitHub 저장소에서 Settings → Branches로 이동한다. Add branch protection rule을 클릭하고, Branch name pattern에 main을 입력한다. “Require status checks to pass before merging”을 체크하고, 검색창에서 lint-and-teste2e를 찾아 추가한다. Save changes를 누르면 완료다.

이제 테스트가 실패한 PR은 머지 버튼이 비활성화된다. 누구도 테스트를 우회해서 코드를 합칠 수 없다.

환경변수 설정

테스트에 환경변수가 필요한 경우가 있다. 데이터베이스 URL, API 키 같은 것들이다. 이런 민감한 정보는 코드에 직접 넣지 않고 GitHub Secrets에 저장한다.

GitHub 저장소에서 Settings → Secrets and variables → Actions로 이동해서 New repository secret을 클릭한다. 이름과 값을 입력하고 저장한다.

YAML에서는 이렇게 참조한다.

- name: 테스트 실행
  run: npm run test:ci
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    API_KEY: ${{ secrets.API_KEY }}

Secrets에 저장된 값은 로그에도 마스킹되어 표시되므로 안전하다.

실행 최적화

프로젝트가 커지면 CI 실행 시간이 길어진다. 몇 가지 최적화 방법을 알아두면 좋다.

특정 파일 변경 시에만 실행

문서만 수정했는데 전체 테스트가 돌아갈 필요는 없다. 경로 필터를 추가하면 관련 파일이 변경되었을 때만 실행된다.

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'e2e/**'
      - 'package.json'
      - 'package-lock.json'
  pull_request:
    branches: [main]
    paths:
      - 'src/**'
      - 'e2e/**'
      - 'package.json'
      - 'package-lock.json'

README나 문서를 수정한 커밋에서는 CI가 실행되지 않아 시간을 절약할 수 있다.

E2E 브라우저 캐싱

Playwright 브라우저 다운로드는 매번 시간이 걸린다. 캐싱하면 두 번째 실행부터 빨라진다.

- name: Playwright 브라우저 캐시
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ hashFiles('package-lock.json') }}

- name: Playwright 브라우저 설치
  run: npx playwright install --with-deps

캐시가 있으면 설치를 건너뛰고, 없거나 버전이 바뀌었으면 새로 설치한다.

E2E 브라우저 줄이기

CI에서 세 브라우저(Chromium, Firefox, WebKit)를 모두 돌리면 시간이 3배 걸린다. 현실적으로 CI에서는 Chromium만 돌리고, 크로스 브라우저 테스트는 배포 전 수동으로 한 번 확인하는 팀이 많다.

playwright.config.ts에서 CI 환경일 때 브라우저를 제한할 수 있다.

projects: process.env.CI
  ? [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }]
  : [
      { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
      { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
      { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    ],

전체 파이프라인 흐름 정리

지금까지 설정한 전체 흐름을 정리하면 이렇다.

개발자가 코드를 수정하고 push 또는 PR을 올림
  ↓
GitHub Actions가 자동으로 실행됨
  ↓

[lint-and-test Job]

├── ESLint 검사 (정적 테스트) ├── TypeScript 타입 검사 (정적 테스트) └── Vitest 실행 (단위 + 통합 테스트) ↓ 통과하면

[e2e Job]

└── Playwright 실행 (E2E 테스트) ↓ 모두 통과 → PR 머지 가능 하나라도 실패 → PR 머지 차단, 실패 로그와 스크린샷 확인 가능

코드가 push되는 순간부터 머지까지, 사람의 개입 없이 4가지 테스트가 모두 자동으로 실행된다. 이것이 테스트 자동화의 완성된 형태다.

다른 CI/CD 도구들

GitHub Actions 외에도 선택지가 있다. 상황에 따라 더 적합한 도구가 있을 수 있으니 간단히 비교한다.

Vercel은 Next.js 배포 플랫폼이다. git push만 하면 자동 빌드와 배포가 이루어지고, PR마다 미리보기 URL을 생성해준다. 다만 Vercel은 빌드 과정에서 TypeScript 에러나 린트 에러를 잡아줄 뿐, vitest나 Playwright 같은 테스트를 직접 실행하는 기능은 없다. 그래서 GitHub Actions에서 테스트를 돌리고, Vercel에서 배포하는 조합이 가장 일반적이다.

GitLab CI/CD는 GitLab을 쓰고 있다면 자연스러운 선택이다. .gitlab-ci.yml 파일로 설정하며 GitHub Actions와 개념이 거의 동일하다.

Jenkins는 오래된 대표적인 CI/CD 도구다. 직접 서버에 설치해서 운영하므로 자유도가 높지만 설치와 관리가 번거롭다. 대기업에서 온프레미스 환경으로 많이 쓴다.

CircleCI는 클라우드 기반 CI/CD로 설정이 간결하고 속도가 빠르다. 무료 플랜도 있다.

AWS CodeBuild는 AWS 인프라를 이미 쓰고 있다면 고려할 만하다. AWS 서비스들과 연동이 자연스럽지만 설정이 복잡하다.

어떤 도구를 쓰든 핵심은 같다. “코드가 변경되면 자동으로 테스트를 실행하고, 실패하면 알려준다.” 도구는 팀이 이미 쓰고 있는 인프라에 맞추면 되고, 새로 시작한다면 GitHub Actions가 가장 무난하다.

다음 편 예고

CI/CD 파이프라인까지 구축했다. 이제 테스트를 작성하고 자동화하는 기술적인 부분은 모두 다뤘다. 마지막 편에서는 실무에서 테스트를 지속적으로 유지하기 위한 전략을 다룬다. 무엇부터 테스트해야 하는지, 커버리지는 얼마나 추구해야 하는지, 테스트가 느려지면 어떻게 해야 하는지, 그리고 팀에 테스트를 점진적으로 도입하는 로드맵을 정리한다.




댓글 남기기