배경
모노레포 간단한 구조
현재 회사 FE 모노레포는 UI 패키지와 Next.js 앱 패키지 여러 개로 나뉘어져 있다. 이 외에도 여러가지 패키지가 있지만 간추려서 보여주면 이렇다.
apps
├── design-system
├── mildang-ui
├── app1
├── app2
└── app3
Plain Text
복사
design-system 같은 경우는 비즈니스 로직 없는 순수한 컴포넌트들이 존재하는 UI 패키지이고, mildang-ui 같은 경우는 밀당PT 비즈니스와 관련되어 자주 쓰이는 컴포넌트들이 존재하는 product system 패키지이다.
보통은 vercel 내의 git integration 기능을 통해 특정 브랜치의 특정 패키지에 변경사항이 생기면 자동으로 배포한다.
하지만 우리 모노레포 구조상, mildang-ui나 design-system 등에만 변경사항이 생기고 서비스 앱은 컴포넌트를 갖다 쓰고 있는 경우가 많아서 이런 자동 배포 시스템이 맞지 않았다.
기존 배포 방식
팀장님에게는 예전에는 git release와 CHANGE_LOG를 통해서 UI 패키지를 버전업하고, 해당 패키지를 사용하는 앱에서 버전을 변경하여 변경사항을 만들어내 자동 배포가 이루어지게 했다고 들었다.
문제가 된 배포 방식
하지만 이를 강제하는 체계가 없기도 하고, 국가주도사업에 참여하기 위해 더욱 바빠지게 되면서 이러한 룰을 지키지 않게 되었다고 한다. 일단 변경사항이 잦게 일어나는 mildang-ui의 경우, 다른 패키지에서 바로 참조할 수 있도록 build output을 없애는 방식으로 전환했다는 배경도 있었다. 이런 상황에서 빠르게 Next.js app 배포를 하는 꼼수(?)를 생각하다보니 README.md를 수정하여 fake commit을 만들어 vercel에 자동배포되도록 했다.
readme를 수정해 배포
이 방식의 문제
1.
불필요한 fake commit 생성
2.
vercel_ignore_step.sh에 매번 피쳐 브랜치 제목 추가해야하는 번거로움
3.
같은 README를 수정하다보니 생기는 불필요한 merge conflict
4.
배포 commit을 까먹으면 배포가 되지 않는 등 휴먼 에러
해당 방식은 위와 같은 문제들이 있었다. 생소한 배포 방식이다보니, 나도 입사 초기에는 이런 배포 방식에 적응하지 못해 배포 커밋을 까먹어 배포가 되지 않는 일도 있었고, merge conflict도 빈번하게 일어났었다. 그리고 vercel_ignore_step 스크립트를 수정하거나 fake commit을 생성해줘야하는 번거로움도 있어서 이런 불편함을 해결하기 위해서는 어떻게 해야할까 고민했다.
FE 개선 과제: 배포 flow 개선
입사 3~4개월 후. 바쁜 국가사업이 마무리 되고 FE 팀에 워크플로우나 프로젝트에 대해 내부적으로 개선할 시간이 주어졌다. 그중에서도 나는 벼르고 있었던 배포 플로우를 개선하기로 했다.
고려 사항
1.
배포할 때 vercel_ignore_step.sh, 특정 브랜치 수정사항(README.md 등) 만들어야함. -> 까먹을 수 있음
2.
어떻게 하면 자동으로 배포되게 할까. 혹은 원하는 패키지를 편하게 배포하게 할까.
3.
요구사항
•
특정 브랜치 기준으로
•
특정 패키지들을 선택해서 vercel에 배포할 수 있게 한다.
개선사항
•
feat, fix 등 으로 시작하는 브랜치는 vercel ignore step 브랜치에서 제외
•
next app 배포 시 readme.md 변경 -> dispatch workflow로 브랜치, 패키지명 선택해서 배포
vercel_ignore_step.sh
vercel_ignore_step.sh은 모든 커밋에 대해 배포가 일어나는 것을 막거나, 수동 배포를 통해 배포 시스템을 통제하고 싶은 경우 사용하는 shell script이다. 우리 프로젝트의 경우 배포가 가능한 브랜치들을 해당 셸 스크립트에 적어주었다.
echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF"
# 기존
if [[ "$VERCEL_GIT_COMMIT_REF" =~ ^(stage|develop|production|beta|gs-certification|)$ ]]; then
# 수정
if [[ "$VERCEL_GIT_COMMIT_REF" =~ ^(stage|develop|production|beta|gs-certification|feat.*|fix.*)$ ]]; then
Shell
복사
기존의 경우, 피쳐 브랜치에서 배포할 때 브랜치 리스트에 피쳐 브랜치 이름을 추가해주었어야 했는데 정규식을 이용해 이런 수고를 덜었다.
workflow_dispatch로 브랜치, 패키지명 선택해서 배포
워크플로우 실행 시 뜨는 popper
workflow 실행 시 브랜치를 선택하는 옵션은 깃헙에서 자동으로 제공해주니 제외하고, 패키지만 input으로 받으면 된다.
당시에는 turbo가 아닌 rush를 쓰고 있었어서(이것도 FE 개선 스프린트 때 바뀌었다.) 워크플로우에서도 rush를 사용하는 부분들이 있다.
workflow 초안
name: Deploy To Vercel
on:
workflow_dispatch:
inputs:
package:
description: '배포할 패키지를 선택하세요'
required: true
type: choice
options:
- app1
- app2
- app3
- app4
- app5
- app6
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: node common/scripts/install-run-rush.js install --purge
- name: Install Vercel CLI
run: npm install -g vercel
- name: Run Deploy Script
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_LINKS: ${{ secrets.VERCEL_LINKS }}
BRANCH_NAME: ${{ github.ref_name }}
run: |
cd scripts/vercel-deploy
npm run link:vercel ${{ github.event.inputs.package }}
- name: List files in project root
run: |
ls -la
cat .vercel/project.json
- name: Deploy to Vercel
run: vercel --token ${{ secrets.VERCEL_TOKEN }} -y
YAML
복사
여기서 제일 핵심적인 부분은 이 부분인데, vercel cli를 통해 배포를 trigger할 때 배포할 프로젝트에 .vercel/project.json 파일을 추가해줘야하기 때문이다.
- name: Run Deploy Script
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_LINKS: ${{ secrets.VERCEL_LINKS }}
BRANCH_NAME: ${{ github.ref_name }}
run: |
cd scripts/vercel-deploy
npm run link:vercel ${{ github.event.inputs.package }}
YAML
복사
scripts/vercel-deploy/generate-vercel-link.ts
워크플로우에서 실행하기 위해 프로젝트 루트의 scripts 폴더에 vercel-deploy 라는 패키지를 만들어 스크립트를 추가했다.
VERCEL_LINKS에는 패키지 이름에 projectId와 orgId가 매핑된 JSON 포맷의 데이터를 JSON.stringify하여 VERCEL_LINKS라는 깃헙 시크릿으로 넣어주었다.
import { writeFileSync, mkdirSync } from 'fs';
import path from 'path';
const deploy = async () => {
try {
const packageName = process.argv[2];
const vercelLinksRaw = process.env.VERCEL_LINKS;
const vercelLinks = JSON.parse(vercelLinksRaw);
console.log(vercelLinks);
if (!packageName || !vercelLinksRaw) {
throw new Error('package name and vercelLinks are required');
}
console.log('packageName', packageName);
console.log('vercelLinksRaw', vercelLinksRaw);
const project = vercelLinks[packageName];
if (!project) {
console.error(`No project found for package: ${packageName}`);
process.exit(1);
}
const { projectId, orgId } = project;
if (!projectId || !orgId) {
console.error(`projectId or orgId is missing for package: ${packageName}`);
process.exit(1);
}
const projectRoot = path.resolve(__dirname, '../../');
const vercelDir = path.join(projectRoot, '.vercel');
mkdirSync(vercelDir, { recursive: true });
const projectJsonPath = path.join(vercelDir, 'project.json');
const projectJson = { projectId, orgId };
writeFileSync(projectJsonPath, JSON.stringify(projectJson, null, 2));
console.log(`.vercel/project.json created for package: ${packageName} at ${projectJsonPath}`);
} catch (error) {
console.error('Error parsing VERCEL_LINKS or writing project.json:', error);
process.exit(1);
}
};
deploy().catch((err) => {
console.error(err);
process.exit(1);
});
TypeScript
복사
그리고 .vercel/project.json 경로에 projectId, orgId 데이터를 넣어주면 된다.
추가 개발사항
turbo로 변경되면서 rush install 부분이 빠지고 pnpm install로 대신하였다.
pnpm 의존성 설치
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Cache pnpm
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Install Dependencies
run: pnpm install --filter @<team>/vercel-deploy
TypeScript
복사
pnpm install --filter @<team>/vercel-deploy
vercel에 배포를 trigger하는 것 뿐이라 프로젝트 의존성을 install할 필요가 없어서 vercel-deploy 패키지만 —filter 옵션을 통해 설치해주었다.
그리고 의존성 변화도 잦지 않기 때문에 Cache pnpm 스텝을 추가해 pnpm install하는 시간을 3초 정도로 줄였다.
전체 프로젝트 의존성을 설치할 때는 1분 30초 정도 걸렸지만 이런 스텝을 추가해 의존성 설치 시간을 3초로 줄일 수 있었다.
배포자 구분
workflow 초안의 경우 token을 하나만 사용하고 있었기 때문에 배포자가 한 명으로만 찍혔다. 때문에 누가 배포했는지 구분하기 어려운 불편함이 있었다.
누가 배포한거지?
그래서 다른 구성원 분들에게 전부 vercel token을 전달 받아 secret에 넣고 jq로 파싱해서 actor의 token을 사용하게 했다.
- name: Install jq
run: sudo apt-get install jq
- name: Set Vercel Token Based on Actor
id: set_token
run: |
VERCEL_TOKEN=$(echo '${{ secrets.VERCEL_TOKENS_BY_ACTOR }}' | jq -r '."${{ github.actor }}"')
if [ -z "$VERCEL_TOKEN" ] || [ "$VERCEL_TOKEN" == "null" ]; then
echo "No valid token found for ${{ github.actor }}, using default token."
VERCEL_TOKEN=${{ secrets.VERCEL_TOKEN }}
fi
echo "VERCEL_TOKEN=$VERCEL_TOKEN" >> $GITHUB_ENV
YAML
복사
vercel alias로 도메인 변경
기존에는 branch 이름으로 통일된 배포 url이 발행되었는데, 수동으로 배포하다보니 매번 랜덤한 url이 생성되었다.
그러다보니 디자이너나 QA 팀에 url을 전달할 때 매번 다른 url을 드려야 해서 번거로움이 있었는데 vercel이 preview url을 만드는 방식 을 참고해서, 브랜치와 패키지 이름을 통한 공통된 url이 나올 수 있게 했다.
<project-name>-git-<branch-name>-<scope-slug>.vercel.app;
YAML
복사
Deploy to Vercel And Extract URL 스텝에서는 https://[…].vercel.app 형태로 나오는 도메인을 DEPLOY_URL으로 저장한다.
그리고 vercel의 프로젝트명과 소스코드 내의 패키지명이 다른 경우가 있어서, Map Package to Display Name스텝을 통해 vercel 기준으로 맞춰주었다.
브랜치 이름의 경우 fix/some-issue와 같이 슬래시가 들어가는 경우가 있는데, 도메인에서 slash 대신 dash가 쓰이도록 Replace slash with dash in branch name에서 변경했다.
vercel alias ${{ env.DEPLOY_URL }} $ALIAS_URL --token ${{ env.VERCEL_TOKEN }} --scope <vercel team name> 그리고 vercel alias를 통해 DEPLOY_URL을 ALIAS_URL로 대체한다.
- name: Deploy to Vercel And Extract URL
id: deploy
run: |
DEPLOY_OUTPUT=$(vercel --token ${{ env.VERCEL_TOKEN }} -y)
echo "$DEPLOY_OUTPUT"
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -o 'https://[^ ]*.vercel.app')
echo "Deployed URL: $DEPLOY_URL"
echo "DEPLOY_URL=$DEPLOY_URL" >> $GITHUB_ENV
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- name: Map Package to Display Name
run: |
case "${{ github.event.inputs.package }}" in
app1) VERCEL_DISPLAY_NAME="vercel-project-name1";;
app2) VERCEL_DISPLAY_NAME="vercel-project-name2";;
app3) VERCEL_DISPLAY_NAME="vercel-project-name3";;
*) VERCEL_DISPLAY_NAME="unknown";;
esac
echo "VERCEL_DISPLAY_NAME=$VERCEL_DISPLAY_NAME" >> $GITHUB_ENV
- name: Replace slash with dash in branch name
run: |
BRANCH_NAME="${{ github.ref_name }}"
# Use sed to replace / with -
CLEANED_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's|/|-|g' | cut -c 1-63)
echo "Branch name with dashes (max 63 chars): $CLEANED_BRANCH_NAME"
echo "CLEANED_BRANCH_NAME=$CLEANED_BRANCH_NAME" >> $GITHUB_ENV
- name: Set Vercel Alias based on Branch Name
run: |
ALIAS_URL="${{ env.VERCEL_DISPLAY_NAME }}-git-${{ env.CLEANED_BRANCH_NAME }}-<vercel team name>.vercel.app"
vercel alias ${{ env.DEPLOY_URL }} $ALIAS_URL --token ${{ env.VERCEL_TOKEN }} --scope <vercel team name>
echo "ALIAS_URL=$ALIAS_URL" >> $GITHUB_ENV
YAML
복사
배포 결과 슬랙 알림
slack에도 vercel 앱이 있긴 하지만 멘션이 안되고, 해당 배포와 관련된 릴리즈 노트같은 것을 적을 수 없어서 아쉬운 부분이 있었다.
기존에는 아래와 같이 사람이 배포가 되었는지 안되었는지 직접 확인하고, deploy 채널에 change log와 함께 배포 결과를 올렸는데, (QA, PM, 디자이너에게 공유해야만 하는 경우)
슬랙 노티 추가 후에는 아래와 같이 봇이 해당 일을 대신해주게 되었다.
workflow 코드
inputs에 change_logs를 추가해, 배포 성공 시 로그와 함께 배포 성공 여부를 전달할 수 있게 했다.
multiline이 지원되면 좋겠지만 github에서 지원되지 않아 \n 으로 줄 구분을 하게 했다. 짜치지만 더 좋은 방법이 없는 것 같다.
커밋 로그를 가져와서 보여줄까 했지만 커밋 로그의 경우 QA나 PM, 디자이너분들에게 딜리버리하기엔 너무 날 것의 정보라 제외했다.
on:
workflow_dispatch:
inputs:
# ...중략
change_logs:
description: '배포 변경사항을 입력하세요. 입력 시 #deploy 채널에 알림이 갑니다. (\n으로 여러줄 입력)'
required: false
type: string
YAML
복사
notify 관련 step
- name: Send Slack Notification (Success)
if: success()
run: |
cd scripts/vercel-deploy
npm run notify:success
env:
SLACK_WEBHOOK_URL_TO_RND_DEPLOY: ${{ secrets.SLACK_WEBHOOK_URL_TO_RND_DEPLOY }}
SLACK_WEBHOOK_URL_TO_DEPLOY_NOTIFICATION: ${{ secrets.SLACK_WEBHOOK_URL_TO_DEPLOY_NOTIFICATION }}
PACKAGE: ${{ github.event.inputs.package }}
DEPLOY_URL: ${{ env.ALIAS_URL }}
BRANCH_NAME: ${{ github.ref_name }}
GITHUB_ACTOR: ${{ github.actor }}
CHANGE_LOGS: ${{ github.event.inputs.change_logs }}
- name: Send Slack Notification (Failure)
if: failure()
run: |
cd scripts/vercel-deploy
npm run notify:failure
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_TO_DEPLOY_NOTIFICATION }}
PACKAGE: ${{ github.event.inputs.package }}
BRANCH_NAME: ${{ github.ref_name }}
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_SERVER_URL: ${{ env.GITHUB_SERVER_URL }}
GITHUB_REPOSITORY: ${{ env.GITHUB_REPOSITORY }}
GITHUB_RUN_ID: ${{ env.GITHUB_RUN_ID }}
YAML
복사
개선된 후 결과
3 step → 1 step
fake commit을 만들고, vercel_ignore_step.sh을 고치고, merge conflict를 수정하는 세 단계의 배포 과정이 workflow를 트리거하는 단계 하나로 줄어들었다.
휴먼 에러 감소
워크플로우를 통해 명시적으로 배포하다보니 fake commit을 까먹고 배포가 왜 안되지?하는 문제가 없어졌다.
배포 슬랙 노티
슬로우워크라는 책을 읽으면서 중요한 일에만 집중할 수 있게, 반복되고 필요없는 일을 자동화해보자 결심했는데 이 일도 그 중의 일환이었다. 일일이 배포 결과에 대해 사람이 수동으로 노티하지 않게 되어 편해졌다. 실제로 팀원들에게도 편하다는 피드백을 받았다.