Github Actions Reusable로 프론트엔드 배포 플로우 통합하기

Yeonbeen Park
원티드랩 기술 블로그
12 min readSep 14, 2023

안녕하세요 원티드랩 인플로우팀에서 프론트엔드 개발을 하고 있는 박연빈입니다.

Github Actions Reusable Workflow를 이용하여 프론트엔드 프로젝트들의 배포 플로우를 통합한 작업을 설명해 드리겠습니다.

배경

현재 원티드에는 어드민 사이트를 포함한 여러가지의 프론트엔드 프로젝트가 존재합니다.

기존 배포 시스템 관련 변경이 있을 때 매번 각 프로젝트에 대해
클론 — 수정— 반영의 작업을 반복해왔습니다.

보통 해당 작업은 서비스 개발자의 편의성을 증가시키기 위함과
수정 작업에서 발생하는 커뮤니케이션의 최소화를 위해
직무 미팅때 작업 내용 및 일정을 공유한 후 한 명이 도맡아 하곤 했습니다.

각 서비스별 확인해야 하는 횟수가 무려 2~4번으로 총 약 30회에 달하다보니 변경을 놓치는 경우도 있었고, 각 서비스 마다 차이가 있어 수정이 필요해 추가 시간이 소요되었습니다.

따라서 배포 시스템 통합 작업을 통해 워크플로 파일을 한 군데서 관리하고자 하는 니즈가 생겨 Github Actions Reusable Workflow를 이용하기로 했습니다.

해당 작업을 통해 관리 포인트를 하나로 통합하고, 각 서비스에서 가져다 사용 하면 되는 모듈화 방식이 되어, 기존 대비 작업 소요시간 단축 및 개발 생산성 증가를 기대했습니다.

Github Actions Reusable이란?

한 워크플로에서 다른 워크플로로 복사하여 붙여넣는 대신 워크플로를 재사용 가능하게 만들 수 있습니다. 재사용 가능한 워크플로에 대한 액세스 권한이 있는 모든 사용자는 다른 워크플로에서 재사용 가능한 워크플로를 호출할 수 있습니다.

workflow를 복사하여 각각 사용하는 대신, 하나의 workflow를 만들고 사용하는 곳에서 호출하는 형태로 재사용이 가능한 workflow를 만드는 것 입니다.

react에서 hook 패턴을 사용하여 동일한 데이터 및 작업이 여러군데서 쓰이는 것을 통합 관리하는 것과 같이 workflow 또한 통합하여 사용할 수 있음을 알 수 있습니다.

한 곳에서 workflow를 만들면 사용하는 곳에선 파라미터를 이용하여 해당 서비스에 맞게 동작하도록 사용할 수 있고, 버전별, 브랜치 별로 다른 workflow를 만들 수 있습니다.

구조

구조는 다음과 같이 변경되었습니다.

기존에는 각 서비스 워크플로에서 하나의 job에서 step으로 나누어 워크플로를 실행했다면

변경 후에는 공통 워크플로 레파지토리에 정의 후 각 서비스 워크플로에서 호출하여 사용합니다.

호출하여 사용할 때는 step 이 아닌, job 단위로 사용하게됩니다.

AS-IS

TO-BE

변경된 점

  • 기존 서비스 워크플로의 job 하위의 step들을 개별 워크플로 파일로 분리했습니다.
  • 분리된 step은 필요한 워크플로를 호출하여 사용하고 job으로 실행됩니다.
  • 호출되는 워크플로 파일에선 필요한 변수를 받아 워크플로를 실행하게됩니다.
  • 개발서버 / 실서버 워크플로 브랜치를 분리했습니다.
  • 개별 워크플로 파일의 결과물을 공유하기 위해 캐싱을 적극적으로 활용했습니다.

장점

관리포인트가 하나로 합쳐지며 공통 수정사항이 생길 때 작업 시간이 단축됩니다.

기존 개별 서비스에 각각 작업해야 했다면, 변경 이후에는 한 곳에서만 작업하면 됩니다.

