Unit Test & e2e Test & TDD

Yarn berry에서 React + Jest + Cypress + TypeScript + Github Actions CI/CD를 세팅해보자

seungmin2 2021. 10. 11. 15:55

이 글은 Yarn berry package manager를 사용하여 React 프로젝트에서 초기 세팅하는 방법을 다루고 있으며, 여러 시행착오를 겪어가며 경험한 것들을 토대로 작성한 글입니다.😉 이 글의 목적은 겪었던 시행착오들을 한 번 더 기억을 되새기기 위해서 작성하였습니다.
추가적으로 TypeScript와 프론트엔드 e2e 테스트 프레임워크 Cypress, 프론트엔드 unit 테스트 프레임워크 Jest 세팅과 더불어 Yarn berry에서 Github Actions을 사용해 CI/CD 파이프라인을 구축 후 Github Pages에 배포하는 것까지 담고 있습니다. 이 내용은 각각의 공식 문서를 기반으로 작성되었습니다.
해당 예를 담은 Github Repository를 참고해주세요.

🙄 Yarn berry?

npm, Yarn v1과 같은 package manager가 이미 존재하는데 Yarn berry는 뭐고 굳이 Yarn berry를 써야하는 이유는 뭘까?
우리는 먼저 Yarn berry를 사용하는 이유부터 알아봐야 합니다. 이 답에 대해서는 이미 잘 작성되어있는 아티클과 블로그 글들이 많아 링크만 남기고 생략하겠습니다. 이 글의 목적과 맞지 않다고 판단하기에 설명은 생략합니다.

👉 Install React with Yarn berry

이 글 작성 당시 최신 버전의 yarn은 v3.0.2입니다.

Yarn berry는 기존에 사용하던 npm, yarn classic과는 다르므로 추가적인 단계가 추가됩니다.
일단, Yarn package manager가 설치되어있지 않은 분들은 Yarn을 먼저 global로 설치 후 설치를 확인하기 위해 yarn 버전을 확인해줍니다.

npm install -g yarn
yarn --version

여기서는 Webpack, 바벨 설정 등을 제외하기 위해 Create React App을 사용하여 React를 설치하겠습니다.

두 가지 방법으로 yarn berry를 사용한 create-react-app을 설치할 수 있습니다. 기본적으로 npm 또는 yarn classic을 사용했을 때와는 다른 방법으로 설치를 해야 합니다.

저희는 TypeScript를 사용할 것이기 때문에 --template typescript 옵션을 사용하겠습니다.

1. npm 또는 yarn classic으로 create-react-app을 설치 후 마이그레이션하는 방법

기존에 Yarn 버전이 v.1.x.x일 때 또는 npm을 사용해 install할 때 설치 후 마이그레이션 하는 방법입니다.

npx create-react-app <설치를 윈하는 폴더> --template typescript
cd <설치를 윈하는 폴더>

이렇게 설치했을 경우 node_modules 의존성 폴더가 존재합니다. 하지만, Yarn berry에서는 node_modules 의존성 폴더를 사용하지 않고 의존성을 관리합니다. 이에 대한 자세한 내용은 공식 문서: yarn berry PnP를 참고해주세요.
그러므로 node_modules 폴더와 package.lock.json 파일을 삭제해줍니다.

rm -rf node_modules
rm -rf package.lock.json

이제 우리는 Yarn berry 버전으로 변경해줘야 합니다.

yarn set version berry

해당 레포지토리에 .yarnrc.yml.yarn/releases 폴더 아래에 yarn-berry.js 또는 yarn-3.0.2.cjs (확장자명은 설정에 따라 다릅니다.) 파일, .pnp.cjs가 생성됩니다.
그 후 Yarn 버전을 확인해보면 3.0.2인 berry로 세팅되어있는 거를 확인할 수 있습니다.

만약 .yarnrc.yml파일에 아래와 같이 nodeLinkernode-modules를 가리키고 있다면, Yarn berry의 PnP 방식의 zip 아카이브로 관리되는 것이 아닌 기존의 node_modules 의존성 폴더 방식으로 관리되게 됩니다. 그러므로 이 속성을 지워주어야 합니다.

