Skip to content

production/staging 환경 분리 — 손으로 밀던 배포에서 릴리스 PR 한 건으로 (2026-09-01)

  • 기간: 2026-08-21 설계·Phase 0~1 랜딩 ~ 2026-09-01 전환 창 집행(세션 3개, 마지막 세션 50f8b252-fd3b-4708-ac42-d5354a86a2eb). 오너 지시: "#667 뭐남았는지 정리좀" → 정리 후 "응 진행해줘"
  • 랜딩: PR #546(Phase 1, 2a1ca620) · #552(Phase 0 런북, 6be5db49) · #553(Phase 2~3, 71803de7) · #1080(a60521a1) · #1081(15352e80) 게이트 forward-fix 2건 · #1082(첫 릴리스 PR, a5c618b7) · #1083(버전 자동 태그, b176efca). 마이그레이션 신규 0건 — staging에 밀려 있던 84건 일괄 적용, Production은 이미 최신이라 적용 0건. Vercel 배포 = staging·Production 각 1회(워크플로 경유)
  • 설계서: 이슈 #667 본문이 정본(§1~§18, "예상 효과·개선사항" 포함). 08-21 artifact c0b783a6-d4f4-40d7-acca-61251fc2cc48(비공개)
  • 정본: .github/workflows/deploy.yml(유일한 배포 경로) · deployment-pipeline.md(배포·버전 규칙) · environment-separation-runbook.md(콘솔 체크리스트)
  • 도구: 없음(콘솔 작업은 오너 수행, 검증은 세션이 번들·네트워크 실측)
  • 게이트: deploy.yml의 잡 의존(database → functions → frontend → smoke) · project-ref 프리플라이트 · staging-applied 게이트 · dry-run 목록 = release diff 대조 · ruleset production-protection
  • 버그리포트: 없음(배포 체계 전환 — 사용자 영향 사고 아님)
  • 계약: deployment-pipeline.md에 "환경 분리 이후의 배포" + 릴리스 버전(SemVer)·merge commit 규칙 신설

Phase 현황

Phase내용상태
Phase 0콘솔 준비(Vercel·Supabase·GitHub·Kakao) + 런북✅ PR #552 (6be5db49), 0-1~0-6 전부 09-01 종료
Phase 1런타임 설정 외부화(app-config.js → 환경변수)✅ PR #546 (2a1ca620)
Phase 2staging 자동 반영(main 머지 → staging)✅ PR #553 (71803de7) + forward-fix #1080·#1081
Phase 3release 파이프라인(main → production PR)✅ 첫 릴리스 PR #1082 (a5c618b7) 완주, D5 발효
Phase 3-1릴리스 버전 체계(SemVer) + 배포 성공 시 자동 태그✅ PR #1083 (b176efca), v0.9.0 발행
Phase 4파생 정리(하드코딩 잔재·네이티브 staging 빌드 스위치)⬜ 미착수 — "남은 것" 참조

1. 배경

이 트랙 전까지 Barbelic의 배포는 이랬다.

  • main에 머지하면 그 순간 Vercel이 실서비스 화면을 바꿨다. 릴리스라는 단위가 없었고, 어디까지가 이번에 나간 것인지 경계가 없었다.
  • DB 변경(마이그레이션)과 서버 함수 배포는 사람이 자기 PC에서 손으로 실행했다. "DB 먼저, 그다음 서버 함수, 마지막에 화면"이라는 순서는 문서에만 있었고, 지키는 주체는 사람의 주의력이었다.
  • PR 미리보기 화면조차 실서비스 DB를 그대로 봤다. 디자인 확인을 하다 실수로 데이터를 건드릴 수 있는 구조였다.
  • 개발과 실서비스 사이에 공유되는 중간 환경이 없었다. CI가 띄우는 일회용 DB와 실서비스, 두 단계뿐이었다.

08-21에 설계와 Phase 0~1을 마쳤지만, 그 뒤로 트랙이 오너 콘솔 작업 앞에서 멈춰 열흘 넘게 정지해 있었다. 그 사이 main에는 마이그레이션이 계속 쌓여 staging과의 간격이 40건 → 124건으로 벌어졌다.

2. 문제 제기

배포 순서를 기계가 아니라 사람이 지키고 있었다

