| name | rn-harness-generator |
| description | Phase 5 — Build the React Native + Expo app in 3 sub-phases (scaffold, API, UI). Self-evaluates before handoff. |
| allowed-tools | ["Agent","Read","Write","Edit","Bash","Glob","Grep"] |
rn-harness-generator — Phase 5: 앱 빌드
계약 기준에 따라 React Native + Expo 앱을 빌드한다.
Trigger
오케스트레이터에서 Phase 5 (Generator)로 호출됨.
Input
docs/harness/contract.md (완료 기준)
docs/harness/plans/YYYY-MM-DD-prd.md (PRD)
docs/harness/plans/YYYY-MM-DD-design.md (디자인)
docs/harness/feedback/round-N-*.md (Round 2+ 시 Evaluator 피드백)
Process
Round 1: 전체 빌드
Step 1: Project Scaffolding
Read app_slug from docs/harness/config.md (set during Phase 2).
The Expo project is created as a subdirectory of the current working directory, keeping the claude session history intact.
npx create-expo-app@latest $APP_SLUG
cd $APP_SLUG
mkdir -p credentials
mv ../docs ./ 2>/dev/null || true
NOTE: Always use npx create-expo-app@latest to get the latest Expo SDK version.
NOTE: After project creation, docs/harness/ is moved inside the project so all artifacts are in one git repo. The parent directory only retains .claude/ session history.
Step 1.1: Clean Up Default Template Files (MANDATORY)
Remove unused default files generated by create-expo-app:
rm -f scripts/reset-project.js
rmdir scripts 2>/dev/null || true
rm -f assets/images/partial-react-logo.png
rm -f assets/images/react-logo.png
rm -f assets/images/react-logo@2x.png
rm -f assets/images/react-logo@3x.png
rm -rf components/__tests__/
rm -f components/EditScreenInfo.tsx
rm -f components/ExternalLink.tsx
rm -f components/HelloWave.tsx
rm -f components/ParallaxScrollView.tsx
rm -f components/ThemedText.tsx
rm -f components/ThemedView.tsx
rm -f components/Collapsible.tsx
rm -f components/HapticTab.tsx
rm -rf components/ 2>/dev/null || true
rm -f app/(tabs)/explore.tsx
rm -f app/(tabs)/index.tsx
rm -f app/+not-found.tsx
rm -f app/+html.tsx
rm -f constants/Colors.ts
rm -rf constants/ 2>/dev/null || true
rm -f hooks/useColorScheme.ts
rm -f hooks/useColorScheme.web.ts
rm -f hooks/useThemeColor.ts
rm -rf hooks/ 2>/dev/null || true
HARD GATE: Default template files MUST be removed. They cause confusion, import errors, and lint warnings. Only keep assets/images/icon.png, assets/images/splash-icon.png, assets/images/adaptive-icon.png, assets/images/favicon.png.
Step 1.2: Environment Setup
cp $CLAUDE_PLUGIN_ROOT/templates/.env.example .env.example
cp .env.example .env
cp $CLAUDE_PLUGIN_ROOT/templates/.gitignore.template .gitignore
IMPORTANT: .env and credentials/ are in .gitignore — never committed to git.
Dependencies install:
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
npm install nativewind tailwindcss
npx tailwindcss init
npm install zustand @tanstack/react-query axios
npm install react-hook-form zod @hookform/resolvers
npm install @shopify/flash-list react-native-reanimated @gorhom/bottom-sheet
npm install react-native-google-mobile-ads
npx expo install expo-tracking-transparency
npx expo install expo-updates
npm install -D vitest @testing-library/react-native
Step 1.3: EAS Init + Update Setup
eas init
eas update:configure
This auto-adds updates.url and runtimeVersion to app.config.ts.
Step 1.5: 플랫폼 공통 설정 (CRITICAL)
app.config.ts 필수 설정:
export default {
name: "$APP_NAME",
slug: "$APP_SLUG",
version: "1.0.0",
primaryLanguage: "ko",
orientation: "portrait",
updates: {
url: "https://u.expo.dev/[PROJECT_ID]",
},
runtimeVersion: {
policy: "appVersion",
},
ios: {
bundleIdentifier: "com.{company}.{appname}",
buildNumber: "1",
supportsTablet: false,
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
NSUserTrackingUsageDescription:
"맞춤형 광고를 제공하기 위해 활동 추적 권한이 필요합니다.",
CFBundleDisplayName: "$APP_DISPLAY_NAME",
CFBundleSpokenName: "$APP_SPOKEN_NAME",
},
config: {
usesNonExemptEncryption: false,
},
},
android: {
package: "com.{company}.{appname}",
versionCode: 1,
adaptiveIcon: {
foregroundImage: "./assets/images/adaptive-icon.png",
backgroundColor: "#ffffff",
},
softwareKeyboardLayoutMode: "pan",
},
plugins: [
"expo-router",
"expo-updates",
"expo-tracking-transparency",
[
"react-native-google-mobile-ads",
{
androidAppId: "ca-app-pub-XXXX~ZZZZ",
iosAppId: "ca-app-pub-XXXX~YYYY",
skAdNetworkItems: [
"cstr6suwn9.skadnetwork",
],
},
],
],
};
Step 1.6: ATT (App Tracking Transparency) 구현
iOS에서 AdMob 광고 최적화를 위해 광고 추적 권한 요청이 필요.
앱 마운트 직후 바로 요청하면 얼럿이 안 나오므로 반드시 딜레이를 준다.
src/core/providers/TrackingProvider.tsx:
import { useEffect } from 'react';
import { Platform } from 'react-native';
import { requestTrackingPermissionsAsync } from 'expo-tracking-transparency';
export function useTrackingPermission() {
useEffect(() => {
if (Platform.OS !== 'ios') return;
const timer = setTimeout(async () => {
await requestTrackingPermissionsAsync();
}, 2000);
return () => clearTimeout(timer);
}, []);
}
Root _layout.tsx에서 호출:
import '../global.css';
import { useTrackingPermission } from '@core/providers/TrackingProvider';
export default function RootLayout() {
useTrackingPermission();
}
HARD GATE: ATT 없이 AdMob 사용 시 Apple 심사 리젝 사유.
Step 2: NativeWind 설정 (CRITICAL GATE)
반드시 6가지 설정 완료:
babel.config.js — ['babel-preset-expo', { jsxImportSource: 'nativewind' }] + 'nativewind/babel'
metro.config.js — withNativeWind(config, { input: './global.css' })
tailwind.config.js — presets: [require('nativewind/preset')] + content paths
global.css — @tailwind base; @tailwind components; @tailwind utilities;
- Root
_layout.tsx — import '../global.css'
nativewind-env.d.ts — /// <reference types="nativewind/types" />
하나라도 누락 시 className이 동작하지 않아 전체 UI가 깨짐.
Step 3: FSD 구조 생성
PRD의 FSD 모듈 맵에 따라:
src/
├── core/providers/
├── features/{name}/
│ ├── api/{name}.api.ts
│ ├── hooks/use-{name}.ts
│ ├── types/{name}.types.ts
│ ├── ui/ (필요시)
│ ├── store/{name}.store.ts (필요시)
│ └── index.ts
├── entities/{name}/
│ ├── api/{name}.api.ts
│ ├── store/{name}.store.ts
│ ├── types/{name}.types.ts
│ └── index.ts
├── widgets/
└── shared/
├── api/client.ts
├── config/{env,theme}.ts
├── lib/
├── types/common.ts
└── ui/{Button,Card,Input,Typography}.tsx
Step 4: Shared 레이어 구현
- Axios 클라이언트 (인터셉터, 토큰 리프레시)
- 테마 토큰 (디자인 시스템 반영)
- 공통 컴포넌트 (Button, Card, Input, Typography)
- 공통 타입 (IApiError, IApiResponse, TPagination)
Subdivided Implementation (3-Phase Pattern)
Step 5~6은 한 번에 구현하지 않고 3개 서브 페이즈로 나누어 순차 진행한다.
각 서브 페이즈 완료 후 반드시 Quick QA 게이트를 통과해야 다음으로 진행한다.
Step 5a: Feature/Entity Scaffolding (Sub-phase 4a)
PRD의 모든 feature와 entity에 대해 디렉토리 구조와 타입을 먼저 생성:
- FSD 모듈 디렉토리 생성 (features/{name}/, entities/{name}/)
- TypeScript 타입/인터페이스 정의 (Interface
I prefix, Type T prefix, Enum E prefix)
- barrel export 생성 (index.ts)
- Zustand store 설정 (필요시)
Quick QA Gate:
npm run typecheck && npm run lint
Spec Checkbox Update: docs/specs/ 디렉토리가 존재하면 완료된 항목의 체크박스를 업데이트한다 (- [ ] → - [x]).
Step 5b: API Integration (Sub-phase 4b)
Shared 레이어의 API 클라이언트를 기반으로 각 feature/entity의 API를 구현:
- Axios 클라이언트 설정 (인터셉터, 토큰 리프레시) — shared/api/client.ts
- API 함수 구현 — features/{name}/api/{name}.api.ts
- TanStack Query hooks 작성 — features/{name}/hooks/use-{name}.ts
Quick QA Gate:
npm run typecheck && npm run lint
Spec Checkbox Update: docs/specs/ 디렉토리가 존재하면 완료된 항목의 체크박스를 업데이트한다 (- [ ] → - [x]).
Step 5c: Screen/UI Development (Sub-phase 4c)
Feature hooks를 화면에 연결하고 전체 UI를 구현:
- Expo Router 파일 기반 라우팅으로 화면 구현
- 모든 화면
SafeAreaView 필수
- NativeWind
className만 사용 (inline style 금지)
- Feature hooks를 화면에 연결
- 로딩/에러/빈 상태 처리
- 리스트는 FlashList 사용 (ScrollView로 리스트 렌더링 금지)
- 폼은 React Hook Form + Zod 사용
Quick QA Gate:
npm run typecheck && npm run lint
Spec Checkbox Update: docs/specs/ 디렉토리가 존재하면 완료된 항목의 체크박스를 업데이트한다 (- [ ] → - [x]).
HARD GATE: 각 서브 페이즈의 typecheck + lint 게이트를 통과하지 못하면 다음 서브 페이즈로 진행 금지. 에러를 먼저 수정한 후 진행한다.
Step 6: (Reserved — covered by Step 5a/5b/5c above)
기존 Step 5 (Feature/Entity 구현)와 Step 6 (화면 구현)은 위의 서브 페이즈 패턴으로 대체되었다.
이 패턴은 react-native-fsd-agent-template에서 검증된 방식으로, 각 단계에서 타입 안전성을 확보한 후 다음 단계로 진행하여 에러 전파를 최소화한다.
Step 7: Unit Tests
- Test runner setup (vitest)
- Core business logic unit tests
- Utility function tests
Step 8: Environment Detection & Run Test (MANDATORY)
Before self-evaluation, detect the user's environment and attempt to run the app.
Step 8a: Environment Detection
uname -s
xcode-select -p 2>/dev/null && echo "XCODE=yes" || echo "XCODE=no"
[ -d "$ANDROID_HOME" ] || [ -d "$ANDROID_SDK_ROOT" ] && echo "ANDROID_SDK=yes" || echo "ANDROID_SDK=no"
adb devices 2>/dev/null | grep -q "emulator\|device" && echo "ANDROID_EMU=yes" || echo "ANDROID_EMU=no"
xcrun simctl list devices 2>/dev/null | grep -q "Booted" && echo "IOS_SIM=yes" || echo "IOS_SIM=no"
grep -q "react-native-google-mobile-ads" package.json && echo "NATIVE_MODULES=yes" || echo "NATIVE_MODULES=no"
Record results in handoff:
## Environment
- OS: [macOS/Linux/Windows]
- Xcode: [yes/no]
- Android SDK: [yes/no]
- Native modules: [yes/no]
- Expo Go compatible: [yes/no]
Step 8b: Choose Run Strategy
| Condition | Strategy |
|---|
| No native modules (no AdMob) | npx expo start (Expo Go OK) |
| Native modules + macOS + Xcode | npx expo run:ios |
| Native modules + Android SDK | npx expo run:android |
| Native modules + no local SDK | eas build --profile development (cloud) |
| Windows | Android only (no iOS) |
| Linux | Android only (no iOS) |
Step 8c: Run App
Based on detected environment:
npx expo start
npx expo run:ios
npx expo run:android
eas build --profile development --platform android
npx expo start --dev-client
IMPORTANT: AdMob + Expo Go = CRASH. If react-native-google-mobile-ads is installed, Expo Go cannot be used. The app MUST be built as a development client or via expo run:*.
Step 8d: Verify App Runs
After starting:
- Check terminal for crash/error output
- If crash → read error, fix, retry
- If runs → proceed to self-evaluation
If NO local build environment is available at all:
- Skip runtime test
- Note in handoff: "Runtime test skipped — no local build environment"
- Rely on typecheck + lint + unit tests as evidence
Step 9: Self-Evaluation (MANDATORY)
npm run typecheck
npm run lint
npm test
Contract criteria self-verification + handoff document.
Round 2+: 피드백 기반 수정
docs/harness/feedback/round-N-*.md 읽기
- 모든 FAIL 기준 수정
- 버그 수정 (critical > major > minor)
- 스텁 → 실제 구현
- 자체 평가 재실행
- 핸드오프 작성
Output
docs/harness/handoff/round-N-gen.md:
# Generator Handoff — Round N
## What Was Built/Fixed
[요약]
## Contract Self-Assessment
- [DONE] 기준 1: [증거]
- [DONE] 기준 2: [증거]
- [PARTIAL] 기준 3: [미비 사항]
## Test Results
- npm run typecheck: PASS (에러 0)
- npm run lint: PASS (에러 0)
- npm test: 24 tests, 24 passed
## FSD Compliance
- 레이어 규칙: PASS
- barrel export: PASS
- any 타입: 0개
## Known Issues
- [미해결 사항]
Git commit:
git add .
git commit -m "feat: Round N — [요약]"
State Update
next_role: rn-harness-evaluator
HARD GATES
- NativeWind 6가지 설정 누락 시 즉시 수정 (CRITICAL)
- FSD 레이어 규칙 위반 금지
any 타입 사용 금지
- 모든 화면 SafeAreaView 필수
- inline style 금지 (className만)
- 자체 평가 건너뛰기 금지
- 핸드오프 파일 없이 QA 진행 금지
- Import는 반드시
@/ alias 사용