코테PT CI 자동화 프로세스 구축기

현승재·2024년 4월 25일
0

계기

같이 하고 싶은 프로젝트에 합류하게 되었고 마침 공부중인 테스트를 적용하고 싶었다
그러나 결합도가 높아 테스트하기 용이한 코드가 아니었고 이를 함수 단위로 분리하는 리팩토링 과정을 거쳤다

예상했지만 역시 regression (이전에 제대로 작동하던 SW 기능에 문제가 생기는 버그) 이 발생했다

프로젝트는 점점 성장하면서 복잡해진다. 지금 같이 프로젝트 초기에 테스트를 구축하여 지속적인 코드 통합을 가져가는 것이 트러블 슈팅하기 쉽지 않을까?

개발자가 매번 빌드 및 테스트 스크립트를 실행하여 저장소에 push 하는 프로세스는 번거롭기도 하고 깜빡할 수 있기 때문에 CI 프로세스를 자동화를 하고 싶다

현재 프로젝트의 CD 는 vercel 이 담당 하고 있으니 나는 Github actions 를 사용하여 CI 자동화를 하기로 했다

CI (Continuous Integration) 란

“지속적 통합” 이라는 뜻으로 여러 개발자가 개발을 진행하면서 빌드와 테스트 과정을 자동화하여 동일한 코드를 품질로 유지 및 관리하는 개념

CI 의 4가지 규칙 - 마틴 파울러

  • 모든 소스코드가 살아 있고 누구든 현재의 소스에 접근할 수 있는 단일 지점을 유지 할 것
  • 빌드 프로세스를 자동화해서 누구든 소스로부터 시스템을 빌드할 수 있게 할 것
  • 테스팅을 자동화해서 언제든지 시스템에 대한 테스트를 실행할 수 있게 할 것
  • 누구든 현재 실행 파일을 얻으면 지금까지 가장 완전한 실행파일을 얻었다는 확신을 하게 할 것

Github Actions 로 CI 구축

공식 Docs

vscode 에서 workflow .yaml 파일 작성하기

github actions vscode extention

vscode extention 을 설치한다.

.yaml 파일의 대략적인 구조

image creadit

.github/workflow 의 yaml 파일의 대략적인 구조를 설명한 이미지

name: CI # 액션의 이름
run-name: ${{ github.actor }} (${{ github.actor_id }}) # 액선을 실행할 때 구분할 명칭
on: # 액션 트리거 이벤트
  pull_request: # main 브랜치를 제외한 모든 브랜치에 대해 PR 가 발생한 경우 트리거
    branches-ignore:
      - main
  push: # main, develop 브랜치를 제외한 모든 브랜치에 push 가 발생한 경우 트리거
		branches-ignore: [main, develop]
		
jobs:
	build: ... 
	test: ...

언제 빌드와 테스트를 실행할 것인가?

  • 빌드와 테스트 권장 이벤트 Github Actions 에서 빌드 및 테스트를 자동화 하기 위해 일반적으로 권장되는 이벤트는 pushpull_request 이벤트다.
    1. push

      특정 브랜치에 코드가 push 될 때마다 workflow 가 실행된다.

      이 단계에서 CI 과정을 수행하면 코드 변경 사항이 저장소에 반영되기 전에 문제가 없는지 확인할 수 있다

    2. pull_request

      새로운 PR 가 열리거나 PR 가 업데이트 될 때마다 workflow 가 실행된다.
      
      이 단계에서 CI 과정을 수행하면 코드 변경 사항이 main 브랜치에 merge 되기 전에 문제가 없는지 확인할 수 있다

      추가적으로 고려할 수 있는 이벤트

    • schedule 특정 시간 간격으로 workflow 를 실행하여 정기적으로 빌드 및 테스트를 수행할 수 있다
    • workflow_dispatch 수동으로 workflow 를 실행하여 필요할 때마다 빌드 및 테스트를 수행할 수 있다

PR 가 close 상태일 때 CI 를 실행하지 않는 이유

  • pull_requestclose상태일 때 CI 를 실행하지 않는 이유
    1. PR merge 여부를 확인하는 것이 더 중요하다

      PR 을 닫는 시점에서는 PR 이 이미 병합되었는지, 아니면 닫히고 있는지를 확인하는 것이 더 중요하다

      PR 가 병합된 경우에는 이미 빌드와 테스트가 수행되었을 것이므로, 다시 실행할 필요가 없다

    2. 코드 변경 사항 없음

      PR 를 닫을 때는 코드 변경 사항이 없다

      빌드와 테스트는 주로 코드 변경 사항에 대한 검증을 위해 수행되므로, PR 를 닫는 시점에서는 이러한 작업이 불필요하다

    3. 리소스 낭비 방지

      빌드와 테스트는 일반적으로 시간과 리소스가 소모되는 작업이다

      PR 를 닫는 시점에서 불필요한 빌드와 테스트를 수행하면 리소스를 낭비할 수 있다

    4. PR Merge 시 검증 수행

      대신에 PR 를 merge 할 때 빌드와 테스트를 수행하는 것이 더 적절하다
      
      PR 가 main 브랜치에 merge 되기 전에 코드 변경 사항에 대한 검증을 수행하는 것이 중요하다

      따라서 GitHub Actions Workflow에서는 일반적으로 pull_request 이벤트의 typesclosed를 포함시키지 않는다

      대신에 opened, reopened, synchronize 등의 types을 사용하여 PR이 열리거나 코드 변경 사항이 있을 때 빌드와 테스트를 수행한다

      pull_requestclose 시점에 특정 작업 (PR 관련 통계 수집, 알림 전송, 리소스 정리) 을 수행하야 하는 경우 closed 이벤트 타입을 추가하여 필요한 작업을 정의할 수 있다

