왜 프리뷰 환경이 필요한가
스테이징 환경 하나를 여러 브랜치가 공유하면 병목이 생긴다. A 브랜치를 배포해 QA가 검증하는 동안 B 브랜치는 대기해야 하고, 두 변경이 섞여 배포되면 버그의 원인 브랜치를 특정하기 어렵다. 리뷰어는 코드만 보고 "실제 화면에서 어떻게 동작하는지"는 상상에 의존한다.
피처 브랜치마다 격리된 프리뷰 환경을 자동으로 띄우면, PR을 열 때 고유 URL이 생성되고 리뷰어·기획자·QA가 실제 동작을 확인한 뒤 병합한다. 핵심은 PR 생성/업데이트 시 자동 생성, 병합/종료 시 자동 파기다. 수동으로는 절대 지속되지 않는다.
구성의 핵심 요소
프리뷰 환경은 세 가지를 브랜치별로 격리해야 한다.
- 애플리케이션 인스턴스: 컨테이너 또는 서버리스로 브랜치당 하나.
- 도메인/라우팅:
pr-123.preview.example.com같은 와일드카드 서브도메인. - 데이터: DB 스키마/네임스페이스 분리. 가장 까다롭고 비용도 여기서 갈린다.
Kubernetes 기반 자동 배포
Kubernetes를 쓴다면 브랜치별 네임스페이스가 가장 깔끔한 격리 단위다. GitHub Actions에서 PR 이벤트를 받아 배포한다.
name: preview
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
deploy:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build & push image
run: |
IMG=registry.example.com/app:pr-${{ github.event.number }}
docker build -t "$IMG" .
docker push "$IMG"
- name: Deploy to namespace
run: |
NS=pr-${{ github.event.number }}
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install app ./chart \
--namespace "$NS" \
--set image.tag=pr-${{ github.event.number }} \
--set ingress.host=pr-${{ github.event.number }}.preview.example.com
teardown:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- run: kubectl delete namespace pr-${{ github.event.number }} --ignore-not-found
closed 이벤트로 병합·미병합 모두를 처리해 네임스페이스를 통째로 삭제하는 것이 요점이다. 리소스 누수를 막는 가장 확실한 방법은 "삭제 트리거를 생성 트리거와 같은 워크플로에 두는 것"이다.
데이터 격리 전략
DB를 어떻게 분리하느냐가 비용과 현실성을 결정한다.
| 전략 | 격리 수준 | 비용/속도 | 적합한 경우 |
|---|---|---|---|
| 브랜치별 신규 DB | 완전 격리 | 느림·비쌈 | 스키마 변경이 잦은 팀 |
| 공유 DB + 스키마 분리 | 중간 | 빠름·저렴 | 대부분의 웹 서비스 |
| 운영 DB 스냅샷 복제 | 완전 격리 | 느림·비쌈 | 실데이터 검증 필요 시 |
PostgreSQL이라면 스키마 분리가 실용적 절충점이다. 애플리케이션은 search_path로 자신의 스키마만 바라보게 한다.
-- PR별 스키마 생성 후 마이그레이션 기준 스키마에서 복제
CREATE SCHEMA IF NOT EXISTS pr_123;
-- 앱 연결 시 search_path 지정 (환경변수로 주입)
-- DATABASE_URL=...&options=-csearch_path%3Dpr_123
-- 테이블 구조만 복제하는 함수 예시
DO $$
DECLARE t text;
BEGIN
FOR t IN SELECT tablename FROM pg_tables WHERE schemaname = 'template' LOOP
EXECUTE format(
'CREATE TABLE pr_123.%I (LIKE template.%I INCLUDING ALL)', t, t);
END LOOP;
END $$;
단, 스키마 분리는 데이터만 나눌 뿐 커넥션 풀·CPU는 공유한다. 부하 테스트용 프리뷰에는 부적합하다.
비용 통제와 정리
프리뷰 환경은 방치하면 유령 리소스가 쌓인다. closed 훅에만 의존하지 말고 시간 기반 청소를 병행한다. 마지막 커밋 후 3일이 지난 환경을 크론으로 회수하는 식이다.
#!/usr/bin/env bash
# 3일 이상 갱신 없는 PR 네임스페이스 정리 (크론 실행)
CUTOFF=$(( $(date +%s) - 3*24*3600 ))
kubectl get ns -l type=preview -o json \
| jq -r '.items[] | [.metadata.name, .metadata.labels.updated] | @tsv' \
| while IFS=$'\t' read -r ns updated; do
if [ "${updated:-0}" -lt "$CUTOFF" ]; then
echo "reaping $ns"
kubectl delete namespace "$ns"
fi
done
배포 시 updated 라벨에 타임스탬프를 갱신해 두면 이 청소기가 안전하게 동작한다. 야간·주말에는 replica를 0으로 스케일 다운하는 것도 비용 절감에 효과적이다.
주의점
- 시크릿 관리: 프리뷰가 운영 시크릿을 쓰면 사고로 직결된다. 반드시 프리뷰 전용 자격증명과 샌드박스 외부 서비스(결제, 메일)를 사용한다.
- 외부 연동: 웹훅 콜백 URL은 브랜치별 도메인으로 동적 등록해야 한다. 고정 URL을 쓰면 콜백이 엉뚱한 환경으로 간다.
- 빌드 시간: 이미지 레이어·의존성 캐시가 없으면 PR마다 수 분이 낭비된다. 캐시 전략이 프리뷰 체감 속도를 좌우한다.
- 접근 제어: 프리뷰 URL이 검색엔진에 노출되지 않도록 인증 또는 IP 제한을 건다.
프리뷰 환경의 성패는 "만들기"가 아니라 "자동으로 사라지게 하기"에 달려 있다. 생성·파기·정리를 하나의 자동화 루프로 묶어야 오래 유지된다.