@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.
Files changed (31) hide show
  1. package/.claude/.project-map-cache +1 -1
  2. package/.claude/CLAUDE.md +80 -18
  3. package/.claude/hooks/prompt-hook.sh +4 -4
  4. package/.claude/prompts/feature.md +11 -0
  5. package/.claude/prompts/fix.md +11 -0
  6. package/.claude/prompts/review.md +7 -0
  7. package/.claude/skills/Coding/SKILL.md +5 -4
  8. package/.claude/skills/Curation/SKILL.md +36 -0
  9. package/.claude/skills/FailureRecovery/SKILL.md +47 -0
  10. package/.claude/skills/{Coding/backend.md → NestJS/SKILL.md} +7 -2
  11. package/.claude/skills/NextJS/SKILL.md +13 -303
  12. package/.claude/skills/NextJS/references/data-fetching.md +96 -0
  13. package/.claude/skills/NextJS/references/middleware-actions.md +74 -0
  14. package/.claude/skills/NextJS/references/optimization.md +127 -0
  15. package/.claude/skills/Planning/SKILL.md +30 -7
  16. package/.claude/skills/React/SKILL.md +25 -287
  17. package/.claude/skills/React/references/a11y-ux.md +134 -0
  18. package/.claude/skills/React/references/rendering-patterns.md +62 -0
  19. package/.claude/skills/React/references/state-hooks.md +73 -0
  20. package/.claude/skills/ReactHookForm/SKILL.md +8 -196
  21. package/.claude/skills/ReactHookForm/references/advanced-patterns.md +193 -0
  22. package/.claude/skills/TDD/SKILL.md +2 -2
  23. package/.claude/skills/TailwindCSS/SKILL.md +14 -240
  24. package/.claude/skills/TailwindCSS/references/patterns-components.md +93 -0
  25. package/.claude/skills/TailwindCSS/references/responsive-dark.md +102 -0
  26. package/.claude/skills/TailwindCSS/references/transitions.md +33 -0
  27. package/README.md +25 -12
  28. package/package.json +1 -1
  29. package/.claude/skills/Coding/frontend.md +0 -11
  30. /package/.claude/skills/TDD/{backend.md → references/backend.md} +0 -0
  31. /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 interface 명시
20
- - 모든 컴포넌트는 Props 타입을 `interface`로 선언한다
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
- interface UserPageProps {
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
- interface ButtonProps {
71
+ type ButtonProps = {
72
72
  clickHandler: () => void;
73
73
  deleteCallback: () => void;
74
- }
74
+ };
75
75
 
76
76
  // Good
77
- interface ButtonProps {
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
- interface UserAvatarProps {
89
+ type UserAvatarProps = {
90
90
  user: User; // user.name, user.avatar만 사용하는데 전체 객체 전달
91
- }
91
+ };
92
92
 
93
93
  // Good - 필요한 값만 전달
94
- interface UserAvatarProps {
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
- ## 7. 네이밍 컨벤션
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
- ## 8. 에러 처리
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
+ ```