9개의 서비스를 변경한다고 가정한다면,
한 서비스를 변경-적용-확인하는데 15분[(변경 5분+배포 10분) x 9개] 이 걸린다고 가정할때 약 130분이 소요되는 반면,
통합 후엔 약 95분[(변경 1 회 5분 + 배포 10분) x 9개] 이 소요된다고 계산하면 35분이 단축된다고 가정할 수 있습니다.

단점

  1. 총 배포시간이 약간 증가하게 됩니다.
    각 job마다 작업을 준비하는데 시간이 소요되고 호출하는 쪽의 코드를 내려받아야 하기 때문에 시간이 소요됩니다.
  2. 통합 워크플로에 에러가 있는 경우 배포 시작이 불가합니다.
    하지만 배포 시작자체가 안되어 실서버에 영향을 끼칠 우려는 현저히 적습니다.
    실서버 배포시 동작하는 워크플로는 이미 테스트 서버에서도 동작을 하기 때문에 동작하지 않는 문제는 미리 예방할 수 있습니다.

적용 과정

배포 플로우 파악

먼저 각 서비스별 배포 플로우가 어떻게 되는지 파악했습니다.

배포 플로우는 기본적으로
[ 패키지 매니저(yarn)설치 — 어플리케이션 빌드 — S3 정적 파일 업로드 — 도커 이미지 빌드/배포 — ECS 업데이트 ] 로 동일하게 되어있습니다.

기술 스택 파악

다음으로는 통합가능한 단계와 통합하기 어려운 단계를 먼저 파악했습니다.

통합이 가능한 단계 — 노드 설치, 정적 파일 S3 업로드, 도커 이미지 빌드/배포, ECS 업데이트
통합이 불가능한 단계 — 패키지 매니저 설치, 어플리케이션 빌드

따라서 통합이 가능한 단계들은 단계별 하나의 워크플로 파일로 만들었고,
패키지 매니저 설치(yarn v1/berry), 어플리케이션(Next.js, React.js) 빌드에 대한 부분은 버전별/스택별 워크플로 파일을 나누어 만들었습니다.

워크플로 정의

대표적으로 빌드 작업을 예시로 설명드리겠습니다.

어플리케이션 빌드

원티드 프론트엔드 프로젝트에서 사용하는 프레임워크는 next.js 와 react.js 두 가지 스택로 나뉘어 있습니다.

먼저 react를 사용하는 서비스들은 모두 yarn v1 버전을 사용하고 있습니다.
모두 webpack을 이용하고 있지만 특정 서비스 어드민 사이트의 경우 vite를 사용하고있어 빌드 명령어가 달랐습니다.

이 부분은 빌드 명령어 자체를 전달하여 실행하는 방식으로 해결했습니다.

또한 어플리케이션 빌드시 패키지 매니저가 필요하여 빌드 단계에서 패키지 설치에 소요되는 시간을 생략하고자 이전단계에서 캐싱한 패키지를 Cache Key를 이용해 불러옵니다.

이를 통해 추가적인 패키지 설치 없이 어플리케이션 빌드를 진행 할 수 있었습니다.

AS-IS

기존에는 각 서비스 워크플로에서 다음과 같이 사용했습니다

build-react.yml

...
- name: Yarn Build Backdoor
run: |
yarn run build --env.server="${SERVER}" --env.date="${DATE}" --env.commit="${HASH}@"
...
...
- name: Yarn Build id-admin
run: VITE_SERVER_TYPE=$SERVER yarn run build --mode $SERVER --base=$PUBLIC_URL_BASE/***/$SERVER/
env:
PUBLIC_URL_BASE: ***
...

TO-BE

변경 후 공통 워크플로 레파지토리엔 다음과 같이 정의했습니다.

build-react.yml

name: Reusable workflow example

on:
workflow_call:
inputs:
yarn_cache_key: # yarn workflow에서 반환된 키
required: true
type: string
node_version: # 노드 버전
required: true
type: string
build_path: # 빌드 결과물 디렉토리명 (ex. dist)
required: false
type: string
build_command: # 빌드 명령어
required: false
type: string

outputs:
build_cache_key: # 빌드 결과물 캐시키
value: ${{ jobs.build-react.outputs.cache-key }}

jobs:
build-react:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.build-cache.outputs.key }}