# nodeLinker: node-modules 삭제!

yarnPath: .yarn/releases/yarn-berry.js

그 후 yarn install을 해줍니다.

yarn install

자세한 설치 방법은 공식 문서 Install 참고

2. yarn berry로 create-react-app을 설치하는 방법

처음부터 Yarn berry를 사용해 create-react-app을 설치하기 위해서는 Yarn berry가 설치되어 있어야 합니다.
그 후 아래와 같이 dlx 명령어를 사용해줍니다.

yarn dlx create-react-app <설치를 윈하는 폴더> --template typescript
cd <설치를 윈하는 폴더>

🎈 Setting Zero-Installs

Yarn berry에서는 Zero-Installs이라는 방법을 사용할 수 있습니다. Zero-Installs을 사용하면 반복적으로 의존성 설치 작업이 이루어지는 CI 단계에서 시간을 단축할 수 있습니다.

또한, Yarn berry의 PnP(Plug & Play)는 의존성을 압축 파일로 관리하기 때문에 의존성의 용량이 작습니다. 따라서 의존성을 Git으로 관리할 수 있습니다. 즉, node_modules처럼 .gitignore파일에 추가시켜 제외할 필요 없이 의존성 관련 파일들을 Github의 Repository에 같이 올려 관리할 수 있게 됩니다. 이렇게 Zero-Installs를 사용해서 저장소를 clone 했을 때나 브랜치 이동 시에 추가적인 yarn install이 필요 없게 되는 장점이 있습니다. 이때 Zero-Installs를 사용하고 사용하지 않고는 선택사항입니다.

두 방법에 따라서 아래와 같이 .gitignore에 추가되는 설정이 다릅니다. (관련 설정에 대한 정보는 공식 문서: Which files should be gitignored?를 참고해주세요.)

  • Zero-Installs를 사용할 때
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
  • Zero-Installs를 사용하지 않을 때
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

여기서는 Zero-Installs를 사용하겠습니다.

yarn start를 cmd에 입력하여 정상적으로 시작이 되나 확인합니다.

yarn start

👉 Setting TypeScript, Eslint with Editor SDK

Yarn berry의 PnP 기능을 사용할 때, TypeScript가 작동하도록 추가적인 구성이 필요합니다. 안전상의 이유로 VSCode에서는 사용자 지정 TypeScript 설정을 명시적으로 활성화해야 합니다.

패키지들이 zip 아카이브로 관리되기 때문에 기존의 방식으로는 정상적으로 타입이 불러와지지 않습니다.

Editor SDK 설정을 하기 전에 먼저 VSCode에서 zipfs을 설치해줍니다. 이 extension는 zip 아카이브에서 직접 파일을 읽을 수 있도록 VSCode에 지원을 추가합니다. 예를 들어 이 extension을 추가하지 않으면 import된 React와 같은 설치한 의존성 파일을 열어볼 수 없습니다.

Editor SDK 설정은 cmd에 다음과 같이 입력 사용할 수 있습니다.

yarn dlx @yarnpkg/sdks vscode

그 후 아래 사진과 같이 현재 프로젝트에 필요한 Editor SDK가 설치가 됩니다. Eslint와 TypeScript SDK가 설치되었습니다.

또한, .vscode폴더와 .yarn/sdks에 추가된 SDK를 확인할 수 있습니다. 이때 Zero-Installs을 사용하기 때문에 .vscode.gitignore에 추가하면 안 됩니다.

추가적으로 typescript plugin을 import 시켜줍니다. 이 플러그인은 자체 types를 포함하지 않는 패키지를 추가할 때 @types/ 패키지를 package.json폴더에 종속성으로 자동으로 추가해줍니다. 이 플러그인 설치는 선택사항이지만 매우 유용하기 때문에 설치하겠습니다.

yarn plugin import typescript

