[CI/CD] Husky, GitHub Actions 로 팀 프로젝트 코드를 지속적으로 통합/배포하기 (ESLint, Prettier, Jest, gh-pages)

Quartz 쿼츠·2022년 12월 27일
2

Tools & DevOps

목록 보기
2/3
post-thumbnail

최근 리액트 네이티브 앱을 빌드하고 구글 플레이스토어에 지속적으로 배포하는 과정을 겪으면서 CI/CD의 필요성을 느끼게 되었다. 그러나 개인 프로젝트 특성상 기능 구현만으로도 일정에 치이다보니 따로 공부할 겨를이 없었다. 그러던 중 원티드 프리온보딩 인턴십에서 CI/CD 관련 세션을 수강하게 되었고 이를 팀 단위 프로젝트에 직접 적용하기 위해 공부한 내용을 적어보려 한다.

본 글은 CI/CD를 처음 겪어본 개발자의 셋업이며 개인 정리용도의 글입니다.

팀 프로젝트 CI 셋업하기

프로젝트를 셋업하는 과정은 아래와 같이 구분하여 정리하였고, 리액트 라이브러리를 사용하는 프론트엔드 개발 셋업을 기준으로 설명할 예정이다.

1) CRA로 리액트 프로젝트를 만들고 gitHub에 올리기
2-3) Test, linter, code formatter를 셋업하기
4-5) Husky, GitHub Actions를 사용하여 2-3)의 과정 자동화하기

1. CRA & git 원격 저장소에 올리기

먼저, 로컬에서 리액트 프로젝트를 만들고 GitHub 페이지에서 원격 저장소를 만들어 연결한다.

  • npx create-react-app project-name
  • GitHub에서 원격 저장소 만들기
  • git init git remote add origin repo-url

2. Jest test 설정하기

본 프로젝트에서 테스팅 툴은 CRA에 내장되어 있는 Jest를 사용하였다. Jest의 기본 사용 방법컴포넌트 렌더링 테스트 방법은 다른 글에 정리해놓았다.

2.1 App 컴포넌트 렌더링 테스트 생성하기

사실 CI/CD 테스트 코드를 다루는 여러 글과 컨퍼런스를 참고하였지만 현재 팀 상황에 마땅한 코드를 찾지 못하였다. 우리 팀은 앞으로 주어지는 3 개의 서로 다른 과제에 대해 미리 CI/CD 셋업을 하는 것을 목표로 하며, 개인 개발 ➡️ 토론 ➡️ 베스트 코드 찾기 ➡️ 코드 통합하기 ➡️ 배포의 과정을 4 일 안에 완료해야 하는 빡빡한 스케줄에 놓여있다. 어떤 과제가 주어질지 아예 모르는 상황에서 테스트 코드를 짜기는 불가능하므로 아래와 같은 사고 과정으로 기준을 세워 보았다.

  • Unit / Integration test
    - 1 차 구현 완료 후 코드를 구현한 개발자가 테스트 코드 작성
  • E2E test
    - 컴포넌트 렌더링 테스트로 대체
    • 짧은 기간 내에 click, hover 등 사용자 이벤트를 고려한 테스트 코드 작성은 어려워보임

그렇다면 어떠한 컴포넌트 렌더링 테스트를 임의의 프로젝트에 적용할 수 있을까? 필자는 팀프로젝트를 처음 겪으면서 여러 팀원들의 코드가 merge될 때 npm run start 커맨드를 입력하면 에러나 흰 화면이 뜨는 것이 가장 두려웠다. 따라서 앱 컴포넌트가 제대로 렌더링되고 있는지를 확인하는 테스트를 셋업 코드로 결정하게 되었다.

// App.js
function App() {
  return (
    <div className="App">
      <h1>APP title</h1>
    </div>
  );
}
export default App;
// src/__test__/DOM.test.js
import "@testing-library/jest-dom"; // CRA의 setupTest.js 파일 삭제 시 추가해야 함
import { render, screen } from "@testing-library/react";
import App from "../App";

