마스킹된 프로덕션 DB로 E2E 테스트 돌리기 — Playwright + Neon 브랜칭 파이프라인 구축기
"테스트 데이터가 너무 깨끗해서 버그를 못 잡았다"는 말, 한 번쯤 들어보셨을 겁니다. 저도 시드 스크립트로 만든 예쁜 데이터 위에서 테스트를 통과시켜놓고, 프로덕션에서 터지는 걸 몇 번이나 겪었는지 모릅니다. 이메일이 200자인 사용자, 주소 필드에 개행 문자가 들어간 레코드, 외래 키가 5단계로 엮인 주문 데이터 — 시드 스크립트로는 절대 재현할 수 없는 것들이죠. 결국 프로덕션 데이터를 쓰자는 결론에 도달하게 되는데, 바로 개인정보 문제가 발목을 잡습니다.
이 글에서는 프로덕션 DB를 Copy-on-Write로 브랜칭하고, PII를 마스킹한 뒤, 그 위에서 Playwright E2E 테스트를 PR마다 자동으로 돌리는 파이프라인의 전체 구축 과정을 다룹니다. Neon 같은 서버리스 DB 플랫폼을 쓰면 브랜칭부터 마스킹까지 거의 원스톱으로 해결되고, Neon 없이 자체 구축하는 경로도 함께 살펴봅니다. 사전 지식으로는 GitHub Actions YAML 기본 문법, PostgreSQL 기초, CI/CD 파이프라인 개념 정도면 충분합니다.
읽는 데 약 15분 소요됩니다. 목차: 핵심 개념 → 실전 적용(Neon 경로 / Greenmask 경로) → 테스트 작성 패턴 → 장단점 → 마무리
핵심 개념
데이터베이스 브랜칭 — Git처럼 DB를 복제하는 기술
데이터베이스 브랜칭은 말 그대로 Git 브랜치를 따듯이 DB를 복제하는 개념입니다. 핵심은 Copy-on-Write(CoW) 방식인데, 전체 데이터를 물리적으로 복사하지 않고 스토리지를 공유하다가 변경이 발생한 부분만 새로 기록합니다. 수백 GB짜리 프로덕션 DB도 수 초 안에 브랜칭이 가능하고, 추가 스토리지 비용도 변경분만큼만 발생합니다.
Copy-on-Write(CoW): 원본 데이터를 즉시 복사하지 않고 참조만 공유하다가, 쓰기가 발생하는 시점에 해당 페이지만 복제하는 기법입니다. 파일 시스템(Btrfs, ZFS)이나 컨테이너 레이어에서도 동일한 원리가 사용됩니다.
2024년부터 Neon, Xata 같은 플랫폼들이 이 기능을 본격적으로 밀면서, "Branch-per-PR" 워크플로가 빠르게 자리잡고 있습니다. 코드 리뷰할 때 프론트엔드는 Vercel Preview로 확인하고, DB는 브랜치로 격리하는 패턴이 이제는 자연스러운 흐름이 된 거죠.
| 플랫폼 | 브랜칭 방식 | 지원 DB | 특징 |
|---|---|---|---|
| Neon | CoW 기반 즉시 브랜칭 | PostgreSQL | 내장 postgresql_anonymizer, GitHub Actions 공식 액션 제공 |
| Xata | CoW 기반 브랜칭 | PostgreSQL | Neon과 유사한 접근 |
| PlanetScale | 스키마 브랜칭 | MySQL / PostgreSQL | 데이터는 별도 시드 필요, 스키마 diff 기능 강력. PostgreSQL 브랜칭도 지원하기 시작했으나 아직 초기 단계 |
데이터 마스킹 — 구조는 살리고 민감 정보만 바꾸기
마스킹의 핵심은 데이터의 형태(shape)는 그대로 유지하면서 민감한 값만 가짜로 대체하는 것입니다. "홍길동"을 "김철수"로, user@company.com을 fake_7xk@example.com으로 바꾸되, 외래 키 관계나 제약 조건은 건드리지 않는 거죠.
여기서 중요한 포인트가 있습니다. CoW로 브랜칭한 DB는 원본의 외래 키 관계를 그대로 가지고 있기 때문에, PK/FK 컬럼 자체를 마스킹하지 않는 한 참조 무결성은 자연스럽게 유지됩니다. users.id를 마스킹하지 않고 users.name이나 users.email만 마스킹하면, orders.user_id → users.id 관계는 전혀 영향을 받지 않습니다. 다만 이메일이나 전화번호를 FK처럼 참조하는 비정규화된 설계가 있다면, 해당 필드들의 마스킹 값이 서로 일치하도록 결정론적 마스킹을 적용해야 합니다.
PostgreSQL 생태계에서는 postgresql_anonymizer(줄여서 anon)가 사실상 표준입니다. 이 확장의 매력적인 점은 마스킹 규칙을 SQL의 SECURITY LABEL 구문으로 스키마 안에 직접 선언할 수 있다는 겁니다.
SECURITY LABEL: PostgreSQL 표준 SQL 구문이지만,FOR anon으로 사용하는 것은postgresql_anonymizer확장이 등록한 레이블 프로바이더를 통해 동작합니다. 즉, 이 구문을 쓰려면anon확장이 설치되어 있어야 합니다.
SECURITY LABEL FOR anon ON COLUMN users.name
IS 'MASKED WITH FUNCTION anon.fake_last_name()';
SECURITY LABEL FOR anon ON COLUMN users.email
IS 'MASKED WITH FUNCTION anon.fake_email()';
SECURITY LABEL FOR anon ON COLUMN users.phone
IS 'MASKED WITH FUNCTION anon.partial(phone, 3, $$***$$, 0)';마스킹 정책이 코드로 관리되니까, 스키마 마이그레이션과 함께 버전 관리가 됩니다.
여기서 한 가지 더 짚고 넘어갈 부분이 있는데, 결정론적(Deterministic) 마스킹입니다. 같은 입력에 대해 매번 다른 결과가 나오면 테스트 재현이 안 됩니다. postgresql_anonymizer에서는 anon.pseudo_first_name(seed)처럼 pseudo_* 계열 함수를 사용하면 시드 기반으로 동일 입력 → 동일 출력을 보장할 수 있습니다. 외부 도구로는 Supabase 커뮤니티가 유지보수하는 @snaplet/copycat(원래 Snaplet 프로젝트였으나, Snaplet 폐업 후 supabase-community/seed 저장소에서 관리)이 있습니다.
결정론적 마스킹: 동일한 원본 값에 대해 항상 동일한 마스킹 결과를 반환하는 방식입니다. 테스트에서 특정 사용자의 마스킹된 이메일이 매 실행마다 달라지면, assertion을 작성할 수 없게 됩니다.
실전 적용
PR이 열리면 → 프로덕션 DB에서 CoW 브랜치를 생성하고 PII를 마스킹 → 마스킹된 브랜치 DB에 스키마 마이그레이션 적용 → Preview 환경에서 Playwright E2E 테스트 실행 → PR이 닫히면 브랜치 DB를 자동 삭제. 이 흐름을 두 가지 경로로 구현해보겠습니다.
예시 1: Neon + Playwright + GitHub Actions — 가장 빠른 시작점
Neon을 사용하는 경우, 공식 GitHub Actions가 제공되어 설정이 상당히 간결합니다. 저도 처음엔 이 조합으로 시작했는데, 워크플로 파일 하나로 전체 생명주기가 관리되는 게 인상적이었습니다.
다만 솔직히 말하면, 처음 세팅할 때 한 가지 삽질을 했습니다. 마스킹 규칙을 적용하고 나서 결제 관련 테스트가 전부 깨지더라고요. 알고 보니 payments.card_number에 부분 마스킹을 걸었는데, 별도 유효성 검증 로직에서 Luhn 체크를 하고 있었습니다. 마스킹된 카드번호가 Luhn 알고리즘을 통과하지 못하니까 전부 실패하는 거였죠. 이런 비즈니스 로직 연계 지점을 미리 파악하는 데 반나절 정도 걸렸습니다.
name: E2E Tests on Masked Preview DB
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
e2e-test:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 1단계: Neon 브랜치 생성 (CoW로 프로덕션 DB 복제)
- name: Create Neon Branch
id: create-branch
uses: neondatabase/create-branch-action@v5
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch_name: preview/pr-${{ github.event.number }}
# 2단계: 마스킹 규칙 적용 후 익명화 실행
- name: Anonymize PII
run: |
psql ${{ steps.create-branch.outputs.db_url }} -f ./sql/masking-rules.sql
psql ${{ steps.create-branch.outputs.db_url }} -c "SELECT anon.anonymize_database();"
# 3단계: 스키마 마이그레이션 (현재 브랜치의 변경사항 반영)
- name: Apply Migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
# 4단계: Preview 환경 배포
- name: Deploy Preview
id: deploy-preview
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
scope: ${{ secrets.VERCEL_ORG_ID }}
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
# 5단계: Playwright 설치 및 E2E 테스트 실행
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Run E2E Tests
run: npx playwright test
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
BASE_URL: ${{ steps.deploy-preview.outputs.preview-url }}
# 6단계: 스키마 diff를 PR 코멘트로 남기기
- name: Schema Diff
if: always()
uses: neondatabase/schema-diff-action@v1
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
# PR이 닫히면 브랜치 자동 정리
cleanup:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch: preview/pr-${{ github.event.number }}Vercel 대신 Netlify나 다른 Preview 배포 플랫폼을 사용하는 경우, 해당 플랫폼의 GitHub Action으로 대체하면 됩니다. 핵심은 Preview 배포 step이 존재해야
BASE_URL을 테스트에 전달할 수 있다는 점입니다.
| 단계 | 역할 | 소요 시간 (대략) |
|---|---|---|
create-branch-action |
CoW로 프로덕션 DB 복제 | ~3초 |
| 마스킹 규칙 적용 | anon.anonymize_database() 실행 |
DB 크기에 비례 (팀 내부 테스트 기준 1GB에 ~30초) |
prisma migrate deploy |
현재 PR의 스키마 변경 적용 | ~5초 |
| Preview 배포 | Vercel/Netlify 등 Preview 환경 프로비저닝 | |
playwright test |
전체 E2E 스위트 실행 | 테스트 수에 따라 다름 |
delete-branch-action |
PR 닫힘 시 브랜치 정리 | ~1초 |
마스킹 규칙 파일(sql/masking-rules.sql)은 별도로 관리하면서, 스키마 변경 시 함께 업데이트하는 것을 권장합니다:
-- sql/masking-rules.sql
CREATE EXTENSION IF NOT EXISTS anon CASCADE;
SELECT anon.init();
-- users 테이블
SECURITY LABEL FOR anon ON COLUMN users.name
IS 'MASKED WITH FUNCTION anon.fake_last_name()';
SECURITY LABEL FOR anon ON COLUMN users.email
IS 'MASKED WITH FUNCTION anon.fake_email()';
SECURITY LABEL FOR anon ON COLUMN users.phone
IS 'MASKED WITH FUNCTION anon.partial(phone, 3, $$***$$, 0)';
-- orders 테이블 — 배송지 주소 마스킹
SECURITY LABEL FOR anon ON COLUMN orders.shipping_address
IS 'MASKED WITH FUNCTION anon.fake_city() || $$ $$ || anon.fake_last_name() || $$ Street$$';
-- payments 테이블 — 카드번호 부분 마스킹
SECURITY LABEL FOR anon ON COLUMN payments.card_number
IS 'MASKED WITH FUNCTION anon.partial(card_number, 0, $$****-****-****-$$, 4)';예시 2: Greenmask + Docker — Neon 없이 자체 구축하기
Neon을 사용하지 않는 팀이라면, 오픈소스 Greenmask와 Docker 조합으로 유사한 파이프라인을 직접 구축할 수 있습니다. 저도 기존 RDS를 쓰는 프로젝트에서 이 방식을 적용해봤는데, 브랜칭의 즉시성은 없지만 충분히 실용적이었습니다.
한 가지 미리 말씀드리면, Greenmask 방식으로 처음 CI를 돌렸을 때 타임아웃을 만났습니다. 프로덕션 DB가 8GB 정도였는데, 전체 덤프 + 마스킹 + 복원에 GitHub Actions 기본 타임아웃(6시간)은 넘지 않았지만 러너 비용이 무시할 수 없었습니다. 결국 테스트에 필요한 테이블만 선별적으로 덤프하는 방식으로 바꾸고 나서야 실용적인 수준이 되었습니다.
name: E2E Tests with Greenmask
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
e2e-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
# 0단계: Greenmask 설치
- name: Install Greenmask
run: |
curl -sSL https://github.com/GreenmaskIO/greenmask/releases/latest/download/greenmask-linux-amd64 \
-o /usr/local/bin/greenmask
chmod +x /usr/local/bin/greenmask
# 1단계: Greenmask로 프로덕션 read replica에서 덤프 + 마스킹 동시 수행
- name: Dump & Mask Production Data
run: |
greenmask dump \
--source "host=${{ secrets.PROD_REPLICA_HOST }} dbname=prod_db user=readonly" \
--config greenmask-config.yml \
--output masked-dump.sql
# 2단계: 마스킹된 데이터를 테스트 컨테이너에 복원
- name: Restore Masked Data
run: |
psql postgresql://test_user:test_pass@localhost:5432/test_db \
< masked-dump.sql
# 3단계: 마이그레이션 + 테스트
- name: Apply Migrations & Run Tests
run: |
npx prisma migrate deploy
npx playwright install --with-deps chromium
npx playwright test
env:
DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db보안 참고: 위 워크플로에서
--source는 프로덕션 DB가 아닌 read replica를 가리키고 있습니다. CI 환경에서 프로덕션 primary에 직접 연결하는 것은 보안과 성능 양면에서 위험합니다. read replica를 사용하고, 해당 replica에 접근하는 네트워크를 VPN이나 IP 화이트리스트로 제한하는 것을 권장합니다.
Greenmask의 설정 파일에서는 결정론적 트랜스포머를 지정할 수 있습니다:
# greenmask-config.yml
tables:
- name: users
columns:
- name: name
transformer:
name: RandomPerson
params:
seed: 42
- name: email
transformer:
name: RandomEmail
params:
seed: 42
- name: phone
transformer:
name: Mask
params:
characters_to_reveal: 3
mask_char: "*"
- name: orders
columns:
- name: shipping_address
transformer:
name: RandomAddress
params:
seed: 42| 비교 항목 | Neon 브랜칭 방식 | Greenmask + Docker 방식 |
|---|---|---|
| 복제 속도 | 수 초 (CoW) | 수 분~수십 분 (덤프/복원) |
| 추가 인프라 | 불필요 (Neon 내장) | PostgreSQL 컨테이너 + Greenmask 바이너리 |
| 비용 | Neon 브랜치 과금 | CI 러너 시간만 소모 |
| DB 플랫폼 제약 | Neon(PostgreSQL)만 가능 | 모든 PostgreSQL 호환 |
| 대규모 DB | 크기 무관하게 빠름 | 덤프 크기에 비례해 느려짐 |
| 마스킹 시점 | 브랜칭 후 별도 실행 | 덤프 중 동시 처리 |
테스트 작성 패턴 — 마스킹된 데이터 위에서의 Playwright
마스킹된 DB 위에서 테스트를 작성할 때는 두 가지 패턴을 구분해서 써야 합니다.
패턴 1: 형태 기반 검증 — 마스킹된 값이 무엇인지 모르는 경우, 데이터의 존재 여부와 형식을 검증합니다:
// playwright/e2e/user-list.spec.ts
import { test, expect } from '@playwright/test';
test.describe('사용자 목록 페이지', () => {
test('프로덕션 규모의 데이터가 올바르게 페이지네이션 되는지', async ({ page }) => {
await page.goto('/admin/users');
const totalCount = page.locator('[data-testid="total-count"]');
await expect(totalCount).toHaveText(/\d{1,3}(,\d{3})*/);
// 이메일 컬럼이 마스킹되어도 이메일 형식은 유지되는지 확인
const firstEmail = page.locator('table tbody tr:first-child td.email');
await expect(firstEmail).toContainText('@');
// 페이지네이션이 대규모 데이터에서 정상 동작하는지
await page.click('[data-testid="next-page"]');
await expect(page.locator('table tbody tr')).toHaveCount(20);
});
});패턴 2: 결정론적 마스킹 값 활용 — 시드 기반 마스킹을 사용하면 특정 원본 데이터가 어떤 값으로 마스킹되는지 예측할 수 있습니다. 이 경우에는 마스킹된 값을 assertion에 사용하는 것이 오히려 맞습니다:
test('검색 기능이 마스킹된 데이터에서도 동작하는지', async ({ page }) => {
await page.goto('/admin/users');
// 결정론적 마스킹이므로 시드 42 기준 원본 '홍길동' → 'Smith'로 항상 변환됨
// 이 값은 마스킹 시드와 원본 데이터가 고정되어 있으므로 안정적
await page.fill('[data-testid="search-input"]', 'Smith');
await page.click('[data-testid="search-button"]');
const results = page.locator('table tbody tr');
await expect(results).not.toHaveCount(0);
});핵심 구분: 비결정론적 마스킹을 쓰고 있다면 패턴 1(형태 기반)만 사용하고, 결정론적 마스킹을 쓰고 있다면 패턴 2(특정 값 검증)도 활용할 수 있습니다. 두 패턴을 혼용할 때는 테스트 파일 상단에 어떤 마스킹 전략을 전제로 하는지 명시해두면 유지보수가 편합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 현실적 엣지 케이스 발견 | 프로덕션 데이터의 구조, 볼륨, 관계가 그대로 유지되므로 시드 스크립트에서 절대 나오지 않는 NULL, 긴 문자열, 복잡한 관계에서 발생하는 버그를 포착할 수 있습니다 |
| 규정 준수 | PII를 마스킹한 상태에서 테스트하므로 GDPR/CCPA 위반 없이 프로덕션 형태의 데이터 활용이 가능합니다 |
| 격리된 환경 | PR마다 독립된 DB 브랜치를 사용하니 테스트 간 데이터 오염이 원천 차단됩니다 |
| 스키마 마이그레이션 사전 검증 | "실제 데이터 위에서" 마이그레이션을 돌려보기 때문에, 빈 DB에서는 문제없지만 프로덕션에서 터지는 실패를 미리 잡을 수 있습니다 |
| 빠른 프로비저닝 | CoW 기반 브랜칭은 수 초 내에 완료되어 CI 파이프라인에 실질적인 병목을 만들지 않습니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 마스킹 누락 리스크 | 새 컬럼 추가 시 마스킹 규칙을 깜빡하면 PII가 노출될 수 있습니다 | 마스킹 규칙을 마이그레이션 PR의 리뷰 체크리스트에 포함하고, Microsoft Presidio 같은 PII 탐지 도구로 정기 스캔하는 것을 권장합니다 |
| 비즈니스 로직 깨짐 | 이메일 도메인 기반 라우팅, 전화번호 지역코드 로직 등이 마스킹 후 의도와 다르게 동작할 수 있습니다 | 비즈니스 로직에 사용되는 필드는 형식 보존(format-preserving) 마스킹을 적용하거나, 해당 필드를 마스킹 대상에서 제외할 수 있습니다 |
| 비용 증가 | PR이 많은 팀에서는 브랜치 DB의 컴퓨팅/스토리지 비용이 누적됩니다 | 자동 삭제 정책 필수. 7일 이상 비활성 브랜치를 정리하는 크론잡을 함께 설정하면 안전합니다 |
| 플랫폼 종속 | Neon 브랜칭에 깊이 의존하면 DB 이전이 어려워집니다 | 마스킹 규칙은 표준 SQL(SECURITY LABEL)로 관리하고, CI 워크플로에서 브랜칭 로직을 추상화해두면 전환 비용을 줄일 수 있습니다 |
| 대규모 DB 병목 | 수백 GB 이상 DB에서 anonymize_database()가 CI 타임아웃을 초과할 수 있습니다 |
마스킹된 베이스라인 브랜치를 주기적(예: 야간)으로 갱신하고, PR 브랜치는 이 베이스라인에서 다시 브랜칭하는 2단계 전략이 효과적입니다 |
Format-Preserving Masking(형식 보존 마스킹): 원본 데이터의 형식(길이, 문자 종류, 패턴)을 유지하면서 값만 바꾸는 기법입니다. 예를 들어 "010-1234-5678"을 "010-9876-5432"로 바꾸면 전화번호 형식은 유지되어, 유효성 검사 로직이 깨지지 않습니다.
실무에서 가장 흔한 실수
-
마스킹 규칙을 스키마 마이그레이션과 분리해서 관리하는 것 — 저희 팀에서 실제로 겪었던 건데, 주소 검색 기능을 추가하면서
users테이블에address_detail컬럼을 넣었는데 마스킹 규칙 업데이트를 깜빡한 적이 있습니다. 2주간 테스트 환경에 실제 주소가 노출되어 있었는데, 다행히 내부 QA 환경이라 큰 사고로 이어지진 않았지만, 그 뒤로 마이그레이션 PR 템플릿에 "마스킹 규칙 확인" 체크박스를 추가했습니다. -
비결정론적 마스킹 함수를 사용하는 것 —
anon.random_string()처럼 매번 다른 결과를 내는 함수를 쓰면 테스트가 non-deterministic하게 됩니다. 디버깅할 때 "이게 코드 문제인지, 데이터가 바뀐 건지" 구분할 수 없게 되죠.anon.pseudo_*계열 함수나 시드 기반 트랜스포머를 사용하는 것이 훨씬 안정적입니다. -
PR 닫힘 시 브랜치 삭제를 빼먹는 것 — cleanup job 없이 배포하면 브랜치가 계속 쌓이면서 비용이 눈덩이처럼 불어납니다. 워크플로에 cleanup을 반드시 포함하고, 혹시 모를 누락에 대비해 7일 이상 비활성 브랜치를 자동 삭제하는 크론잡도 함께 설정해두는 것을 권장합니다.
마치며
이 워크플로를 적용하면 PR마다 프로덕션 데이터 형태 기반의 E2E 검증이 추가됩니다. Neon CoW 브랜칭 기준으로 DB 프로비저닝에 ~3초, 마스킹에 수십 초 수준이라 기존 CI 파이프라인에 실질적인 병목 없이 통합할 수 있습니다. 시드 스크립트의 한계를 체감하고 있었다면, 적은 노력으로 한 단계 올라갈 수 있는 방법입니다.
지금 바로 시작해볼 수 있는 3단계:
-
마스킹 규칙부터 정의해보기 — 프로덕션 DB의 테이블을 훑으면서 PII가 포함된 컬럼을 식별하고,
SECURITY LABEL구문으로 마스킹 규칙을 작성해볼 수 있습니다.sql/masking-rules.sql파일 하나로 시작하면 됩니다. 이 단계만으로도 "우리 데이터에 PII가 어디에 있는지" 파악하는 좋은 기회가 됩니다. -
로컬에서 먼저 검증해보기 — Neon을 쓴다면 CLI로
neonctl branches create --name test-mask를 실행해 브랜치를 하나 만들고, 마스킹 규칙을 적용한 뒤 Playwright 테스트를 로컬에서 돌려볼 수 있습니다. Neon 없이도 Docker로 PostgreSQL 컨테이너를 띄우고postgresql_anonymizer확장을 설치하면 동일한 실험이 가능합니다. -
CI에 올리기 — 로컬에서 검증이 끝나면 위 예시의 GitHub Actions 워크플로를
.github/workflows/e2e-preview.yml로 추가하면 됩니다. 처음에는 가장 핵심적인 사용자 시나리오 2~3개만 테스트로 작성하고, 점진적으로 커버리지를 넓혀가는 것이 실패 없이 정착시키는 좋은 전략입니다.
다음 글: 마스킹 규칙의 완전성을 자동으로 검증하는 방법 — PII 탐지 도구와 CI를 연동해서 마스킹 누락을 배포 전에 잡아내는 파이프라인 구축기
참고 자료
핵심 자료
- Playwright + Neon Branching으로 E2E 테스트하기 | Neon
- 마스킹된 프로덕션 데이터로 환경 구성하기 | Neon Blog
- PostgreSQL Anonymizer 공식 문서
- Greenmask 공식 사이트
- Playwright 공식 베스트 프랙티스
- Neon Playwright 예제 저장소 | GitHub
추가 읽을거리
- PII 익명화와 브랜칭 | Neon Blog
- Neon 데이터 익명화 문서 | Neon Docs
- postgresql_anonymizer 확장 문서 | Neon Docs
- PostgreSQL Anonymizer 마스킹 규칙 선언 | 공식 문서
- Greenmask GitHub 저장소
- Neosync GitHub 저장소
- 스테이징 DB에서 PII 다루기 | Neon Blog
- Playwright CI 설정 가이드
- 데이터 마스킹 In-Place vs In-Flight 비교 | Synthesized
- Supabase Seed (Snaplet 후속) | GitHub