설치 후에는 .yarn/plugins/@yarnpkg 폴더 아래에 plugin-typescript.cjs 파일이 생성됩니다. 기타 플러그인 관련해서는 여기를 참고해주세요.

마지막으로 command + shift + p키 (맥북 기준)를 눌러 TypeScript 버전 선택..을 검색해 Use Workspace Version을 선택해 workspace의 typescript sdk로 변경해줍니다.

자세한 사항은 Editor SDK 참고해주세요.
참고로 Eslint 설정과 관련된 작업은 이 글의 목적과 동떨어진 내용이기 때문에 생략하겠습니다. 관련 소스는 예제의 .eslintrc.js를 참고해주세요.

👉 Setting unit test using Jest

Jest는 프론트엔드(JavaScript)에서 단위 테스트를 작성하기 위한 테스트 프레임워크로 페이스북에 의해 만들어졌습니다.
create-react-app에 jest 설정이 기본적으로 되어 있는 관계로 간단하게 설정만 정리하겠습니다.

현재 기본적으로 작성되어있는 테스트는 App.tsx입니다. App.tsx를 다음과 같이 describe-it을 사용하여 변경해줍시다.

import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  it('should renders learn react link', () => {
    render(<App />);
    const linkElement = screen.getByText(/learn react/i);

    expect(linkElement).toBeInTheDocument();
  });
});

만약 해당 파일에 expect(linkElement).toBeInTheDocument();에 에러가 발생한다면, @testing-library/jest-dom을 삭제 후 다시 설치해주세요.

yarn remove @testing-library/jest-dom
yarn add -D @testing-library/jest-dom

CI 파이프라인에서 unit test에 대해 검증을 하기 위해서 package.jsontest:coverage 스크립트를 추가시켜줍니다. 또한, jest에 대한 설정을 아래 코드와 같이 해주겠습니다. Jest Config에 대한 설명은 공식 문서를 참고해주세요.

create-react-app의 jest 기본 설정은 여기를 참고해주세요.

{
  // ...
  "scripts": {
    // ...
    "test:coverage": "yarn test --watchAll=false --coverage",
  }
  "jest": {
      // 이 두 파일을 제외해주는 이유는 이 파일들에 대한 테스트를 작성할 이유가 전혀 없기 때문입니다.
    "coveragePathIgnorePatterns": [
      "src/index.tsx",
      "src/reportWebVitals.ts"
    ],
    // code coverage가 90프로 이하면 에러를 발생시킵니다.
    "coverageThreshold": {
      "global": {
        "branches": 90,
        "functions": 90,
        "lines": 90,
        "statements": 90
      }
    }
  },
  // ...
}

작성한 스크립트를 정상적으로 아래와 같이 실행해 하는지 확인해줍니다.

yarn run test:coverage

👉 Setting e2e test using Cypress

Jest는 단위 테스트를 작성하는 역할을 담당했다면 Cypress는 프론트엔드 e2e 테스트 프레임워크입니다.

먼저 Cypress를 설치해줍니다.

yarn add -D cypress

그리고 package.json에 Cypress를 실행하는 cypress:open 스크립트를 추가시켜줍니다.

{
  // ...
  "scripts": {
    // ...
    "cypress:open": "cypress open",
  }
  // ...
}

이 스크립트를 실행하면 cypress.json 파일과 cypress폴더가 생성된 걸 확인하실 수 있습니다.

yarn run cypress:open

더불어 예제 테스트가 생성된 걸 확인할 수 있습니다.

🎈 TypeScript를 사용한 프로젝트에서 Jest와 Cypress의 충돌 문제

실행한 Cypress는 종료시킨 뒤 Cypress에서 TypeScript를 사용하려면 추가적인 설정을 해줘야 합니다. 안타깝게도 TypeScript를 사용한 동일한 프로젝트에서 Jest와 Cypress를 모두 사용하는 경우 두 테스트 runner가 전역적으로 등록한 TypeScript types가 충돌할 수 있습니다. 이러한 이유는 Jest와 Cypress는 describeit 함수에 대해 충돌 types을 제공하기 때문입니다. 또한, Jest와 Cypress 둘 다 expect assertion 등에 대한 충돌하는 types도 제공하기 때문입니다. 추가적으로 명령어가 비슷한 이유는 Cypress는 Mocha라는 테스트 프레임워크를 의존하고 있기 때문입니다. (참고)

