Skip to main content

Pull Request마다 테스트 실행하기

Views 90약 6분생성형 AI 활용
Summary

PR마다 자동으로 검사해야 하는 이유

Pull Request는 main 브랜치에 코드를 합치기 전 마지막 검토 구간입니다. 이때 사람이 코드만 읽고 모든 문제를 찾기는 어렵습니다. 테스트를 깜빡했을 수도 있고, 내 컴퓨터에서는 통과했지만 다른 환경에서는 실패할 수도 있습니다.

그래서 실무에서는 PR이 열릴 때마다 자동으로 테스트와 린트를 실행합니다. 이것을 보통 CI라고 부릅니다. CI는 Continuous Integration의 줄임말로, 코드를 자주 합치기 전에 자동으로 빌드, 테스트, 코드 스타일 검사를 수행하는 흐름입니다. GitHub Actions를 사용하면 이런 CI 결과가 PR 화면에 표시되어, 변경 사항이 문제를 만들었는지 병합 전에 확인할 수 있습니다.1

핵심은 단순합니다.

자동 테스트는 main 브랜치에 문제가 들어가기 전에 막아주는 안전장치입니다.

이번 장에서는 Node.js 프로젝트를 기준으로, PR이 열릴 때마다 npm test와 npm run lint를 자동 실행하는 기본 CI 파이프라인을 만들겠습니다.

Pull Request마다 테스트 실행하기

워크플로우 파일 만들기

GitHub Actions 워크플로우 파일은 저장소의 다음 경로에 둡니다.

.github/workflows/pr-ci.yml

프로젝트 루트에 .github 폴더가 없다면 직접 만들면 됩니다.

my-project/
├─ .github/
│  └─ workflows/
│     └─ pr-ci.yml
├─ package.json
├─ package-lock.json
└─ src/

이제 pr-ci.yml 파일에 다음 내용을 작성합니다.

name: PR CI

on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

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

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

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

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Run lint
        run: npm run lint

이 파일 하나가 PR용 CI의 기본 형태입니다. 코드를 올리고 PR을 만들면 GitHub가 임시 실행 환경을 만들고, 위 단계들을 순서대로 실행합니다.

설정을 한 줄씩 이해하기

먼저 워크플로우 이름입니다.

name: PR CI

GitHub의 Actions 탭과 PR의 Checks 영역에 표시되는 이름입니다. 팀원이 봤을 때 무슨 검사인지 알 수 있도록 짧고 명확하게 짓는 것이 좋습니다.

다음은 실행 조건입니다.

on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

이 설정은 main 브랜치로 향하는 PR에서 워크플로우를 실행하겠다는 뜻입니다.

  • opened: PR이 처음 열렸을 때

  • synchronize: PR 브랜치에 새 커밋이 추가됐을 때

  • reopened: 닫혔던 PR이 다시 열렸을 때

즉, 처음 PR을 만들 때 한 번 실행되고, 이후 코드를 고쳐서 다시 push하면 또 실행됩니다. 리뷰 중 발견한 문제를 수정해도 CI가 다시 확인해 주는 구조입니다.

다음은 job입니다.

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

jobs는 실제로 실행할 작업 묶음입니다. 여기서는 test-and-lint라는 작업 하나를 만들었습니다. runs-on: ubuntu-latest는 이 작업을 Ubuntu 리눅스 환경에서 실행하겠다는 뜻입니다. 대부분의 웹 프로젝트는 이 설정으로 충분히 시작할 수 있습니다.

step은 실제 실행 순서다

job 안의 steps는 위에서 아래로 실행됩니다.

첫 단계는 코드를 내려받는 일입니다.

- name: Checkout code
  uses: actions/checkout@v4

GitHub Actions의 실행 환경은 처음에는 빈 컴퓨터와 비슷합니다. 따라서 먼저 현재 PR의 코드를 가져와야 합니다. 이때 자주 쓰는 공식 액션이 actions/checkout입니다.