describe("App 컴포넌트 렌더링 테스트", () => {
  test("<App /> 렌더링이 되나요?", async () => {
    render(<App />);

    // App 컴포넌트의 `h1` 렌더링 여부 확인하는 테스트 코드
    const headingEl = screen.getByRole("heading", makeOptions(1, "APP title"));
    expect(headingEl).toBeInTheDocument();
  });
});

const makeOptions = (level, name) => {
  return { level, name };
};

2.2 test script 수정하기

  • Jest Unexpected Token Error 방지를 위해
//package.json
  "scripts": {
    "test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!@toolz/allow-react)/\" --env=jsdom --watchAll",
  },

3. ESLint & Prettier 설정하기

여러 개발자가 관리하는 프로젝트 내의 문법, 코드 포맷팅을 정하기 위해 JavaScript에서는 Linter로 ESLintCode formatter로 Prettier를 사용한다. 팀원들과 상의를 통해 컨벤션을 정하여 아래와 같이 셋팅을 진행하였다.

3.1 패키지 설치하기

npm install eslint --save-dev
npm install prettier --save-dev
npm install eslint-config-prettier --save-dev // ESLint의 포맷팅 셋팅을 꺼주는 역할

// .eslintrc
{
  "extends": ["react-app", "eslint:recommended"],
  "rules": {
    "no-var": "error",
    "no-multiple-empty-lines": "error",
    "no-console": ["warn", { "allow": ["warn", "error", "info"] }],
    "eqeqeq": "error",
    "no-unused-vars": "warn",
    "no-undef": "warn"
  }
}
// .prettierrc.js
module.exports = {
  singleQuote: true,
  semi: true,
  useTabs: false,
  tabWidth: 2,
  trailingComma: "all",
  printWidth: 80,
};

3.2 script 추가하기

  • .gitignore 파일에 .eslintcache 추가
//package.json
  "scripts": {
    "format": "prettier --write --cache .",
    "lint": "eslint --cache ."
  },

터미널로 명령을 실행한다는 것은 자동화가 가능하는 뜻이다. #2-3에서 package.json 파일에 추가한 scripts를 통해 해당 커맨드들을 자동화해보자.

4. Husky를 사용한 #2-3 자동화

Husky는 git hook 설정을 간단하게 도와주는 npm 패키지이다. Git hook이란 commit, push와 같은 git의 특정 이벤트 전후로 특정 동작을 실행하도록 하는 것이다. Husky는npm i 과정에서 최초 프로젝트 셋업시 사전에 설정해둔 git hook이 적용되므로 모든 팀원이 사용도록 하기가 편하다.

4.1 Husky 설치

  • npm install husky --save-dev (git init이 되어있어야 함)
  • npx husky install
    - husky에 등록된 hook을 .git에 적용시키기 위한 스크립트. 이후 저장소를 클론 받는 사람들은 npm i 이후 자동으로 postinstall 스크립트가 실행됨
// package.json
  "scripts": {
    "postinstall": "husky install"
  },

4.2 Git hooks 추가하기

  • 커밋 이전에 코드 포맷팅하기
    npx husky add .husky/pre-commit "npm run format"
  • 푸시 이전에 린터&테스트 실행하기
    • npx husky add .husky/pre-push "npm run lint"
    • npx husky add .husky/pre-push "npm run test"
      • ./husky/pre-push의 커맨드 수정: npm run test -- --watchAll=false
      • 테스트가 한 번만 실행되고 종료되도록 한다.

5. GitHub Actions를 사용한 #3 자동화

여기까지 왔다면 프로젝트의 셋업이 거의 마무리되었다고 생각하면 된다. 필자는 추가로 pull request 이벤트에 대한 훅을 설정하기 위해 GitHub에서 제공하는 클라우드형 CI/CD 툴 GitHub Actions를 추가하였다.

  • 목적:develop 브랜치의 코드가 검증되었는가?
  • 테스트 코드: 현재 develop 브랜치의 코드를 테스트
    • 주의: 테스트 코드 또한 현재 브랜치의 테스트 코드를 사용함

