@choblue/claude-code-toolkit 1.0.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.
@@ -0,0 +1,368 @@
1
+ # TailwindCSS Skill - Tailwind CSS 규칙
2
+
3
+ Tailwind CSS를 사용하는 프로젝트에 적용되는 스타일링 규칙이다.
4
+ React 규칙은 `../React/SKILL.md`, 공통 원칙은 `../Coding/SKILL.md`를 함께 참고한다.
5
+
6
+ ---
7
+
8
+ ## 1. 유틸리티 클래스 사용 원칙
9
+
10
+ ### 인라인 스타일 대신 유틸리티 클래스
11
+ - `style={{}}` 대신 Tailwind 유틸리티 클래스를 사용한다
12
+ - CSS 파일을 별도로 작성하지 않는다 (특수한 경우 제외)
13
+
14
+ ```typescript
15
+ // Bad - 인라인 스타일
16
+ <div style={{ display: 'flex', gap: '8px', padding: '16px' }}>
17
+
18
+ // Good - 유틸리티 클래스
19
+ <div className="flex gap-2 p-4">
20
+ ```
21
+
22
+ ### 클래스 조합
23
+ - 관련 있는 클래스를 논리적 그룹으로 정렬한다
24
+ - 권장 순서: 레이아웃 -> 크기 -> 간격 -> 타이포그래피 -> 색상 -> 효과
25
+
26
+ ```typescript
27
+ // Good - 논리적 순서로 정렬
28
+ <button className="flex items-center justify-center w-full h-10 px-4 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
29
+ 버튼
30
+ </button>
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 2. 반응형 디자인
36
+
37
+ ### 브레이크포인트
38
+ | 접두사 | 최소 너비 | 용도 |
39
+ |--------|-----------|------|
40
+ | (없음) | 0px | 모바일 기본 |
41
+ | `sm:` | 640px | 소형 태블릿 |
42
+ | `md:` | 768px | 태블릿 |
43
+ | `lg:` | 1024px | 데스크톱 |
44
+ | `xl:` | 1280px | 대형 데스크톱 |
45
+ | `2xl:` | 1536px | 초대형 데스크톱 |
46
+
47
+ ### Mobile-First 원칙
48
+ - 모바일 스타일을 기본으로 작성하고, 큰 화면에 대해 재정의한다
49
+
50
+ ```typescript
51
+ // Good - Mobile-First
52
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
53
+ {items.map((item) => (
54
+ <Card key={item.id} data={item} />
55
+ ))}
56
+ </div>
57
+
58
+ // Good - 반응형 타이포그래피
59
+ <h1 className="text-2xl font-bold md:text-3xl lg:text-4xl">
60
+ 제목
61
+ </h1>
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 3. 다크 모드
67
+
68
+ ### dark: prefix 사용
69
+ - `dark:` 접두사로 다크 모드 스타일을 정의한다
70
+ - v4에서는 기본적으로 `prefers-color-scheme` 미디어 쿼리로 동작한다
71
+ - 수동 토글(class 전략)이 필요하면 CSS에서 `@custom-variant`를 설정한다
72
+
73
+ ```css
74
+ /* app.css - 수동 다크 모드 토글 시 */
75
+ @import "tailwindcss";
76
+ @custom-variant dark (&:where(.dark, .dark *));
77
+ ```
78
+
79
+ ```typescript
80
+ // 컴포넌트에서 사용
81
+ <div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
82
+ <p className="text-gray-600 dark:text-gray-300">
83
+ 라이트/다크 모드를 지원하는 텍스트
84
+ </p>
85
+ </div>
86
+ ```
87
+
88
+ ### 시맨틱 색상 활용
89
+ - 반복되는 라이트/다크 색상 조합은 `@theme`과 CSS 변수로 추출한다
90
+
91
+ ```css
92
+ /* app.css */
93
+ @import "tailwindcss";
94
+
95
+ @theme {
96
+ --color-background: #ffffff;
97
+ --color-foreground: #0a0a0a;
98
+ }
99
+
100
+ .dark {
101
+ --color-background: #0a0a0a;
102
+ --color-foreground: #fafafa;
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 4. 커스텀 설정
109
+
110
+ ### @theme 디렉티브 사용
111
+ - CSS 파일에서 `@theme` 디렉티브로 디자인 토큰을 정의한다
112
+ - v4에서는 `tailwind.config.ts`가 불필요하다. 모든 커스텀 설정은 CSS의 `@theme`에서 정의한다
113
+ - v4는 콘텐츠 파일을 자동 감지하므로 `content` 배열 설정이 불필요하다
114
+
115
+ ```css
116
+ /* app.css */
117
+ @import "tailwindcss";
118
+
119
+ @theme {
120
+ --color-primary-50: #eff6ff;
121
+ --color-primary-100: #dbeafe;
122
+ --color-primary-500: #3b82f6;
123
+ --color-primary-600: #2563eb;
124
+ --color-primary-700: #1d4ed8;
125
+ --color-success: #10b981;
126
+ --color-warning: #f59e0b;
127
+ --color-danger: #ef4444;
128
+
129
+ --spacing-18: 4.5rem;
130
+ --spacing-88: 22rem;
131
+
132
+ --font-sans: 'Inter', sans-serif;
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## 5. 자주 쓰는 패턴
139
+
140
+ ### Flexbox 레이아웃
141
+ ```typescript
142
+ // 가로 정렬 (중앙)
143
+ <div className="flex items-center justify-center">
144
+
145
+ // 가로 정렬 (양쪽 분산)
146
+ <div className="flex items-center justify-between">
147
+
148
+ // 세로 정렬
149
+ <div className="flex flex-col gap-4">
150
+
151
+ // 자식 요소 간격
152
+ <div className="flex gap-2">
153
+ ```
154
+
155
+ ### Grid 레이아웃
156
+ ```typescript
157
+ // 반응형 그리드
158
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
159
+
160
+ // 고정 비율 그리드
161
+ <div className="grid grid-cols-[1fr_2fr] gap-4">
162
+
163
+ // 사이드바 레이아웃
164
+ <div className="grid grid-cols-[240px_1fr] gap-8">
165
+ ```
166
+
167
+ ### Spacing 패턴
168
+ ```typescript
169
+ // 섹션 간격
170
+ <section className="py-12 md:py-16 lg:py-20">
171
+
172
+ // 컨테이너
173
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
174
+
175
+ // 카드
176
+ <div className="rounded-lg border p-6 shadow-sm">
177
+ ```
178
+
179
+ ### Typography 패턴
180
+ ```typescript
181
+ // 텍스트 말줄임
182
+ <p className="truncate">긴 텍스트...</p>
183
+
184
+ // 여러 줄 말줄임
185
+ <p className="line-clamp-3">긴 텍스트...</p>
186
+
187
+ // 반응형 텍스트
188
+ <h1 className="text-2xl font-bold tracking-tight md:text-4xl">
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 6. cn() 유틸리티
194
+
195
+ ### clsx + tailwind-merge 조합
196
+ - 조건부 클래스 결합에는 `cn()` 유틸리티를 사용한다
197
+ - `clsx`로 조건부 결합하고, `tailwind-merge`로 충돌을 해결한다
198
+
199
+ ```typescript
200
+ // lib/utils.ts
201
+ import { type ClassValue, clsx } from 'clsx';
202
+ import { twMerge } from 'tailwind-merge';
203
+
204
+ export function cn(...inputs: ClassValue[]) {
205
+ return twMerge(clsx(inputs));
206
+ }
207
+ ```
208
+
209
+ ### 사용 예시
210
+ ```typescript
211
+ import { cn } from '@/lib/utils';
212
+
213
+ interface ButtonProps {
214
+ variant?: 'primary' | 'secondary' | 'danger';
215
+ size?: 'sm' | 'md' | 'lg';
216
+ className?: string;
217
+ children: React.ReactNode;
218
+ }
219
+
220
+ export function Button({ variant = 'primary', size = 'md', className, children }: ButtonProps) {
221
+ return (
222
+ <button
223
+ className={cn(
224
+ // 기본 스타일
225
+ 'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
226
+ // variant
227
+ {
228
+ 'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
229
+ 'bg-gray-100 text-gray-900 hover:bg-gray-200': variant === 'secondary',
230
+ 'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
231
+ },
232
+ // size
233
+ {
234
+ 'h-8 px-3 text-sm': size === 'sm',
235
+ 'h-10 px-4 text-sm': size === 'md',
236
+ 'h-12 px-6 text-base': size === 'lg',
237
+ },
238
+ // 외부에서 전달된 클래스 (오버라이드 가능)
239
+ className,
240
+ )}
241
+ >
242
+ {children}
243
+ </button>
244
+ );
245
+ }
246
+ ```
247
+
248
+ ### CVA (Class Variance Authority)
249
+ - variant가 있는 컴포넌트는 `cva`로 스타일을 정의한다
250
+ - `cn()` + `cva` 조합으로 variant 관리와 클래스 오버라이드를 동시에 처리한다
251
+
252
+ ```typescript
253
+ // components/Button.tsx
254
+ import { cva, type VariantProps } from 'class-variance-authority';
255
+ import { cn } from '@/lib/utils';
256
+
257
+ const buttonVariants = cva(
258
+ // 기본 스타일
259
+ 'inline-flex items-center justify-center rounded-lg font-medium transition-colors disabled:pointer-events-none disabled:opacity-50',
260
+ {
261
+ variants: {
262
+ variant: {
263
+ primary: 'bg-blue-600 text-white hover:bg-blue-700',
264
+ secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
265
+ danger: 'bg-red-600 text-white hover:bg-red-700',
266
+ ghost: 'hover:bg-gray-100',
267
+ link: 'text-blue-600 underline-offset-4 hover:underline',
268
+ },
269
+ size: {
270
+ sm: 'h-8 px-3 text-sm',
271
+ md: 'h-10 px-4 text-sm',
272
+ lg: 'h-12 px-6 text-base',
273
+ icon: 'h-10 w-10',
274
+ },
275
+ },
276
+ defaultVariants: {
277
+ variant: 'primary',
278
+ size: 'md',
279
+ },
280
+ }
281
+ );
282
+
283
+ interface ButtonProps
284
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
285
+ VariantProps<typeof buttonVariants> {}
286
+
287
+ export function Button({ variant, size, className, ...props }: ButtonProps) {
288
+ return (
289
+ <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
290
+ );
291
+ }
292
+ ```
293
+
294
+ ### CVA 사용 원칙
295
+ - variant가 2개 이상이면 `cva`를 사용한다 (1개면 `cn()`으로 충분)
296
+ - `VariantProps<typeof xxxVariants>`로 Props 타입을 자동 추론한다
297
+ - `defaultVariants`로 기본값을 선언한다
298
+ - 컴포넌트 외부에서 `className`으로 오버라이드할 수 있게 `cn()`을 함께 사용한다
299
+
300
+ ---
301
+
302
+ ## 7. 컴포넌트 추출
303
+
304
+ ### 반복 클래스 처리
305
+ - 동일한 클래스 조합이 3회 이상 반복되면 컴포넌트로 추출한다
306
+ - `@apply`보다 컴포넌트 추출을 우선한다
307
+
308
+ ```typescript
309
+ // Bad - 동일한 클래스 반복
310
+ <button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">저장</button>
311
+ <button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">확인</button>
312
+ <button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">전송</button>
313
+
314
+ // Good - 컴포넌트로 추출
315
+ <Button>저장</Button>
316
+ <Button>확인</Button>
317
+ <Button>전송</Button>
318
+ ```
319
+
320
+ ### @apply 사용 (제한적 허용)
321
+ - 컴포넌트 추출이 어려운 경우에만 `@apply`를 사용한다
322
+ - 주로 base layer의 글로벌 스타일에서 사용한다
323
+ - v4에서도 `@apply`는 지원되지만, 여전히 컴포넌트 추출을 우선한다
324
+
325
+ ```css
326
+ /* 허용 - 글로벌 기본 스타일 */
327
+ @layer base {
328
+ body {
329
+ @apply bg-background text-foreground;
330
+ }
331
+ }
332
+
333
+ /* 지양 - 컴포넌트 스타일을 @apply로 작성 */
334
+ .btn-primary {
335
+ @apply rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700;
336
+ }
337
+ ```
338
+
339
+ ---
340
+
341
+ ## 8. 금지 사항
342
+
343
+ - 인라인 `style={{}}` 사용 금지 - Tailwind 유틸리티 클래스를 사용한다
344
+ - 임의값(arbitrary values) 남용 금지 - `w-[137px]` 같은 임의값은 최소화한다
345
+ - 디자인 시스템의 토큰에 맞는 값을 사용한다
346
+ - 불가피한 경우에만 임의값을 사용한다
347
+ - `@apply` 남용 금지 - 컴포넌트 추출을 우선한다
348
+ - `!important` 사용 금지 - Tailwind의 `!` 접두사(`!p-4`)도 최소화한다
349
+ - 커스텀 CSS 파일 남발 금지 - `globals.css` 외에 CSS 파일을 추가하지 않는다
350
+ - Tailwind 기본 테마 토큰 삭제 금지 - `@theme`에서 필요한 토큰만 추가한다
351
+ - 사용하지 않는 커스텀 색상/간격 정의 금지 - 실제 사용하는 값만 정의한다
352
+ - `tailwind.config.ts` 사용 금지 - v4에서는 CSS `@theme`으로 설정한다
353
+ - 클래스 문자열 동적 생성 금지 - Tailwind의 JIT가 감지하지 못한다
354
+
355
+ ```typescript
356
+ // Bad - 동적 클래스 생성 (JIT가 감지 불가)
357
+ const color = 'blue';
358
+ <div className={`bg-${color}-500`}>
359
+
360
+ // Good - 완전한 클래스명 사용
361
+ const colorClasses = {
362
+ blue: 'bg-blue-500',
363
+ red: 'bg-red-500',
364
+ green: 'bg-green-500',
365
+ } as const;
366
+
367
+ <div className={colorClasses[color]}>
368
+ ```
@@ -0,0 +1,242 @@
1
+ # TanStack Query Skill - 서버 상태 관리 규칙
2
+
3
+ TanStack Query (React Query)를 사용한 서버 상태 관리 규칙을 정의한다.
4
+ 클라이언트 상태 관리는 `../Zustand/SKILL.md`를 참고한다.
5
+
6
+ ---
7
+
8
+ ## 1. 기본 개념
9
+
10
+ ### 서버 상태 vs 클라이언트 상태
11
+ - **서버 상태**: API에서 가져오는 데이터 (사용자 목록, 게시글 등)
12
+ - **클라이언트 상태**: UI에서만 존재하는 데이터 (모달 열림/닫힘, 폼 입력값 등)
13
+ - TanStack Query는 서버 상태만 관리한다
14
+
15
+ ### Stale-While-Revalidate
16
+ - 캐시된 데이터(stale)를 먼저 보여주고, 백그라운드에서 최신 데이터를 가져온다
17
+ - 사용자 경험과 데이터 최신성을 동시에 확보한다
18
+
19
+ ---
20
+
21
+ ## 2. useQuery 패턴
22
+
23
+ ### 기본 사용법
24
+
25
+ ```typescript
26
+ // Good
27
+ const { data, isLoading, error } = useQuery({
28
+ queryKey: ['users', { status: 'active' }],
29
+ queryFn: () => userApi.getUsers({ status: 'active' }),
30
+ });
31
+
32
+ // Good - enabled로 조건부 실행
33
+ const { data: user } = useQuery({
34
+ queryKey: ['users', userId],
35
+ queryFn: () => userApi.getUserById(userId),
36
+ enabled: !!userId,
37
+ });
38
+
39
+ // Good - select로 데이터 변환
40
+ const { data: userNames } = useQuery({
41
+ queryKey: ['users'],
42
+ queryFn: () => userApi.getUsers(),
43
+ select: (users) => users.map((user) => user.name),
44
+ });
45
+ ```
46
+
47
+ ### queryKey 컨벤션
48
+ - 배열 형태로 작성한다
49
+ - 첫 번째 요소는 엔티티명 (복수형)
50
+ - 이후 요소는 필터/파라미터
51
+
52
+ ```typescript
53
+ // 목록
54
+ ['users']
55
+ ['users', { status: 'active', page: 1 }]
56
+
57
+ // 단건
58
+ ['users', userId]
59
+
60
+ // 관계 데이터
61
+ ['users', userId, 'posts']
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 3. queryKey 팩토리 패턴
67
+
68
+ queryKey를 객체로 중앙 관리하여 일관성과 재사용성을 확보한다.
69
+
70
+ ```typescript
71
+ export const userKeys = {
72
+ all: ['users'] as const,
73
+ lists: () => [...userKeys.all, 'list'] as const,
74
+ list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
75
+ details: () => [...userKeys.all, 'detail'] as const,
76
+ detail: (id: string) => [...userKeys.details(), id] as const,
77
+ };
78
+
79
+ // 사용
80
+ useQuery({
81
+ queryKey: userKeys.detail(userId),
82
+ queryFn: () => userApi.getUserById(userId),
83
+ });
84
+
85
+ // Invalidation - 모든 user 목록 캐시 무효화
86
+ queryClient.invalidateQueries({ queryKey: userKeys.lists() });
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 4. useMutation 패턴
92
+
93
+ ### 기본 사용법
94
+
95
+ ```typescript
96
+ const createUser = useMutation({
97
+ mutationFn: (data: CreateUserDto) => userApi.createUser(data),
98
+ onSuccess: () => {
99
+ queryClient.invalidateQueries({ queryKey: userKeys.lists() });
100
+ },
101
+ onError: (error) => {
102
+ toast.error(`사용자 생성 실패: ${error.message}`);
103
+ },
104
+ });
105
+
106
+ // 호출
107
+ createUser.mutate({ name: '홍길동', email: 'hong@example.com' });
108
+ ```
109
+
110
+ ### onSuccess에서 반드시 캐시를 무효화한다
111
+
112
+ | 작업 | invalidateQueries 대상 |
113
+ |------|------------------------|
114
+ | 생성 | 목록 쿼리 (`lists()`) |
115
+ | 수정 | 목록 + 해당 상세 (`all`) 또는 정밀 지정 |
116
+ | 삭제 | 목록 쿼리 (`lists()`) |
117
+
118
+ ---
119
+
120
+ ## 5. Optimistic Update 패턴
121
+
122
+ 사용자 경험을 위해 서버 응답 전에 UI를 먼저 업데이트한다.
123
+
124
+ ```typescript
125
+ const updateUser = useMutation({
126
+ mutationFn: (data: UpdateUserDto) => userApi.updateUser(data),
127
+ onMutate: async (newData) => {
128
+ // 1. 진행 중인 쿼리 취소 (덮어쓰기 방지)
129
+ await queryClient.cancelQueries({ queryKey: userKeys.detail(newData.id) });
130
+
131
+ // 2. 이전 데이터 스냅샷 저장
132
+ const previousUser = queryClient.getQueryData(userKeys.detail(newData.id));
133
+
134
+ // 3. 캐시를 낙관적으로 업데이트
135
+ queryClient.setQueryData(userKeys.detail(newData.id), (old: User) => ({
136
+ ...old,
137
+ ...newData,
138
+ }));
139
+
140
+ // 4. 롤백용 컨텍스트 반환
141
+ return { previousUser };
142
+ },
143
+ onError: (_error, newData, context) => {
144
+ // 에러 시 이전 데이터로 롤백
145
+ queryClient.setQueryData(userKeys.detail(newData.id), context?.previousUser);
146
+ },
147
+ onSettled: (_data, _error, variables) => {
148
+ // 성공/실패 무관하게 캐시 재검증
149
+ queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) });
150
+ },
151
+ });
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 6. Prefetching
157
+
158
+ ### 라우트 전환 시 Prefetching
159
+
160
+ ```typescript
161
+ // 마우스 호버 시 미리 데이터를 가져온다
162
+ function UserListItem({ userId }: { userId: string }) {
163
+ const queryClient = useQueryClient();
164
+
165
+ const handleMouseEnter = () => {
166
+ queryClient.prefetchQuery({
167
+ queryKey: userKeys.detail(userId),
168
+ queryFn: () => userApi.getUserById(userId),
169
+ staleTime: 1000 * 60 * 5, // 5분 동안 재요청 방지
170
+ });
171
+ };
172
+
173
+ return (
174
+ <Link to={`/users/${userId}`} onMouseEnter={handleMouseEnter}>
175
+ 사용자 상세
176
+ </Link>
177
+ );
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## 7. Suspense 모드
184
+
185
+ React Suspense와 함께 사용하여 로딩 처리를 선언적으로 한다.
186
+
187
+ ```typescript
188
+ // useSuspenseQuery는 data가 항상 존재함을 보장한다
189
+ function UserProfile({ userId }: { userId: string }) {
190
+ const { data: user } = useSuspenseQuery({
191
+ queryKey: userKeys.detail(userId),
192
+ queryFn: () => userApi.getUserById(userId),
193
+ });
194
+
195
+ // user는 undefined가 아님 - 타입 안전
196
+ return <div>{user.name}</div>;
197
+ }
198
+
199
+ // 부모에서 Suspense로 감싼다
200
+ function UserPage({ userId }: { userId: string }) {
201
+ return (
202
+ <Suspense fallback={<UserProfileSkeleton />}>
203
+ <UserProfile userId={userId} />
204
+ </Suspense>
205
+ );
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## 8. QueryClient 설정
212
+
213
+ ```typescript
214
+ const queryClient = new QueryClient({
215
+ defaultOptions: {
216
+ queries: {
217
+ staleTime: 1000 * 60 * 5, // 5분 - 데이터가 stale로 간주되기까지의 시간
218
+ gcTime: 1000 * 60 * 30, // 30분 - 미사용 캐시 제거까지의 시간
219
+ retry: 1, // 실패 시 1회 재시도
220
+ refetchOnWindowFocus: false, // 윈도우 포커스 시 자동 refetch 비활성화
221
+ },
222
+ },
223
+ });
224
+ ```
225
+
226
+ | 옵션 | 권장값 | 설명 |
227
+ |------|--------|------|
228
+ | `staleTime` | `5분` | 짧으면 요청 과다, 길면 데이터 지연 |
229
+ | `gcTime` | `30분` | staleTime보다 길어야 한다 |
230
+ | `retry` | `1` | 네트워크 오류 대비 최소 재시도 |
231
+ | `refetchOnWindowFocus` | `false` | 프로젝트 요구사항에 따라 조정 |
232
+
233
+ ---
234
+
235
+ ## 9. 금지 사항
236
+
237
+ - `useEffect`로 데이터 페칭 금지 - TanStack Query를 사용한다
238
+ - queryKey 하드코딩 금지 - queryKey 팩토리 패턴을 사용한다
239
+ - 불필요한 `refetchOnWindowFocus: true` 설정 금지 - 기본값 대신 명시적으로 관리한다
240
+ - `onSuccess` 콜백 안에서 상태 동기화 금지 (`useState`에 서버 데이터 복사 등)
241
+ - `queryFn` 안에서 에러를 삼키는 try-catch 금지 - TanStack Query가 에러를 관리하게 한다
242
+ - `cacheTime` 사용 금지 - v5부터 `gcTime`으로 변경되었다