다음은 Node.js 실행 환경을 준비합니다.

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

이 단계는 CI 서버에 Node.js 20 버전을 설치합니다. cache: npm은 npm 의존성 설치 속도를 높이기 위한 캐시 설정입니다. 처음 실행할 때는 크게 차이가 없지만, 이후 실행부터는 시간이 줄어들 수 있습니다.

다음은 의존성 설치입니다.

- name: Install dependencies
  run: npm ci

여기서 npm install 대신 npm ci를 사용했습니다. npm ci는 package-lock.json에 기록된 버전을 기준으로 의존성을 설치합니다. 그래서 CI처럼 매번 같은 결과가 중요할 때 더 적합합니다.

마지막으로 테스트와 린트를 실행합니다.

- name: Run tests
  run: npm test

- name: Run lint
  run: npm run lint

npm test는 테스트 코드를 실행합니다. npm run lint는 코드 스타일이나 기본적인 문법 문제를 검사합니다. 예를 들어 JavaScript나 TypeScript 프로젝트에서는 ESLint를 많이 사용합니다.

이 명령들은 여러분의 package.json에 정의되어 있어야 합니다.

{
  "scripts": {
    "test": "jest",
    "lint": "eslint ."
  }
}

만약 npm run lint를 실행했는데 Missing script: lint 같은 오류가 난다면, 아직 package.json에 lint 스크립트가 없다는 뜻입니다.

실패하면 어떻게 보일까

GitHub Actions에서 각 step은 명령의 종료 코드를 기준으로 성공과 실패를 판단합니다. 쉽게 말해 명령이 정상 종료되면 성공, 오류와 함께 종료되면 실패입니다.

예를 들어 테스트 하나가 깨져서 npm test가 실패하면 그 뒤의 워크플로우도 실패로 표시됩니다. PR 화면의 Checks 영역에는 빨간 표시가 생기고, 실패한 항목 옆의 Details를 누르면 로그를 볼 수 있습니다.

초보자에게 중요한 습관은 이것입니다.

  1. PR 화면에서 실패한 Check를 확인한다.

  2. Details를 눌러 어느 step에서 실패했는지 본다.

  3. 로그에서 에러 메시지를 찾는다.

  4. 코드를 수정한다.

  5. 같은 브랜치에 다시 push한다.

  6. CI가 다시 실행되는지 확인한다.

CI 실패는 혼나는 신호가 아닙니다. main 브랜치에 문제가 들어가기 전에 미리 발견했다는 좋은 신호입니다.

실전 예시: 일부러 실패시켜 보기

처음 CI를 만들었다면 한 번은 일부러 실패시켜 보는 것이 좋습니다. 그래야 실제 문제가 생겼을 때 어디를 봐야 하는지 익숙해집니다.

예를 들어 Jest를 쓰는 프로젝트라면 다음처럼 실패하는 테스트를 하나 만들어 볼 수 있습니다.

// sum.test.js
test('1 + 1은 3이다', () => {
  expect(1 + 1).toBe(3);
});

이 상태로 브랜치를 push하고 PR을 만들면 npm test 단계가 실패합니다. PR 화면에는 빨간 X 표시가 나타납니다. 로그를 열어 보면 기대한 값은 3인데 실제 값은 2라는 식의 메시지를 확인할 수 있습니다.

이후 테스트를 올바르게 고칩니다.

test('1 + 1은 2이다', () => {
  expect(1 + 1).toBe(2);
});

다시 커밋하고 push하면 같은 PR에서 CI가 다시 실행됩니다. 이번에는 테스트가 통과하고 초록 체크가 표시됩니다.

git add sum.test.js
git commit -m "test: fix sum test"
git push

이 흐름이 실무에서 매일 반복되는 기본 패턴입니다.

Java 프로젝트라면 이렇게 바꾼다

