콘텐츠로 이동

아키텍처

intro에서 산출물은 테스트 스위트라고 짚었습니다. 여기서는 그 스위트가 디렉토리에서 어떻게 생겼는지, 어떤 규칙이 AI가 임의로 구조를 흐리지 못하게 막는지를 봅니다. POM과 fixtures 조합, 트랙 분리, 시나리오와 테스트의 1:1, 그리고 그걸 세우고 채우고 검증하는 세 에이전트.

트랙은 디렉토리부터 갈립니다. 선택 안 한 트랙은 /6-cleanup-residue가 지우므로, fork 직후 모든 디렉토리가 있다가 결정 뒤 정리됩니다.

<project>/
├── tests/
│ ├── web/ ← Playwright
│ │ ├── pages/ ← Page Object (로케이터 + 동작)
│ │ ├── fixtures/ ← test.extend 픽스처 + storageState setup
│ │ ├── specs/ ← <flow>.spec.ts (시나리오 1개 = 1 spec)
│ │ └── utils/
│ ├── electron/ ← Playwright _electron
│ │ ├── specs/
│ │ └── helpers/ ← 메인 프로세스 evaluate, dialog/menu/tray stub
│ └── mobile/ ← Maestro 또는 Detox
│ ├── flows/ ← Maestro .yml (또는 Detox e2e/)
│ └── helpers/
├── playwright.config.ts ← web/electron 트랙
├── .detoxrc.js ← mobile=detox 선택 시
├── .maestro/ ← mobile=maestro 선택 시(또는 tests/mobile/flows)
├── .github/workflows/e2e.yml ← implement-ci-workflow 산출
├── docs/design/e2e-spec.md ← 설계 SSOT (SUT·플로우·도구·환경·CI)
└── .env.test.example ← 테스트 환경변수 키 목록 (실키 0건)

로케이터는 pages/의 Page Object 클래스 안에만 둡니다. spec 파일에 셀렉터를 흩으면 SUT의 DOM이 한 번 바뀔 때 따라가야 할 자리를 잃습니다.

2026 권장 조합입니다. fixtures가 setup과 teardown, 인증을 관리하고, Page Object가 페이지 상호작용을 캡슐화합니다. 둘의 책임이 갈립니다.

  • fixtures/: test.extend로 로그인된 페이지나 준비된 데이터를 테스트에 주입합니다. web의 auth.setup.ts는 setup project로 한 번 로그인해 storageState를 저장하고, 다른 project가 dependencies: ['setup']으로 그 상태를 재사용합니다.
  • pages/: LoginPage, DashboardPage 같은 클래스. 로케이터와 그 페이지에서 할 수 있는 동작을 담습니다. spec은 이 클래스의 메서드만 부릅니다.

인증을 매 테스트 UI로 다시 밟지 않는 게 핵심입니다. 매번 로그인하면 느리고 flaky합니다. web은 storageState, mobile은 사전 로그인 subflow, electron은 세션 주입으로 1회 로그인 후 재사용합니다. 전략은 analyze-and-design에서, 트랙별 구현은 tracks에서 봅니다.

web, electron, mobile은 디렉토리뿐 아니라 config와 CI 잡도 분리됩니다. 공유하는 건 utils/의 트랙 무관 유틸뿐이고, 트랙 간 공유는 최소로 둡니다. electron은 단일 인스턴스라 workers: 1로 도는데 web은 fullyParallel: true로 도니까, 한 config에 섞으면 서로의 가정이 깨집니다.

테스트 범위의 SSOT는 docs/design/e2e-spec.md §2입니다. 거기 적힌 크리티컬 플로우 한 줄이 테스트 한 개(또는 한 spec)로 변환됩니다. 스펙에 없는 임의 테스트를 늘리지 않습니다. AI에게 맡기면 커버리지를 채우려고 스펙 밖 케이스를 발명하는데, 그 자리가 유지보수 비용이 됩니다.

워크플로우는 Atomic Phase입니다. /start가 진입 라우터고, stage를 감지해 호흡을 정하고 첫 phase부터 순차 실행합니다.

graph LR
start["프로젝트 시작<br/>/start"] --> pace["/1-pace"]
pace --> setup["/2-setup-base"]
setup --> analyze["/3-analyze-target"]
analyze --> design["/4-design-suite"]
design --> impl["/5-implement-suite"]
impl --> cleanup["/6-cleanup-residue"]
cleanup --> run["/run-suite"]
run --> review["/review-e2e"]
review -->|changes_requested| impl

/review-e2e가 통과 못 하면 그 트랙의 implement-*로 돌아갑니다. 이 루프는 iteration 두 번까지입니다.

Claude Code에서는 이 흐름의 무거운 자리를 세 서브에이전트가 별도 컨텍스트에서 맡습니다.

graph TB
spec["e2e-spec.md §2<br/>크리티컬 플로우"] --> architect["e2e-architect<br/>골격 + 도구 배선 (1회)"]
architect --> builder["e2e-builder<br/>POM·픽스처·spec·CI 구현"]
builder --> reviewer["e2e-reviewer<br/>6차원 검증"]
reviewer -->|changes_requested| builder
reviewer -->|passed| done["통과"]
auth["인증 재사용<br/>storageState / subflow / 세션 주입"] -.공유.- builder
  • e2e-architect: 디렉토리, playwright.config.ts, .detoxrc.js 또는 .maestro/, CI 골격을 한 번 세웁니다. 본문은 비우고 뼈대까지.
  • e2e-builder: 골격에 실제 플로우와 POM, 픽스처를 채웁니다. implement-web-suite, implement-electron-suite, implement-mobile-suite, implement-auth-fixtures, implement-ci-workflow가 실제 도구입니다.
  • e2e-reviewer: 6차원으로 검증만 합니다. 코드는 고치지 않습니다. 고치는 건 builder의 몫이라 검증자와 수정자가 갈립니다.

세 에이전트가 별도 컨텍스트로 도는 이유는 메인 컨텍스트를 보호하기 위해서입니다. 트랙별 구현 디테일이 메인 대화에 쌓이면 결정 맥락이 묻힙니다. 산출물은 파일 경로로 핸드오프하고 노트만 .claude/state/builder-<track>.md에 남깁니다.

되돌리기 비싼 결정은 docs/adr/에 한 장씩 쌓습니다. 추천 외 도구를 고른 자리, 예를 들어 web에서 Playwright 대신 Cypress, mobile에서 Maestro 대신 Detox를 고른 근거가 여기 들어갑니다. 코드는 무엇을 했는지를, ADR은 왜 그랬는지를 들고 있습니다. 어떤 결정에 ADR이 붙는지는 analyze-and-design에서 봅니다.

다음 섹션은 tracks입니다. web의 Playwright config와 Supabase localStorage, Stripe cross-origin 함정, electron의 메인 프로세스 stub, mobile의 Maestro 선언형 플로우와 타겟 매트릭스를 트랙별로 봅니다.