TypeScript를 사용한 Jest와 Cypress 충돌에 대한 자세한 내용은 Cypress 공식 문서: typescript-support를 참고해주세요.

Cypress와 Jest의 types가 충돌하는 문제 때문에 추가적으로 cypress 폴더 안에 tsconfig.json 파일을 생성해주고 다음과 같이 작성해줍니다.

// ./cypress/tsconfig.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "isolatedModules": false,
    "noEmit": true,
    // jest types와 충돌을 피하기 위해 명시적으로 작성해줍니다.
    "types": ["cypress"]
  },
  "include": ["./**/*.ts"]
}

추가적으로 Eslint도 Jest와 충돌이 발생하기 때문에 eslint-plugin-cypress도 의존성을 설치해줍니다.

yarn add -D eslint-plugin-cypress

그리고 Cypress 폴더에만 Cypress Eslint 룰을 적용하기 위해서 .eslintrc.js도 룰을 추가시켜줍니다.

module.exports = {
  // ...
  overrides: [
    {
      extends: [
        'plugin:cypress/recommended',
      ],
      files: [
        'cypress/**/*.ts',
      ],
      rules: {
          // 추후 필요한 rule를 추가해주면 됩니다.
      },
    },
  ]
  // ...
}

이제 기존의 JavaScript 작성된 example 파일들을 TypeScript 확장자로 변경해보면 정상적으로 적용된 것을 확인할 수 있을 겁니다!

🎈 Cypress e2e 테스트 실행하기

테스트를 작성하기 전에 cypress/integration 폴더에 생성된 예제 테스트와 fixtures 폴더 안에 있는 example.json는 불필요하므로 삭제해줍니다.
그리고 아래 기본으로 생성된 페이지에 대한 간단한 e2e 테스트를 작성해 보겠습니다.

먼저 cypress.jsonbaseUrl를 설정해줍니다.

// cypress.json
{
  "baseUrl": "http://localhost:3000"
}

그리고 cypress/integration/sample 폴더에 sample.spec.ts에 다음과 같이 작성해줍니다.

// sample.spec.ts
describe('sample', () => {
  it('finds the content "Learn React"', () => {
    cy.visit('/');

    cy.contains('Learn React');
  });
});

이제 개발 서버를 실행 후 Cypress e2e 테스트가 정상적으로 통과하는지 확인해봅시다.

yarn start
yarn cypress:open

sample.spec.ts에 대한 테스트가 성공적으로 성공하는 것을 확인할 수 있습니다.

마지막으로 Github Actions CI 파이프라인에서 Cypress e2e 테스트를 검증하기 위해서 한가지 설정을 추가해주죠!
e2e 테스트를 할 때마다 개발 서버를 실행 후 Cypress를 실행해야 합니다. 또한, 개발 서버 실행이 오래걸리면 서버가 실행 전에 Cypress가 실행되어 에러를 유발할 수 있습니다. 그러므로 Cypress의 작업을 한 번에 가능하게 설정해주고, CI 단계에서 HEADLESS된 Cypress 실행을 위해 스크립트를 추가로 작성해줍니다.

먼저 개발 서버 시작과 실행되기까지 기다린 후 Cypress를 실행해주기 위해 start-server-and-test에 대한 의존성을 설치해줍니다.

yarn add -D start-server-and-test

그리고 다음과 같이 package.jsoncypress:runtest:e2e 스크립트 두 개를 추가시켜줍니다.

{
  // ...
  "scripts": {
    // ...
    "cypress:run": "cypress run",
    "test:e2e": "start-server-and-test start http://localhost:3000 cypress:run",
  }
  // ...
}

이제 정상적으로 개발 서버가 실행 후 Cypress가 실행되는지 확인합니다.

yarn run test:e2e