DB보다 화면이 먼저 나가면 저장 기능이 즉시 깨진다(구 RPC 대체 경로를 두지 않는 정책). 그런데 이 순서를 강제하는 장치가 없어, 사고를 막는 것은 문서와 습관뿐이었다.

릴리스 단위가 없어 "지금 나간 것"을 특정할 수 없었다

버그가 보고돼도 어느 변경 묶음에서 들어왔는지 되짚을 기준점이 없었다.

미리보기가 실서비스 DB를 봤다

유저 A(오너)가 디자인 확인용 미리보기 링크를 열면, 그 화면은 실서비스 데이터를 읽고 쓰는 화면이었다.

트랙이 멈춘 지점이 오너 콘솔 작업이었다

세션은 Vercel·Supabase 콘솔에 접근할 수 없다. 남은 일이 전부 "오너가 클릭해야 하는 것"이라 트랙이 자연스럽게 정지했고, 정지한 만큼 staging 간격이 벌어져 재개 비용이 커졌다.

3. 해결 방안

원칙 (오너 결정 D1~D7, 2026-08-21 확정 / D1 대체안 09-01)

  • D1 릴리스 = main → production PR 머지, 머지는 오너만. 09-01 실측 결과 GitHub Environment의 "Required reviewers"가 이 요금제에 없어 → ruleset + 운영 규칙으로 대체(설계 §16-①이 예고한 경로).
  • D2 마이그레이션 자동 적용, 단 "staging에서 확인된 뒤" → staging-applied 게이트가 기계로 확인.
  • D5 사람이 Production에 직접 db push·functions deploy 하지 않는다 → 09-01 첫 Production 배포 완주로 발효.
  • D6 미리보기 = staging 데이터. 오너 본인 기록이 미리보기에 안 보이는 것을 감수.
  • D7 staging 데이터는 폐기 가능(재베이스라인 전제).
  • 오너 결정 추가(09-01): 릴리스 버전은 SemVer. 날짜 기반(CalVer)을 제안했으나 오너가 "semver가 더 좋은 거 같은데"로 선택. 오늘 첫 릴리스를 v0.9.0으로 확정(정식 출시에 v1.0.0).

접근

갈래채택기각한 대안과 이유
DB 격리별도 Supabase 프로젝트(staging)Supabase Branching = 브랜치별 과금 + 이 레포의 순서 규율·이름 충돌 게이트와 맞물리기 어려움 / 한 프로젝트 스키마 분리 = auth·storage 공유라 실질 격리 아님
프론트 환경Vercel Custom Environment staging별도 Vercel 프로젝트 = 모든 푸시가 3프로젝트를 빌드(08-20 빌드 한도 소진 사고 재현)
릴리스 트리거production 브랜치 PR태그 = diff 리뷰 면 없음 / 수동 실행 = 기록 없음
실행 주체Action이 순서 집행, Vercel은 CLI 배포git 통합 유지 = 화면이 DB보다 먼저 활성화될 수 있음
버전 체계SemVer + 배포 성공 시 자동 태그CalVer(날짜) = 판단 불필요하나 오너가 의미 전달을 우선

4. 적용한 내용

Phase 0 마무리 — 콘솔 작업 6종 (09-01)

  • GitHub secrets 15개 완비(마지막 PROD_SUPABASE_DB_PASSWORD는 값 분실로 Production DB 비밀번호 재설정 후 등록 — 설계가 예고한 전환 창 절차 그대로).
  • ruleset production-protection(id 22006830, Active) 생성: 대상 production, PR 필수·강제 푸시 차단·삭제 차단·status check verify.
  • Vercel: staging 환경 확인, staging 변수 4종을 Preview 환경에도 적용, Deployment Protection 해제, Production Branch = production / staging Branch Tracking = main.
  • 0-6 검증 실측: 검증용 claude/ui-* 브랜치로 미리보기를 빌드해 번들에 staging URL 주입 확인 + 화면에서 카카오 로그인 시작 시 redirect_uri가 staging 콜백으로 나가는 것까지 확인(동작 기준). 검증 후 브랜치 삭제.

