Nx 웹 모노레포에서 PR 라벨 기반 배포를 걷어내고, Git 태그 + Doppler로 배포 파이프라인을 재설계한 과정을 공유합니다.
들어가며
저희 팀은 Nx 기반 웹 모노레포에서 여러 앱을 운영하고 있습니다. 관리자 페이지, 백오피스, 로그인 페이지, 웹뷰 등 성격이 다른 앱들이 하나의 레포에 들어 있고, 각각 독립적으로 빌드/배포됩니다.
초기에는 PR 라벨로 배포를 트리거하고, 환경변수는 GitHub Secrets에 앱별로 등록하는 방식이었습니다. 앱이 2~3개일 때는 문제가 없었지만, 앱과 환경이 늘어나면서 관리가 어려워졌습니다.
이 글에서는 기존 방식의 문제점, 태그 기반 배포로의 전환, Doppler를 활용한 환경변수 관리, 그리고 최종 CI/CD 파이프라인 구조를 다룹니다.
기존 방식의 문제점
PR 라벨 기반 배포
기존에는 PR에 라벨을 붙여서 어떤 앱을 어떤 환경에 배포할지 결정했습니다.
PR에 "deploy:care-admin:dev" 라벨 → 머지 시 care-admin dev 배포
PR에 "deploy:care-admin:prod" 라벨 → 머지 시 care-admin prod 배포
앱이 적을 때는 괜찮았지만, 앱 × 환경 조합이 늘어나면서 문제가 생겼습니다.
문제 상황
| 라벨 조합 폭발 | 앱 7개 × 환경 2~4개 = 라벨 20개 이상. 어떤 라벨을 붙여야 하는지 헷갈림 |
| 실수 위험 | 라벨을 잘못 붙이면 의도하지 않은 앱/환경에 배포. 라벨을 빼먹으면 배포 누락 |
| 배포 시점 제어 불가 | PR 머지 = 배포. "코드는 머지하되 배포는 나중에"가 불가능 |
| 이력 추적 어려움 | "이 앱이 마지막으로 언제 배포됐지?" → PR 라벨을 하나하나 뒤져야 함 |
GitHub Secrets 직접 관리
앱별 환경변수(S3 버킷, CloudFront Distribution ID 등)를 GitHub Secrets에 개별 등록했습니다.
AWS_S3_BUCKET_CARE_ADMIN_DEV
AWS_S3_BUCKET_CARE_ADMIN_PROD
AWS_CLOUDFRONT_ID_CARE_ADMIN_DEV
AWS_CLOUDFRONT_ID_CARE_ADMIN_PROD
AWS_S3_BUCKET_BACKOFFICE_DEV
AWS_S3_BUCKET_BACKOFFICE_PROD
...
문제 상황
| Secrets 수 폭발 | 앱 추가 시마다 S3 버킷, CloudFront ID, 기타 설정을 수동 등록 |
| 이중 관리 | 로컬 개발용 .env와 CI Secrets를 따로 관리. 값이 달라져도 모름 |
| 가시성 부족 | GitHub Secrets는 값을 볼 수 없음. "이 값이 맞나?" 확인 불가 |
개편 방향
두 가지를 바꿨습니다.
- 배포 트리거: PR 라벨 → Git 태그
- 환경변수 관리: GitHub Secrets 직접 관리 → Doppler
태그 기반 배포
태그 네이밍 컨벤션
{앱이름}/{환경}/YYYY.MM.DD[.N]
- 날짜 기반 버전을 사용합니다. SemVer가 필요한 라이브러리가 아니라 배포 단위의 웹 앱이기 때문입니다.
- 같은 날 여러 번 배포 시 suffix를 붙입니다: .1, .2, ...
태그 설명
| care-admin/dev/2026.04.17 | care-admin을 dev 환경에 배포 |
| care-admin/prod/2026.04.17 | care-admin을 prod 환경에 배포 |
| care-admin/prod/2026.04.17.1 | 같은 날 prod 재배포 |
| rehab-manager/dev/2026.04.17 | rehab-manager를 dev 환경에 배포 |
배포 방법
# CLI
git tag care-admin/dev/2026.04.17
git push origin care-admin/dev/2026.04.17
GitHub GUI로도 가능합니다. Releases → Draft a new release → 태그 이름 입력 → Create new tag → Publish release.
라벨 대비 장점
PR 라벨 Git 태그
| 배포 시점 | PR 머지 시 (제어 불가) | 태그 push 시 (원하는 시점) |
| 이력 추적 | PR 라벨 뒤지기 | git tag -l "care-admin/prod/*" |
| 실수 가능성 | 라벨 선택 실수 | 태그 이름이 곧 배포 대상이라 명확 |
| 롤백 | 이전 PR 찾아서 재배포 | 이전 태그의 커밋에서 새 태그 생성 |
Doppler로 환경변수 관리
Doppler는 환경변수를 중앙에서 관리하는 서비스입니다. 앱별, 환경별로 config를 만들어두면 로컬 개발과 CI 양쪽에서 동일한 환경변수를 사용할 수 있습니다.
기존 vs 개편
Before: 앱별 S3 버킷, CloudFront ID 등을 GitHub Secrets에 개별 등록
# 워크플로우에서 Secrets를 하나하나 꺼내서 주입
env:
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET_CARE_ADMIN_DEV }}
CLOUDFRONT_ID: ${{ secrets.AWS_CLOUDFRONT_ID_CARE_ADMIN_DEV }}
VITE_API_URL: ${{ secrets.VITE_API_URL_CARE_ADMIN_DEV }}
After: Doppler config 이름 하나만 넘기면 환경변수가 자동 주입
# 워크플로우에서 doppler run으로 실행하면 끝
- run: doppler run --project ipixel-admin --config dev_care_manager -- bash scripts/deploy.sh
GitHub Secrets에 남는 것
Doppler 도입 후 GitHub Secrets에는 인프라 인증 정보만 남깁니다.
Secret 용도
| DOPPLER_TOKEN_* | 앱/환경별 Doppler 접근 토큰 |
| AWS_ACCESS_KEY_ID | AWS 인증 |
| AWS_SECRET_ACCESS_KEY | AWS 인증 |
| AWS_REGION | AWS 리전 |
앱별 S3 버킷, CloudFront ID, API URL 등은 전부 Doppler에서 관리합니다. 앱이 추가되어도 GitHub Secrets에 등록할 건 Doppler 토큰 하나뿐입니다.
로컬 개발에서의 사용
# 앱별 .env 파일 다운로드
nx run care-admin:doppler # → .env 파일 생성
CI에서는 doppler run으로 환경변수를 직접 주입하므로 .env 파일이 필요 없습니다.
CI/CD 파이프라인 구조
워크플로우 파일
파일 역할
| workspace-ci.yml | PR 시 nx affected --target=build로 영향받은 앱만 빌드 검증 |
| workspace-cd.yml | 태그 push 트리거. 태그 prefix로 앱/환경을 분기하여 cd.yml 호출 |
| cd.yml | 재사용 워크플로우. 실제 빌드 + 배포 실행 |
핵심은 workspace-cd.yml과 cd.yml의 분리입니다. workspace-cd.yml은 태그를 파싱해서 "어떤 앱을, 어떤 환경으로" 배포할지 결정하고, 실제 빌드/배포 로직은 cd.yml에 위임합니다.
cd.yml — 재사용 워크플로우
# cd.yml (재사용 워크플로우)
inputs:
script-path # 실행할 배포 스크립트 경로
doppler-config # Doppler config 이름
secrets:
DOPPLER_TOKEN
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_REGION
모든 앱의 배포가 동일한 흐름을 따릅니다:
코드 체크아웃
→ Doppler CLI 설치
→ node_modules 캐시 복원
→ AWS 인증
→ doppler run으로 환경변수 주입 + 배포 스크립트 실행
배포 스크립트 프로세스
각 앱의 배포 스크립트(scripts/deploy_*.sh)는 동일한 구조입니다:
#!/bin/bash
set -e
# 1. 의존성 설치
yarn install --immutable
# 2. 빌드 (Nx 캐시 스킵 — CI에서는 항상 fresh 빌드)
yarn build:care-dev --skip-nx-cache
# 3. 기존 파일 삭제 + 빌드 결과물 업로드
aws s3 rm s3://$S3_BUCKET --recursive
aws s3 sync dist/ s3://$S3_BUCKET --delete
# 4. index.html에 no-cache 메타데이터 설정
aws s3 cp s3://$S3_BUCKET/index.html s3://$S3_BUCKET/index.html \
--metadata-directive REPLACE --cache-control "no-cache"
# 5. CloudFront 캐시 무효화
aws cloudfront create-invalidation \
--distribution-id $CLOUDFRONT_ID --paths "/*"
S3_BUCKET, CLOUDFRONT_ID 등의 환경변수는 스크립트에 하드코딩하지 않습니다. doppler run이 Doppler config에서 가져와 자동으로 주입해줍니다. 덕분에 하나의 스크립트 구조로 모든 앱/환경을 처리할 수 있습니다.
set -e가 적용되어 있어서 빌드가 실패하면 S3 업로드 없이 즉시 중단됩니다.
CI 흐름 (PR)
PR이 main으로 열리면:
PR → nx affected --target=build → 변경된 앱만 빌드 검증
전체 앱을 빌드하지 않고 nx affected로 변경된 앱만 검증합니다.
CD 흐름 (배포)
태그가 push되면:
태그 push
→ workspace-cd.yml: 태그 prefix로 앱/환경 분기
→ cd.yml: Doppler config + 배포 스크립트 전달
→ doppler run으로 환경변수 주입 + 스크립트 실행
→ S3 업로드 + CloudFront 캐시 무효화
전체 구성 요약