아래와 사진과 같이 e2e 테스트가 성공적으로 통과하는 걸 확인할 수 있습니다!

추가로 .gitignore에 Cypress 관련 파일을 추가시켜줍니다.

# cypress
cypress/screenshots
cypress/videos
cypress/log

관련한 내용은 Cypress 공식문서: continuous-integration를 참고해주세요.

👉 Github Actions CI/CD

이제 마지막으로 Github ActionsCI/CD 파이프라인을 구축해봅시다.

CI/CD가 무엇이고 왜 해야 하는지에 대한 내용은 역시 주제에 벗어나기 때문에 생략하겠습니다.

CI 단계에서는 main 브랜치로 pull request 된 상태일 때만 unit 테스트와 e2e 테스트 검증과 lint 체크를 하도록 하겠습니다.
CD 단계에서는 main 브랜치로 push될 때 프로젝트 빌드 후 간단하게 Github Pages에 배포가 되도록 하겠습니다.

🎈 Github Actions CI 파이프라인 설정

우선 Eslnit에 대한 lint 체크를 하기 위해 package.jsonlint 스크립트를 추가시켜줍시다.

{
  // ...
  "scripts": {
    // ...
    // eslint의 검사 범위는 ts, tsx 파일
    "lint": "eslint --ext ts,tsx .",
  }
  // ...
}

Github Actions에 CI 파이프라인을 작성하기 위해서 .github/workflows 폴더 안에 .yml(YAML) 확장자로 작성해야 합니다. (참고)

그러면 .github/workflows 폴더 안에 ci.yml 파일을 생성한 뒤 다음과 같이 작성해줍니다.

name: CI

on:
  # main 브랜치로 pull request 일 때만 수행한다.
  pull_request:
    branches: [main]

jobs:
  continuous-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js 14.x
        uses: actions/setup-node@v1
        with:
          node-version: 14.x

      - name: Install dependencies
        # install 옵션에 대해서는 yarn 공식 문서를 참고해주세요.
        # https://yarnpkg.com/cli/install
        run: yarn install --immutable --immutable-cache

      - name: Check Lint
        # lint 검사
        run: yarn lint

      - name: Check Unit Test
        # unut test 검증
        run: yarn test:coverage

Cypress Github Actions

Cypress는 공식적으로 제공하는 Github Actions가 있습니다. 물론, 굳이 사용할 필요는 없습니다. 다만, 더 편하게 cypress 관련 Cli 명령어를 사용할 수 있습니다. (Cypress Github Actions)

안타깝게도 공식적으로 제공하는 Cypress Github Actions는 yarn berry에서 완벽한 호환은 안 됩니다. 설치된 Cypress 모듈을 찾지 못해서 생기는 오류로 이 문제에 대해서 여러 테스트를 진행해보았지만 해결되지 않았습니다. 이 문제를 해결하기 위해 직접 Cypress Github Actions의 Github issue 란에 질문을 올려보았었지만, 역시 제공하는 방법처럼 사용은 불가능하다는 것이었습니다.

해결 방법으로는 custom command를 입력해야 하는 것이고, 이로 인해 Cypress Github Actions에서 제공하는 command 들은 작성해도 동작하지 않습니다. 예를 들어 아래와 같이 brower 옵션chrome 설정해도 테스트 환경은 chrome으로 변하지 않습니다.

# ...
- name: Cypress run
  uses: cypress-io/github-action@v2
  with:
    install: false
    command: yarn test:e2e
    # 동작하지 않음!
    brower: chrome

그래서 이 역시 아래와 같이 command 옵션에 직접 추가 해주어야 합니다.

# ...
- name: Cypress run
  uses: cypress-io/github-action@v2
  with:
    install: false
    # https://docs.cypress.io/guides/guides/command-line#cypress-run-browser-lt-browser-name-or-path-gt
    command: yarn test:e2e --browser chrome