Phase 2~3 — deploy.yml 랜딩 (#553)

08-21 작성분을 main 최신으로 병합(충돌 0, 429커밋 뒤였음). 런북 secret 표에 PROD 4종 보강(§18 잔여 정리). 머지 즉시 staging 레인이 시작되어 밀린 마이그레이션 84건을 일괄 적용.

forward-fix 2건 — remote-schema 게이트 (#1080 · #1081)

첫 staging 실행이 마지막 검증 단계에서 죽었다. 원인은 두 겹이었다.

  1. check-supabase-remote-schema.mjs가 CLI stdout에서 첫 {부터 통째로 JSON.parse → CI에서 결과 앞에 다른 JSON 문서가 섞여 나와 파싱 사망(#1080: 문서 단위로 파싱, 실패 시 원문 덤프).
  2. 원문 덤프가 나오자 실원인이 드러남 — CI(linux)의 supabase db query --output json{rows:[...]} 포장 없이 행 배열을 그대로 출력한다. 로컬 Windows는 포장 형식이라 로컬 실측으로는 절대 안 잡히는 차이였다(#1081: 두 형식 모두 수용).

Phase 3-1 — 릴리스 버전 자동 태그 (#1083)

release-tag 잡 신설: Production 레인이 smoke까지 초록일 때만, 릴리스 머지 커밋 메시지의 vX.Y.Z로 GitHub Release(자동 노트) 생성. 버전이 없으면 notice 후 건너뛰고 배포는 정상 진행.

주요 결정과 그 근거

  • 릴리스 PR은 merge commit으로만 머지(squash/rebase 금지). squash하면 production 역사가 main과 어긋나 다음 릴리스의 diff 계산이 꼬인다. 문서에 명시.
  • 버전 판단만 사람, 나머지는 자동. 세션이 릴리스 PR 제목에 버전을 제안하고 오너가 머지 전 수정 가능 — 머지가 곧 버전 확정.
  • 실패한 배포에는 버전이 붙지 않는다 → 태그 목록 = 실제로 나간 릴리스 목록.

작업 중 드러난 것

  • ruleset이 브랜치 생성까지 막는다. production 브랜치를 만들려면 ruleset을 잠시 끄고 만든 뒤 다시 켜야 한다(이번엔 그렇게 처리). 다음에 같은 상황이면 브랜치를 먼저 만들고 ruleset을 나중에 만드는 순서가 낫다.
  • Vercel은 없는 브랜치를 Branch Tracking에 못 넣는다. "Failed to save branch tracking" = 브랜치 부재. 순서는 브랜치 생성 → Vercel 설정.
  • VITE_ 접두사 변수는 Vercel에서 Secret 유형으로 만들 수 없다("public framework prefix cannot use visibility: secret"). Config 유형으로 삭제 후 재생성해야 하고, 이때 환경 체크박스가 기본 "Production, Preview"로 잡히므로 Production 체크 해제를 반드시 확인해야 한다(안 하면 실서비스가 staging DB를 보게 된다).
  • Vercel의 Production Branch 설정은 Settings → Git이 아니라 Settings → Environments 안에 있다(UI 변경).
  • barbelic-git-main-... 별칭은 낡은 실서비스 빌드로 굳어 있다. main의 git 통합 빌드를 끈 뒤로 갱신되지 않으며 Production DB를 본다 — staging 확인용으로 열면 안 된다.
  • 로컬 Windows와 CI linux의 CLI 출력 형식이 다르다(위 forward-fix). 배포 워크플로처럼 CI에서만 도는 코드는 로컬 초록이 보증이 되지 않는다.

5. 적용 결과

항목전 → 후
배포 순서 보장 주체사람의 8단계 수동 실행 → 워크플로 잡 의존(DB·함수 실패 시 화면 미배포)
릴리스 단위없음(개별 push 누적) → main → production PR 1건의 diff
Production DB 반영 경로각 세션이 로컬에서 db push --linked워크플로만(D5 발효, 09-01 12:43 첫 실행 완주)
중간 환경없음(일회용 CI DB ↔ 실서비스) → staging 상시 운영(마이그레이션 40건 → 124건, main과 동기)
미리보기가 보는 DB실서비스(kobxeylancdimqhfkbnl) → staging(ajqddlglezvxzsawquku) — 번들·로그인 요청 2중 실측
릴리스 버전없음 → SemVer 자동 태그, v0.9.0 발행
Production 배포 후 확인canary 1종 → staging·Production 양쪽 smoke(CRUD roundtrip)
브랜치 보호ruleset 0개 → production-protection(PR 필수·강제 푸시/삭제 차단·verify 필수)

검증 방식: 전부 자동 증거 — deploy.yml 실행 결과(staging run 33507952720 / Production run 33509274780 모두 전 잡 success), 번들 정적 검사, 미리보기 화면의 로그인 요청 호스트 실측, Production 읽기 전용 스키마 점검(EXIT 0, release_contract_failures []).

미검증으로 남긴 것:

  • 서버리스(api/*)용 비-VITE 변수 2종(SUPABASE_URL·SUPABASE_PUBLISHABLE_KEY)의 Preview 적용은 화면 실측 범위 밖 — 미리보기에서 관리자 API를 쓸 때 드러난다.
  • release-tag 잡은 다음 릴리스가 첫 실검증(오늘 v0.9.0은 수동 발행).
  • staging 화면의 시각·감각적 품질은 사람 눈의 몫(자동 증거 없음).

6. 이번 개선으로 향상된 것

화면이 DB보다 먼저 나가는 사고가 구조적으로 불가능해졌다

잡 의존이 순서를 강제하므로, DB나 서버 함수가 빨간불이면 화면은 배포되지 않는다. 오늘 실제로 이 방어가 작동했다 — 게이트 실패 2회 모두 화면 배포 전에 멈췄다.

오너가 "무엇이 나가는지" 보고 결정한다

릴리스 PR의 diff가 곧 이번에 나가는 것 전부다. 머지 버튼 하나가 승인이고, 안 누르면 실서비스는 그대로다.

디자인 미리보기가 실서비스 데이터에서 분리됐다

오너가 미리보기 링크를 열어 무엇을 눌러도 실서비스에는 닿지 않는다. 대신 오너 본인 기록이 안 보인다(D6에서 감수한 대가).

실서비스에 나간 것에 이름이 붙는다

v0.9.0처럼 버전으로 부를 수 있고, 릴리스 노트에 포함된 작업 목록이 자동으로 쌓인다. 버그 보고 시 "어느 버전에서"가 성립한다.

구조적으로 남는 것

  • deploy.yml 하나가 두 환경의 유일한 배포 경로 — 배포 계약이 문서가 아니라 코드로 존재한다.
  • project-ref 프리플라이트 + 장부 꼬리 단언 — 엉뚱한 DB에 닿는 것을 기계가 막는다.
  • staging-applied 게이트 — staging을 거치지 않은 마이그레이션은 Production에 못 들어간다.
  • ruleset production-protection — 실서비스 브랜치에 사람이 직접 손댈 수 없다.
  • SemVer + 자동 태그 — 릴리스 이력이 저절로 남는다.

남은 것

  • Phase 4 파생 정리(미착수): 하드코딩된 Production 주소가 남은 소스 4곳(app-config.js 폴백은 의도된 것 — 유지) · 네이티브 3곳(android/app/build.gradle · ios/App/App/MainViewController.swift · ios/Debug.xcconfig·Release.xcconfig) · 문서 8곳 · 테스트 5곳. iOS/Android --mode staging 빌드 스위치 포함.
  • 아이폰 앱으로 staging 확인 불가: 설치된 네이티브 앱은 실서비스만 본다. 사파리로 staging 주소를 열면 확인 가능(홈 화면 추가 시 앱처럼 사용). 네이티브 staging 빌드는 Phase 4 + TestFlight 배포가 필요하다.
  • staging 고정 주소 없음: 배포마다 URL이 바뀐다. Vercel → Environments → staging → Domains에 도메인을 하나 붙이면 해결(오너 작업, 미착수).
  • 낡은 별칭 정리: barbelic-git-main-...이 옛 실서비스 빌드로 굳어 있다. 혼동 방지를 위해 정리 검토.
  • prod-smoke.yml: CLI 배포는 deployment_status를 만들지 않아 자동 트리거가 멈췄다(smoke는 deploy.yml이 직접 돈다). 수동 실행 경로만 남은 상태 — 정리 여부 미결정.