[GitHub Actions] Jest 실행 Action 성능 개선하기

2024-11-11 hit count image

GitHub Actions에서 Jest를 실행하는 Action의 성능을 개선하는 방법에 대해서 알아보겠습니다.

개요

회사에서 테스트 코드가 많아지면서 GitHub Actions에서 코드를 체크하는 Action에 시간이 오래 걸리는 문제가 발생했습니다. 이 문제를 해결하기 위해 Jest를 실행하는 Action의 성능을 개선한 내용을 공유하고자 합니다.

문제점

현재 다음과 같은 Action를 사용하여 PR을 체크하고 있습니다.

jobs:
  check-code:
    name: Check Code
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Prettier
        run: yarn format
      - name: CSpell
        run: yarn cspell
      - name: ESLint
        run: yarn lint
      - name: Stylelint
        run: yarn stylelint
      - name: Test
        run: |
          yarn test:ci
          yarn test:storybook
      - name: Build
        run: |
          yarn build
      - name: Build SCSS
        run: |
          yarn typegen:scss

          # Get the changed files
          CHANGED_FILES=$(git diff --name-only HEAD)
          # Check if there are changes in the generated files
          if [ -n "$CHANGED_FILES" ]; then
            echo "Error: There are changes in the following files: $CHANGED_FILES"
            exit 1
          fi

PR이 생성된면 Prettier, CSpell, ESLint, Stylelint, Test, Build, Build SCSS를 실행하고 있습니다. 이 Action는 다음과 같이 약 25분 정도 소요됩니다.

GitHub Actions - Improve Jest test performance

성능 개선

PR을 생성할 때마다 매번 25분이 소요되는 것은 매우 비효율적입니다. 이번 블로그 포스트에서는 Jest를 실행하는 Action의 성능을 개선하는 방법에 대해서 알아보겠습니다.

Dependencies Cache

가장 먼저 수행한 것은 yarn install로 설치되는 Dependencies를 캐시하는 것 입니다. 이를 통해 Dependencies를 다시 설치하는 시간을 줄일 수 있습니다.

이 부분은 다른 Acions에서도 사용할 수 있기 때문에 별도의 Composite Action로 만들었습니다. Composite Action에 대해서는 다음 링크를 참고해 주시기 바랍니다.

캐시는 actions/cache를 사용하여 캐시하였습니다.

actions/cache를 사용하여 Dependencies를 캐시하는 Composite Action는 다음과 같습니다.


name: 'Install Dependencies'
description: 'Install Dependencies'
runs:
  using: 'composite'
  steps:
    - name: Setup node
      uses: actions/setup-node@v4
      with:
        node-version: 20.3.0
    - name: Install dependencies
    - name: Enable Yarn 3.7.0
      shell: bash
      run: corepack enable
    - name: Get yarn cache directory path
      shell: bash
      id: yarn-cache-dir-path
      run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
    - uses: actions/cache@v4
      id: yarn-cache
      with:
        path: |
          node_modules
          **/node_modules
          ${{ steps.yarn-cache-dir-path.outputs.dir }}
        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
        restore-keys: |
          ${{ runner.os }}-yarn-
    - name: Install dependencies
      if: steps.yarn-cache.outputs.cache-hit != 'true'
      shell: bash
      run: yarn install --frozen-lockfile

저희 팀에서는 Yarn3.7.0 버전을 사용하고 있습니다. 그래서 corepack enable을 추가하고, Yarn 3.7.0의 캐시 폴더를 가져와서 node_modules와 함께 캐시하였습니다.

또한 프로젝트가 모노레포이기 때문에 **/node_modules를 사용하여 하위 프로젝트에 있는 node_modules도 함께 캐시하였습니다.

모노레포가 아니고, Yarn 3.7.0을 사용하고 있지않다면, 공식 문서를 참고하여 적절한 설정을 해주시기 바랍니다.

