개요
회사에서 테스트 코드가 많아지면서 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분 정도 소요됩니다.

성능 개선
PR을 생성할 때마다 매번 25분이 소요되는 것은 매우 비효율적입니다. 이번 블로그 포스트에서는 Jest를 실행하는 Action의 성능을 개선하는 방법에 대해서 알아보겠습니다.
Dependencies Cache
가장 먼저 수행한 것은 yarn install로 설치되는 Dependencies를 캐시하는 것 입니다. 이를 통해 Dependencies를 다시 설치하는 시간을 줄일 수 있습니다.
이 부분은 다른 Actions에서도 사용할 수 있기 때문에 별도의 Composite Action로 만들었습니다. Composite Action에 대해서는 다음 링크를 참고해 주시기 바랍니다.
캐시는 actions/cache를 사용하여 캐시하였습니다.
actions/cache공식 문서: https://github.com/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
저희 팀에서는 Yarn의 3.7.0 버전을 사용하고 있습니다. 그래서 corepack enable을 추가하고, Yarn 3.7.0의 캐시 폴더를 가져와서 node_modules와 함께 캐시하였습니다.
또한 프로젝트가 모노레포이기 때문에 **/node_modules를 사용하여 하위 프로젝트에 있는 node_modules도 함께 캐시하였습니다.
모노레포가 아니고, Yarn 3.7.0을 사용하고 있지않다면, 공식 문서를 참고하여 적절한 설정을 해주시기 바랍니다.
이를 통해 아주 조금 성능이 개선되었습니다.
- Before: 1m 25s

- After: 9s

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분 정도 소요되었습니다.

분리한 후, 이 Action는 약 13분 정도로 성능이 개선되었습니다.
Jest의 bail
Jest의 bail 옵션을 사용하면, 테스트 중에 하나라도 테스트가 실패하면 테스트를 중지하도록 설정할 수 있습니다.
이 옵션을 추가하면 테스트가 실패했을 때, 모든 테스트를 실행하지 않고 바로 중지하게 되어 시간을 단축할 수 있습니다.
Jest의 bail 옵션은 설정하기 위해 jest를 실행하는 명령어가 포함된 package.json 파일을 열고 다음과 같이 수정합니다.
{
...
"scripts": {
...
"test:ci": "jest --ci --bail"
},
...
}
Jest의 shard 옵션
Jest의 shard 옵션을 사용하면, 테스트를 병렬로 실행할 수 있습니다. 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를 사용하고 있습니다.
Turborepo공식 문서: https://turborepo.com/
이 Action에서 실행하는 yarn test:ci:service_1는 turbo test:ci --parallel --filter=service_1 명령어를 실행합니다. 그래서 Jest의 --shard 옵션을 전달하기 위해 --를 사용하여 옵션을 전달하였습니다.
shard 옵션을 추가하기전 테스트는 약 13분 정도 소요되었습니다.
shard 옵션을 추가한 후 테스트는 약 2~3분 정도 소요되었습니다.

완료
이번 블로그 포스트에서는 Jest를 실행하는 Action의 성능을 개선하는 방법에 대해서 알아보았습니다. 성능 개선전에는 약 25분 정도 소요되었지만, 성능 개선 후에는 약 2~3분 정도 소요되었습니다.
여러분도 Cache, Actions 분리 그리고 shard 옵션을 통해 Jest를 실행하는 Action의 성능을 개선해 보시기 바랍니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.














![[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통](https://img1c.coupangcdn.com/image/affiliate/banner/7cba8cb0601eebaf88a17a0c3cf65a63@2x.jpg)