-
[AI] UI를 개발할 때 화면 하나를 TDD 3단계로 강제한 방법Dev 2026. 7. 23. 18:02반응형
AI에게 “TDD로 화면을 만들어 줘”라고 요청하면 정말 TDD로 끝까지 구현할까요?
제 경험에서는 그렇지 않았습니다. 처음에는 테스트부터 시작하더라도 어느 순간 SwiftUI 뷰를 먼저 만들거나, 깨진 스냅샷을 원인 분석 없이 새 이미지로 덮어쓰는 일이 반복됐습니다. 지시를 더 길고 자세하게 적을수록 좋아질 것 같았지만, 600줄에 가까운 프롬프트 안에서는 오히려 중요한 규칙이 중간에서 흐려졌습니다.
DDD 동아리에서 Pickflow라는 iOS 앱을 개발하며 이 문제를 해결하기 위해 선택한 방법은 “더 강한 프롬프트”가 아니었습니다. 화면 하나를 만들 때 반드시 통과해야 하는 산출물과 게이트를 정하고, 이를 Claude Code 스킬로 시스템화했습니다.
이 글에서는 SwiftUI + MVVM + Tuist 프로젝트에서 사용한 화면 단위 TDD 3단계 워크플로우를 소개합니다.
AI와 화면을 만들며 반복해서 겪은 문제
새 화면을 몇 개 구현해 보니 비슷한 문제가 계속 나타났습니다.
- TDD를 요청해도 테스트와 구현 순서가 섞였습니다.
- 스냅샷 테스트가 깨지면 회귀인지 의도된 변경인지 확인하지 않고 record 모드로 덮어썼습니다.
- 구현 중 색상, 아이콘, 폰트가 궁금해져 Figma를 계속 다시 열었습니다.
- 한 문서에 모든 규칙을 담으니 단계별 지시가 서로 섞였습니다.
특히 위험했던 것은 단계 침범이었습니다. ViewModel 테스트를 작성하는 Phase A에서 “나중에 필요할 것 같다”는 이유로 SwiftUI Preview를 미리 만들면, 그 순간부터 테스트가 설계를 이끄는 것이 아니라 이미 만든 뷰에 테스트를 맞추게 됩니다.
문제는 AI가 TDD를 모른다는 데 있지 않았습니다. 긴 작업 동안 지금 어느 단계에 있는지, 무엇을 아직 하면 안 되는지를 유지하기 어려운 구조에 있었습니다.
해법: 화면 티켓 하나당 구현 프롬프트 한 장
프로젝트에 screen-tdd-prompt라는 Claude Code 스킬을 만들었습니다. 화면 티켓을 입력하고 구현 프롬프트 생성을 요청하면 다음 두 파일이 만들어집니다.
docs/<TICKET>/ ├── <screen-slug>-implementation-prompt.md └── ui-test-cases.md첫 번째 파일은 해당 화면 구현에 필요한 사실만 담은 약 150줄의 프롬프트입니다. 화면 범위, API 매핑, 정책 결정, 에셋, Figma 노드처럼 그 화면에만 해당하는 정보를 적습니다.
두 번째 파일은 Phase B에서 채울 UI 테스트 케이스 표입니다. 이 표는 Phase C에서 만들 스냅샷 테스트의 단일 진실 소스가 됩니다.
스킬은 구현 프롬프트에 다음 네 가지 게이트가 반드시 들어가도록 강제합니다.
Gate 강제하는 것 필요한 이유 1 Phase A → B → C 직렬 진행 단계가 섞이면 TDD의 의미가 사라지기 때문 2 ui-test-cases.md 선행 작성 스냅샷 케이스를 구현 후 임의로 정하지 않기 위해 3 스냅샷 테스트를 머지 조건으로 사용 시각 회귀를 사람의 기억에만 맡기지 않기 위해 4 에셋 입력 매트릭스 선행 확정 구현 중 Figma를 재방문하며 컨텍스트가 끊기는 것을 줄이기 위해 핵심은 “해야 할 일”만 나열하는 것이 아닙니다. 다음 단계로 넘어갈 수 있는 조건과 현재 단계에서 해서는 안 되는 일도 함께 정의하는 것입니다.
방법론과 화면의 사실을 분리한다
초기에는 모든 내용을 하나의 긴 프롬프트에 넣었습니다. 하지만 Phase A를 진행하는 동안 Phase C의 세부 규칙까지 함께 읽으면 단계 경계가 흐려졌습니다.
그래서 문서를 두 종류로 분리했습니다.
.claude/skills/screen-tdd-prompt/ ├── SKILL.md └── prompt-template.md docs/phases/ ├── phase-a-viewmodel-tdd.md ├── phase-b-ui-cases.md └── phase-c-snapshot.md화면별 구현 프롬프트에는 해당 화면의 사실만 둡니다.
- 구현 범위와 제외 범위
- API와 화면 상태의 매핑
- ViewModel 인터페이스
- 정책 결정
- 컬러, 아이콘, 폰트 에셋
- Figma 노드 매핑
반대로 RED → GREEN 사이클, UI 테스트 표의 컬럼 정의, 스냅샷 record 규칙처럼 모든 화면이 공유하는 방법론은 docs/phases 아래에 둡니다.
읽는 시점도 중요합니다. 화면 프롬프트는 작업을 시작할 때 읽지만, 각 Phase 문서는 해당 단계에 진입할 때만 읽습니다. Phase A를 수행하는 동안 Phase C 문서는 아직 작업 컨텍스트에 넣지 않습니다. 이 단계 격리가 3단계 워크플로우의 본체입니다.
Phase A: ViewModel TDD, SwiftUI 뷰는 0줄
Phase A는 Mock 기반 ViewModel 단위 테스트 단계입니다. 에셋 매트릭스, API 매핑, ViewModel 시그니처가 확정되어야 시작할 수 있습니다.
작업은 인터랙션 하나씩 RED → GREEN으로 반복합니다.
1. 테스트 한 개 작성 2. xcodebuild test로 RED 확인 3. ViewModel에 최소 구현 추가 4. xcodebuild test로 GREEN 확인 5. 필요하면 리팩터링 6. 다음 인터랙션으로 이동테스트 이름은 상황, 조건, 기대 결과가 드러나는 한국어 패턴을 사용했습니다.
func test_onAppear_정상응답_상태가loaded로전환된다() async throws func test_toggleBookmark_API실패시_상태가롤백되고toast가설정된다() async throws func test_openExternalApp_미설치면_AppStoreURL을연다()이렇게 하면 리뷰어가 테스트 목록만 읽어도 화면 동작을 파악할 수 있습니다.
Phase A 종료 체크리스트에는 다소 과격해 보이는 조건도 넣었습니다.
Feature// 아래 SwiftUI 뷰 파일이 0개인가?
“Preview 하나 정도는 미리 만들어도 되지 않을까?”라는 단계 침범을 막기 위한 자가 점검입니다. Phase A에서는 상태와 인터랙션만 검증하고, 화면 코드는 만들지 않습니다.
Phase B: UI 테스트 케이스를 먼저 설계한다
Phase A가 끝나면 곧바로 SwiftUI를 구현하지 않습니다. 먼저 docs//ui-test-cases.md를 채웁니다.
case id 컴포넌트 상태/입력 테마 언어/Dynamic Type Light/Dark 디바이스 기대 시각 결과 파일명 이 표의 한 행은 Phase C의 스냅샷 테스트 한 개와 정확히 대응합니다.
운영하며 정한 규칙은 다음과 같습니다.
- Light와 Dark는 서로 다른 행으로 작성합니다.
- “Figma와 동일”처럼 확인할 수 없는 표현을 쓰지 않습니다.
- 기대 시각 결과는 이미지 없이 읽어도 장면이 그려질 정도로 적습니다.
- accessibilityExtraLarge 같은 큰 Dynamic Type 케이스를 최소 1~2개 포함합니다.
- 스냅샷 파일명을 표에서 미리 확정합니다.
예를 들어 “헤더가 Figma와 동일하다”가 아니라 “노을 배경의 헤더 칩에 PM 6:20 라벨이 표시되고, progress 위치가 트랙의 약 0.79 지점에 있다”처럼 작성합니다.
작은 화면은 약 15행, 일반적인 화면은 약 30행, 컴포넌트가 많은 화면은 약 50행이 나왔습니다. 중요한 것은 정확한 행 수보다 구현 전에 상태와 시각 결과를 합의하는 순서입니다.
이 표를 구현이 끝난 뒤 문서 정리 차원에서 작성하면 사후 합리화 문서가 됩니다. Phase B를 독립 단계로 둔 이유입니다.
Phase C: 스냅샷 RED → SwiftUI 구현 → GREEN
Phase C에서야 SwiftUI 코드를 작성합니다. Pickflow에서는 pointfreeco의 swift-snapshot-testing을 사용했습니다.
각 케이스는 다음 순서로 진행합니다.
1. ui-test-cases.md의 한 행을 스냅샷 테스트로 옮긴다 2. 구현 없이 assertSnapshot 호출을 작성한다 3. 기준 이미지가 없어 실패하는 것을 확인한다 4. 대응하는 SwiftUI 컴포넌트를 구현한다 5. record 모드로 기준 이미지를 한 번 생성한다 6. PNG를 눈으로 확인하고 표의 기대 결과와 비교한다 7. record를 끄고 다시 테스트해 GREEN을 확인한다시간에 따라 바뀌는 UI는 clock을 고정하고, 비동기 이미지는 동기 Mock으로 주입합니다. 디바이스, 언어, Dynamic Type도 케이스에 명시해 실행할 때마다 결과가 달라지지 않도록 했습니다.
가장 중요한 금지 규칙은 이것입니다.
스냅샷이 깨졌다는 이유만으로 원인 분석 없이 record 모드로 덮어쓰지 않는다.
record는 새 케이스를 추가할 때 한 번, 또는 의도적인 시각 변경을 검토한 뒤에만 사용합니다. 그렇지 않으면 회귀가 새로운 정상 상태로 바뀌어 스냅샷 테스트의 의미가 사라집니다.
마지막에는 Figma MCP로 컴포넌트별 디자인 스크린샷과 생성된 스냅샷을 비교합니다. 차이가 있으면 구현을 수정하고, 기준 이미지를 다시 생성한 뒤, record를 끈 상태로 테스트를 통과시킵니다.
세 단계를 병렬화하지 않은 이유
AI 작업은 병렬화하면 빨라 보입니다. 하지만 이 워크플로우에서 A, B, C는 의도적으로 직렬입니다.
Phase A: 동작을 확정한다 ↓ Phase B: 시각 상태와 케이스를 확정한다 ↓ Phase C: 확정된 계약을 코드와 스냅샷으로 구현한다Phase B가 끝나기 전에 Phase C를 시작하면 구현된 UI에 맞춰 테스트 케이스를 쓰게 됩니다. Phase A와 C를 동시에 진행하면 ViewModel 설계가 뷰 구현에 끌려갑니다.
병렬화할 수 있는 것은 같은 Phase 안의 독립적인 조사나 준비 작업입니다. 단계 자체의 순서는 바꾸지 않습니다.
실제 프로젝트에서 얻은 결과
안티 패턴도 스킬에 넣는다
좋은 절차만 적는 것으로는 부족했습니다. 실제로 발생했던 실패를 금지 규칙으로 함께 넣었습니다.
- 빈 섹션에 “추후”라고 적고 구현을 시작하지 않는다.
- Phase A, B, C를 병렬로 진행하지 않는다.
- 스냅샷 테스트를 선택 사항으로 약화하지 않는다.
- 에셋 매트릭스를 만들기 전에 화면 코드를 작성하지 않는다.
- 깨진 스냅샷을 분석 없이 record로 덮어쓰지 않는다.
AI에게는 “무엇을 하라”는 지시만큼 “어떤 상태에서는 다음 단계로 가지 말라”는 게이트가 효과적이었습니다.
마치며
이 워크플로우의 핵심은 TDD 기법 자체보다 AI 협업에서 프로세스를 구조로 강제하는 데 있습니다.
- 반복하는 규율은 스킬, 템플릿, 체크리스트로 옮깁니다.
- 긴 지시문은 화면의 사실과 공통 방법론으로 분리합니다.
- Phase 문서는 필요한 시점에만 읽습니다.
- 각 단계의 산출물을 다음 단계의 입력으로 사용합니다.
- 금지 규칙과 종료 조건을 명시합니다.
사람 팀에서 컨벤션을 린터와 CI로 강제하듯, AI 팀원에게는 워크플로우를 스킬과 게이트로 강제할 수 있습니다.
참고한 블로그
https://sonjh1217.github.io/posts/ios-agentic-flywheel/
Designing an Agentic Flywheel for iOS Development
My previous post about the agentic flywheel was intentionally generic.
sonjh1217.github.io
반응형'Dev' 카테고리의 다른 글
[AI] iOS API 디버깅하기: AI와 만든 핑퐁 테스트 워크플로우 (0) 2026.07.23 [AI] git worktree와 cmux 활용한 AI 병렬 개발 워크플로우 (0) 2026.07.23 youtube 요약 n8n 워크플로우 구축기 (0) 2026.05.21 Claude Desktop로 WWDC 요약하기 (Puppeteer, MCP) (0) 2025.04.11 [jekyll] 로컬에서 구동시 Dependency Error 문제 해결하기 (0) 2024.03.21