@su-record/vibe 0.1.0
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/LICENSE +21 -0
- package/README.md +448 -0
- package/agents/backend-python-expert.md +453 -0
- package/agents/database-postgres-expert.md +538 -0
- package/agents/frontend-flutter-expert.md +487 -0
- package/agents/frontend-react-expert.md +424 -0
- package/agents/quality-reviewer.md +542 -0
- package/agents/specification-agent.md +505 -0
- package/bin/sutory +332 -0
- package/bin/vibe +338 -0
- package/mcp/dist/__tests__/complexity.test.js +126 -0
- package/mcp/dist/__tests__/memory.test.js +120 -0
- package/mcp/dist/__tests__/python-dart-complexity.test.js +146 -0
- package/mcp/dist/index.js +230 -0
- package/mcp/dist/lib/ContextCompressor.js +305 -0
- package/mcp/dist/lib/MemoryManager.js +334 -0
- package/mcp/dist/lib/ProjectCache.js +126 -0
- package/mcp/dist/lib/PythonParser.js +241 -0
- package/mcp/dist/tools/browser/browserPool.js +76 -0
- package/mcp/dist/tools/browser/browserUtils.js +135 -0
- package/mcp/dist/tools/browser/inspectNetworkRequests.js +140 -0
- package/mcp/dist/tools/browser/monitorConsoleLogs.js +97 -0
- package/mcp/dist/tools/convention/analyzeComplexity.js +248 -0
- package/mcp/dist/tools/convention/applyQualityRules.js +102 -0
- package/mcp/dist/tools/convention/checkCouplingCohesion.js +233 -0
- package/mcp/dist/tools/convention/complexityMetrics.js +133 -0
- package/mcp/dist/tools/convention/dartComplexity.js +117 -0
- package/mcp/dist/tools/convention/getCodingGuide.js +64 -0
- package/mcp/dist/tools/convention/languageDetector.js +50 -0
- package/mcp/dist/tools/convention/pythonComplexity.js +109 -0
- package/mcp/dist/tools/convention/suggestImprovements.js +257 -0
- package/mcp/dist/tools/convention/validateCodeQuality.js +177 -0
- package/mcp/dist/tools/memory/autoSaveContext.js +79 -0
- package/mcp/dist/tools/memory/database.js +123 -0
- package/mcp/dist/tools/memory/deleteMemory.js +39 -0
- package/mcp/dist/tools/memory/listMemories.js +38 -0
- package/mcp/dist/tools/memory/memoryConfig.js +27 -0
- package/mcp/dist/tools/memory/memorySQLite.js +138 -0
- package/mcp/dist/tools/memory/memoryUtils.js +34 -0
- package/mcp/dist/tools/memory/migrate.js +113 -0
- package/mcp/dist/tools/memory/prioritizeMemory.js +109 -0
- package/mcp/dist/tools/memory/recallMemory.js +40 -0
- package/mcp/dist/tools/memory/restoreSessionContext.js +69 -0
- package/mcp/dist/tools/memory/saveMemory.js +34 -0
- package/mcp/dist/tools/memory/searchMemories.js +37 -0
- package/mcp/dist/tools/memory/startSession.js +100 -0
- package/mcp/dist/tools/memory/updateMemory.js +46 -0
- package/mcp/dist/tools/planning/analyzeRequirements.js +166 -0
- package/mcp/dist/tools/planning/createUserStories.js +119 -0
- package/mcp/dist/tools/planning/featureRoadmap.js +202 -0
- package/mcp/dist/tools/planning/generatePrd.js +156 -0
- package/mcp/dist/tools/prompt/analyzePrompt.js +145 -0
- package/mcp/dist/tools/prompt/enhancePrompt.js +105 -0
- package/mcp/dist/tools/semantic/findReferences.js +195 -0
- package/mcp/dist/tools/semantic/findSymbol.js +200 -0
- package/mcp/dist/tools/thinking/analyzeProblem.js +50 -0
- package/mcp/dist/tools/thinking/breakDownProblem.js +140 -0
- package/mcp/dist/tools/thinking/createThinkingChain.js +39 -0
- package/mcp/dist/tools/thinking/formatAsPlan.js +73 -0
- package/mcp/dist/tools/thinking/stepByStepAnalysis.js +58 -0
- package/mcp/dist/tools/thinking/thinkAloudProcess.js +75 -0
- package/mcp/dist/tools/time/getCurrentTime.js +61 -0
- package/mcp/dist/tools/ui/previewUiAscii.js +232 -0
- package/mcp/dist/types/tool.js +2 -0
- package/mcp/package.json +53 -0
- package/package.json +49 -0
- package/scripts/install-mcp.js +48 -0
- package/scripts/install.sh +70 -0
- package/skills/core/communication-guide.md +104 -0
- package/skills/core/development-philosophy.md +53 -0
- package/skills/core/quick-start.md +121 -0
- package/skills/languages/dart-flutter.md +509 -0
- package/skills/languages/python-fastapi.md +386 -0
- package/skills/languages/typescript-nextjs.md +441 -0
- package/skills/languages/typescript-react-native.md +446 -0
- package/skills/languages/typescript-react.md +525 -0
- package/skills/quality/checklist.md +276 -0
- package/skills/quality/testing-strategy.md +437 -0
- package/skills/standards/anti-patterns.md +369 -0
- package/skills/standards/code-structure.md +291 -0
- package/skills/standards/complexity-metrics.md +312 -0
- package/skills/standards/naming-conventions.md +198 -0
- package/skills/tools/mcp-hi-ai-guide.md +665 -0
- package/skills/tools/mcp-workflow.md +51 -0
- package/templates/constitution-template.md +193 -0
- package/templates/plan-template.md +237 -0
- package/templates/spec-template.md +142 -0
- package/templates/tasks-template.md +132 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# 🚫 자동 안티패턴 회피
|
|
2
|
+
|
|
3
|
+
## TypeScript 안티패턴
|
|
4
|
+
|
|
5
|
+
### 1. any 타입 사용
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// ❌ any 사용
|
|
9
|
+
function processData(data: any) {
|
|
10
|
+
return data.value; // 타입 안전성 상실
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ✅ unknown + type guard
|
|
14
|
+
function processData(data: unknown) {
|
|
15
|
+
if (isValidData(data)) {
|
|
16
|
+
return data.value; // 타입 안전
|
|
17
|
+
}
|
|
18
|
+
throw new Error('Invalid data');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isValidData(data: unknown): data is { value: string } {
|
|
22
|
+
return typeof data === 'object' && data !== null && 'value' in data;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. as any 강제 타입 캐스팅
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// ❌ as any로 타입 우회
|
|
30
|
+
const user = response as any;
|
|
31
|
+
user.name; // 런타임 에러 위험
|
|
32
|
+
|
|
33
|
+
// ✅ 적절한 타입 정의
|
|
34
|
+
interface User {
|
|
35
|
+
name: string;
|
|
36
|
+
email: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const user = response as User;
|
|
40
|
+
user.name; // 타입 안전
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. @ts-ignore 남용
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// ❌ @ts-ignore로 에러 무시
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
const result = problematicCode();
|
|
49
|
+
|
|
50
|
+
// ✅ 타입 문제 근본 해결
|
|
51
|
+
interface Expected {
|
|
52
|
+
id: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result: Expected = {
|
|
56
|
+
id: String(problematicCode()),
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## React 안티패턴
|
|
61
|
+
|
|
62
|
+
### 1. dangerouslySetInnerHTML 사용
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// ❌ XSS 취약점
|
|
66
|
+
function Component({ html }: { html: string }) {
|
|
67
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ✅ 안전한 렌더링
|
|
71
|
+
import DOMPurify from 'dompurify';
|
|
72
|
+
|
|
73
|
+
function Component({ html }: { html: string }) {
|
|
74
|
+
const sanitized = DOMPurify.sanitize(html);
|
|
75
|
+
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ✅ 더 나은 방법: 마크다운 라이브러리 사용
|
|
79
|
+
import ReactMarkdown from 'react-markdown';
|
|
80
|
+
|
|
81
|
+
function Component({ markdown }: { markdown: string }) {
|
|
82
|
+
return <ReactMarkdown>{markdown}</ReactMarkdown>;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Props Drilling (3단계 이상)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// ❌ Props drilling
|
|
90
|
+
function App() {
|
|
91
|
+
const [user, setUser] = useState<User>();
|
|
92
|
+
return <Parent user={user} />;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Parent({ user }: { user: User }) {
|
|
96
|
+
return <Child user={user} />;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function Child({ user }: { user: User }) {
|
|
100
|
+
return <GrandChild user={user} />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function GrandChild({ user }: { user: User }) {
|
|
104
|
+
return <div>{user.name}</div>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ✅ Context API 사용
|
|
108
|
+
const UserContext = createContext<User | undefined>(undefined);
|
|
109
|
+
|
|
110
|
+
function App() {
|
|
111
|
+
const [user, setUser] = useState<User>();
|
|
112
|
+
return (
|
|
113
|
+
<UserContext.Provider value={user}>
|
|
114
|
+
<Parent />
|
|
115
|
+
</UserContext.Provider>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function GrandChild() {
|
|
120
|
+
const user = useContext(UserContext);
|
|
121
|
+
return <div>{user?.name}</div>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 3. useEffect 의존성 배열 누락
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ❌ 의존성 누락
|
|
129
|
+
function Component({ userId }: { userId: string }) {
|
|
130
|
+
const [user, setUser] = useState<User>();
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
fetchUser(userId).then(setUser);
|
|
134
|
+
}, []); // userId 의존성 누락!
|
|
135
|
+
|
|
136
|
+
return <div>{user?.name}</div>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ✅ 모든 의존성 명시
|
|
140
|
+
function Component({ userId }: { userId: string }) {
|
|
141
|
+
const [user, setUser] = useState<User>();
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
fetchUser(userId).then(setUser);
|
|
145
|
+
}, [userId]); // 의존성 명시
|
|
146
|
+
|
|
147
|
+
return <div>{user?.name}</div>;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## JavaScript 안티패턴
|
|
152
|
+
|
|
153
|
+
### 1. var 사용
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// ❌ var 사용
|
|
157
|
+
var count = 0;
|
|
158
|
+
if (true) {
|
|
159
|
+
var count = 1; // 같은 변수!
|
|
160
|
+
}
|
|
161
|
+
console.log(count); // 1
|
|
162
|
+
|
|
163
|
+
// ✅ const/let 사용
|
|
164
|
+
let count = 0;
|
|
165
|
+
if (true) {
|
|
166
|
+
let count = 1; // 블록 스코프
|
|
167
|
+
}
|
|
168
|
+
console.log(count); // 0
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 2. == 사용 (느슨한 비교)
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// ❌ == 사용
|
|
175
|
+
if (value == null) { } // undefined도 매칭
|
|
176
|
+
if ('5' == 5) { } // true (타입 강제 변환)
|
|
177
|
+
|
|
178
|
+
// ✅ === 사용
|
|
179
|
+
if (value === null) { }
|
|
180
|
+
if (value === undefined) { }
|
|
181
|
+
if ('5' === 5) { } // false
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 3. eval() 사용
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// ❌ eval() 사용 (보안 위험)
|
|
188
|
+
const code = userInput;
|
|
189
|
+
eval(code); // 임의 코드 실행 가능
|
|
190
|
+
|
|
191
|
+
// ✅ 대안 구현
|
|
192
|
+
const allowedOperations = {
|
|
193
|
+
add: (a: number, b: number) => a + b,
|
|
194
|
+
subtract: (a: number, b: number) => a - b,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const operation = allowedOperations[userInput];
|
|
198
|
+
if (operation) {
|
|
199
|
+
result = operation(a, b);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## CSS 안티패턴
|
|
204
|
+
|
|
205
|
+
### 1. !important 남용
|
|
206
|
+
|
|
207
|
+
```css
|
|
208
|
+
/* ❌ !important 남용 */
|
|
209
|
+
.button {
|
|
210
|
+
color: blue !important;
|
|
211
|
+
background: red !important;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ✅ 구체적인 선택자 사용 */
|
|
215
|
+
.navigation .button.primary {
|
|
216
|
+
color: blue;
|
|
217
|
+
background: red;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 2. 인라인 스타일 남용
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// ❌ 인라인 스타일
|
|
225
|
+
function Button() {
|
|
226
|
+
return (
|
|
227
|
+
<button
|
|
228
|
+
style={{
|
|
229
|
+
backgroundColor: 'blue',
|
|
230
|
+
color: 'white',
|
|
231
|
+
padding: '10px',
|
|
232
|
+
borderRadius: '5px',
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
Click me
|
|
236
|
+
</button>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ✅ CSS 클래스 사용
|
|
241
|
+
function Button() {
|
|
242
|
+
return <button className="btn-primary">Click me</button>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// styles.css
|
|
246
|
+
.btn-primary {
|
|
247
|
+
background-color: blue;
|
|
248
|
+
color: white;
|
|
249
|
+
padding: 10px;
|
|
250
|
+
border-radius: 5px;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## 성능 안티패턴
|
|
255
|
+
|
|
256
|
+
### 1. 불필요한 리렌더링
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// ❌ 매번 새 객체/함수 생성
|
|
260
|
+
function Parent() {
|
|
261
|
+
return <Child config={{ theme: 'dark' }} onClick={() => {}} />;
|
|
262
|
+
// 매 렌더마다 새 객체/함수 생성 → Child 리렌더
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ✅ useMemo/useCallback 사용
|
|
266
|
+
function Parent() {
|
|
267
|
+
const config = useMemo(() => ({ theme: 'dark' }), []);
|
|
268
|
+
const handleClick = useCallback(() => {}, []);
|
|
269
|
+
|
|
270
|
+
return <Child config={config} onClick={handleClick} />;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 2. 동기적 무거운 연산
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// ❌ 메인 스레드 블로킹
|
|
278
|
+
function Component({ data }: { data: number[] }) {
|
|
279
|
+
const result = data
|
|
280
|
+
.map(heavyComputation)
|
|
281
|
+
.filter(x => x > 0)
|
|
282
|
+
.reduce((a, b) => a + b);
|
|
283
|
+
|
|
284
|
+
return <div>{result}</div>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ✅ useMemo로 메모이제이션
|
|
288
|
+
function Component({ data }: { data: number[] }) {
|
|
289
|
+
const result = useMemo(
|
|
290
|
+
() =>
|
|
291
|
+
data
|
|
292
|
+
.map(heavyComputation)
|
|
293
|
+
.filter(x => x > 0)
|
|
294
|
+
.reduce((a, b) => a + b),
|
|
295
|
+
[data]
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return <div>{result}</div>;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## 보안 안티패턴
|
|
303
|
+
|
|
304
|
+
### 1. 민감 정보 하드코딩
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// ❌ API 키 하드코딩
|
|
308
|
+
const API_KEY = 'sk-1234567890abcdef';
|
|
309
|
+
|
|
310
|
+
// ✅ 환경 변수 사용
|
|
311
|
+
const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 2. SQL Injection 취약점
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// ❌ 직접 문자열 연결
|
|
318
|
+
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
|
319
|
+
|
|
320
|
+
// ✅ 파라미터화된 쿼리
|
|
321
|
+
const query = 'SELECT * FROM users WHERE id = ?';
|
|
322
|
+
db.execute(query, [userId]);
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## 에러 처리 안티패턴
|
|
326
|
+
|
|
327
|
+
### 1. 빈 catch 블록
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// ❌ 에러 무시
|
|
331
|
+
try {
|
|
332
|
+
riskyOperation();
|
|
333
|
+
} catch (e) {
|
|
334
|
+
// 아무것도 안 함
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ✅ 적절한 에러 처리
|
|
338
|
+
try {
|
|
339
|
+
riskyOperation();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error('Operation failed:', error);
|
|
342
|
+
showErrorNotification(error);
|
|
343
|
+
trackError(error);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 2. 에러 타입 확인 없이 처리
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// ❌ 모든 에러 동일하게 처리
|
|
351
|
+
try {
|
|
352
|
+
await fetchData();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
showError('Failed'); // 구체적이지 않음
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ✅ 에러 타입별 처리
|
|
358
|
+
try {
|
|
359
|
+
await fetchData();
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error instanceof NetworkError) {
|
|
362
|
+
showError('네트워크 연결을 확인해주세요');
|
|
363
|
+
} else if (error instanceof AuthError) {
|
|
364
|
+
redirectToLogin();
|
|
365
|
+
} else {
|
|
366
|
+
showError('알 수 없는 오류가 발생했습니다');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# 🏗️ 코드 구조 자동화 규칙
|
|
2
|
+
|
|
3
|
+
## 컴포넌트 구조 (엄격한 순서)
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// 1. Import 문
|
|
7
|
+
import React, { useState, useEffect } from 'react';
|
|
8
|
+
|
|
9
|
+
// 2. 타입/인터페이스 정의
|
|
10
|
+
interface Props {
|
|
11
|
+
userId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 3. 컴포넌트 정의
|
|
15
|
+
function UserProfile({ userId }: Props) {
|
|
16
|
+
// 4. State & Refs
|
|
17
|
+
const [user, setUser] = useState<User | null>(null);
|
|
18
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
19
|
+
|
|
20
|
+
// 5. Custom Hooks
|
|
21
|
+
const { isAuthenticated } = useAuth();
|
|
22
|
+
const { data, loading } = useUserData(userId);
|
|
23
|
+
|
|
24
|
+
// 6. Event Handlers
|
|
25
|
+
const handleSubmit = (e: FormEvent) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
// ...
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 7. Effects
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// ...
|
|
33
|
+
}, [userId]);
|
|
34
|
+
|
|
35
|
+
// 8. Early returns
|
|
36
|
+
if (loading) return <Spinner />;
|
|
37
|
+
if (!user) return <NotFound />;
|
|
38
|
+
|
|
39
|
+
// 9. Main return JSX
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
{/* ... */}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 함수 분리 기준
|
|
49
|
+
|
|
50
|
+
### 1. 함수 길이 기준
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// ❌ 20줄 초과 - 분리 필요
|
|
54
|
+
function processUserData(user: User) {
|
|
55
|
+
// 30줄의 복잡한 로직
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ✅ 단일 책임으로 분리
|
|
59
|
+
function processUserData(user: User) {
|
|
60
|
+
const validated = validateUser(user);
|
|
61
|
+
const transformed = transformUserData(validated);
|
|
62
|
+
return saveUserData(transformed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateUser(user: User) { /* ... */ }
|
|
66
|
+
function transformUserData(user: User) { /* ... */ }
|
|
67
|
+
function saveUserData(user: User) { /* ... */ }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. 컴포넌트 JSX 길이 기준
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// ❌ JSX 50줄 초과 - 분리 필요
|
|
74
|
+
function Dashboard() {
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
{/* 60줄의 복잡한 JSX */}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ✅ 서브 컴포넌트 추출
|
|
83
|
+
function Dashboard() {
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
<DashboardHeader />
|
|
87
|
+
<DashboardContent />
|
|
88
|
+
<DashboardFooter />
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function DashboardHeader() { /* ... */ }
|
|
94
|
+
function DashboardContent() { /* ... */ }
|
|
95
|
+
function DashboardFooter() { /* ... */ }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. 중첩 깊이 기준
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// ❌ 중첩 3단계 초과
|
|
102
|
+
function processData(data: Data) {
|
|
103
|
+
if (data) {
|
|
104
|
+
if (data.isValid) {
|
|
105
|
+
if (data.user) {
|
|
106
|
+
if (data.user.isActive) {
|
|
107
|
+
// 너무 깊은 중첩
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ✅ Early return으로 평탄화
|
|
115
|
+
function processData(data: Data) {
|
|
116
|
+
if (!data) return null;
|
|
117
|
+
if (!data.isValid) return null;
|
|
118
|
+
if (!data.user) return null;
|
|
119
|
+
if (!data.user.isActive) return null;
|
|
120
|
+
|
|
121
|
+
// 로직 실행
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 4. Cyclomatic Complexity > 10
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ❌ 복잡도 높음 (15)
|
|
129
|
+
function calculatePrice(item: Item) {
|
|
130
|
+
let price = item.basePrice;
|
|
131
|
+
if (item.discount) price *= 0.9;
|
|
132
|
+
if (item.bulk) price *= 0.8;
|
|
133
|
+
if (item.seasonal) price *= 0.95;
|
|
134
|
+
if (item.member) price *= 0.85;
|
|
135
|
+
if (item.firstTime) price *= 0.9;
|
|
136
|
+
// ... 더 많은 조건
|
|
137
|
+
return price;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ✅ 복잡도 감소 (3)
|
|
141
|
+
function calculatePrice(item: Item) {
|
|
142
|
+
const basePrice = item.basePrice;
|
|
143
|
+
const discounts = getApplicableDiscounts(item);
|
|
144
|
+
return applyDiscounts(basePrice, discounts);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 5. Cognitive Complexity > 15
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// ❌ 인지 복잡도 높음
|
|
152
|
+
function processOrder(order: Order) {
|
|
153
|
+
if (order.isPremium) {
|
|
154
|
+
for (let item of order.items) {
|
|
155
|
+
if (item.category === 'electronics') {
|
|
156
|
+
if (item.price > 1000) {
|
|
157
|
+
// 중첩된 복잡한 로직
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ✅ 인지 복잡도 감소
|
|
165
|
+
function processOrder(order: Order) {
|
|
166
|
+
if (!order.isPremium) return;
|
|
167
|
+
|
|
168
|
+
const electronics = filterElectronics(order.items);
|
|
169
|
+
const expensive = filterExpensive(electronics);
|
|
170
|
+
|
|
171
|
+
processItems(expensive);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 파일 구조 표준
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// 📁 user-profile.component.tsx
|
|
179
|
+
|
|
180
|
+
// 1. Imports
|
|
181
|
+
import { ... } from 'react';
|
|
182
|
+
import { ... } from '@/lib';
|
|
183
|
+
|
|
184
|
+
// 2. Types
|
|
185
|
+
interface UserProfileProps { }
|
|
186
|
+
type UserRole = 'admin' | 'user';
|
|
187
|
+
|
|
188
|
+
// 3. Constants
|
|
189
|
+
const MAX_BIO_LENGTH = 500;
|
|
190
|
+
const DEFAULT_AVATAR = '/avatar.png';
|
|
191
|
+
|
|
192
|
+
// 4. Helper Functions (내부 전용)
|
|
193
|
+
function formatUserName(name: string) { }
|
|
194
|
+
|
|
195
|
+
// 5. Main Component
|
|
196
|
+
export function UserProfile() { }
|
|
197
|
+
|
|
198
|
+
// 6. Sub Components (export하지 않음)
|
|
199
|
+
function ProfileHeader() { }
|
|
200
|
+
function ProfileContent() { }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 모듈 구성 원칙
|
|
204
|
+
|
|
205
|
+
### 1. 응집도 (Cohesion)
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// ✅ 높은 응집도 - 관련 기능만 모음
|
|
209
|
+
// 📁 user.service.ts
|
|
210
|
+
export class UserService {
|
|
211
|
+
getUser(id: string) { }
|
|
212
|
+
updateUser(id: string, data: User) { }
|
|
213
|
+
deleteUser(id: string) { }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ❌ 낮은 응집도 - 관련 없는 기능 혼재
|
|
217
|
+
// 📁 utils.ts (안티패턴)
|
|
218
|
+
export class Utils {
|
|
219
|
+
validateEmail(email: string) { }
|
|
220
|
+
formatCurrency(amount: number) { }
|
|
221
|
+
uploadFile(file: File) { }
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 2. 결합도 (Coupling)
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// ✅ 느슨한 결합 - 인터페이스 의존
|
|
229
|
+
interface Storage {
|
|
230
|
+
save(key: string, value: unknown): void;
|
|
231
|
+
load(key: string): unknown;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class UserService {
|
|
235
|
+
constructor(private storage: Storage) { }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ❌ 강한 결합 - 구현체 직접 의존
|
|
239
|
+
class UserService {
|
|
240
|
+
private storage = new LocalStorage(); // 직접 의존
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 함수 매개변수 제한
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// ❌ 매개변수 5개 초과
|
|
248
|
+
function createUser(
|
|
249
|
+
name: string,
|
|
250
|
+
email: string,
|
|
251
|
+
age: number,
|
|
252
|
+
address: string,
|
|
253
|
+
phone: string,
|
|
254
|
+
role: string
|
|
255
|
+
) { }
|
|
256
|
+
|
|
257
|
+
// ✅ 객체로 그룹화
|
|
258
|
+
interface CreateUserParams {
|
|
259
|
+
name: string;
|
|
260
|
+
email: string;
|
|
261
|
+
age: number;
|
|
262
|
+
address: string;
|
|
263
|
+
phone: string;
|
|
264
|
+
role: string;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createUser(params: CreateUserParams) { }
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 순환 의존성 방지
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// ❌ 순환 의존성
|
|
274
|
+
// fileA.ts
|
|
275
|
+
import { funcB } from './fileB';
|
|
276
|
+
export function funcA() { funcB(); }
|
|
277
|
+
|
|
278
|
+
// fileB.ts
|
|
279
|
+
import { funcA } from './fileA'; // 순환!
|
|
280
|
+
export function funcB() { funcA(); }
|
|
281
|
+
|
|
282
|
+
// ✅ 공통 모듈 분리
|
|
283
|
+
// shared.ts
|
|
284
|
+
export function sharedFunc() { }
|
|
285
|
+
|
|
286
|
+
// fileA.ts
|
|
287
|
+
import { sharedFunc } from './shared';
|
|
288
|
+
|
|
289
|
+
// fileB.ts
|
|
290
|
+
import { sharedFunc } from './shared';
|
|
291
|
+
```
|