@choblue/claude-code-toolkit 1.2.5 → 1.2.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/.claude/.project-map-cache +1 -1
- package/.claude/CLAUDE.md +80 -18
- package/.claude/hooks/prompt-hook.sh +4 -4
- package/.claude/prompts/feature.md +11 -0
- package/.claude/prompts/fix.md +11 -0
- package/.claude/prompts/review.md +7 -0
- package/.claude/skills/Coding/SKILL.md +5 -4
- package/.claude/skills/Curation/SKILL.md +36 -0
- package/.claude/skills/FailureRecovery/SKILL.md +47 -0
- package/.claude/skills/{Coding/backend.md → NestJS/SKILL.md} +7 -2
- package/.claude/skills/NextJS/SKILL.md +13 -303
- package/.claude/skills/NextJS/references/data-fetching.md +96 -0
- package/.claude/skills/NextJS/references/middleware-actions.md +74 -0
- package/.claude/skills/NextJS/references/optimization.md +127 -0
- package/.claude/skills/Planning/SKILL.md +30 -7
- package/.claude/skills/React/SKILL.md +25 -287
- package/.claude/skills/React/references/a11y-ux.md +134 -0
- package/.claude/skills/React/references/rendering-patterns.md +62 -0
- package/.claude/skills/React/references/state-hooks.md +73 -0
- package/.claude/skills/ReactHookForm/SKILL.md +8 -196
- package/.claude/skills/ReactHookForm/references/advanced-patterns.md +193 -0
- package/.claude/skills/TDD/SKILL.md +2 -2
- package/.claude/skills/TailwindCSS/SKILL.md +14 -240
- package/.claude/skills/TailwindCSS/references/patterns-components.md +93 -0
- package/.claude/skills/TailwindCSS/references/responsive-dark.md +102 -0
- package/.claude/skills/TailwindCSS/references/transitions.md +33 -0
- package/README.md +25 -12
- package/package.json +1 -1
- package/.claude/skills/Coding/frontend.md +0 -11
- /package/.claude/skills/TDD/{backend.md → references/backend.md} +0 -0
- /package/.claude/skills/TDD/{frontend.md → references/frontend.md} +0 -0
|
@@ -16,8 +16,8 @@ React 컴포넌트 설계 및 개발에 적용되는 핵심 규칙이다.
|
|
|
16
16
|
- 클래스 컴포넌트를 사용하지 않는다
|
|
17
17
|
- `React.FC`를 사용하지 않는다 - Props를 직접 타이핑한다
|
|
18
18
|
|
|
19
|
-
### Props
|
|
20
|
-
- 모든 컴포넌트는 Props 타입을 `
|
|
19
|
+
### Props 타입 명시
|
|
20
|
+
- 모든 컴포넌트는 Props 타입을 `type`으로 선언한다
|
|
21
21
|
- Props 네이밍은 `컴포넌트명 + Props`로 한다
|
|
22
22
|
|
|
23
23
|
### SRP (Single Responsibility Principle)
|
|
@@ -35,9 +35,9 @@ const UserPage = () => {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
// Good
|
|
38
|
-
|
|
38
|
+
type UserPageProps = {
|
|
39
39
|
userId: string;
|
|
40
|
-
}
|
|
40
|
+
};
|
|
41
41
|
|
|
42
42
|
export function UserPage({ userId }: UserPageProps) {
|
|
43
43
|
const { user, isLoading } = useUser(userId);
|
|
@@ -68,16 +68,16 @@ export function UserPage({ userId }: UserPageProps) {
|
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
70
|
// Bad
|
|
71
|
-
|
|
71
|
+
type ButtonProps = {
|
|
72
72
|
clickHandler: () => void;
|
|
73
73
|
deleteCallback: () => void;
|
|
74
|
-
}
|
|
74
|
+
};
|
|
75
75
|
|
|
76
76
|
// Good
|
|
77
|
-
|
|
77
|
+
type ButtonProps = {
|
|
78
78
|
onClick: () => void;
|
|
79
79
|
onDelete: () => void;
|
|
80
|
-
}
|
|
80
|
+
};
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
### 객체 통째 전달 지양
|
|
@@ -86,157 +86,20 @@ interface ButtonProps {
|
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
88
|
// Bad - 불필요한 데이터까지 전달
|
|
89
|
-
|
|
89
|
+
type UserAvatarProps = {
|
|
90
90
|
user: User; // user.name, user.avatar만 사용하는데 전체 객체 전달
|
|
91
|
-
}
|
|
91
|
+
};
|
|
92
92
|
|
|
93
93
|
// Good - 필요한 값만 전달
|
|
94
|
-
|
|
94
|
+
type UserAvatarProps = {
|
|
95
95
|
name: string;
|
|
96
96
|
avatarUrl: string;
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## 3. 상태 관리 원칙
|
|
103
|
-
|
|
104
|
-
### 가까운 곳에 배치
|
|
105
|
-
- 상태는 그것을 사용하는 가장 가까운 컴포넌트에 배치한다
|
|
106
|
-
- 상위 컴포넌트로의 lifting은 실제로 필요할 때만 수행한다
|
|
107
|
-
|
|
108
|
-
### 파생값은 상태가 아니다
|
|
109
|
-
- 기존 상태에서 계산할 수 있는 값은 별도 상태로 만들지 않는다
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
// Bad - 파생값을 상태로 관리
|
|
113
|
-
const [items, setItems] = useState<Item[]>([]);
|
|
114
|
-
const [itemCount, setItemCount] = useState(0);
|
|
115
|
-
// items가 변경될 때마다 setItemCount를 호출해야 함
|
|
116
|
-
|
|
117
|
-
// Good - 파생값은 계산
|
|
118
|
-
const [items, setItems] = useState<Item[]>([]);
|
|
119
|
-
const itemCount = items.length;
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### 서버 상태와 클라이언트 상태 분리
|
|
123
|
-
- **서버 상태**: API에서 가져온 데이터 -> React Query / SWR 사용
|
|
124
|
-
- **클라이언트 상태**: UI 상태 (모달 열림, 탭 선택 등) -> useState / useReducer 사용
|
|
125
|
-
- 서버 데이터를 `useState`로 복사하지 않는다
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## 4. 커스텀 훅
|
|
130
|
-
|
|
131
|
-
### use 접두사
|
|
132
|
-
- 모든 커스텀 훅은 `use`로 시작한다
|
|
133
|
-
|
|
134
|
-
### 하나의 관심사
|
|
135
|
-
- 하나의 훅은 하나의 관심사만 다룬다
|
|
136
|
-
|
|
137
|
-
### 데이터 페칭/상태 로직 분리
|
|
138
|
-
- 컴포넌트에서 데이터 페칭과 상태 로직을 커스텀 훅으로 분리한다
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
// Bad - 컴포넌트에 로직이 섞여 있음
|
|
142
|
-
function UserList() {
|
|
143
|
-
const [users, setUsers] = useState<User[]>([]);
|
|
144
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
145
|
-
const [error, setError] = useState<Error | null>(null);
|
|
146
|
-
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
setIsLoading(true);
|
|
149
|
-
fetchUsers()
|
|
150
|
-
.then(setUsers)
|
|
151
|
-
.catch(setError)
|
|
152
|
-
.finally(() => setIsLoading(false));
|
|
153
|
-
}, []);
|
|
154
|
-
|
|
155
|
-
// ... 렌더링
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Good - 커스텀 훅으로 분리
|
|
159
|
-
function useUserList() {
|
|
160
|
-
const { data: users = [], isLoading, error } = useQuery({
|
|
161
|
-
queryKey: ['users'],
|
|
162
|
-
queryFn: fetchUsers,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
return { users, isLoading, error };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function UserList() {
|
|
169
|
-
const { users, isLoading, error } = useUserList();
|
|
170
|
-
// ... 렌더링만 담당
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## 5. 렌더링 최적화
|
|
177
|
-
|
|
178
|
-
### 불필요한 useMemo/useCallback 금지
|
|
179
|
-
- 성능 문제가 실제로 측정된 경우에만 사용한다
|
|
180
|
-
- 참조 동일성이 필요한 경우(의존성 배열, memo된 자식 컴포넌트)에만 사용한다
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
// Bad - 불필요한 메모이제이션
|
|
184
|
-
const userName = useMemo(() => `${first} ${last}`, [first, last]);
|
|
185
|
-
|
|
186
|
-
// Good - 단순 계산은 그냥 수행
|
|
187
|
-
const userName = `${first} ${last}`;
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### key 올바르게 사용
|
|
191
|
-
- 리스트 렌더링 시 고유한 식별자를 `key`로 사용한다
|
|
192
|
-
- 배열 인덱스를 `key`로 사용하지 않는다 (정적 리스트 제외)
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
// Bad
|
|
196
|
-
{items.map((item, index) => (
|
|
197
|
-
<Item key={index} data={item} />
|
|
198
|
-
))}
|
|
199
|
-
|
|
200
|
-
// Good
|
|
201
|
-
{items.map((item) => (
|
|
202
|
-
<Item key={item.id} data={item} />
|
|
203
|
-
))}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
## 6. 조건부 렌더링 패턴
|
|
209
|
-
|
|
210
|
-
### 단순 조건
|
|
211
|
-
- 2개 이하의 조건은 삼항 연산자 또는 `&&`를 사용한다
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
// 단순 표시/숨김
|
|
215
|
-
{isVisible && <Modal />}
|
|
216
|
-
|
|
217
|
-
// 이분기
|
|
218
|
-
{isLoading ? <Skeleton /> : <Content />}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### 복잡한 조건
|
|
222
|
-
- 3개 이상의 분기는 early return 또는 별도 컴포넌트로 분리한다
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
// Bad - 중첩된 삼항
|
|
226
|
-
{isLoading ? <Skeleton /> : error ? <Error /> : data ? <Content /> : <Empty />}
|
|
227
|
-
|
|
228
|
-
// Good - early return
|
|
229
|
-
function UserContent({ isLoading, error, data }: UserContentProps) {
|
|
230
|
-
if (isLoading) return <Skeleton />;
|
|
231
|
-
if (error) return <ErrorDisplay error={error} />;
|
|
232
|
-
if (!data) return <EmptyState />;
|
|
233
|
-
return <Content data={data} />;
|
|
234
|
-
}
|
|
97
|
+
};
|
|
235
98
|
```
|
|
236
99
|
|
|
237
100
|
---
|
|
238
101
|
|
|
239
|
-
##
|
|
102
|
+
## 3. 네이밍 컨벤션
|
|
240
103
|
|
|
241
104
|
| 대상 | 규칙 | 예시 |
|
|
242
105
|
|------|------|------|
|
|
@@ -253,142 +116,7 @@ function UserContent({ isLoading, error, data }: UserContentProps) {
|
|
|
253
116
|
|
|
254
117
|
---
|
|
255
118
|
|
|
256
|
-
##
|
|
257
|
-
|
|
258
|
-
### 원칙: 에러 처리를 각 함수/컴포넌트에 흩뿌리지 않는다
|
|
259
|
-
- 각 함수마다 try-catch를 덕지덕지 붙이지 않는다
|
|
260
|
-
- `useState`로 에러 상태를 직접 관리하지 않는다 (TanStack Query가 제공)
|
|
261
|
-
- 에러 처리는 **경계(Boundary)**에서 한 번에 처리한다
|
|
262
|
-
|
|
263
|
-
### API 에러: 서버 상태 라이브러리에 위임
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// Bad - useState + useEffect로 에러 직접 관리
|
|
267
|
-
function UserList() {
|
|
268
|
-
const [error, setError] = useState<Error | null>(null);
|
|
269
|
-
const [data, setData] = useState(null);
|
|
270
|
-
const [loading, setLoading] = useState(false);
|
|
271
|
-
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
setLoading(true);
|
|
274
|
-
fetchUsers()
|
|
275
|
-
.then(setData)
|
|
276
|
-
.catch(setError)
|
|
277
|
-
.finally(() => setLoading(false));
|
|
278
|
-
}, []);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Good - 서버 상태 라이브러리가 에러를 관리
|
|
282
|
-
function UserList() {
|
|
283
|
-
const { data: users, isLoading, error } = useUserList();
|
|
284
|
-
if (error) return <ErrorDisplay error={error} />;
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### 렌더링 에러: Error Boundary
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
<ErrorBoundary fallback={<ErrorFallback />}>
|
|
292
|
-
<UserContent />
|
|
293
|
-
</ErrorBoundary>
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### 전역 API 에러: interceptor 또는 라이브러리 설정에서 일괄 처리
|
|
297
|
-
- axios interceptor, QueryClient defaultOptions 등 프로젝트 설정에 따른다
|
|
298
|
-
- 개별 컴포넌트가 아닌 앱 수준에서 에러 알림(toast 등)을 처리한다
|
|
299
|
-
|
|
300
|
-
### 에러 UI 분기: early return 패턴
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
function UserPage({ userId }: UserPageProps) {
|
|
304
|
-
const { data: user, isLoading, error } = useUser(userId);
|
|
305
|
-
|
|
306
|
-
if (isLoading) return <Skeleton />;
|
|
307
|
-
if (error) return <ErrorDisplay error={error} />;
|
|
308
|
-
if (!user) return <EmptyState />;
|
|
309
|
-
|
|
310
|
-
return <UserContent user={user} />;
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
---
|
|
315
|
-
|
|
316
|
-
## 9. 접근성 (a11y)
|
|
317
|
-
|
|
318
|
-
### 시맨틱 HTML 사용
|
|
319
|
-
- 클릭 가능한 요소는 반드시 `<button>` 또는 `<a>`를 사용한다
|
|
320
|
-
- `<div onClick>`, `<span onClick>`을 금지한다
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// Bad
|
|
324
|
-
<div onClick={handleDelete} className="cursor-pointer">삭제</div>
|
|
325
|
-
|
|
326
|
-
// Good
|
|
327
|
-
<button type="button" onClick={handleDelete}>삭제</button>
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### 아이콘 버튼에 aria-label 필수
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
// Bad - 스크린 리더가 내용을 알 수 없음
|
|
334
|
-
<button onClick={onClose}><XIcon /></button>
|
|
335
|
-
|
|
336
|
-
// Good
|
|
337
|
-
<button onClick={onClose} aria-label="닫기"><XIcon /></button>
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### 이미지에 alt, width, height 필수
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
// Bad
|
|
344
|
-
<img src={user.avatar} />
|
|
345
|
-
|
|
346
|
-
// Good
|
|
347
|
-
<img src={user.avatar} alt={`${user.name} 프로필`} width={40} height={40} />
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### focus-visible 보장
|
|
351
|
-
- `outline: none`을 사용할 때 반드시 `focus-visible` 대체 스타일을 제공한다
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
// Bad - 포커스 표시 완전 제거
|
|
355
|
-
<button className="outline-none">
|
|
356
|
-
|
|
357
|
-
// Good - 키보드 포커스 시 표시
|
|
358
|
-
<button className="outline-none focus-visible:ring-2 focus-visible:ring-blue-500">
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### 폼 접근성
|
|
362
|
-
- 모든 입력 필드에 `<label>` 또는 `aria-label`을 연결한다
|
|
363
|
-
- 적절한 `type`, `inputMode`, `autoComplete` 속성을 사용한다
|
|
364
|
-
- 붙여넣기(`onPaste`)를 차단하지 않는다
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## 10. UX 패턴
|
|
369
|
-
|
|
370
|
-
### 파괴적 액션에 확인 단계
|
|
371
|
-
- 삭제, 초기화 등 되돌릴 수 없는 동작에는 확인 UI를 추가한다
|
|
372
|
-
|
|
373
|
-
### URL 파라미터와 UI 상태 동기화
|
|
374
|
-
- 탭, 필터, 페이지 등 공유 가능한 UI 상태는 URL 파라미터에 반영한다
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
// Bad - 새로고침하면 상태 소실
|
|
378
|
-
const [tab, setTab] = useState('overview');
|
|
379
|
-
|
|
380
|
-
// Good - URL에 상태 반영 (deep-link 가능)
|
|
381
|
-
const [searchParams, setSearchParams] = useSearchParams();
|
|
382
|
-
const tab = searchParams.get('tab') ?? 'overview';
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### 대규모 리스트 가상화
|
|
386
|
-
- 50개 이상의 항목을 렌더링할 때는 가상화 라이브러리를 사용한다
|
|
387
|
-
- `@tanstack/react-virtual`, `react-window` 등을 활용한다
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
|
-
## 11. 금지 사항
|
|
119
|
+
## 4. 금지 사항
|
|
392
120
|
|
|
393
121
|
- `any` 타입 사용 금지
|
|
394
122
|
- 인라인 스타일(`style={{}}`) 사용 금지 - 프로젝트 스타일링 방식을 따른다
|
|
@@ -404,4 +132,14 @@ const tab = searchParams.get('tab') ?? 'overview';
|
|
|
404
132
|
- `<div onClick>`, `<span onClick>` 금지 (`<button>` 또는 `<a>` 사용)
|
|
405
133
|
- 아이콘 버튼에 `aria-label` 누락 금지
|
|
406
134
|
- `outline: none` 단독 사용 금지 (`focus-visible` 대체 필수)
|
|
407
|
-
- 입력 필드에 `onPaste` 차단 금지
|
|
135
|
+
- 입력 필드에 `onPaste` 차단 금지
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 심화 참조
|
|
140
|
+
|
|
141
|
+
| 파일 | 설명 |
|
|
142
|
+
|------|------|
|
|
143
|
+
| `references/state-hooks.md` | 상태 관리 원칙 (배치, 파생값, 서버/클라이언트 분리) + 커스텀 훅 패턴 |
|
|
144
|
+
| `references/rendering-patterns.md` | 렌더링 최적화 (useMemo/useCallback, key) + 조건부 렌더링 패턴 |
|
|
145
|
+
| `references/a11y-ux.md` | 에러 처리 (Error Boundary, 서버 상태 위임) + 접근성 (시맨틱 HTML, aria-label, focus-visible) + UX 패턴 (파괴적 액션 확인, URL 동기화, 가상화) |
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# 에러 처리, 접근성 (a11y) 및 UX 패턴
|
|
2
|
+
|
|
3
|
+
## 1. 에러 처리
|
|
4
|
+
|
|
5
|
+
### 원칙: 에러 처리를 각 함수/컴포넌트에 흩뿌리지 않는다
|
|
6
|
+
- 각 함수마다 try-catch를 덕지덕지 붙이지 않는다
|
|
7
|
+
- `useState`로 에러 상태를 직접 관리하지 않는다 (TanStack Query가 제공)
|
|
8
|
+
- 에러 처리는 **경계(Boundary)**에서 한 번에 처리한다
|
|
9
|
+
|
|
10
|
+
### API 에러: 서버 상태 라이브러리에 위임
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// Bad - useState + useEffect로 에러 직접 관리
|
|
14
|
+
function UserList() {
|
|
15
|
+
const [error, setError] = useState<Error | null>(null);
|
|
16
|
+
const [data, setData] = useState(null);
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
fetchUsers()
|
|
22
|
+
.then(setData)
|
|
23
|
+
.catch(setError)
|
|
24
|
+
.finally(() => setLoading(false));
|
|
25
|
+
}, []);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Good - 서버 상태 라이브러리가 에러를 관리
|
|
29
|
+
function UserList() {
|
|
30
|
+
const { data: users, isLoading, error } = useUserList();
|
|
31
|
+
if (error) return <ErrorDisplay error={error} />;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 렌더링 에러: Error Boundary
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
<ErrorBoundary fallback={<ErrorFallback />}>
|
|
39
|
+
<UserContent />
|
|
40
|
+
</ErrorBoundary>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 전역 API 에러: interceptor 또는 라이브러리 설정에서 일괄 처리
|
|
44
|
+
- axios interceptor, QueryClient defaultOptions 등 프로젝트 설정에 따른다
|
|
45
|
+
- 개별 컴포넌트가 아닌 앱 수준에서 에러 알림(toast 등)을 처리한다
|
|
46
|
+
|
|
47
|
+
### 에러 UI 분기: early return 패턴
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
function UserPage({ userId }: UserPageProps) {
|
|
51
|
+
const { data: user, isLoading, error } = useUser(userId);
|
|
52
|
+
|
|
53
|
+
if (isLoading) return <Skeleton />;
|
|
54
|
+
if (error) return <ErrorDisplay error={error} />;
|
|
55
|
+
if (!user) return <EmptyState />;
|
|
56
|
+
|
|
57
|
+
return <UserContent user={user} />;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 2. 접근성 (a11y)
|
|
64
|
+
|
|
65
|
+
### 시맨틱 HTML 사용
|
|
66
|
+
- 클릭 가능한 요소는 반드시 `<button>` 또는 `<a>`를 사용한다
|
|
67
|
+
- `<div onClick>`, `<span onClick>`을 금지한다
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Bad
|
|
71
|
+
<div onClick={handleDelete} className="cursor-pointer">삭제</div>
|
|
72
|
+
|
|
73
|
+
// Good
|
|
74
|
+
<button type="button" onClick={handleDelete}>삭제</button>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 아이콘 버튼에 aria-label 필수
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Bad - 스크린 리더가 내용을 알 수 없음
|
|
81
|
+
<button onClick={onClose}><XIcon /></button>
|
|
82
|
+
|
|
83
|
+
// Good
|
|
84
|
+
<button onClick={onClose} aria-label="닫기"><XIcon /></button>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 이미지에 alt, width, height 필수
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Bad
|
|
91
|
+
<img src={user.avatar} />
|
|
92
|
+
|
|
93
|
+
// Good
|
|
94
|
+
<img src={user.avatar} alt={`${user.name} 프로필`} width={40} height={40} />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### focus-visible 보장
|
|
98
|
+
- `outline: none`을 사용할 때 반드시 `focus-visible` 대체 스타일을 제공한다
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Bad - 포커스 표시 완전 제거
|
|
102
|
+
<button className="outline-none">
|
|
103
|
+
|
|
104
|
+
// Good - 키보드 포커스 시 표시
|
|
105
|
+
<button className="outline-none focus-visible:ring-2 focus-visible:ring-blue-500">
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 폼 접근성
|
|
109
|
+
- 모든 입력 필드에 `<label>` 또는 `aria-label`을 연결한다
|
|
110
|
+
- 적절한 `type`, `inputMode`, `autoComplete` 속성을 사용한다
|
|
111
|
+
- 붙여넣기(`onPaste`)를 차단하지 않는다
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 3. UX 패턴
|
|
116
|
+
|
|
117
|
+
### 파괴적 액션에 확인 단계
|
|
118
|
+
- 삭제, 초기화 등 되돌릴 수 없는 동작에는 확인 UI를 추가한다
|
|
119
|
+
|
|
120
|
+
### URL 파라미터와 UI 상태 동기화
|
|
121
|
+
- 탭, 필터, 페이지 등 공유 가능한 UI 상태는 URL 파라미터에 반영한다
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Bad - 새로고침하면 상태 소실
|
|
125
|
+
const [tab, setTab] = useState('overview');
|
|
126
|
+
|
|
127
|
+
// Good - URL에 상태 반영 (deep-link 가능)
|
|
128
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
129
|
+
const tab = searchParams.get('tab') ?? 'overview';
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 대규모 리스트 가상화
|
|
133
|
+
- 50개 이상의 항목을 렌더링할 때는 가상화 라이브러리를 사용한다
|
|
134
|
+
- `@tanstack/react-virtual`, `react-window` 등을 활용한다
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 렌더링 최적화 및 조건부 렌더링 패턴
|
|
2
|
+
|
|
3
|
+
## 1. 렌더링 최적화
|
|
4
|
+
|
|
5
|
+
### 불필요한 useMemo/useCallback 금지
|
|
6
|
+
- 성능 문제가 실제로 측정된 경우에만 사용한다
|
|
7
|
+
- 참조 동일성이 필요한 경우(의존성 배열, memo된 자식 컴포넌트)에만 사용한다
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Bad - 불필요한 메모이제이션
|
|
11
|
+
const userName = useMemo(() => `${first} ${last}`, [first, last]);
|
|
12
|
+
|
|
13
|
+
// Good - 단순 계산은 그냥 수행
|
|
14
|
+
const userName = `${first} ${last}`;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### key 올바르게 사용
|
|
18
|
+
- 리스트 렌더링 시 고유한 식별자를 `key`로 사용한다
|
|
19
|
+
- 배열 인덱스를 `key`로 사용하지 않는다 (정적 리스트 제외)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Bad
|
|
23
|
+
{items.map((item, index) => (
|
|
24
|
+
<Item key={index} data={item} />
|
|
25
|
+
))}
|
|
26
|
+
|
|
27
|
+
// Good
|
|
28
|
+
{items.map((item) => (
|
|
29
|
+
<Item key={item.id} data={item} />
|
|
30
|
+
))}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. 조건부 렌더링 패턴
|
|
36
|
+
|
|
37
|
+
### 단순 조건
|
|
38
|
+
- 2개 이하의 조건은 삼항 연산자 또는 `&&`를 사용한다
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// 단순 표시/숨김
|
|
42
|
+
{isVisible && <Modal />}
|
|
43
|
+
|
|
44
|
+
// 이분기
|
|
45
|
+
{isLoading ? <Skeleton /> : <Content />}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 복잡한 조건
|
|
49
|
+
- 3개 이상의 분기는 early return 또는 별도 컴포넌트로 분리한다
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Bad - 중첩된 삼항
|
|
53
|
+
{isLoading ? <Skeleton /> : error ? <Error /> : data ? <Content /> : <Empty />}
|
|
54
|
+
|
|
55
|
+
// Good - early return
|
|
56
|
+
function UserContent({ isLoading, error, data }: UserContentProps) {
|
|
57
|
+
if (isLoading) return <Skeleton />;
|
|
58
|
+
if (error) return <ErrorDisplay error={error} />;
|
|
59
|
+
if (!data) return <EmptyState />;
|
|
60
|
+
return <Content data={data} />;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 상태 관리 및 커스텀 훅
|
|
2
|
+
|
|
3
|
+
## 1. 상태 관리 원칙
|
|
4
|
+
|
|
5
|
+
### 가까운 곳에 배치
|
|
6
|
+
- 상태는 그것을 사용하는 가장 가까운 컴포넌트에 배치한다
|
|
7
|
+
- 상위 컴포넌트로의 lifting은 실제로 필요할 때만 수행한다
|
|
8
|
+
|
|
9
|
+
### 파생값은 상태가 아니다
|
|
10
|
+
- 기존 상태에서 계산할 수 있는 값은 별도 상태로 만들지 않는다
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// Bad - 파생값을 상태로 관리
|
|
14
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
15
|
+
const [itemCount, setItemCount] = useState(0);
|
|
16
|
+
// items가 변경될 때마다 setItemCount를 호출해야 함
|
|
17
|
+
|
|
18
|
+
// Good - 파생값은 계산
|
|
19
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
20
|
+
const itemCount = items.length;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 서버 상태와 클라이언트 상태 분리
|
|
24
|
+
- **서버 상태**: API에서 가져온 데이터 -> React Query / SWR 사용
|
|
25
|
+
- **클라이언트 상태**: UI 상태 (모달 열림, 탭 선택 등) -> useState / useReducer 사용
|
|
26
|
+
- 서버 데이터를 `useState`로 복사하지 않는다
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. 커스텀 훅
|
|
31
|
+
|
|
32
|
+
### use 접두사
|
|
33
|
+
- 모든 커스텀 훅은 `use`로 시작한다
|
|
34
|
+
|
|
35
|
+
### 하나의 관심사
|
|
36
|
+
- 하나의 훅은 하나의 관심사만 다룬다
|
|
37
|
+
|
|
38
|
+
### 데이터 페칭/상태 로직 분리
|
|
39
|
+
- 컴포넌트에서 데이터 페칭과 상태 로직을 커스텀 훅으로 분리한다
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// Bad - 컴포넌트에 로직이 섞여 있음
|
|
43
|
+
function UserList() {
|
|
44
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
45
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
46
|
+
const [error, setError] = useState<Error | null>(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
fetchUsers()
|
|
51
|
+
.then(setUsers)
|
|
52
|
+
.catch(setError)
|
|
53
|
+
.finally(() => setIsLoading(false));
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
// ... 렌더링
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Good - 커스텀 훅으로 분리
|
|
60
|
+
function useUserList() {
|
|
61
|
+
const { data: users = [], isLoading, error } = useQuery({
|
|
62
|
+
queryKey: ['users'],
|
|
63
|
+
queryFn: fetchUsers,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return { users, isLoading, error };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function UserList() {
|
|
70
|
+
const { users, isLoading, error } = useUserList();
|
|
71
|
+
// ... 렌더링만 담당
|
|
72
|
+
}
|
|
73
|
+
```
|