콘텐츠로 이동

검증

architecture에서 잡은 FSD 슬라이스, API 어댑터, RLS 정책에 코드를 채워 넣은 다음, 그 코드가 사용자가 적은 시나리오대로 도는지, 그리고 service_role 키가 새지 않았는지 한 번 묻는 자리가 필요합니다. base는 그 질문에 정적과 동작 두 갈래로 답합니다.

도구보는 자리
review-saas정적 6차원. 보안, 시크릿, a11y, SEO, 타입, 빌드
/test-saas동작 차원. docs/design/saas-spec.md §2 시나리오를 실제 함수 호출과 페이지 진입과 RLS 정책 검증으로

둘은 같은 코드를 다른 각도에서 봅니다. review가 “코드가 깨끗한가”를 본다면 test는 “시나리오대로 도는가”를 봅니다. 한쪽만 통과해서는 진짜 통과가 아닙니다.

implement-* (영역별 구현)
/test-saas (spec → Vitest + Playwright + supabase test db → 실행)
review-saas (정적 6차원)
build → deploy-saas

implement 직후에 test가 먼저 도는 이유는 단순합니다. 정적 검사는 동작 결과 없이도 통과할 수 있는데, 동작이 깨졌으면 review가 무엇을 통과시켜도 의미가 없습니다. 동작 신호를 먼저 받고 그 위에 정적 차원을 얹는 순서입니다.

루브릭은 AI_AUTOMATION.md 섹션 7에 SSOT가 있습니다. 통과 = 모든 차원 3점 이상.

차원5점 자리1점 자리
보안RLS 모든 테이블 + 보호 라우트 가드 + webhook 서명 검증 + CSP connect-src 명시service_role 클라이언트 노출 또는 RLS 비활성 테이블
시크릿코드, .env, 번들 깨끗. VITE_*만 클라이언트. .env.example만 git. gitleaks 0건실제 키가 git 히스토리에
a11yjsx-a11y 0 errors, axe-core 0 violations, 키보드 navigation 완전, focus indicatorsemantic HTML 위반 또는 ARIA 오용
SEOmeta(title/description/og) + sitemap.xml + robots.txt + JSON-LD + Lighthouse SEO 90 이상빈 title, no meta, robots 누락
타입tsc --noEmit 0 errors. Supabase generated types 최신타입 에러 1건 이상
빌드빌드 성공 + 라우트별 200KB gzip 미만 (초기 chunk) + bundle analyzer 청정 + code splitting 동작실패

iteration은 두 번까지입니다. 두 번 연속 changes_requested가 떨어지면 stage가 blocked으로 바뀌고 /recover-from-blocked가 트리거 자리에 들어갑니다. 그 흐름은 architecture의 복구 슬래시 명령 단락에 있습니다.

검증 명령:

Terminal window
pnpm typecheck # tsc --noEmit
pnpm lint # FSD boundaries + jsx-a11y
pnpm build # 프로덕션 빌드 + bundle size 보고
npx gitleaks detect # 시크릿 grep
npx lhci autorun # Lighthouse CI (a11y + SEO + perf)
supabase test db # RLS 정책 단위 테스트

review-saas 스킬은 위 명령을 차례로 돌려 그 결과를 6차원 점수로 합성합니다. 결과는 .claude/state/review-{timestamp}.md에 한 장. 통과 또는 차원별 부족 사유와 같이 떨어집니다.