steps:
...
- name: Get Yarn Version #패키지 매니저 버전 확인
id: yarn-version
run: echo "version=$(printf '%s' $(yarn -v) | cut -c1)" >> $GITHUB_OUTPUT

- name: Restore yarn v1 cache #패키지 매니저 버전이 3보다 작다면 실행
if: ${{ steps.yarn-version.outputs.version < 3 }}
uses: actions/cache@v3
with:
path: node_modules
key: ${{ inputs.yarn_cache_key }}

- name: Restore yarn berry cache #패키지 매니저 버전이 3이상이면 실행
if: ${{ steps.yarn-version.outputs.version >= 3 }}
uses: actions/cache@v3
with:
path: .yarn
key: ${{ inputs.yarn_cache_key }}

- name: Build React #전달받은 명령어로 빌드 실행
run: ${{ inputs.build_command }}

- name: Get Build Cache #빌드 결과물 캐싱
id: build-cache
run: echo "key=${{ runner.os }}-${{ github.repository }}-${{ hashFiles(format('{0}/index.html', inputs.build_path)) }}" >> $GITHUB_OUTPUT
env:
BUILD_PATH: ${{ inputs.build_path }}

- name: Cache Build #캐시키 반환
uses: actions/cache@v3
with:
path: ${{ inputs.build_path }}
key: ${{ steps.build-cache.outputs.key }}

변경 후 호출하는 서비스에서 다음과 같이 위의 워크플로를 호출합니다.

build-react:
needs: [set-env, set-yarn-v1]
uses: wanteddev/github-actions-module/.github/workflows/build-react.yml@dev
with:
node_version: ${{ needs.set-env.outputs.node_version }}
yarn_cache_key: ${{ needs.set-yarn-v1.outputs.yarn_cache_key }}
server_type: ${{ needs.set-env.outputs.server_type }}
build_path: dist
build_command: yarn run build-"${{ needs.set-env.outputs.build }}" --env.server="${{ needs.set-env.outputs.server_type }}" --env.date="${{ needs.set-env.outputs.date }}" --env.commit="${{ needs.set-env.outputs.hash }}@"

워크플로의 전체 흐름도를 보시면 다음과 같습니다.

정적 파일 업로드 와 도커 이미지 빌드 작업은 서로 연관이 없어 어플리케이션 빌드 작업이 끝난 이후 병렬로 실행하여 실행시간을 조금이나마 단축시켰습니다.

고려했던 점

  • yarn v1/berry 등의 분기를 태워야할 때 어떤 기준으로 분기처리를 해야하는지 고민했습니다.
    분기처리가 많이 필요하다면( 3번 이상)는 개별 파일로 분리하였습니다.
  • 가급적 하나의 단계는 하나의 workflow를 사용할 수 있도록 하였습니다.— 너무 세분화하면 모듈화하는 의미가 희미해지기 때문입니다.
  • 빌드 명령어만 다르듯이 같은 단계지만 사소한 차이가 있을 때 어떻게 할 것인지 고민했습니다.
    워크플로 호출시 파라미터로 전달하여 동적으로 실행할 수 있게 대응했습니다.
  • 각 job 마다 공통된 step을 어떻게 해결할 것인가 고민했습니다.
    node-version 세팅, checkout, yarn install 등 공통된 스텝들이 있었는데
    캐싱으로 활용할 수 있는 것들은 최대한 활용하고 (어쩔수없이) 그 외는 각 job마다 수행하도록 했습니다.

마주친 이슈들

  • 각 작업의 내용이 공유가 불가능해 작업마다 패키지 및 어플리케이션 빌드를 다시 해야 하는 번거로움이 있었습니다. 해당 문제는 캐싱을 적극적으로 활용하여 해결했습니다.
  • CacheKey 생성시 스테이지 별 고유값을 갖지 않으면 상위 스테이지에 배포를 해도 하위 스테이지 서버를 바라보는 이슈가 있었습니다.
    Key 생성시 스테이지를 포함하여 스테이지별 고유 CacheKey를 가지는 방법으로 해결했습니다.

아직 개발 생산성 증가율을 측정하지 못했지만, 기존 방식보다 더 간편하다고 느끼고, 변경사항이 있을 때 더 빠르게 적용할 수 있기를 바라며 글을 마치겠습니다.

--

--