개발 & 배포 CI 환경 분리

# FE-CI-dev.yaml
# 개발 환경 액션
# 트리거 이벤트 및 개발 환경 변수 설정
name: CI - Development
run-name: ${{ github.actor }} (${{ github.actor_id }})
on:
  pull_request:
    branches-ignore:
      - main
  push:
    branches-ignore: [main, develop]
# FE-CI-prod.yaml
# 배포 환경 액션
# 트리거 이벤트 및 배포 환경 변수 설정
name: CI - Production
run-name: ${{ github.actor }} (${{ github.actor_id }})

on: 
  pull_request:
    branches: main

CI jobs 목록

Lint

Build

빌드시 노드 모듈과 빌드 결과를 캐싱하여 Test 단계에서 재사용하여 workflow 실행시간을 개선한다

jobs: # workflow 작업 목록
  build: # build 작업 정의
    runs-on: ubuntu-latest # runner 서버 지정
    strategy: # 작업 내부에서 사용할 전략 정의
      matrix: # 작업 내부에서 사용할 내부 변수 목록
        node-version: ['18.x'] # 사용할 Node.js의 버전을 변수화
    steps: # 작업을 구성 단계
      - name: Checkout # 코드 체크아웃 (가장 최신 버전의 HEAD commit 코드 사용)
        uses: actions/checkout@v4 # github에서 공식적으로 제공하는 체크아웃 액션

      - name: Install Node.js ${{ matrix.node-version }} # Node 설치
        uses: actions/setup-node@v4
        with: # 액션에 전달할 인자
          node-version: ${{ matrix.node-version }} # matrix 에서 정의한 Node.js 버전 변수

      - name: Cache Node modules # node_modules 캐싱
        id: cache # 이 단계의 식별자
        uses: actions/cache@v3
        with: 
          path: '**/node_modules' # 캐시할 경로
          # 캐시 키 지정
          key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 

      - name: Install dependencies # 의존성 설치
        if: steps.cache.outputs.cache-hit != 'true' # 캐시 히트가 아닐 경우에만 실행
        run: npm ci # package-lock.json 에 정의된 버전으로 설치

      - name: Run build #  빌드 실행
        env: # 이 단계에서 사용할 환경 변수
          VITE_SERVER_URL: ${{ secrets.VITE_SERVER_URL }} # github secret 환경 변수 사용
          VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} 
          VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} 
          VITE_KAKAO_JS_SDK_KEY: ${{ secrets.VITE_KAKAO_JS_SDK_KEY }} 
          VITE_GOOGLE_ID: ${{ secrets.VITE_GOOGLE_ID }} 
        run: npm run build # 프로젝트 빌드 미리 정의한 환경변수 사용시 -- development 등을 사용
      - uses: actions/upload-artifact@v3 # 빌드 결과물을 재사용하기 위해 업로드
        with: 
          name: build-output # 업로드할 파일의 이름
          path: dist # 업로드할 파일의 경로 빌드시 지정한 폴더명을 사용해야 한다.

  test: ... # 테스트 작업 정의 (생략)

Test

"build"와 "test" 작업이 동일한 값을 사용하여 캐시 키를 생성하기 때문에 캐시를 공유할 수 있다

test 작업의 Install Dependencies 단계에서 build 작업에서 캐시된 node_modules 를 캐시 키로 접근할 수 있기 때문에 test 작업에서 Cache Node modules 단계가 필요 없다고 생각했다.

하지만 "Cache Node modules" 단계가 "test" 작업에 포함하는 이유가 있다

  • 캐시 접근성 "build" 작업에서 생성된 캐시는 "test" 작업에서 사용 가능하지만, 이를 위해서는 "Cache Node modules" 단계에서 캐시가 존재하는지 확인하고, 존재할 경우 해당 캐시를 사용하여 노드 모듈을 복원하는 작업이 필요하기 때문이다.
  • 캐시 미스 처리 만약 "build" 작업에서 생성된 캐시를 "test" 작업에서 찾을 수 없는 경우(예: 첫 실행에서 캐시가 아직 생성되지 않았을 때), "Cache Node modules" 단계는 새로운 캐시를 생성하는 역할한다.
  • 각 작업( job )의 독립적인 실행 보장 각 작업에서 필요한 모든 단계가 포함되어 있어야 한다. 이는 작업 간의 의존성을 최소화하고, 각 작업을 독립적으로 실행하거나 디버깅할 수 있게 한다.
jobs:
  build: ...
  test:
    needs: build  # build 작업을 완료하고 test 작업을 수행
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: ['18.x']
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: Cache Node modules # 캐시 접근
        id: cache
        uses: actions/cache@v3
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}

      - name: Install Dependencies # 캐시 사용
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm ci

      - name: Download artifact # build 단계에서 업로드한 빌드 결과물 다운로드
        uses: actions/download-artifact@v3
        with:
          name: build-output
          path: dist

      - name: Run test # 테스트 수행
        env:
          VITE_SERVER_URL: ${{ secrets.VITE_SERVER_URL }}
          VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
          VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
          VITE_KAKAO_JS_SDK_KEY: ${{ secrets.VITE_KAKAO_JS_SDK_KEY }}
          VITE_GOOGLE_ID: ${{ secrets.VITE_GOOGLE_ID }}
        run: npm run test

추가할 프로세스

CI 테스트 보고서

vitest-coverage-report-action 를 사용해 PR 결과에 test coverage 가 추가되면 좋을 것 같다

Lighthouse CI 점수 추가

카카오 FE 기술 블로그 참고

보안 취약점 검사

https://github.com/lirantal/is-website-vulnerable 를 사용해 특정 사이트 취약점 검사

실패 성공 시 Slack 연동

profile
프론트엔드 공부용 저장소

0개의 댓글