review-saas의 보안 차원이 보는 자리는 AI_AUTOMATION.md 섹션 4의 S1~S8입니다. 줄 하나씩.

  • S1: 모든 Supabase 테이블 RLS 활성 + default deny. 보호 라우트는 <RequireAuth> 누락 0건. 검증 자리는 migrations와 reviewer.
  • S2: SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET은 서버 전용. 클라이언트에는 VITE_* prefix가 붙은 anon key, publishable key만. .env*.gitignore, .env.example만 git에. 검증 자리는 env.ts zod 검증, reviewer grep, gitleaks.
  • S3: 모든 사용자 입력은 zod schema로 검증 후 DB와 Stripe로 전달. URL 파라미터, 쿼리, 폼, webhook payload 전부. 검증 자리는 shared/lib/validation과 reviewer.
  • S4: dangerouslySetInnerHTML 사용 시 DOMPurify 필수. 사용자 입력 텍스트가 SQL이나 HTML로 직접 들어가는 자리 금지. 검증 자리는 reviewer.
  • S5: rm -rf, git push --force, DROP TABLE, 원격 supabase db reset --linked, production stripe trigger는 자동 실행 금지. 사용자 명시 확인 필수. 검증 자리는 모든 작업의 진입 게이트.
  • S6: 명령은 프로젝트 디렉토리 안에서만 실행. Supabase production 프로젝트는 별도 환경변수로 격리. 검증 자리는 모든 작업.
  • S7: JWT, session token, Stripe key, customer id, email은 앞 4자 + *** 마스킹. production 로그에 평문 PII 금지. 검증 자리는 logging util과 reviewer.
  • S8: CSP connect-src에 명시된 도메인만 fetch 허용. Supabase, Stripe, 측정 도구 한정. 임의 외부 URL fetch 금지. 검증 자리는 index.html의 meta와 reviewer.

S1과 S2는 reviewer가 점수 1점으로 즉시 차단하는 자리입니다. 나머지는 점수 깎이지만 차단 단계까지 가지 않는 경우가 많습니다. 본인 도메인 entity에 따라 어떤 테이블에 RLS를 어떻게 잡을지가 달라지므로 구체 정책은 phase 4의 데이터 스키마 결정 자리에서 박힙니다. 이 자리는 fork 시 채워집니다.

AI_AUTOMATION.md 섹션 5가 reviewer가 차단하는 패턴 목록을 들고 있습니다. 정적 grep으로 잡히는 자리들이라 같은 코드를 두 번 작성할 일이 없습니다.

금지사유대체
SUPABASE_SERVICE_ROLE_KEYsrc/ 코드에서 import클라이언트 번들에 포함되면 RLS 우회Edge Function 내부에서만 Deno.env.get(...)
RLS 비활성 public 테이블anon key로 누구나 read/writealter table ... enable row level security + 명시 policy
localStorage에 JWT 직접 저장XSS 시 탈취supabase-js의 세션 관리 + 보호 라우트 가드
Stripe webhook에서 서명 검증 누락위조 webhook으로 임의 구독 활성화stripe.webhooks.constructEvent(body, sig, secret) 필수
dangerouslySetInnerHTML 무 sanitizeXSSDOMPurify, 가능하면 컴포넌트 분해
보호 라우트에 가드 없음비인증 상태에서 보호 UI 노출<RequireAuth> element wrapper
console.log(session), console.log(token)로그, 스크린샷, 세션 리플레이로 유출mask(token) 헬퍼 또는 제거
select * from users 무 limit대규모 응답 + 사이드 채널 노출명시 컬럼 + .limit() + RLS로 행 제한
import.meta.env.SUPABASE_URL (VITE_ prefix 누락)Vite는 VITE_*만 클라이언트에 노출. 누락 시 undefinedimport.meta.env.VITE_SUPABASE_URL + env.ts zod 검증

일반 boilerplate는 “Vitest 셋업”을 줍니다. 사용자가 직접 테스트 코드를 짜야 합니다. /test-saas는 같은 자리에 한 단계를 더 둡니다.

docs/design/saas-spec.md §2 시나리오 (사용자가 phase 3에서 작성)
/test-saas 가 시나리오 파싱과 매핑
시나리오 단위 → Vitest 단위 + Playwright e2e + supabase test db RLS (AI 생성)
src/{slice}/__tests__/ 와 api/policies.test.sql 안에 떨어짐

사용자는 시나리오 한 줄을 적습니다. “사용자가 Pro 플랜을 고른 다음 Stripe Checkout을 통과해 돌아오면 /app에 active subscription 상태로 들어간다.” 이 한 줄을 스킬이 검증 가능한 단위 셋으로 분해합니다.

분해는 FSD 슬라이스 또는 RLS 정책을 기준으로 합니다.