이러한 문제는 모든 명령어가 동일하게 적용됩니다. 하지만, 괜찮습니다. Cypress Github Actions를 사용하는 거보단 번거롭긴 하겠지만 Cypress에서 제공하는 Cli를 작성하여 모든 명령어를 사용할 수 있습니다. (Cypress Command Line 참고)

여기서는 Cypress Github Actions를 사용하여 작성하겠습니다.

아래의 코드는 ci.yml의 전체 코드입니다.

name: CI

on:
  pull_request:
    branches: [main]

jobs:
  continuous-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js 14.x
        uses: actions/setup-node@v1
        with:
          node-version: 14.x

      - name: Install dependencies
        run: yarn install --immutable --immutable-cache

      - name: Check Lint
        run: yarn lint

      - name: Check Unit Test
        run: yarn test:coverage

      - name: Check e2e Test
        uses: cypress-io/github-action@v2
        with:
          install: false
          command: yarn test:e2e

이제 여태까지 작업 된 내용들을 Github Repository의 main 브랜치로 Pull Request를 보내 Github Actions CI 파이프라인이 아래 사진과 같이 정상적으로 동작하는지 확인해봅시다.

🎈 Github Actions CD 파이프라인 설정 및 Github Pages에 배포

이제 마지막으로 Github Pages에 배포해보겠습니다. github-pages-deploy-action를 사용하여 Github Page에 배포하는 건 간단하게 할 수 있습니다.

다음과 같이 .github/workflows폴더 안에 cd.yml을 작성해 줍니다.

name: CD
# main 브랜치로 push될 때만 동작한다.
on:
  push:
    branches:
      - main
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v2.3.1

      - name: Install and Build 🔧
        run: |
          yarn install --immutable --immutable-cache
          yarn run build
      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@4.1.5
        with:
          branch: gh-pages
          # 빌드되는 폴더명
          folder: build

한가지 주의사항으로 create-react-app을 사용한 React 프로젝트는 기본적으로 루트(/) URL을 기준으로 프로젝트가 생성됩니다. 하지만 아래 예시와 같이 GitHub Pasges의 URL은 Repository 이름으로 된 path를 기본적으로 가지고 있습니다.

// 저장소
https://github.com/<username>/<repository>
https://github.com/saseungmin/yarn-berry-example

// Github Pages
https://<username>.github.io/<repository>
https://saseungmin.github.io/yarn-berry-example

따라서 create-react-app을 사용한 React 프로젝트에서 Repository 이름을 가진 path를 사용하도록 수정해줘야 합니다. 그러므로 package.jsonhomepage 옵션을 작성해줍니다.

{
  // ...
  "homepage": "https://<username>.github.io/<repository>",
  // ...
}

이제 main으로 push를 하면 cd.yml workflow가 트리거 됩니다. 그리고 아래와 같이 Actions 탭에 정상적으로 CD 파이프라인이 동작하는 것을 확인할 수 있습니다.

이제 아래 사진과 같이 Github Repository에 Settings 탭의 Pages 섹션으로 이동해서 Source의 gh-pages를 선택 후 Save 버튼을 눌러 저장합니다.

그러면 이제 Github Pages에 자신이 빌드 후 배포한 사이트에 접속할 수 있게 됩니다!

👉 마무리하며

우리는 이러한 작업을 통해 빠르게 개발할 수 있게 되었고 빠르게 피드백을 얻어 고쳐나갈 수 있는 프로젝트의 동작하는 골격을 만들었습니다! 이제 오로지 개발에만 신경 쓰고 진행하면 됩니다!

최근에 회사에서 Yarn berry로 프로젝트를 세팅할 일이 있어서 겪었던 것들을 정리하였습니다. 부족한 부분이 있거나 잘못된 내용이 있으면 댓글로 남겨주시면 감사하겠습니다. 🙇 해당 예를 담은 Github Repository를 참고해주세요.

쓰다 보니 생각보다 너무 길어졌지만, 한 번쯤 정리해야겠다는 생각을 실천해서 만족스럽습니다. 많은 분께 도움이 되었으면 좋겠고 끝까지 긴 글 읽어주셔서 감사드립니다. 🙏