콘텐츠로 이동

검증

architecture에서 정한 FSD 슬라이스와 API 어댑터, 행 수준 접근 정책(RLS)에 코드를 채워 넣은 다음에는, 그 코드가 사용자가 적은 시나리오대로 동작하는지 그리고 service_role 키가 노출되지 않았는지 한 번 확인하는 단계가 필요합니다. base는 그 질문에 정적 검사와 동작 검사 두 갈래로 답합니다.

도구무엇을 보나
review-saas정적 검사 여섯 관점을 봅니다. 보안, 시크릿, 접근성, 검색 대응, 타입, 빌드입니다
/test-saas동작을 봅니다. docs/design/saas-spec.md §2 시나리오를 실제 함수 호출과 페이지 진입, 행 수준 접근 정책 검증으로 옮깁니다

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

implement-* (영역별 구현)
↓
/test-saas (spec 을 Vitest 와 Playwright, supabase test db 로 변환해 실행)
↓
review-saas (정적 검사 여섯 관점)
↓
build → deploy-saas

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

채점 기준은 AI_AUTOMATION.md 섹션 7에 기준 문서로 정리돼 있습니다. 모든 관점이 3점 이상이면 통과입니다.

관점5점 기준1점 기준
보안모든 테이블에 행 수준 접근 정책 적용 + 보호 라우트 가드 + webhook 서명 검증 + CSP connect-src 명시service_role 이 클라이언트에 노출되거나 행 수준 접근 정책이 꺼진 테이블이 있음
시크릿코드와 .env, 번들이 모두 깨끗함. VITE_*만 클라이언트로. .env.example만 git 에. gitleaks 0건실제 키가 git 이력에 남아 있음
접근성jsx-a11y 0 errors, axe-core 0 violations, 키보드 navigation 완전, focus indicator 있음semantic HTML 위반 또는 ARIA 오용
검색 대응meta(title/description/og) + sitemap.xml + robots.txt + JSON-LD + Lighthouse SEO 90 이상빈 title, 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 # 시크릿을 검색합니다
npx lhci autorun # Lighthouse CI 로 접근성과 검색 대응, 성능을 봅니다
supabase test db # 행 수준 접근 정책 단위 테스트를 실행합니다

review-saas 스킬은 위 명령을 차례로 실행해 그 결과를 여섯 관점 점수로 정리합니다. 결과는 .claude/state/review-{timestamp}.md에 한 장으로 남습니다. 통과 여부와 관점별 부족 사유가 같이 적힙니다.

review-saas의 보안 관점이 보는 것은 AI_AUTOMATION.md 섹션 4의 S1부터 S8까지입니다. 한 줄씩 정리하면 다음과 같습니다.

  • S1: 모든 Supabase 테이블에 행 수준 접근 정책을 켜고 default deny 로 둡니다. 보호 라우트는 <RequireAuth> 누락이 0건이어야 합니다. 검증하는 곳은 migrations와 reviewer입니다.
  • S2: SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET은 서버 전용입니다. 클라이언트에는 VITE_* 접두사가 붙은 anon key와 publishable key만 둡니다. .env*는 .gitignore에 넣고 .env.example만 git에 올립니다. 검증하는 곳은 env.ts zod 검증과 reviewer 검색, 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, 운영 환경 stripe trigger는 자동 실행을 금지합니다. 사용자의 명시적 확인이 필요합니다. 검증하는 곳은 모든 작업의 진입 게이트입니다.
  • S6: 명령은 프로젝트 디렉터리 안에서만 실행합니다. Supabase 운영 환경 프로젝트는 별도 환경변수로 격리합니다. 검증하는 곳은 모든 작업입니다.
  • S7: JWT와 session token, Stripe key, customer id, email은 앞 4자만 남기고 ***로 가립니다. 운영 환경 로그에 평문 개인정보를 남기지 않습니다. 검증하는 곳은 logging util과 reviewer입니다.
  • S8: CSP connect-src에 명시된 도메인만 fetch를 허용합니다. Supabase와 Stripe, 측정 도구로 한정합니다. 임의의 외부 URL fetch를 금지합니다. 검증하는 곳은 index.html의 meta와 reviewer입니다.

