@su-record/vibe 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +35 -35
- package/.claude/settings.local.json +24 -25
- package/.claude/vibe/constitution.md +184 -184
- package/.claude/vibe/rules/core/communication-guide.md +104 -104
- package/.claude/vibe/rules/core/development-philosophy.md +52 -52
- package/.claude/vibe/rules/core/quick-start.md +120 -120
- package/.claude/vibe/rules/languages/dart-flutter.md +509 -509
- package/.claude/vibe/rules/languages/go.md +396 -396
- package/.claude/vibe/rules/languages/java-spring.md +586 -586
- package/.claude/vibe/rules/languages/kotlin-android.md +491 -491
- package/.claude/vibe/rules/languages/python-django.md +371 -371
- package/.claude/vibe/rules/languages/python-fastapi.md +386 -386
- package/.claude/vibe/rules/languages/rust.md +425 -425
- package/.claude/vibe/rules/languages/swift-ios.md +516 -516
- package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -441
- package/.claude/vibe/rules/languages/typescript-node.md +375 -375
- package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -521
- package/.claude/vibe/rules/languages/typescript-react-native.md +446 -446
- package/.claude/vibe/rules/languages/typescript-react.md +525 -525
- package/.claude/vibe/rules/languages/typescript-vue.md +353 -353
- package/.claude/vibe/rules/quality/bdd-contract-testing.md +388 -388
- package/.claude/vibe/rules/quality/checklist.md +276 -276
- package/.claude/vibe/rules/quality/testing-strategy.md +437 -437
- package/.claude/vibe/rules/standards/anti-patterns.md +369 -369
- package/.claude/vibe/rules/standards/code-structure.md +291 -291
- package/.claude/vibe/rules/standards/complexity-metrics.md +312 -312
- package/.claude/vibe/rules/standards/naming-conventions.md +198 -198
- package/.claude/vibe/setup.sh +31 -31
- package/.claude/vibe/templates/constitution-template.md +184 -184
- package/.claude/vibe/templates/contract-backend-template.md +517 -517
- package/.claude/vibe/templates/contract-frontend-template.md +594 -594
- package/.claude/vibe/templates/feature-template.md +96 -96
- package/.claude/vibe/templates/spec-template.md +199 -199
- package/CLAUDE.md +345 -323
- package/LICENSE +21 -21
- package/README.md +744 -724
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/e2e-tester.md +266 -266
- package/agents/explorer.md +48 -48
- package/agents/implementer.md +53 -53
- package/agents/research/best-practices-agent.md +139 -139
- package/agents/research/codebase-patterns-agent.md +147 -147
- package/agents/research/framework-docs-agent.md +181 -181
- package/agents/research/security-advisory-agent.md +167 -167
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +152 -152
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +119 -119
- package/agents/tester.md +49 -49
- package/agents/ui-previewer.md +137 -137
- package/commands/vibe.analyze.md +245 -180
- package/commands/vibe.reason.md +223 -183
- package/commands/vibe.review.md +200 -136
- package/commands/vibe.run.md +838 -836
- package/commands/vibe.spec.md +419 -383
- package/commands/vibe.utils.md +101 -101
- package/commands/vibe.verify.md +282 -241
- package/dist/cli/index.js +385 -385
- package/dist/lib/MemoryManager.d.ts.map +1 -1
- package/dist/lib/MemoryManager.js +119 -114
- package/dist/lib/MemoryManager.js.map +1 -1
- package/dist/lib/PythonParser.js +108 -108
- package/dist/lib/gemini-mcp.js +15 -15
- package/dist/lib/gemini-oauth.js +35 -35
- package/dist/lib/gpt-mcp.js +17 -17
- package/dist/lib/gpt-oauth.js +44 -44
- package/dist/tools/analytics/getUsageAnalytics.js +12 -12
- package/dist/tools/index.d.ts +50 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +61 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
- package/dist/tools/planning/generatePrd.js +46 -46
- package/dist/tools/prompt/enhancePromptGemini.js +160 -160
- package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/hooks/hooks.json +121 -103
- package/package.json +73 -69
- package/skills/git-worktree.md +178 -178
- package/skills/priority-todos.md +236 -236
|
@@ -1,446 +1,446 @@
|
|
|
1
|
-
# 📱 TypeScript + React Native 품질 규칙
|
|
2
|
-
|
|
3
|
-
## 핵심 원칙 (core + React에서 상속)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ 단일 책임 (SRP)
|
|
7
|
-
✅ 중복 제거 (DRY)
|
|
8
|
-
✅ 재사용성
|
|
9
|
-
✅ 낮은 복잡도
|
|
10
|
-
✅ 함수 ≤ 30줄, JSX ≤ 50줄
|
|
11
|
-
✅ React 규칙 모두 적용
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## React Native 특화 규칙
|
|
15
|
-
|
|
16
|
-
### 1. 플랫폼별 코드 분리
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
// ✅ 파일 확장자로 분리
|
|
20
|
-
Button.ios.tsx // iOS 전용
|
|
21
|
-
Button.android.tsx // Android 전용
|
|
22
|
-
Button.tsx // 공통
|
|
23
|
-
|
|
24
|
-
// ✅ Platform API 사용
|
|
25
|
-
import { Platform, StyleSheet } from 'react-native';
|
|
26
|
-
|
|
27
|
-
const styles = StyleSheet.create({
|
|
28
|
-
container: {
|
|
29
|
-
...Platform.select({
|
|
30
|
-
ios: {
|
|
31
|
-
shadowColor: '#000',
|
|
32
|
-
shadowOffset: { width: 0, height: 2 },
|
|
33
|
-
shadowOpacity: 0.25,
|
|
34
|
-
},
|
|
35
|
-
android: {
|
|
36
|
-
elevation: 4,
|
|
37
|
-
},
|
|
38
|
-
}),
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// ✅ Platform.OS 체크
|
|
43
|
-
if (Platform.OS === 'ios') {
|
|
44
|
-
// iOS 전용 로직
|
|
45
|
-
} else if (Platform.OS === 'android') {
|
|
46
|
-
// Android 전용 로직
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### 2. StyleSheet 사용 (인라인 스타일 지양)
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
// ❌ 인라인 스타일 (성능 저하)
|
|
54
|
-
<View style={{ flex: 1, padding: 16, backgroundColor: '#fff' }} />
|
|
55
|
-
|
|
56
|
-
// ✅ StyleSheet (최적화됨)
|
|
57
|
-
import { StyleSheet } from 'react-native';
|
|
58
|
-
|
|
59
|
-
const styles = StyleSheet.create({
|
|
60
|
-
container: {
|
|
61
|
-
flex: 1,
|
|
62
|
-
padding: 16,
|
|
63
|
-
backgroundColor: '#fff',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
<View style={styles.container} />
|
|
68
|
-
|
|
69
|
-
// ✅ 조건부 스타일
|
|
70
|
-
<View style={[
|
|
71
|
-
styles.container,
|
|
72
|
-
isActive && styles.active,
|
|
73
|
-
{ marginTop: offset }, // 동적 값만 인라인
|
|
74
|
-
]} />
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### 3. FlatList 최적화
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
// ✅ FlatList 성능 최적화
|
|
81
|
-
interface User {
|
|
82
|
-
id: string;
|
|
83
|
-
name: string;
|
|
84
|
-
avatar: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const UserList = ({ users }: { users: User[] }) => {
|
|
88
|
-
const renderItem = useCallback(({ item }: { item: User }) => {
|
|
89
|
-
return <UserCard user={item} />;
|
|
90
|
-
}, []);
|
|
91
|
-
|
|
92
|
-
const keyExtractor = useCallback((item: User) => item.id, []);
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<FlatList
|
|
96
|
-
data={users}
|
|
97
|
-
renderItem={renderItem}
|
|
98
|
-
keyExtractor={keyExtractor}
|
|
99
|
-
// 성능 최적화 옵션
|
|
100
|
-
removeClippedSubviews={true}
|
|
101
|
-
maxToRenderPerBatch={10}
|
|
102
|
-
updateCellsBatchingPeriod={50}
|
|
103
|
-
initialNumToRender={10}
|
|
104
|
-
windowSize={5}
|
|
105
|
-
// 헤더 고정
|
|
106
|
-
stickyHeaderIndices={[0]}
|
|
107
|
-
// 리스트 분리
|
|
108
|
-
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
|
109
|
-
// 빈 상태
|
|
110
|
-
ListEmptyComponent={<EmptyState />}
|
|
111
|
-
/>
|
|
112
|
-
);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// ✅ UserCard 메모이제이션
|
|
116
|
-
const UserCard = React.memo<{ user: User }>(({ user }) => {
|
|
117
|
-
return (
|
|
118
|
-
<View style={styles.card}>
|
|
119
|
-
<Image source={{ uri: user.avatar }} style={styles.avatar} />
|
|
120
|
-
<Text>{user.name}</Text>
|
|
121
|
-
</View>
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### 4. Navigation (React Navigation)
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
// ✅ 타입 안전한 네비게이션
|
|
130
|
-
import { NavigationContainer } from '@react-navigation/native';
|
|
131
|
-
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
132
|
-
|
|
133
|
-
// 네비게이션 타입 정의
|
|
134
|
-
type RootStackParamList = {
|
|
135
|
-
Home: undefined;
|
|
136
|
-
UserProfile: { userId: string };
|
|
137
|
-
Settings: { section?: string };
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const Stack = createNativeStackNavigator<RootStackParamList>();
|
|
141
|
-
|
|
142
|
-
function App() {
|
|
143
|
-
return (
|
|
144
|
-
<NavigationContainer>
|
|
145
|
-
<Stack.Navigator>
|
|
146
|
-
<Stack.Screen name="Home" component={HomeScreen} />
|
|
147
|
-
<Stack.Screen name="UserProfile" component={UserProfileScreen} />
|
|
148
|
-
<Stack.Screen name="Settings" component={SettingsScreen} />
|
|
149
|
-
</Stack.Navigator>
|
|
150
|
-
</NavigationContainer>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ✅ 타입 안전한 네비게이션 훅
|
|
155
|
-
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
156
|
-
import { useNavigation } from '@react-navigation/native';
|
|
157
|
-
|
|
158
|
-
type HomeScreenNavigationProp = NativeStackNavigationProp<
|
|
159
|
-
RootStackParamList,
|
|
160
|
-
'Home'
|
|
161
|
-
>;
|
|
162
|
-
|
|
163
|
-
function HomeScreen() {
|
|
164
|
-
const navigation = useNavigation<HomeScreenNavigationProp>();
|
|
165
|
-
|
|
166
|
-
const navigateToProfile = (userId: string) => {
|
|
167
|
-
navigation.navigate('UserProfile', { userId }); // 타입 안전
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
return <Button onPress={() => navigateToProfile('123')} title="Profile" />;
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### 5. AsyncStorage (데이터 저장)
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
178
|
-
|
|
179
|
-
// ✅ 타입 안전한 Storage 래퍼
|
|
180
|
-
class Storage {
|
|
181
|
-
static async set<T>(key: string, value: T): Promise<void> {
|
|
182
|
-
await AsyncStorage.setItem(key, JSON.stringify(value));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
static async get<T>(key: string): Promise<T | null> {
|
|
186
|
-
const value = await AsyncStorage.getItem(key);
|
|
187
|
-
return value ? JSON.parse(value) : null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
static async remove(key: string): Promise<void> {
|
|
191
|
-
await AsyncStorage.removeItem(key);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 사용
|
|
196
|
-
interface User {
|
|
197
|
-
id: string;
|
|
198
|
-
name: string;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
await Storage.set<User>('user', { id: '123', name: 'John' });
|
|
202
|
-
const user = await Storage.get<User>('user');
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### 6. 이미지 최적화
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
import { Image } from 'react-native';
|
|
209
|
-
import FastImage from 'react-native-fast-image';
|
|
210
|
-
|
|
211
|
-
// ✅ FastImage 사용 (캐싱, 성능)
|
|
212
|
-
<FastImage
|
|
213
|
-
source={{
|
|
214
|
-
uri: user.avatar,
|
|
215
|
-
priority: FastImage.priority.high,
|
|
216
|
-
}}
|
|
217
|
-
style={styles.avatar}
|
|
218
|
-
resizeMode={FastImage.resizeMode.cover}
|
|
219
|
-
/>
|
|
220
|
-
|
|
221
|
-
// ✅ 로컬 이미지
|
|
222
|
-
<Image source={require('./assets/logo.png')} style={styles.logo} />
|
|
223
|
-
|
|
224
|
-
// ✅ 조건부 로딩
|
|
225
|
-
{imageUrl && (
|
|
226
|
-
<Image
|
|
227
|
-
source={{ uri: imageUrl }}
|
|
228
|
-
defaultSource={require('./assets/placeholder.png')}
|
|
229
|
-
/>
|
|
230
|
-
)}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### 7. SafeAreaView (안전 영역)
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
237
|
-
|
|
238
|
-
// ✅ SafeAreaView 사용 (노치/상태바 대응)
|
|
239
|
-
function Screen() {
|
|
240
|
-
return (
|
|
241
|
-
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
242
|
-
<Text>Content</Text>
|
|
243
|
-
</SafeAreaView>
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ✅ useSafeAreaInsets 훅
|
|
248
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
249
|
-
|
|
250
|
-
function CustomHeader() {
|
|
251
|
-
const insets = useSafeAreaInsets();
|
|
252
|
-
|
|
253
|
-
return (
|
|
254
|
-
<View style={{ paddingTop: insets.top }}>
|
|
255
|
-
<Text>Header</Text>
|
|
256
|
-
</View>
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### 8. Hooks 최적화
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
// ✅ useCallback (이벤트 핸들러)
|
|
265
|
-
const handlePress = useCallback(() => {
|
|
266
|
-
navigation.navigate('UserProfile', { userId });
|
|
267
|
-
}, [navigation, userId]);
|
|
268
|
-
|
|
269
|
-
// ✅ useMemo (무거운 계산)
|
|
270
|
-
const sortedUsers = useMemo(() => {
|
|
271
|
-
return users.sort((a, b) => a.name.localeCompare(b.name));
|
|
272
|
-
}, [users]);
|
|
273
|
-
|
|
274
|
-
// ✅ Custom Hook (로직 재사용)
|
|
275
|
-
function useKeyboard() {
|
|
276
|
-
const [isVisible, setIsVisible] = useState(false);
|
|
277
|
-
|
|
278
|
-
useEffect(() => {
|
|
279
|
-
const showSubscription = Keyboard.addListener('keyboardDidShow', () => {
|
|
280
|
-
setIsVisible(true);
|
|
281
|
-
});
|
|
282
|
-
const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
|
|
283
|
-
setIsVisible(false);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
return () => {
|
|
287
|
-
showSubscription.remove();
|
|
288
|
-
hideSubscription.remove();
|
|
289
|
-
};
|
|
290
|
-
}, []);
|
|
291
|
-
|
|
292
|
-
return isVisible;
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### 9. 권한 처리
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
|
300
|
-
import { Platform } from 'react-native';
|
|
301
|
-
|
|
302
|
-
// ✅ 권한 체크 및 요청
|
|
303
|
-
async function requestCameraPermission(): Promise<boolean> {
|
|
304
|
-
const permission =
|
|
305
|
-
Platform.OS === 'ios'
|
|
306
|
-
? PERMISSIONS.IOS.CAMERA
|
|
307
|
-
: PERMISSIONS.ANDROID.CAMERA;
|
|
308
|
-
|
|
309
|
-
const result = await check(permission);
|
|
310
|
-
|
|
311
|
-
switch (result) {
|
|
312
|
-
case RESULTS.GRANTED:
|
|
313
|
-
return true;
|
|
314
|
-
case RESULTS.DENIED:
|
|
315
|
-
const requested = await request(permission);
|
|
316
|
-
return requested === RESULTS.GRANTED;
|
|
317
|
-
case RESULTS.BLOCKED:
|
|
318
|
-
// 설정으로 이동 안내
|
|
319
|
-
return false;
|
|
320
|
-
default:
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### 10. 에러 경계 (Error Boundary)
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
// ✅ React Native용 Error Boundary
|
|
330
|
-
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
331
|
-
import { View, Text, Button } from 'react-native';
|
|
332
|
-
|
|
333
|
-
interface Props {
|
|
334
|
-
children: ReactNode;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
interface State {
|
|
338
|
-
hasError: boolean;
|
|
339
|
-
error?: Error;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
class ErrorBoundary extends Component<Props, State> {
|
|
343
|
-
constructor(props: Props) {
|
|
344
|
-
super(props);
|
|
345
|
-
this.state = { hasError: false };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
static getDerivedStateFromError(error: Error): State {
|
|
349
|
-
return { hasError: true, error };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
353
|
-
console.error('Error caught:', error, errorInfo);
|
|
354
|
-
// 에러 로깅 서비스 (Sentry 등)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
handleReset = () => {
|
|
358
|
-
this.setState({ hasError: false, error: undefined });
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
render() {
|
|
362
|
-
if (this.state.hasError) {
|
|
363
|
-
return (
|
|
364
|
-
<View style={styles.errorContainer}>
|
|
365
|
-
<Text style={styles.errorText}>Something went wrong</Text>
|
|
366
|
-
<Button title="Try Again" onPress={this.handleReset} />
|
|
367
|
-
</View>
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return this.props.children;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
## 안티패턴
|
|
377
|
-
|
|
378
|
-
```typescript
|
|
379
|
-
// ❌ ScrollView로 긴 리스트
|
|
380
|
-
<ScrollView>
|
|
381
|
-
{users.map(user => <UserCard key={user.id} user={user} />)}
|
|
382
|
-
</ScrollView>
|
|
383
|
-
|
|
384
|
-
// ✅ FlatList 사용
|
|
385
|
-
<FlatList data={users} renderItem={renderItem} />
|
|
386
|
-
|
|
387
|
-
// ❌ 중첩된 FlatList (성능 저하)
|
|
388
|
-
<FlatList
|
|
389
|
-
data={categories}
|
|
390
|
-
renderItem={({ item }) => (
|
|
391
|
-
<FlatList data={item.items} renderItem={renderItem} />
|
|
392
|
-
)}
|
|
393
|
-
/>
|
|
394
|
-
|
|
395
|
-
// ✅ 단일 FlatList + 섹션
|
|
396
|
-
<SectionList sections={sections} renderItem={renderItem} />
|
|
397
|
-
|
|
398
|
-
// ❌ 비동기 setState in useEffect cleanup
|
|
399
|
-
useEffect(() => {
|
|
400
|
-
return () => {
|
|
401
|
-
setData(null); // ❌ 언마운트 후 setState
|
|
402
|
-
};
|
|
403
|
-
}, []);
|
|
404
|
-
|
|
405
|
-
// ✅ isMounted 체크
|
|
406
|
-
useEffect(() => {
|
|
407
|
-
let isMounted = true;
|
|
408
|
-
|
|
409
|
-
fetchData().then(data => {
|
|
410
|
-
if (isMounted) setData(data);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
return () => {
|
|
414
|
-
isMounted = false;
|
|
415
|
-
};
|
|
416
|
-
}, []);
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
## 성능 최적화 도구
|
|
420
|
-
|
|
421
|
-
```bash
|
|
422
|
-
# Flipper (디버깅)
|
|
423
|
-
npx react-native-flipper
|
|
424
|
-
|
|
425
|
-
# Bundle 분석
|
|
426
|
-
npx react-native bundle --platform android --dev false \
|
|
427
|
-
--entry-file index.js --bundle-output android.bundle
|
|
428
|
-
|
|
429
|
-
# 메모리 프로파일링 (Flipper 사용)
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
## 체크리스트
|
|
433
|
-
|
|
434
|
-
React Native 코드 작성 시:
|
|
435
|
-
|
|
436
|
-
- [ ] StyleSheet 사용 (인라인 지양)
|
|
437
|
-
- [ ] FlatList 최적화 (긴 리스트)
|
|
438
|
-
- [ ] Platform 분기 처리
|
|
439
|
-
- [ ] 타입 안전한 Navigation
|
|
440
|
-
- [ ] SafeAreaView 사용
|
|
441
|
-
- [ ] FastImage 사용 (이미지)
|
|
442
|
-
- [ ] useCallback/useMemo 최적화
|
|
443
|
-
- [ ] 권한 처리 (카메라, 위치 등)
|
|
444
|
-
- [ ] Error Boundary 적용
|
|
445
|
-
- [ ] AsyncStorage 타입 래퍼
|
|
446
|
-
- [ ] 복잡도 ≤ 10
|
|
1
|
+
# 📱 TypeScript + React Native 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (core + React에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄, JSX ≤ 50줄
|
|
11
|
+
✅ React 규칙 모두 적용
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## React Native 특화 규칙
|
|
15
|
+
|
|
16
|
+
### 1. 플랫폼별 코드 분리
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// ✅ 파일 확장자로 분리
|
|
20
|
+
Button.ios.tsx // iOS 전용
|
|
21
|
+
Button.android.tsx // Android 전용
|
|
22
|
+
Button.tsx // 공통
|
|
23
|
+
|
|
24
|
+
// ✅ Platform API 사용
|
|
25
|
+
import { Platform, StyleSheet } from 'react-native';
|
|
26
|
+
|
|
27
|
+
const styles = StyleSheet.create({
|
|
28
|
+
container: {
|
|
29
|
+
...Platform.select({
|
|
30
|
+
ios: {
|
|
31
|
+
shadowColor: '#000',
|
|
32
|
+
shadowOffset: { width: 0, height: 2 },
|
|
33
|
+
shadowOpacity: 0.25,
|
|
34
|
+
},
|
|
35
|
+
android: {
|
|
36
|
+
elevation: 4,
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ✅ Platform.OS 체크
|
|
43
|
+
if (Platform.OS === 'ios') {
|
|
44
|
+
// iOS 전용 로직
|
|
45
|
+
} else if (Platform.OS === 'android') {
|
|
46
|
+
// Android 전용 로직
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. StyleSheet 사용 (인라인 스타일 지양)
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// ❌ 인라인 스타일 (성능 저하)
|
|
54
|
+
<View style={{ flex: 1, padding: 16, backgroundColor: '#fff' }} />
|
|
55
|
+
|
|
56
|
+
// ✅ StyleSheet (최적화됨)
|
|
57
|
+
import { StyleSheet } from 'react-native';
|
|
58
|
+
|
|
59
|
+
const styles = StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
padding: 16,
|
|
63
|
+
backgroundColor: '#fff',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
<View style={styles.container} />
|
|
68
|
+
|
|
69
|
+
// ✅ 조건부 스타일
|
|
70
|
+
<View style={[
|
|
71
|
+
styles.container,
|
|
72
|
+
isActive && styles.active,
|
|
73
|
+
{ marginTop: offset }, // 동적 값만 인라인
|
|
74
|
+
]} />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. FlatList 최적화
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// ✅ FlatList 성능 최적화
|
|
81
|
+
interface User {
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
avatar: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const UserList = ({ users }: { users: User[] }) => {
|
|
88
|
+
const renderItem = useCallback(({ item }: { item: User }) => {
|
|
89
|
+
return <UserCard user={item} />;
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const keyExtractor = useCallback((item: User) => item.id, []);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<FlatList
|
|
96
|
+
data={users}
|
|
97
|
+
renderItem={renderItem}
|
|
98
|
+
keyExtractor={keyExtractor}
|
|
99
|
+
// 성능 최적화 옵션
|
|
100
|
+
removeClippedSubviews={true}
|
|
101
|
+
maxToRenderPerBatch={10}
|
|
102
|
+
updateCellsBatchingPeriod={50}
|
|
103
|
+
initialNumToRender={10}
|
|
104
|
+
windowSize={5}
|
|
105
|
+
// 헤더 고정
|
|
106
|
+
stickyHeaderIndices={[0]}
|
|
107
|
+
// 리스트 분리
|
|
108
|
+
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
|
109
|
+
// 빈 상태
|
|
110
|
+
ListEmptyComponent={<EmptyState />}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ✅ UserCard 메모이제이션
|
|
116
|
+
const UserCard = React.memo<{ user: User }>(({ user }) => {
|
|
117
|
+
return (
|
|
118
|
+
<View style={styles.card}>
|
|
119
|
+
<Image source={{ uri: user.avatar }} style={styles.avatar} />
|
|
120
|
+
<Text>{user.name}</Text>
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 4. Navigation (React Navigation)
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// ✅ 타입 안전한 네비게이션
|
|
130
|
+
import { NavigationContainer } from '@react-navigation/native';
|
|
131
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
132
|
+
|
|
133
|
+
// 네비게이션 타입 정의
|
|
134
|
+
type RootStackParamList = {
|
|
135
|
+
Home: undefined;
|
|
136
|
+
UserProfile: { userId: string };
|
|
137
|
+
Settings: { section?: string };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const Stack = createNativeStackNavigator<RootStackParamList>();
|
|
141
|
+
|
|
142
|
+
function App() {
|
|
143
|
+
return (
|
|
144
|
+
<NavigationContainer>
|
|
145
|
+
<Stack.Navigator>
|
|
146
|
+
<Stack.Screen name="Home" component={HomeScreen} />
|
|
147
|
+
<Stack.Screen name="UserProfile" component={UserProfileScreen} />
|
|
148
|
+
<Stack.Screen name="Settings" component={SettingsScreen} />
|
|
149
|
+
</Stack.Navigator>
|
|
150
|
+
</NavigationContainer>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ✅ 타입 안전한 네비게이션 훅
|
|
155
|
+
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
156
|
+
import { useNavigation } from '@react-navigation/native';
|
|
157
|
+
|
|
158
|
+
type HomeScreenNavigationProp = NativeStackNavigationProp<
|
|
159
|
+
RootStackParamList,
|
|
160
|
+
'Home'
|
|
161
|
+
>;
|
|
162
|
+
|
|
163
|
+
function HomeScreen() {
|
|
164
|
+
const navigation = useNavigation<HomeScreenNavigationProp>();
|
|
165
|
+
|
|
166
|
+
const navigateToProfile = (userId: string) => {
|
|
167
|
+
navigation.navigate('UserProfile', { userId }); // 타입 안전
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return <Button onPress={() => navigateToProfile('123')} title="Profile" />;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 5. AsyncStorage (데이터 저장)
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
178
|
+
|
|
179
|
+
// ✅ 타입 안전한 Storage 래퍼
|
|
180
|
+
class Storage {
|
|
181
|
+
static async set<T>(key: string, value: T): Promise<void> {
|
|
182
|
+
await AsyncStorage.setItem(key, JSON.stringify(value));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static async get<T>(key: string): Promise<T | null> {
|
|
186
|
+
const value = await AsyncStorage.getItem(key);
|
|
187
|
+
return value ? JSON.parse(value) : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static async remove(key: string): Promise<void> {
|
|
191
|
+
await AsyncStorage.removeItem(key);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 사용
|
|
196
|
+
interface User {
|
|
197
|
+
id: string;
|
|
198
|
+
name: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await Storage.set<User>('user', { id: '123', name: 'John' });
|
|
202
|
+
const user = await Storage.get<User>('user');
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 6. 이미지 최적화
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { Image } from 'react-native';
|
|
209
|
+
import FastImage from 'react-native-fast-image';
|
|
210
|
+
|
|
211
|
+
// ✅ FastImage 사용 (캐싱, 성능)
|
|
212
|
+
<FastImage
|
|
213
|
+
source={{
|
|
214
|
+
uri: user.avatar,
|
|
215
|
+
priority: FastImage.priority.high,
|
|
216
|
+
}}
|
|
217
|
+
style={styles.avatar}
|
|
218
|
+
resizeMode={FastImage.resizeMode.cover}
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
// ✅ 로컬 이미지
|
|
222
|
+
<Image source={require('./assets/logo.png')} style={styles.logo} />
|
|
223
|
+
|
|
224
|
+
// ✅ 조건부 로딩
|
|
225
|
+
{imageUrl && (
|
|
226
|
+
<Image
|
|
227
|
+
source={{ uri: imageUrl }}
|
|
228
|
+
defaultSource={require('./assets/placeholder.png')}
|
|
229
|
+
/>
|
|
230
|
+
)}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 7. SafeAreaView (안전 영역)
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
237
|
+
|
|
238
|
+
// ✅ SafeAreaView 사용 (노치/상태바 대응)
|
|
239
|
+
function Screen() {
|
|
240
|
+
return (
|
|
241
|
+
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
242
|
+
<Text>Content</Text>
|
|
243
|
+
</SafeAreaView>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ✅ useSafeAreaInsets 훅
|
|
248
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
249
|
+
|
|
250
|
+
function CustomHeader() {
|
|
251
|
+
const insets = useSafeAreaInsets();
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<View style={{ paddingTop: insets.top }}>
|
|
255
|
+
<Text>Header</Text>
|
|
256
|
+
</View>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 8. Hooks 최적화
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// ✅ useCallback (이벤트 핸들러)
|
|
265
|
+
const handlePress = useCallback(() => {
|
|
266
|
+
navigation.navigate('UserProfile', { userId });
|
|
267
|
+
}, [navigation, userId]);
|
|
268
|
+
|
|
269
|
+
// ✅ useMemo (무거운 계산)
|
|
270
|
+
const sortedUsers = useMemo(() => {
|
|
271
|
+
return users.sort((a, b) => a.name.localeCompare(b.name));
|
|
272
|
+
}, [users]);
|
|
273
|
+
|
|
274
|
+
// ✅ Custom Hook (로직 재사용)
|
|
275
|
+
function useKeyboard() {
|
|
276
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
const showSubscription = Keyboard.addListener('keyboardDidShow', () => {
|
|
280
|
+
setIsVisible(true);
|
|
281
|
+
});
|
|
282
|
+
const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
|
|
283
|
+
setIsVisible(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return () => {
|
|
287
|
+
showSubscription.remove();
|
|
288
|
+
hideSubscription.remove();
|
|
289
|
+
};
|
|
290
|
+
}, []);
|
|
291
|
+
|
|
292
|
+
return isVisible;
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 9. 권한 처리
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
|
300
|
+
import { Platform } from 'react-native';
|
|
301
|
+
|
|
302
|
+
// ✅ 권한 체크 및 요청
|
|
303
|
+
async function requestCameraPermission(): Promise<boolean> {
|
|
304
|
+
const permission =
|
|
305
|
+
Platform.OS === 'ios'
|
|
306
|
+
? PERMISSIONS.IOS.CAMERA
|
|
307
|
+
: PERMISSIONS.ANDROID.CAMERA;
|
|
308
|
+
|
|
309
|
+
const result = await check(permission);
|
|
310
|
+
|
|
311
|
+
switch (result) {
|
|
312
|
+
case RESULTS.GRANTED:
|
|
313
|
+
return true;
|
|
314
|
+
case RESULTS.DENIED:
|
|
315
|
+
const requested = await request(permission);
|
|
316
|
+
return requested === RESULTS.GRANTED;
|
|
317
|
+
case RESULTS.BLOCKED:
|
|
318
|
+
// 설정으로 이동 안내
|
|
319
|
+
return false;
|
|
320
|
+
default:
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 10. 에러 경계 (Error Boundary)
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// ✅ React Native용 Error Boundary
|
|
330
|
+
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
331
|
+
import { View, Text, Button } from 'react-native';
|
|
332
|
+
|
|
333
|
+
interface Props {
|
|
334
|
+
children: ReactNode;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface State {
|
|
338
|
+
hasError: boolean;
|
|
339
|
+
error?: Error;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
class ErrorBoundary extends Component<Props, State> {
|
|
343
|
+
constructor(props: Props) {
|
|
344
|
+
super(props);
|
|
345
|
+
this.state = { hasError: false };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
static getDerivedStateFromError(error: Error): State {
|
|
349
|
+
return { hasError: true, error };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
353
|
+
console.error('Error caught:', error, errorInfo);
|
|
354
|
+
// 에러 로깅 서비스 (Sentry 등)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
handleReset = () => {
|
|
358
|
+
this.setState({ hasError: false, error: undefined });
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
render() {
|
|
362
|
+
if (this.state.hasError) {
|
|
363
|
+
return (
|
|
364
|
+
<View style={styles.errorContainer}>
|
|
365
|
+
<Text style={styles.errorText}>Something went wrong</Text>
|
|
366
|
+
<Button title="Try Again" onPress={this.handleReset} />
|
|
367
|
+
</View>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return this.props.children;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## 안티패턴
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// ❌ ScrollView로 긴 리스트
|
|
380
|
+
<ScrollView>
|
|
381
|
+
{users.map(user => <UserCard key={user.id} user={user} />)}
|
|
382
|
+
</ScrollView>
|
|
383
|
+
|
|
384
|
+
// ✅ FlatList 사용
|
|
385
|
+
<FlatList data={users} renderItem={renderItem} />
|
|
386
|
+
|
|
387
|
+
// ❌ 중첩된 FlatList (성능 저하)
|
|
388
|
+
<FlatList
|
|
389
|
+
data={categories}
|
|
390
|
+
renderItem={({ item }) => (
|
|
391
|
+
<FlatList data={item.items} renderItem={renderItem} />
|
|
392
|
+
)}
|
|
393
|
+
/>
|
|
394
|
+
|
|
395
|
+
// ✅ 단일 FlatList + 섹션
|
|
396
|
+
<SectionList sections={sections} renderItem={renderItem} />
|
|
397
|
+
|
|
398
|
+
// ❌ 비동기 setState in useEffect cleanup
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
return () => {
|
|
401
|
+
setData(null); // ❌ 언마운트 후 setState
|
|
402
|
+
};
|
|
403
|
+
}, []);
|
|
404
|
+
|
|
405
|
+
// ✅ isMounted 체크
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
let isMounted = true;
|
|
408
|
+
|
|
409
|
+
fetchData().then(data => {
|
|
410
|
+
if (isMounted) setData(data);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return () => {
|
|
414
|
+
isMounted = false;
|
|
415
|
+
};
|
|
416
|
+
}, []);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## 성능 최적화 도구
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Flipper (디버깅)
|
|
423
|
+
npx react-native-flipper
|
|
424
|
+
|
|
425
|
+
# Bundle 분석
|
|
426
|
+
npx react-native bundle --platform android --dev false \
|
|
427
|
+
--entry-file index.js --bundle-output android.bundle
|
|
428
|
+
|
|
429
|
+
# 메모리 프로파일링 (Flipper 사용)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## 체크리스트
|
|
433
|
+
|
|
434
|
+
React Native 코드 작성 시:
|
|
435
|
+
|
|
436
|
+
- [ ] StyleSheet 사용 (인라인 지양)
|
|
437
|
+
- [ ] FlatList 최적화 (긴 리스트)
|
|
438
|
+
- [ ] Platform 분기 처리
|
|
439
|
+
- [ ] 타입 안전한 Navigation
|
|
440
|
+
- [ ] SafeAreaView 사용
|
|
441
|
+
- [ ] FastImage 사용 (이미지)
|
|
442
|
+
- [ ] useCallback/useMemo 최적화
|
|
443
|
+
- [ ] 권한 처리 (카메라, 위치 등)
|
|
444
|
+
- [ ] Error Boundary 적용
|
|
445
|
+
- [ ] AsyncStorage 타입 래퍼
|
|
446
|
+
- [ ] 복잡도 ≤ 10
|