모노레포를 운영하는 많은 팀들이 겪는 공통적인 고민 중 하나는, “각 프로젝트의 배포 주기를 어떻게 분리할 것인가?”입니다. 코드는 하나의 저장소에 있지만, 실제로는 앱과 웹, 국가별 서비스, 등 프로젝트마다 릴리즈 전략과 타이밍이 모두 다릅니다. 그래서 단일한 브랜치 흐름과 배포 구조로는 점점 현실적인 요구를 감당하기 어려워집니다.
피처링코 프론트엔드팀도 app, landing, core같은 프로젝트들을 하나의 모노레포에서 관리하고 있으며, 초기에는 단순한 Git Flow – feature 브랜치에서 작업해 develop → release → master로 머지하는 구조를 사용했습니다.
Vercel에 GitHub 연동을 설정해두면 브랜치 푸시만으로 Preview/Production 환경이 자동 생성되고, 복잡한 설정 없이 빠르게 결과를 확인할 수 있어 스타트업 초기에 매우 유용했습니다.
하지만 당시에는 모든 프로젝트가 동일한 브랜치 흐름 위에 얹혀 있었기 때문에, release 브랜치에 어떤 변경사항이 쌓이든 app, landing, core 등 모든 프로젝트가 한꺼번에 배포되는 구조였습니다. 단순한 구조였던 만큼 관리가 편하긴 했지만, 서비스별로 배포 시점이 달라지기 시작하면서 점점 복잡한 요구를 감당하기 어려워졌습니다.
가장 먼저 문제로 떠오른 건 국가별 앱의 배포 분리였습니다.
app 레포지토리에서 배포되는 한국(app-ko)과 일본(app-ja) 버전의 피처링코 애플리케이션은 동일한 코드베이스를 공유하지만, 기능 출시 시점이나 마케팅 일정은 전혀 다르게 운영되어야 했습니다. 예를 들어, 한국은 v1.5.18 버전까지 출시된 상태지만, 일본은 정책적으로 v1.5.16에서 멈춰 있어야 하는 상황이 자주 발생했습니다. 하지만 기존 구조에서는 동일한 브랜치를 기준으로 두 앱이 함께 배포되었기 때문에, 이 요구를 수용하기 위해 불필요한 브랜치 분기나 커밋 롤백이 반복됐습니다.
랜딩 프로젝트에서도 마찬가지로 구조적인 한계가 드러났습니다.
랜딩은 앱과 달리 Growth 팀과의 협업을 통해 Amplitude 로그를 기반으로 UI/UX 테스트를 반복적으로 수행하고, 그 결과를 바탕으로 즉시 배포하여 전환율을 검증해야 하는 특성이 있습니다.
하지만 기존에는 landing도 app과 동일한 배포 흐름에 묶여 있었기 때문에, 테스트 템포를 릴리즈 일정에 맞춰야 하는 제약이 생겼고, 이는 테스트 사이클의 속도를 현저히 떨어뜨리는 원인이 되었습니다.
운영 환경 분리에서도 제약은 계속됐습니다.
당시 사용하던 Vercel은 Preview/Production 두 환경만을 기본 제공하고 있었기 때문에, develop, staging, staging-qa, production처럼 실제 운영에서 필요한 세분화된 환경 구조를 만들 수 없었습니다.
뿐만 아니라, 환경별 .env 분기, 서비스별 도메인 지정, 국가별 배포 조건 등의 요구사항을 반영하기도 어려웠습니다.
이러한 문제들이 쌓이면서, 기존처럼 브랜치 단위로 전체 프로젝트를 묶어 배포하는 전략 대신, 서비스 단위 · 국가 단위 · 환경 단위로 명확하게 분리된 배포 전략을 갖춰야 한다는 결론에 도달하게 되었습니다.
재설계한 배포 흐름
운영 중 발생한 문제들을 해결하기 위해, 기존 구조를 완전히 재설계하기로 했습니다.
각 프로젝트는 라벨을 통해 어떤 대상이 배포될지를 명시하고, 그에 따라 적절한 환경설정과 도메인이 적용되어 Vercel CLI를 통해 배포됩니다.
이 구조를 구현하기 위해 다음과 같은 전략을 중심으로 배포 흐름을 재설계했습니다.
- PR 라벨을 기준으로 배포 대상 분리
PR에 부착된 deploy:* 라벨을 기준으로, GitHub Actions 내에서 배포할 프로젝트를 분기합니다. - GitHub Actions 기반 배포 흐름 설계
브랜치 연동 대신 vercel deploy 명령어를 직접 실행하는 방식으로 배포를 구성하고, 전 과정을 코드로 제어합니다. - 환경별 배포 세분화 및 Secrets Value 관리
dev, stg, stg-qa, prod 환경을 구분하고, 각 환경에 필요한 .env는 AWS Secrets Manager에서 불러옵니다. - 배포 중심으로 재정의한 Git Flow
기존 Git Flow 방식에서 벗어나, 브랜치를 배포 목적과 QA 주체에 따라 명확히 분리하여 릴리즈 시점과 대상을 통제할 수 있는 구조로 재정의합니다.
PR 라벨을 기준으로 배포 대상 분리
기존에는 release 브랜치에 머지되는 모든 변경사항이 한꺼번에 배포되는 구조였기 때문에, 여러 프로젝트가 동시에 배포되어야 했습니다. 이로 인해 landing만 배포하고 싶어도 app이 함께 배포되거나, 일본 앱만 릴리즈해야 하는 상황에서도 한국 앱이 같이 배포되는 문제가 발생하곤 했습니다.
이 문제를 해결하기 위해, PR에 부착된 라벨을 기준으로 배포 대상을 명시적으로 분리하는 방식을 도입했습니다.
PR이 열릴 때 자동으로 deploy:* 라벨이 붙고, GitHub Actions에서는 해당 라벨을 기준으로 어떤 워크플로우가 실행될지를 결정합니다.
예를 들어 아래와 같이 라벨을 사용합니다:
- deploy:app → 한국 앱(app-ko) 배포
- deploy:jp-app → 일본 앱(app-ja) 배포
- deploy:landing → 한국 랜딩 배포
- deploy:jp-landing → 일본 랜딩 배포
- …
라벨은 actions/labeler@v5와 .github/labeler.yml을 활용해 디렉토리 변경 내역을 기반으로 자동 지정됩니다.
# .github/labeler.yml deploy:app: - changed-files: - any-glob-to-any-file: 'packages/apps/**' deploy:landing: - changed-files: - any-glob-to-any-file: 'packages/landing/**' ...
해당 라벨이 PR에 부착되면, GitHub Actions에서 다음과 같이 조건 분기를 수행합니다:
if: contains(needs.check_merged_pr_labels.outputs.PR_LABELS, 'deploy:app')
어떤 프로젝트를 배포할지를 개발자가 직접 PR 라벨로 제어할 수 있기 때문에, 같은 변경이라도 필요한 서비스에만 배포를 적용하거나, 영향을 받지 않아야 하는 서비스는 배포 대상에서 제외할 수 있게 되었습니다. 배포 시점을 코드 변경 시점과 분리할 수 있다는 점에서, 팀 운영의 유연성과 안정성이 크게 개선되었습니다.
Github Actions 기반 배포 흐름 설계
초기에는 Vercel과 GitHub를 직접 연동해 브랜치 기반으로 Preview/Production 환경을 자동 생성하는 방식으로 배포를 관리했습니다.
하지만 브랜치가 곧 배포를 의미하는 구조는 점점 복잡해지는 환경별 요구사항을 수용하기에 한계가 있었습니다.
Vercel은 오랫동안 Preview와 Production 두 가지 환경만 제공했기 때문에, stg, stg-qa, dev처럼 세분화된 환경을 분리하려면 별도의 프로젝트를 추가하거나 비효율적인 우회 방식이 필요했습니다.
또한, GitHub Deployments와의 연동도 커스터마이징이 어려워 정확한 릴리즈 추적이 어려운 상황이 반복되었습니다.
이러한 문제를 해결하기 위해, 브랜치 연동 배포를 완전히 제거하고 GitHub Actions 내부에서 Vercel CLI를 사용한 명시적 배포 방식으로 전환했습니다.
# 예시: Vercel CLI를 통한 명시적 배포 - name: Deploy to Vercel run: | DEPLOYMENT_URL=$(vercel deploy --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ env.VERCEL_SCOPE }} --yes) vercel alias set $DEPLOYMENT_URL ${{ env.ALIAS_DOMAINS }} --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ env.VERCEL_SCOPE }}
이 방식으로 바꾸면서 다음과 같은 개선이 이루어졌습니다:
-
브랜치와 배포 흐름을 분리할 수 있게 되어, 하나의 브랜치에서 여러 환경으로 분기 배포가 가능해졌습니다.
-
도메인(alias) 지정, 릴리즈 타이밍 통제, 환경별 배포 로그 분리 등 세밀한 제어가 가능해졌습니다.
-
GitHub Deployments를 직접 설정할 수 있기 때문에, 환경별로 명확하게 구분된 배포 내역이 GitHub UI에 정갈하게 나타나도록 구성할 수 있었습니다.
또한 피처링코 팀은 Jira를 통해 각 티켓의 배포 상황을 실시간으로 추적하고 있었기 때문에, GitHub Deployments와 Jira Release가 연결된 구조는 실제 운영에 큰 도움이 되었습니다. 아래 이미지와 같이, 어떤 기능이 어떤 환경에 언제 배포되었는지를 Jira 타임라인 상에서 명확하게 확인할 수 있었습니다.
환경별 배포 세분화 및 Secrets Value 관리
서비스가 성장하면서 dev, stg, stg-qa, prod처럼 더 세분화된 환경 구성이 필요해졌습니다. 단순히 Vercel의 Preview/Production 기반 환경 변수나 브랜치별 환경 변수 구성만으로는 서비스별, 국가별, 목적별로 나뉜 다양한 배포 요구를 감당하기 어려웠습니다.
이에 따라 각 환경에 맞는 .env 파일을 AWS Secrets Manager에서 불러오는 방식으로 전환했습니다. GitHub Actions는 배포 대상에 맞는 secret ID를 기준으로 .env를 생성하며, 이를 통해 모든 환경의 설정을 명확하게 구분하고, 안전하게 관리할 수 있게 되었습니다.
# 예시: 환경별 Secrets 주입 - name: Create .env file run: | SECRETS=$(aws secretsmanager get-secret-value \ --secret-id ${{ env.AWS_SECRET_ID }} \ --query SecretString \ --output text \ --region ${{ env.AWS_SECRET_REGION }}) echo "$SECRETS" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"' > ${{ env.ENV_FILE_PATH }}
배포 환경에 따라 AWS_SECRET_ID 값만 다르게 지정하면, 같은 GitHub Actions 템플릿으로도 각 환경별 .env 파일을 생성할 수 있습니다.
예를 들어,
-
featuring-co-front/apps/dev/ko → dev 한국 앱
-
featuring-co-front/apps/stg-qa/ja → stg-qa 일본 앱
-
featuring-co-front/landing/prod → 프로덕션 랜딩
이처럼 각 환경에 맞는 값을 정확하게 분리해 관리할 수 있게 되었고, 보안상 민감한 설정도 GitHub나 Vercel에 노출하지 않으면서 안전하게 유지할 수 있었습니다.
또한 이 구조는 신규 개발자의 온보딩 자동화와 서비스별 테스트 환경전환에도 활용되었습니다. 언어, 환경, 프로젝트를 선택하면 AWS Secrets Manager에서 설정값을 자동으로 가져와 .env, .npmrc, i18n-key.json 파일을 생성해주는 CLI 스크립트를 함께 작성했습니다.
$ node scripts/set-up-env.js --task env --project apps --environment dev --lang ko
이처럼, 무거운 컨테이너 기반 개발 환경 없이도 스크립트 한 줄로 동일한 개발 경험을 공유할 수 있도록 설계했습니다.
배포 중심으로 재정의한 Git Flow
앞서 설명했듯, 기존에는 develop → release → master라는 전형적인 Git Flow를 따르며 각 브랜치에 병합된 시점에 배포되는 구조를 사용하고 있었습니다. 이 흐름은 규모가 작고 배포 주기가 단순했던 시기에는 적합했지만, 서비스별로 릴리즈 전략이 달라지고, 국가별 앱과 랜딩 페이지가 각기 다른 속도로 운영되기 시작하면서 브랜치 흐름과 실제 배포 시점 사이에 명확한 분리 기준이 필요해졌습니다.
따라서 기존 GitHub/Vercel의 Preview/Production 이분법을 벗어나 서비스 운영 관점에 맞춘 환경을 직접 정의했습니다.
브랜치 | 배포 환경 | 주요 목적 |
---|---|---|
develop | develop (dev) | 개발자가 실제 개발 중인 기능을 개발 서버에 반영하여 테스트 |
stage | stage (stg) | 디자이너, 기획자 등 내부 팀원이 상용 환경과 유사하게 QA 테스트를 수행 |
stage-qa | stage-qa (stg-qa) | 외부 QA팀 또는 협력사가 검수할 기능만 선별하여 테스트 |
master | production (prod) | 실제 사용자에게 배포되는 최종 브랜치 |
feature/* fix/* refactor/* | – | Jira 티켓 단위의 기능 개발 브랜치 |
stage & stage-qa
서비스가 성장하면서 QA의 목적도 점점 다양해졌습니다.
내부 디자이너나 기획자가 UI 흐름을 빠르게 확인하고 피드백을 주는 내부 QA, 외부 QA팀 또는 협력사가 기능 단위로 상세하게 검수하는 외부 QA는 그 방식과 요구사항이 완전히 다릅니다.
기존에는 단일 release 브랜치에서 이 모든 QA를 동시에 처리했기 때문에, QA 범위가 넓어질수록 배포 일정이 지연되는 일이 반복됐습니다. 특히 외부 QA가 오래 걸릴 경우, 긴급 수정이나 마케팅 일정까지 묶여버리는 문제가 발생하곤 했습니다.
이러한 문제를 해결하기 위해 QA의 목적에 따라 브랜치를 분리했습니다.
-
stage는 내부 팀원을 위한 QA 환경입니다. 디자이너나 기획자가 기능 흐름을 확인하고, 빠르게 피드백할 수 있도록 구성했습니다.
-
stage-qa는 외부 QA 전용 환경입니다. 외부 검수 대상이 되는 기능만 선별적으로 배포해, 장기적인 QA 일정이 전체 릴리즈 흐름에 영향을 주지 않도록 설계했습니다.
단계적 릴리즈 구조 확립 (develop -> master X)
초기에는 각자 브랜치에서 작업한 뒤 develop에 병합하고, 기능이 충분히 쌓이면 master로 배포하는 구조를 사용했습니다. 이 구조는 단순하고 직관적이었지만, 실제 운영에서는 다음과 같은 문제가 발생했습니다.
-
develop에 병합된 변경사항이 바로 프로덕션에 배포되기엔 위험한 코드가 많은 경우가 있었고,
-
QA되지 않은 작업물이 실서버에 올라가는 일도 발생했습니다.
-
반대로, 특정 기능만 master에 골라서 배포하고 싶을 때는, 롤백이나 cherry-pick이 반복되어 비효율적이었습니다.
따라서 feature → develop의 작업 방식과 feature → stage/stage-qa → master의 구조를 명확히 분리했습니다.
-
develop은 개발자가 직접 개발 서버에서 기능을 확인하고 검토하는 공간입니다.
-
QA가 필요한 기능은 stage 또는 stage-qa 브랜치에 올려 검증을 거치고, 최종 릴리즈 후보만 master에 머지되어 프로덕션에 배포됩니다.
이 구조를 통해, 배포 전 검증 단계를 명확히 분리할 수 있었고, 릴리즈 대상도 개발자가 원하는 범위 내에서 정확하게 통제할 수 있게 됐습니다.
실제 운영에서 체감한 변화
새롭게 재설계된 배포 구조는 반복적으로 마주쳤던 문제들을 실질적으로 해결하는 구조로 작동했습니다. 아래 대표적인 사례들을 간략히 적어보았습니다.
외부 검수가 3주 넘게 걸린 기능, 하지만 릴리즈는 멈추지 않았다
DM/이메일 발송 기능은 외부 QA 업체의 검수와 동시에 외부 시스템 연동에 대한 권한 승인 절차가 필요했습니다. 전체 QA와 승인 절차를 마치는 데 약 3주가 걸렸습니다.
기존 구조였다면 해당 기능 하나로 전체 릴리즈 일정이 미뤄졌겠지만, stage-qa 브랜치를 통해 이 기능을 고립시켜 검수했고, 나머지 기능은 stage 브랜치를 통해 매주 정기적으로 배포할 수 있었습니다. 이후에도 외부 승인이 필요한 기능은 별도로 운영하는 방식이 정착되었습니다.
랜딩 배포는 배포 병목에서 제외되었다
Growth 팀이 신설된 이후, 마케팅팀과 협업하며 다양한 랜딩 테스트들이 동시에 진행됐습니다. 버튼 문구, 폼 구성, UI 등 작은 변화들을 Amplitude 로그 기반으로 바로 배포하는 것이 핵심 과제였습니다. 하지만 기존에는 app과 landing이 같은 배포 흐름에 묶여 있어, 작은 테스트 배포조차 앱 전체에 영향을 주곤 했습니다.
이제는 라벨을 통해 독립적인 배포가 가능해졌고, Growth 팀은 매주 혹은 격일 단위로 테스트를 반복하고 바로 결과를 확인할 수 있게 되었습니다.
일본은 QA 중, 한국은 릴리즈해야 할 때
일본 측 세일즈팀과 협의가 완료되지 않은 기능이 있었고, 일본은 이 기능을 릴리즈 전 stage-qa 환경에서 내부 QA를 한 번 더 진행해야 했습니다. 반면 한국은 QA가 끝났고, 이미 세일즈 이메일까지 발송된 상태였습니다.
과거 구조였다면 일본과 한국 앱이 함께 묶여 있어 QA 일정에 따라 전체 배포가 지연됐겠지만, 이제는 라벨을 통한 배포가 명확히 분리되어, 한국 앱은 먼저 릴리즈하고 일본 앱은 일정에 맞춰 개별적으로 배포할 수 있었습니다.
결론
이번 구조 개선의 핵심은, 단순한 자동화가 아니라 개발자가 배포를 스스로 설계하고 선택할 수 있게 만든 것에 있습니다. 이전에는 브랜치 하나에 여러 기능이 섞이고, 모든 프로젝트가 함께 배포되는 구조였지만, 이제는 기능, 국가, 환경 단위로 명확히 분리하고, “무엇을 언제 배포할지”를 개발자가 직접 정의할 수 있게 되었습니다.
그 결과,
-
릴리즈 시점을 조정할 수 있는 유연성이 생겼고,
-
QA, 테스트, 배포를 각 팀이 독립적으로 운영할 수 있게 되었으며,
-
반복적인 병목 없이 실무 흐름에 맞는 배포 전략을 선택할 수 있게 되었습니다.
이 구조가 완성형이라고 생각하지는 않습니다.
더 나은 방법들이 있을 거라고 생각하고, 앞으로도 프로세스를 계속 발전시켜 나갈 계획입니다.
무엇보다 중요한 건, 이 구조가 정답이기 때문이 아니라, 우리 팀의 환경 속에서 불필요한 반복 작업을 줄이고, 각자의 역할에 더 집중할 수 있는 구조였기 때문에 유의미했다는 점입니다.
조직마다 환경은 다르지만, 그 환경 안에서 지속적으로 개선을 고민하고 실행에 옮기는 구조가 결국 가장 현실적인 정답 아닐까 라고 생각합니다.
참고
현재 Vercel은 2024.12.11부터 Custom Environment를 제공하고 있습니다.