최근 리액트 네이티브 앱을 빌드하고 구글 플레이스토어에 지속적으로 배포하는 과정을 겪으면서 CI/CD의 필요성을 느끼게 되었다. 그러나 개인 프로젝트 특성상 기능 구현만으로도 일정에 치이다보니 따로 공부할 겨를이 없었다. 그러던 중 원티드 프리온보딩 인턴십에서 CI/CD 관련 세션을 수강하게 되었고 이를 팀 단위 프로젝트에 직접 적용하기 위해 공부한 내용을 적어보려 한다.
본 글은 CI/CD를 처음 겪어본 개발자의 셋업이며 개인 정리용도의 글입니다.
프로젝트를 셋업하는 과정은 아래와 같이 구분하여 정리하였고, 리액트 라이브러리를 사용하는 프론트엔드 개발 셋업을 기준으로 설명할 예정이다.
1) CRA로 리액트 프로젝트를 만들고 gitHub에 올리기
2-3) Test, linter, code formatter를 셋업하기
4-5) Husky, GitHub Actions를 사용하여 2-3)의 과정 자동화하기
먼저, 로컬에서 리액트 프로젝트를 만들고 GitHub 페이지에서 원격 저장소를 만들어 연결한다.
npx create-react-app project-name
git init
git remote add origin repo-url
본 프로젝트에서 테스팅 툴은 CRA에 내장되어 있는 Jest를 사용하였다. Jest의 기본 사용 방법과 컴포넌트 렌더링 테스트 방법은 다른 글에 정리해놓았다.
사실 CI/CD 테스트 코드를 다루는 여러 글과 컨퍼런스를 참고하였지만 현재 팀 상황에 마땅한 코드를 찾지 못하였다. 우리 팀은 앞으로 주어지는 3 개의 서로 다른 과제에 대해 미리 CI/CD 셋업을 하는 것을 목표로 하며, 개인 개발 ➡️ 토론 ➡️ 베스트 코드 찾기 ➡️ 코드 통합하기 ➡️ 배포의 과정을 4 일 안에 완료해야 하는 빡빡한 스케줄에 놓여있다. 어떤 과제가 주어질지 아예 모르는 상황에서 테스트 코드를 짜기는 불가능하므로 아래와 같은 사고 과정으로 기준을 세워 보았다.
그렇다면 어떠한 컴포넌트 렌더링 테스트를 임의의 프로젝트에 적용할 수 있을까? 필자는 팀프로젝트를 처음 겪으면서 여러 팀원들의 코드가 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 };
};
test
script 수정하기//package.json
"scripts": {
"test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!@toolz/allow-react)/\" --env=jsdom --watchAll",
},
여러 개발자가 관리하는 프로젝트 내의 문법, 코드 포맷팅을 정하기 위해 JavaScript에서는 Linter로 ESLint를 Code formatter로 Prettier를 사용한다. 팀원들과 상의를 통해 컨벤션을 정하여 아래와 같이 셋팅을 진행하였다.
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,
};
.gitignore
파일에 .eslintcache 추가//package.json
"scripts": {
"format": "prettier --write --cache .",
"lint": "eslint --cache ."
},
터미널로 명령을 실행한다는 것은 자동화가 가능하는 뜻이다. #2-3에서 package.json
파일에 추가한 scripts를 통해 해당 커맨드들을 자동화해보자.
Husky는 git hook 설정을 간단하게 도와주는 npm 패키지이다. Git hook이란 commit, push와 같은 git의 특정 이벤트 전후로 특정 동작을 실행하도록 하는 것이다. Husky는npm i
과정에서 최초 프로젝트 셋업시 사전에 설정해둔 git hook이 적용되므로 모든 팀원이 사용도록 하기가 편하다.
npm install husky --save-dev
(git init
이 되어있어야 함)npx husky install
npm i
이후 자동으로 postinstall
스크립트가 실행됨// package.json
"scripts": {
"postinstall": "husky install"
},
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
여기까지 왔다면 프로젝트의 셋업이 거의 마무리되었다고 생각하면 된다. 필자는 추가로 pull request
이벤트에 대한 훅을 설정하기 위해 GitHub에서 제공하는 클라우드형 CI/CD 툴 GitHub Actions를 추가하였다.
develop
브랜치의 코드가 검증되었는가?개발이 이루어지고 있는 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
// .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
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는 아래와 같이 두 가지 경우에 대해 자동 배포가 트리거되게 만들 수 있다.
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.
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로 배포가 완료된 것을 확인할 수 있다.