5.1 PR test의 trigger 결정하기

개발이 이루어지고 있는 develop 브랜치 코드의 검증 시기와 테스트 결과 확인 사용성을 고려하여 두 가지 방안을 고안하였다.

A안) PR이 merge되었을 떄
공식문서에 의하면 아래와 같은 설정을 사용하여 PR이 병합되어 닫힌 경우 테스트 코드를 실행 할 수 있다. 이 경우 코드의 충돌이 없는 상태에서 머지가 완료되면 테스트 성공 여부를 확인할 수 있으며, 저장소의 Actions 탭에서 테스트 결과를 확인하거나 fail할 경우 메일(알림 설정이 되어있으면)을 받을 수 있다.

on:
  pull_request:
    types:
      - closed

jobs:
  if_merged:
    if: github.event.pull_request.merged == true


B안) PR 생성되었을 때
이 경우 PR을 처음 생성했을 뿐만 아니라 추가 커밋으로 PR이 갱신되거나 다른 PR이 먼저 병합되어 현재 develop 브랜치의 코드가 업데이트 되었을 때에도 트리거된다. A안과 달리 PR 창에서 테스트를 확인할 수 있어 해당 PR을 올린 팀원은 현재 develop 브랜치의 코드가 검증되어 있는지를 쉽게 확인할 수 있다. 그러나 B안의 단점은 여러 PR이 열려있는 경우 훅이 너무 많이 트리거된다는 단점이 있다. About billing for GitHub Actions에 따르면 공개 저장소와 self-hosted runner를 사용하면 무료로 사용할 수 있지만, 필자는 GitHub-hosted runners를 사용하므로 1 달에 2000 분 제한이 존재한다.

on:
  pull_request:
    branches:
      - develop

5.2 저장소에 .yml 파일 추가하기

// .github/workflows/testPR.yml
name: test PR

on:
  pull_request:
    branches:
      - develop

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          ref: "develop"
      - run: npm ci
      - run: npm run test -- --watchAll=false

GitHub Actions를 사용한 CD 구축하기

gh-pages를 사용한 자동 정적 배포를 자동화할 수 있는 GitHub Actions를 아래와 같이 설정해보았다.

먼저 npm i gh-pages로 패키지를 설치해주고, package.json에 predeploy deploy 관련 스크립트를 아래와 같이 작성한다.

package.json
{
  "scripts": {
    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
  },
   "homepage": "https://[githubID].github.io/[repo-name]" 
}

ESLint 를 사용하고 있다면 빌드 파일을 검사하지 않도록 .eslintignore 파일을 만들어야 한다.

.eslintignore
/build

GitHub Actions는 아래와 같이 두 가지 경우에 대해 자동 배포가 트리거되게 만들 수 있다.

1) Main 브랜치에 코드가 push되거나 actions 버튼을 눌러 자동 배포

GitHub Actions 관련 코드는 Marketplace에서 필요한 작업을 찾아 사용하였다.

name: gh-pages deploy

on:
  push:
    branches:
      - main
  workflow_dispatch:
  
permissions:
  contents: write

jobs:
  build-and-deploy:
    concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v3

      - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
        run: |
          npm ci
          npm run build
      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          folder: build # The folder the action should deploy.


2) Main 브랜치에서 PR이 merge되었을 때 자동 배포

name: gh-pages deploy

on:
  pull_request:
    branches: ['main']
    types:
      - closed

permissions:
  contents: write

jobs:
  if_merged:
    if: github.event.pull_request.merged == true
    concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v3

      - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
        run: |
          npm ci
          npm run build

      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          folder: build # The folder the action should deploy.

아래 이미지에서 main 브랜치에 코드가 merge되고 자동 배포 코드가 실행되어 gh-pages로 배포가 완료된 것을 확인할 수 있다.

참고자료

profile
Code what we love. 좋아하는 것들을 구현하고 있는 프론트엔드 개발자입니다. 사용자도 함께 만족하는 서비스를 만들고 싶습니다.

0개의 댓글