S1과 S2는 reviewer가 1점을 주고 즉시 차단합니다. 나머지는 점수가 깎이지만 차단 단계까지 가지 않는 경우가 많습니다. 본인 도메인 entity에 따라 어떤 테이블에 어떤 정책을 적용할지가 달라지므로 구체적인 정책은 phase 4의 데이터 스키마 결정에서 정해집니다. 이 항목은 fork 할 때 채워집니다.

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

금지사유대체
SUPABASE_SERVICE_ROLE_KEY를 src/ 코드에서 import클라이언트 번들에 포함되면 행 수준 접근 정책을 우회Edge Function 내부에서만 Deno.env.get(...)
행 수준 접근 정책이 꺼진 public 테이블anon key로 누구나 읽고 쓸 수 있음alter table ... enable row level security 와 명시적 policy
localStorage에 JWT 직접 저장XSS 발생 시 탈취supabase-js의 세션 관리와 보호 라우트 가드
Stripe webhook에서 서명 검증 누락위조된 webhook으로 임의 구독 활성화stripe.webhooks.constructEvent(body, sig, secret) 필수
dangerouslySetInnerHTML을 sanitize 없이 사용XSSDOMPurify, 가능하면 컴포넌트 분해
보호 라우트에 가드 없음비인증 상태에서 보호 UI 노출<RequireAuth> element wrapper
console.log(session), console.log(token)로그와 스크린샷, 세션 리플레이로 유출mask(token) 헬퍼 또는 제거
select * from users 를 limit 없이 사용대규모 응답과 사이드 채널 노출명시적 컬럼과 .limit(), 행 수준 접근 정책으로 행 제한
import.meta.env.SUPABASE_URL (VITE_ 접두사 누락)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 정책 테스트로 (AI 생성)
↓
src/{slice}/__tests__/ 와 api/policies.test.sql 안에 생성됩니다

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

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

시나리오 조각테스트 단위파일 경로
Pro 플랜 카드를 클릭해 Edge Function 을 호출합니다features/checkout 액션 단위src/features/checkout/__tests__/ (Vitest)
Edge Function이 Stripe session URL 을 반환합니다api/functions/checkout-sessionapi/functions/checkout-session/__tests__/ (Vitest, Stripe 를 mock 처리)
Stripe 결제를 완료하고 /checkout/success 로 진입합니다라우트 진입과 UI 상태Playwright e2e
webhook이 subscriptions 테이블을 갱신합니다webhook 서명 검증과 DB upsertapi/functions/stripe-webhook/__tests__/ (Vitest)
다른 user는 본인 subscription만 select 합니다행 수준 접근 정책 단위 테스트api/policies.test.sql (supabase test db)

테스트 패턴은 네 갈래로 정해져 있습니다.

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

테스트 파일이 어디에 만들어질지는 시나리오가 무엇을 다루는지에 따라 다릅니다. 슬라이스를 다루면 슬라이스 안에, 행 수준 접근 정책을 다루면 api/policies.test.sql에 만들어집니다. base가 만들어 둔 것은 위 네 갈래의 디렉터리 골격뿐입니다.

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 거부: 행 수준 접근 정책 정상 동작

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

Terminal window
# 동작 검사
/test-saas # spec 으로 테스트를 생성하고 실행한 다음 시나리오 언어로 보고합니다
pnpm test # Vitest 를 직접 실행합니다
pnpm test:e2e # Playwright 를 직접 실행합니다
supabase test db # 행 수준 접근 정책 단위 테스트를 실행합니다
# 정적 검사
/review-saas # 여섯 관점으로 채점합니다
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.env의 VITE_* 노출 경계, env.ts의 zod 검증이 빌드 차단 게이트 역할을 하는 방식, 라우트별 chunk size 200KB gzip 목표, 그리고 Edge Functions가 별도 경로로 supabase functions deploy를 통해 배포되는 흐름을 봅니다.