Node.js가 아니라 Java 프로젝트라면 실행 환경과 명령만 바꾸면 됩니다. 예를 들어 Gradle 기반 프로젝트라면 다음처럼 작성할 수 있습니다.

name: PR CI

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Grant execute permission for Gradle
        run: chmod +x ./gradlew

      - name: Run tests
        run: ./gradlew test

핵심 구조는 같습니다.

  1. 코드를 내려받는다.

  2. 실행 환경을 준비한다.

  3. 의존성을 설치하거나 빌드 도구를 준비한다.

  4. 테스트와 검사를 실행한다.

언어가 달라도 CI의 뼈대는 크게 다르지 않습니다.

CI가 성공해야만 병합되게 만들기

여기까지 설정하면 PR마다 테스트는 자동으로 실행됩니다. 하지만 저장소 설정에 따라 CI가 실패해도 사람이 강제로 병합할 수 있습니다. 실무에서는 보통 main 브랜치를 보호해서, CI가 성공한 PR만 병합되도록 만듭니다.

GitHub 저장소에서 다음 경로로 이동합니다.

Settings → Branches → Branch protection rules

또는 최신 화면에서는 ruleset 메뉴로 보일 수 있습니다. 여기서 main 브랜치에 대해 다음과 같은 규칙을 설정합니다.

  • 보호할 브랜치 이름: main

  • Require status checks to pass before merging 활성화

  • 필수 체크로 PR CI 또는 test-and-lint 선택

  • 필요하면 Require pull request reviews before merging도 활성화

이렇게 하면 CI가 실패한 PR은 병합할 수 없습니다. 리뷰어가 실수로 Merge 버튼을 누르더라도 GitHub가 한 번 더 막아줍니다.

자동화의 가치는 여기서 커집니다. "테스트 꼭 돌리고 머지하세요"라고 말로 약속하는 대신, 저장소 규칙으로 지키게 만드는 것입니다.

필요하면 PR 댓글도 추가할 수 있다

기본적으로는 Checks 영역과 Actions 로그만으로 충분합니다. 하지만 팀에 따라 테스트 결과를 PR 댓글로 남기기도 합니다. 예를 들어 테스트 성공 시 "테스트가 통과했습니다", 실패 시 "로그를 확인해 주세요" 같은 메시지를 자동으로 남길 수 있습니다.

다만 초보 단계에서는 처음부터 댓글 자동화까지 붙일 필요는 없습니다. 먼저 아래 세 가지만 안정적으로 만드는 것이 중요합니다.

  • PR이 열릴 때 CI가 실행된다.

  • 테스트와 린트 실패가 PR에 표시된다.

  • CI가 실패하면 main에 병합할 수 없다.

이 세 가지가 갖춰지면 이미 기본적인 실무 CI는 시작된 것입니다.

이번 장의 정리

PR마다 테스트를 실행하는 CI는 협업의 안전벨트입니다. 코드를 작성한 사람도, 리뷰하는 사람도, main 브랜치를 사용하는 팀원도 더 안심할 수 있습니다.

이번 장에서 만든 기본 흐름은 다음과 같습니다.

PR 생성
→ GitHub Actions 실행
→ 코드 checkout
→ 실행 환경 준비
→ 의존성 설치
→ 테스트 실행
→ 린트 실행
→ 결과를 PR Checks에 표시
→ 성공한 PR만 병합

처음에는 npm test, npm run lint 정도만 자동화해도 충분합니다. 이후 프로젝트가 커지면 빌드 검사, 보안 검사, 테스트 리포트, 배포 미리보기 같은 단계를 추가하면 됩니다.

중요한 것은 한 번에 완벽한 파이프라인을 만들려는 것이 아닙니다. main 브랜치를 지키는 가장 작은 자동 검사부터 시작하는 것입니다.

참고

1Continuous integration - GitHub Docs

책 목록

GitHub 첫걸음부터 실무 협업까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기