@su-record/vibe 0.4.6 → 0.4.7
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/.vibe/rules/core/communication-guide.md +104 -0
- package/.vibe/rules/core/development-philosophy.md +53 -0
- package/.vibe/rules/core/quick-start.md +121 -0
- package/.vibe/rules/languages/dart-flutter.md +509 -0
- package/.vibe/rules/languages/go.md +396 -0
- package/.vibe/rules/languages/java-spring.md +586 -0
- package/.vibe/rules/languages/kotlin-android.md +491 -0
- package/.vibe/rules/languages/python-django.md +371 -0
- package/.vibe/rules/languages/python-fastapi.md +386 -0
- package/.vibe/rules/languages/rust.md +425 -0
- package/.vibe/rules/languages/swift-ios.md +516 -0
- package/.vibe/rules/languages/typescript-nextjs.md +441 -0
- package/.vibe/rules/languages/typescript-node.md +375 -0
- package/.vibe/rules/languages/typescript-react-native.md +446 -0
- package/.vibe/rules/languages/typescript-react.md +525 -0
- package/.vibe/rules/languages/typescript-vue.md +353 -0
- package/.vibe/rules/quality/bdd-contract-testing.md +388 -0
- package/.vibe/rules/quality/checklist.md +276 -0
- package/.vibe/rules/quality/testing-strategy.md +437 -0
- package/.vibe/rules/standards/anti-patterns.md +369 -0
- package/.vibe/rules/standards/code-structure.md +291 -0
- package/.vibe/rules/standards/complexity-metrics.md +312 -0
- package/.vibe/rules/standards/naming-conventions.md +198 -0
- package/.vibe/rules/tools/mcp-hi-ai-guide.md +665 -0
- package/.vibe/rules/tools/mcp-workflow.md +51 -0
- package/package.json +2 -2
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# ⚛️ TypeScript + React 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (core에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄, JSX ≤ 50줄
|
|
11
|
+
✅ 중첩 ≤ 3단계
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## TypeScript/React 특화 규칙
|
|
16
|
+
|
|
17
|
+
### 1. 타입 안전성 100%
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// ❌ any 사용
|
|
21
|
+
function processData(data: any) {
|
|
22
|
+
return data.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ✅ 명확한 타입 정의
|
|
26
|
+
interface User {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
email: string;
|
|
30
|
+
age: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function processUser(user: User): string {
|
|
34
|
+
return user.name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ✅ Generic 활용
|
|
38
|
+
interface ApiResponse<T> {
|
|
39
|
+
success: boolean;
|
|
40
|
+
data: T;
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type UserResponse = ApiResponse<User>;
|
|
45
|
+
type ProductResponse = ApiResponse<Product>;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. 함수형 컴포넌트 + Hooks
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// ✅ 함수형 컴포넌트 (권장)
|
|
52
|
+
interface UserCardProps {
|
|
53
|
+
user: User;
|
|
54
|
+
onEdit?: (user: User) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function UserCard({ user, onEdit }: UserCardProps) {
|
|
58
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
59
|
+
|
|
60
|
+
const handleEdit = useCallback(() => {
|
|
61
|
+
if (onEdit) {
|
|
62
|
+
onEdit(user);
|
|
63
|
+
}
|
|
64
|
+
}, [user, onEdit]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
<h2>{user.name}</h2>
|
|
69
|
+
<button onClick={handleEdit}>Edit</button>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ❌ 클래스 컴포넌트 (레거시)
|
|
75
|
+
class UserCard extends React.Component<UserCardProps> {
|
|
76
|
+
// 복잡하고 장황함
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Custom Hook으로 로직 분리
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// ✅ Custom Hook (재사용 가능한 로직)
|
|
84
|
+
interface UseUserOptions {
|
|
85
|
+
userId: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface UseUserReturn {
|
|
89
|
+
user: User | null;
|
|
90
|
+
isLoading: boolean;
|
|
91
|
+
error: string | null;
|
|
92
|
+
refetch: () => Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function useUser({ userId }: UseUserOptions): UseUserReturn {
|
|
96
|
+
const [user, setUser] = useState<User | null>(null);
|
|
97
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
98
|
+
const [error, setError] = useState<string | null>(null);
|
|
99
|
+
|
|
100
|
+
const fetchUser = useCallback(async () => {
|
|
101
|
+
setIsLoading(true);
|
|
102
|
+
setError(null);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
setUser(data);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
110
|
+
} finally {
|
|
111
|
+
setIsLoading(false);
|
|
112
|
+
}
|
|
113
|
+
}, [userId]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
fetchUser();
|
|
117
|
+
}, [fetchUser]);
|
|
118
|
+
|
|
119
|
+
return { user, isLoading, error, refetch: fetchUser };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 사용
|
|
123
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
124
|
+
const { user, isLoading, error } = useUser({ userId });
|
|
125
|
+
|
|
126
|
+
if (isLoading) return <Spinner />;
|
|
127
|
+
if (error) return <Error message={error} />;
|
|
128
|
+
if (!user) return <NotFound />;
|
|
129
|
+
|
|
130
|
+
return <UserCard user={user} />;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 4. Props 타입 정의
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// ✅ Props 타입 명확히
|
|
138
|
+
interface ButtonProps {
|
|
139
|
+
variant?: 'primary' | 'secondary' | 'danger';
|
|
140
|
+
size?: 'sm' | 'md' | 'lg';
|
|
141
|
+
disabled?: boolean;
|
|
142
|
+
loading?: boolean;
|
|
143
|
+
onClick?: () => void;
|
|
144
|
+
children: React.ReactNode;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function Button({
|
|
148
|
+
variant = 'primary',
|
|
149
|
+
size = 'md',
|
|
150
|
+
disabled = false,
|
|
151
|
+
loading = false,
|
|
152
|
+
onClick,
|
|
153
|
+
children,
|
|
154
|
+
}: ButtonProps) {
|
|
155
|
+
return (
|
|
156
|
+
<button
|
|
157
|
+
className={`btn btn-${variant} btn-${size}`}
|
|
158
|
+
disabled={disabled || loading}
|
|
159
|
+
onClick={onClick}
|
|
160
|
+
>
|
|
161
|
+
{loading ? <Spinner /> : children}
|
|
162
|
+
</button>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ✅ PropsWithChildren 활용
|
|
167
|
+
import { PropsWithChildren } from 'react';
|
|
168
|
+
|
|
169
|
+
interface CardProps {
|
|
170
|
+
title: string;
|
|
171
|
+
subtitle?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function Card({
|
|
175
|
+
title,
|
|
176
|
+
subtitle,
|
|
177
|
+
children,
|
|
178
|
+
}: PropsWithChildren<CardProps>) {
|
|
179
|
+
return (
|
|
180
|
+
<div>
|
|
181
|
+
<h2>{title}</h2>
|
|
182
|
+
{subtitle && <p>{subtitle}</p>}
|
|
183
|
+
{children}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 5. React Query (서버 상태 관리)
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// ✅ React Query로 서버 상태 관리
|
|
193
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
194
|
+
|
|
195
|
+
function useUser(userId: string) {
|
|
196
|
+
return useQuery({
|
|
197
|
+
queryKey: ['user', userId],
|
|
198
|
+
queryFn: () => fetchUser(userId),
|
|
199
|
+
staleTime: 5 * 60 * 1000, // 5분
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function useUpdateUser() {
|
|
204
|
+
const queryClient = useQueryClient();
|
|
205
|
+
|
|
206
|
+
return useMutation({
|
|
207
|
+
mutationFn: (data: UpdateUserData) => updateUser(data),
|
|
208
|
+
onSuccess: (updatedUser) => {
|
|
209
|
+
// 캐시 업데이트
|
|
210
|
+
queryClient.setQueryData(['user', updatedUser.id], updatedUser);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 사용
|
|
216
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
217
|
+
const { data: user, isLoading, error } = useUser(userId);
|
|
218
|
+
const updateMutation = useUpdateUser();
|
|
219
|
+
|
|
220
|
+
if (isLoading) return <Spinner />;
|
|
221
|
+
if (error) return <Error />;
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div>
|
|
225
|
+
<h1>{user.name}</h1>
|
|
226
|
+
<button
|
|
227
|
+
onClick={() => updateMutation.mutate({ id: userId, name: 'New Name' })}
|
|
228
|
+
disabled={updateMutation.isPending}
|
|
229
|
+
>
|
|
230
|
+
Update
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 6. Zod로 Contract 정의
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// ✅ Zod 스키마 (런타임 + 타입 검증)
|
|
241
|
+
import { z } from 'zod';
|
|
242
|
+
|
|
243
|
+
const createUserSchema = z.object({
|
|
244
|
+
email: z.string().email(),
|
|
245
|
+
username: z.string().min(3).max(50),
|
|
246
|
+
password: z.string().min(8),
|
|
247
|
+
age: z.number().min(0).max(150),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
type CreateUserRequest = z.infer<typeof createUserSchema>;
|
|
251
|
+
|
|
252
|
+
// 사용 (React Hook Form)
|
|
253
|
+
import { useForm } from 'react-hook-form';
|
|
254
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
255
|
+
|
|
256
|
+
function SignUpForm() {
|
|
257
|
+
const {
|
|
258
|
+
register,
|
|
259
|
+
handleSubmit,
|
|
260
|
+
formState: { errors, isSubmitting },
|
|
261
|
+
} = useForm<CreateUserRequest>({
|
|
262
|
+
resolver: zodResolver(createUserSchema),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const onSubmit = async (data: CreateUserRequest) => {
|
|
266
|
+
await registerUser(data);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
271
|
+
<input {...register('email')} />
|
|
272
|
+
{errors.email && <span>{errors.email.message}</span>}
|
|
273
|
+
|
|
274
|
+
<input {...register('username')} />
|
|
275
|
+
{errors.username && <span>{errors.username.message}</span>}
|
|
276
|
+
|
|
277
|
+
<input type="password" {...register('password')} />
|
|
278
|
+
{errors.password && <span>{errors.password.message}</span>}
|
|
279
|
+
|
|
280
|
+
<input type="number" {...register('age', { valueAsNumber: true })} />
|
|
281
|
+
{errors.age && <span>{errors.age.message}</span>}
|
|
282
|
+
|
|
283
|
+
<button type="submit" disabled={isSubmitting}>
|
|
284
|
+
Sign Up
|
|
285
|
+
</button>
|
|
286
|
+
</form>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 7. 컴포넌트 분리 (Extract Component)
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// ❌ 긴 JSX (80줄)
|
|
295
|
+
function UserDashboard() {
|
|
296
|
+
return (
|
|
297
|
+
<div>
|
|
298
|
+
<header>
|
|
299
|
+
<h1>Dashboard</h1>
|
|
300
|
+
{/* 20줄 */}
|
|
301
|
+
</header>
|
|
302
|
+
<main>
|
|
303
|
+
{/* 40줄 */}
|
|
304
|
+
</main>
|
|
305
|
+
<footer>
|
|
306
|
+
{/* 20줄 */}
|
|
307
|
+
</footer>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ✅ 서브 컴포넌트 분리
|
|
313
|
+
function UserDashboard() {
|
|
314
|
+
return (
|
|
315
|
+
<div>
|
|
316
|
+
<DashboardHeader />
|
|
317
|
+
<DashboardMain />
|
|
318
|
+
<DashboardFooter />
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function DashboardHeader() {
|
|
324
|
+
return <header>{/* ... */}</header>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function DashboardMain() {
|
|
328
|
+
return <main>{/* ... */}</main>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function DashboardFooter() {
|
|
332
|
+
return <footer>{/* ... */}</footer>;
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 8. useCallback + useMemo 최적화
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// ✅ useCallback (함수 메모이제이션)
|
|
340
|
+
function Parent() {
|
|
341
|
+
const [count, setCount] = useState(0);
|
|
342
|
+
|
|
343
|
+
// 매번 새 함수 생성 방지
|
|
344
|
+
const handleClick = useCallback(() => {
|
|
345
|
+
setCount(prev => prev + 1);
|
|
346
|
+
}, []);
|
|
347
|
+
|
|
348
|
+
return <Child onClick={handleClick} />;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const Child = React.memo<{ onClick: () => void }>(({ onClick }) => {
|
|
352
|
+
return <button onClick={onClick}>Click</button>;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ✅ useMemo (값 메모이제이션)
|
|
356
|
+
function ExpensiveComponent({ data }: { data: number[] }) {
|
|
357
|
+
const processedData = useMemo(() => {
|
|
358
|
+
return data
|
|
359
|
+
.map(expensiveCalculation)
|
|
360
|
+
.filter(x => x > 0)
|
|
361
|
+
.reduce((a, b) => a + b, 0);
|
|
362
|
+
}, [data]);
|
|
363
|
+
|
|
364
|
+
return <div>{processedData}</div>;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### 9. Error Boundary
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// ✅ Error Boundary (클래스 컴포넌트 필수)
|
|
372
|
+
interface ErrorBoundaryProps {
|
|
373
|
+
children: React.ReactNode;
|
|
374
|
+
fallback?: React.ReactNode;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
interface ErrorBoundaryState {
|
|
378
|
+
hasError: boolean;
|
|
379
|
+
error?: Error;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
class ErrorBoundary extends React.Component<
|
|
383
|
+
ErrorBoundaryProps,
|
|
384
|
+
ErrorBoundaryState
|
|
385
|
+
> {
|
|
386
|
+
constructor(props: ErrorBoundaryProps) {
|
|
387
|
+
super(props);
|
|
388
|
+
this.state = { hasError: false };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
392
|
+
return { hasError: true, error };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
396
|
+
console.error('Error caught by boundary:', error, errorInfo);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
render() {
|
|
400
|
+
if (this.state.hasError) {
|
|
401
|
+
return this.props.fallback || <ErrorFallback error={this.state.error} />;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return this.props.children;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 사용
|
|
409
|
+
function App() {
|
|
410
|
+
return (
|
|
411
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
412
|
+
<UserDashboard />
|
|
413
|
+
</ErrorBoundary>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### 10. 타입 가드 활용
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// ✅ 타입 가드
|
|
422
|
+
interface Dog {
|
|
423
|
+
type: 'dog';
|
|
424
|
+
bark: () => void;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
interface Cat {
|
|
428
|
+
type: 'cat';
|
|
429
|
+
meow: () => void;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
type Animal = Dog | Cat;
|
|
433
|
+
|
|
434
|
+
function isDog(animal: Animal): animal is Dog {
|
|
435
|
+
return animal.type === 'dog';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function makeSound(animal: Animal) {
|
|
439
|
+
if (isDog(animal)) {
|
|
440
|
+
animal.bark(); // 타입 안전
|
|
441
|
+
} else {
|
|
442
|
+
animal.meow(); // 타입 안전
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ✅ Discriminated Union
|
|
447
|
+
function AnimalCard({ animal }: { animal: Animal }) {
|
|
448
|
+
switch (animal.type) {
|
|
449
|
+
case 'dog':
|
|
450
|
+
return <DogCard dog={animal} />;
|
|
451
|
+
case 'cat':
|
|
452
|
+
return <CatCard cat={animal} />;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## 안티패턴
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// ❌ Props drilling (3단계 이상)
|
|
461
|
+
<GrandParent user={user}>
|
|
462
|
+
<Parent user={user}>
|
|
463
|
+
<Child user={user} />
|
|
464
|
+
</Parent>
|
|
465
|
+
</GrandParent>
|
|
466
|
+
|
|
467
|
+
// ✅ Context 사용
|
|
468
|
+
const UserContext = createContext<User | undefined>(undefined);
|
|
469
|
+
|
|
470
|
+
<UserContext.Provider value={user}>
|
|
471
|
+
<GrandParent />
|
|
472
|
+
</UserContext.Provider>
|
|
473
|
+
|
|
474
|
+
// ❌ useEffect 의존성 누락
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
fetchUser(userId);
|
|
477
|
+
}, []); // userId 의존성 누락!
|
|
478
|
+
|
|
479
|
+
// ✅ 모든 의존성 명시
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
fetchUser(userId);
|
|
482
|
+
}, [userId]);
|
|
483
|
+
|
|
484
|
+
// ❌ 인라인 객체/함수 (리렌더 유발)
|
|
485
|
+
<Child config={{ theme: 'dark' }} onClick={() => {}} />
|
|
486
|
+
|
|
487
|
+
// ✅ useMemo/useCallback
|
|
488
|
+
const config = useMemo(() => ({ theme: 'dark' }), []);
|
|
489
|
+
const handleClick = useCallback(() => {}, []);
|
|
490
|
+
<Child config={config} onClick={handleClick} />
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## 코드 품질 도구
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
# TypeScript 컴파일
|
|
497
|
+
tsc --noEmit
|
|
498
|
+
|
|
499
|
+
# ESLint
|
|
500
|
+
eslint src/ --ext .ts,.tsx
|
|
501
|
+
|
|
502
|
+
# Prettier
|
|
503
|
+
prettier --write src/
|
|
504
|
+
|
|
505
|
+
# 테스트
|
|
506
|
+
vitest
|
|
507
|
+
# or
|
|
508
|
+
jest
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## 체크리스트
|
|
512
|
+
|
|
513
|
+
TypeScript/React 코드 작성 시:
|
|
514
|
+
|
|
515
|
+
- [ ] 타입 안전성 100% (no any)
|
|
516
|
+
- [ ] 함수형 컴포넌트 + Hooks
|
|
517
|
+
- [ ] Custom Hook으로 로직 분리
|
|
518
|
+
- [ ] Props 타입 명확히 정의
|
|
519
|
+
- [ ] React Query로 서버 상태 관리
|
|
520
|
+
- [ ] Zod로 Contract 정의
|
|
521
|
+
- [ ] JSX ≤ 50줄 (컴포넌트 분리)
|
|
522
|
+
- [ ] useCallback/useMemo 최적화
|
|
523
|
+
- [ ] Error Boundary 사용
|
|
524
|
+
- [ ] 타입 가드 활용
|
|
525
|
+
- [ ] 복잡도 ≤ 10
|