@su-record/vibe 0.4.5 โ 0.4.6
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/agents/simplifier.md +1 -1
- package/.claude/commands/vibe.analyze.md +1 -1
- package/.claude/commands/vibe.run.md +1 -1
- package/.claude/commands/vibe.spec.md +2 -2
- package/.claude/commands/vibe.verify.md +1 -1
- package/.claude/settings.local.json +3 -1
- package/README.md +4 -4
- package/bin/vibe +41 -13
- package/package.json +1 -1
- package/templates/hooks-template.json +1 -1
- package/.agent/rules/core/communication-guide.md +0 -104
- package/.agent/rules/core/development-philosophy.md +0 -53
- package/.agent/rules/core/quick-start.md +0 -121
- package/.agent/rules/languages/dart-flutter.md +0 -509
- package/.agent/rules/languages/go.md +0 -396
- package/.agent/rules/languages/java-spring.md +0 -586
- package/.agent/rules/languages/kotlin-android.md +0 -491
- package/.agent/rules/languages/python-django.md +0 -371
- package/.agent/rules/languages/python-fastapi.md +0 -386
- package/.agent/rules/languages/rust.md +0 -425
- package/.agent/rules/languages/swift-ios.md +0 -516
- package/.agent/rules/languages/typescript-nextjs.md +0 -441
- package/.agent/rules/languages/typescript-node.md +0 -375
- package/.agent/rules/languages/typescript-react-native.md +0 -446
- package/.agent/rules/languages/typescript-react.md +0 -525
- package/.agent/rules/languages/typescript-vue.md +0 -353
- package/.agent/rules/quality/bdd-contract-testing.md +0 -388
- package/.agent/rules/quality/checklist.md +0 -276
- package/.agent/rules/quality/testing-strategy.md +0 -437
- package/.agent/rules/standards/anti-patterns.md +0 -369
- package/.agent/rules/standards/code-structure.md +0 -291
- package/.agent/rules/standards/complexity-metrics.md +0 -312
- package/.agent/rules/standards/naming-conventions.md +0 -198
- package/.agent/rules/tools/mcp-hi-ai-guide.md +0 -665
- package/.agent/rules/tools/mcp-workflow.md +0 -51
|
@@ -1,446 +0,0 @@
|
|
|
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
|