| 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 ์ฌ์ฉ