시나리오 조각테스트 단위자리
Pro 플랜 카드 클릭 → Edge Function 호출features/checkout 액션 단위src/features/checkout/__tests__/ (Vitest)
Edge Function이 Stripe session URL 반환api/functions/checkout-sessionapi/functions/checkout-session/__tests__/ (Vitest, mocked Stripe)
Stripe 결제 완료 → /checkout/success 진입라우트 진입 + UI 상태Playwright e2e
webhook이 subscriptions 테이블 갱신webhook signature 검증 + DB upsertapi/functions/stripe-webhook/__tests__/ (Vitest)
다른 user는 본인 subscription만 selectRLS 정책 단위 테스트api/policies.test.sql (supabase test db)

테스트 패턴은 네 갈래가 박혀 있습니다.

  • Vitest 단위: src/{slice}/__tests__/에 co-located. FSD public API를 통과하지 않고 슬라이스 내부를 직접 import 가능합니다. 테스트가 슬라이스의 일부지 외부 소비자가 아니라는 신호입니다.
  • Edge Function 단위: api/functions/<name>/__tests__/. Supabase client와 Stripe SDK를 vi.mock으로 stub. 서명 검증의 경우 의도적으로 위조 signature를 던지는 negative 케이스가 같이 들어갑니다.
  • Playwright e2e: 라우트 진입과 인증 흐름과 결제 redirect를 실제로 밟습니다. supabase start로 로컬 Postgres와 Edge runtime을 띄운 자리 위에서 돕니다.
  • RLS 단위: api/policies.test.sql에 “이 user로 이 쿼리를 보냈을 때 거절돼야 한다” 형태로 적습니다. supabase test db가 로컬 DB에서 실행합니다.

테스트 파일이 어디로 떨어질지는 시나리오의 자리에 따라 다릅니다. 슬라이스에 닿는 자리는 슬라이스 안, RLS에 닿는 자리는 api/policies.test.sql. 베이스가 박은 자리는 위 네 갈래의 디렉토리 골격뿐입니다.

raw Vitest와 Playwright 출력은 사용자에게 그대로 가지 않습니다. 스킬이 결과를 시나리오 단위로 다시 합성합니다.

총 시나리오: 5개 — 4개 통과, 1개 실패
✓ Pro 플랜 결제 시작 — Edge Function checkout-session 정상 호출, Stripe URL 반환
✓ Stripe webhook 서명 검증 — 위조 signature 거부, 정상 signature는 통과
✓ 결제 완료 후 /app 진입 — active subscription 상태로 보호 라우트 통과
✗ Customer Portal 진입 — Edge Function customer-portal 호출은 성공했으나 URL 반환 없음
파일: src/features/manage-subscription/__tests__/index.test.ts:24
고치는 곳: features/manage-subscription/model/openPortal.ts
✓ 다른 user의 subscription select 거부 — RLS 정책 정상 동작

raw를 그대로 보여주지 않는 자리는 의도된 것입니다. PM 또는 비개발자 입장에서 expected 'foo' to be 'bar' at line 32보다 “이 시나리오는 통과했고 이 시나리오는 실패했다”가 의사 결정에 직접 닿습니다. 동시에 raw 로그는 .claude/state/last-test.log에 통째로 남아 있어 깊이 파야 할 때 손이 닿습니다.

Terminal window
# 동작
/test-saas # spec → 테스트 생성 → 실행 → 시나리오 언어 보고
pnpm test # Vitest 직접
pnpm test:e2e # Playwright 직접
supabase test db # RLS 정책 단위 테스트
# 정적
/review-saas # 6차원 합성
pnpm typecheck
pnpm lint
pnpm build
npx gitleaks detect
npx lhci autorun

비개발자 입장에서 같은 자리를 한 줄 안내로 정리한 곳이 비개발자용 04장 real-backend 끝에 있습니다. 시나리오를 적는 사람은 spec 작성자고, 검증 코드는 AI가 만들고, 보고는 시나리오 언어로 돌아온다는 같은 원리입니다.

다음 섹션은 build-and-package입니다. Vite SPA 빌드가 dist/에 어떻게 떨어지는지, import.meta.envVITE_* 노출 경계, env.ts의 zod 검증이 빌드 차단 게이트 역할을 하는 자리, route별 chunk size 200KB gzip 목표, 그리고 Edge Functions가 별도 트랙으로 supabase functions deploy로 떨어지는 자리를 봅니다.