이를 통해 아주 조금 성능이 개선되었습니다.

  • Before: 1m 25s
GitHub Actions - Before using actions/cache for dependencies
  • After: 9s
GitHub Actions - After using actions/cache for dependencies

Actions 분리

Prettier, CSpell, ESLint, Stylelint, Test, Build, Build SCSS를 모두 한 Action에서 실행하고 있습니다. 이 중에서 Test를 실행하는 부분이 가장 시간이 오래걸렸습니다.

그래서 Test가 실행되는 동안 다른 Actions를 수행할 수 있도록 Actions를 분리하였습니다.

jobs:
  cspell:
    name: CSpell
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: CSpell
        run: yarn cspell
  remark:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Remark-lint
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Remark-lint
        run: yarn remark:service_1
  eslint:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: ESLint
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: ESLint
        run: yarn lint:service_1
  stylelint:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Stylelint
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Stylelint
        run: yarn stylelint:service_1
  build:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Build
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Build
        run: yarn build:service_1
  test-service_1:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Test service_1
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Test
        run: yarn test:ci:service_1

모노레포를 사용하고 있기 때문에 이와 같은 GitHub Actions가 서비스별로 존재하고 있으며, if문을 사용하여 해당 서비스에만 실행되도록 하였습니다.

분리하기 전, 이 Action는 약 25분 정도 소요되었습니다.

GitHub Actions - Before sperating actions

분리한 후, 이 Action는 약 13분 정도로 성능이 개선되었습니다.

GitHub Actions - After sperating actions

Jest의 bail

Jestbail 옵션을 사용하면, 테스트 중에 하나라도 테스트가 실패하면 테스트를 중지하도록 설정할 수 있습니다.

이 옵션을 추가하면 테스트가 실패했을 때, 모든 테스트를 실행하지 않고 바로 중지하게 되어 시간을 단축할 수 있습니다.

Jestbail 옵션은 설정하기 위해 jest를 실행하는 명령어가 포함된 package.json 파일을 열고 다음과 같이 수정합니다.

{
  ...
  "scripts": {
    ...
    "test:ci": "jest --ci --bail"
  },
  ...
}

Jest의 shard 옵션

Jestshard 옵션을 사용하면, 테스트를 병렬로 실행할 수 있습니다. shard를 사용하여 테스트를 병렬로 실행하기 위해 Jest를 실행하는 action를 열고 다음과 같이 수정합니다.

  ...
  test-service_1:
    if: contains(github.head_ref, 'service_1') || contains(github.head_ref, 'npm_and_yarn') || contains(github.head_ref, 'github_actions') || contains(github.head_ref, 'components') || contains(github.head_ref, 'config') || contains(github.head_ref, 'common')
    name: Test service_1
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install dependencies
        uses: ./.github/actions/install_dependencies
      - name: Test
        run: yarn test:ci:service_1 -- --shard=$/10

이 프로젝트에서는 모노레포를 관리하기 위해 Turborepo를 사용하고 있습니다.

Action에서 실행하는 yarn test:ci:service_1turbo test:ci --parallel --filter=service_1 명령어를 실행합니다. 그래서 Jest--shard 옵션을 전달하기 위해 --를 사용하여 옵션을 전달하였습니다.

shard 옵션을 추가하기전 테스트는 약 13분 정도 소요되었습니다.

GitHub Actions - After sperating action

shard 옵션을 추가한 후 테스트는 약 2~3분 정도 소요되었습니다.

GitHub Actions - Use Jest shard option

완료

이번 블로그 포스트에서는 Jest를 실행하는 Action의 성능을 개선하는 방법에 대해서 알아보았습니다. 성능 개선전에는 약 25분 정도 소요되었지만, 성능 개선 후에는 약 2~3분 정도 소요되었습니다.

여러분도 Cache, Actions 분리 그리고 shard 옵션을 통해 Jest를 실행하는 Action의 성능을 개선해 보시기 바랍니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통
Posts