항목 Before After
| 배포 트리거 | PR 라벨 | Git 태그 (앱/환경/날짜) |
| 환경변수 관리 | GitHub Secrets (앱별 개별 등록) | Doppler (중앙 관리) |
| CI 검증 | 전체 빌드 | nx affected (변경된 앱만) |
| 배포 워크플로우 | 앱별 개별 워크플로우 | workspace-cd.yml + cd.yml 재사용 구조 |
| 롤백 | 이전 PR 찾아서 재배포 | 이전 태그 커밋에서 새 태그 생성 |
인프라 구성
항목 기술
| 호스팅 | AWS S3 (정적 파일) |
| CDN | AWS CloudFront |
| 환경변수 | Doppler |
| 빌드 도구 | Nx + Vite |
| 패키지 매니저 | Yarn Berry (PnP) |
| CI/CD | GitHub Actions |
주의사항
- macOS 대소문자: macOS는 파일명 대소문자를 구분하지 않지만, CI(Linux)에서는 구분합니다. import 경로의 대소문자를 정확히 맞춰야 CI에서 빌드가 깨지지 않습니다.
- 빌드 실패 시 안전장치: 모든 배포 스크립트에 set -e가 적용되어 있어, 빌드가 실패하면 S3 업로드 없이 즉시 중단됩니다.
- 롤백 방법: 이전 태그의 커밋에서 새 태그를 생성하여 재배포하면 됩니다. 별도의 롤백 메커니즘은 필요 없습니다.
마치며
정리하면 이번 개편에서 바꾼 건 두 가지입니다.
- 배포 트리거를 PR 라벨에서 Git 태그로 — 배포 시점을 명시적으로 제어하고, 태그 자체가 배포 이력이 됩니다.
- 환경변수를 GitHub Secrets에서 Doppler로 — 로컬/CI 환경변수를 한 곳에서 관리하고, 앱 추가 시 Secrets 수동 등록 작업을 없앴습니다.
특별히 복잡한 기술을 도입한 것은 아닙니다. 태그 네이밍 컨벤션을 정하고, 재사용 워크플로우로 중복을 제거하고, 환경변수 관리를 외부 서비스로 위임한 것뿐입니다. 하지만 이 세 가지만으로도 "이 앱 어디에 배포된 거지?", "이 환경변수 값이 맞나?" 같은 질문이 사라졌습니다.
모노레포에서 앱이 늘어날수록 CI/CD 관리 비용은 빠르게 증가합니다. 비슷한 고민을 하고 계시다면, 태그 컨벤션 하나 정하는 것부터 시작해보시길 추천드립니다.
'FrontEnd > React' 카테고리의 다른 글
| Yarn Berry PnP 마이그레이션 — node_modules를 없앤 이유와 과정 (0) | 2026.04.17 |
|---|---|
| React에서 CRA보다 Vite가 좋다고? - 프로덕션 버전으로 빌드하기 (0) | 2022.12.27 |
| MSW로 프론트엔드 개발 생산성 높이기! - MSW 사용하기 (0) | 2022.12.26 |
| MSW로 프론트엔드 개발 생산성 높이기! - MSW 알아보기 (0) | 2022.12.26 |
| MSW로 프론트엔드 개발 생산성 높이기! - Mocking을 해야하는 이유 (0) | 2022.12.23 |