@choblue/claude-code-toolkit 1.2.2 → 1.2.4
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
27c83ec4fadcaf1ee2f771651fa76d31262a831a
|
|
@@ -253,7 +253,142 @@ function UserContent({ isLoading, error, data }: UserContentProps) {
|
|
|
253
253
|
|
|
254
254
|
---
|
|
255
255
|
|
|
256
|
-
## 8.
|
|
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. 금지 사항
|
|
257
392
|
|
|
258
393
|
- `any` 타입 사용 금지
|
|
259
394
|
- 인라인 스타일(`style={{}}`) 사용 금지 - 프로젝트 스타일링 방식을 따른다
|
|
@@ -263,4 +398,10 @@ function UserContent({ isLoading, error, data }: UserContentProps) {
|
|
|
263
398
|
- `React.FC` 사용 금지
|
|
264
399
|
- 배열 인덱스를 `key`로 사용 금지 (정적 리스트 제외)
|
|
265
400
|
- `useEffect` 내에서 상태 동기화 로직 작성 금지 (파생값으로 처리)
|
|
266
|
-
- Props drilling이 3단계 이상일 때 Context 또는 상태 관리 라이브러리 미사용 금지
|
|
401
|
+
- Props drilling이 3단계 이상일 때 Context 또는 상태 관리 라이브러리 미사용 금지
|
|
402
|
+
- 서버 상태(API 데이터)를 `useState` + `useEffect`로 관리 금지 (서버 상태 라이브러리 사용)
|
|
403
|
+
- 각 함수/컴포넌트마다 try-catch 남발 금지 (에러 경계에서 일괄 처리)
|
|
404
|
+
- `<div onClick>`, `<span onClick>` 금지 (`<button>` 또는 `<a>` 사용)
|
|
405
|
+
- 아이콘 버튼에 `aria-label` 누락 금지
|
|
406
|
+
- `outline: none` 단독 사용 금지 (`focus-visible` 대체 필수)
|
|
407
|
+
- 입력 필드에 `onPaste` 차단 금지
|
|
@@ -361,7 +361,43 @@ export function Button({ variant, size, className, ...props }: ButtonProps) {
|
|
|
361
361
|
|
|
362
362
|
---
|
|
363
363
|
|
|
364
|
-
## 8.
|
|
364
|
+
## 8. 트랜지션 & 모션
|
|
365
|
+
|
|
366
|
+
### transition 속성 명시
|
|
367
|
+
- `transition-all` 사용을 금지한다 - 변경되는 속성만 명시한다
|
|
368
|
+
- 불필요한 속성까지 트랜지션되면 성능이 저하된다
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// Bad - 모든 속성에 트랜지션
|
|
372
|
+
<button className="transition-all">
|
|
373
|
+
|
|
374
|
+
// Good - 필요한 속성만
|
|
375
|
+
<button className="transition-colors">
|
|
376
|
+
<div className="transition-[transform,opacity]">
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### prefers-reduced-motion 존중
|
|
380
|
+
- 애니메이션/트랜지션이 있는 요소에는 `motion-reduce:` 변형을 고려한다
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// 모션 감소 선호 시 애니메이션 비활성화
|
|
384
|
+
<div className="animate-bounce motion-reduce:animate-none">
|
|
385
|
+
<div className="transition-transform motion-reduce:transition-none">
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 다크 모드 color-scheme
|
|
389
|
+
- 다크 모드 지원 시 `color-scheme: dark`를 설정하여 네이티브 UI(스크롤바, 입력 필드 등)도 다크 테마에 맞춘다
|
|
390
|
+
|
|
391
|
+
```css
|
|
392
|
+
/* app.css */
|
|
393
|
+
.dark {
|
|
394
|
+
color-scheme: dark;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 9. 금지 사항
|
|
365
401
|
|
|
366
402
|
- 인라인 `style={{}}` 사용 금지 - Tailwind 유틸리티 클래스를 사용한다
|
|
367
403
|
- 임의값(arbitrary values) 남용 금지 - `w-[137px]` 같은 임의값은 최소화한다
|
|
@@ -373,6 +409,7 @@ export function Button({ variant, size, className, ...props }: ButtonProps) {
|
|
|
373
409
|
- Tailwind 기본 테마 토큰 삭제 금지 - `@theme`에서 필요한 토큰만 추가한다
|
|
374
410
|
- 사용하지 않는 커스텀 색상/간격 정의 금지 - 실제 사용하는 값만 정의한다
|
|
375
411
|
- `tailwind.config.ts` 사용 금지 - v4에서는 CSS `@theme`으로 설정한다
|
|
412
|
+
- `transition-all` 사용 금지 - 변경되는 속성만 명시한다 (`transition-colors`, `transition-opacity` 등)
|
|
376
413
|
- 클래스 문자열 동적 생성 금지 - Tailwind의 JIT가 감지하지 못한다
|
|
377
414
|
|
|
378
415
|
```typescript
|
|
@@ -237,7 +237,56 @@ const queryClient = new QueryClient({
|
|
|
237
237
|
|
|
238
238
|
---
|
|
239
239
|
|
|
240
|
-
## 9.
|
|
240
|
+
## 9. 캐시 전략 가이드
|
|
241
|
+
|
|
242
|
+
데이터 특성에 따라 `staleTime`을 다르게 설정한다.
|
|
243
|
+
|
|
244
|
+
| 데이터 유형 | staleTime | 예시 |
|
|
245
|
+
|------------|-----------|------|
|
|
246
|
+
| 거의 안 바뀜 | `Infinity` | 코드 테이블, 카테고리 목록, 약관 |
|
|
247
|
+
| 가끔 바뀜 | `10~30분` | 사용자 프로필, 설정 |
|
|
248
|
+
| 자주 바뀜 | `1~5분` | 게시글 목록, 댓글 |
|
|
249
|
+
| 실시간 필요 | `0` | 채팅, 알림, 재고 수량 |
|
|
250
|
+
|
|
251
|
+
### 도메인별 staleTime 설정
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// queryKey 팩토리에서 기본 옵션을 함께 관리
|
|
255
|
+
export const categoryKeys = {
|
|
256
|
+
all: ['categories'] as const,
|
|
257
|
+
list: () => [...categoryKeys.all, 'list'] as const,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// 거의 안 바뀌는 데이터
|
|
261
|
+
useQuery({
|
|
262
|
+
queryKey: categoryKeys.list(),
|
|
263
|
+
queryFn: fetchCategories,
|
|
264
|
+
staleTime: Infinity,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// 자주 바뀌는 데이터
|
|
268
|
+
useQuery({
|
|
269
|
+
queryKey: postKeys.list(filters),
|
|
270
|
+
queryFn: () => fetchPosts(filters),
|
|
271
|
+
staleTime: 1000 * 60, // 1분
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 실시간 데이터: refetchInterval 사용
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// 폴링 방식 (WebSocket이 없을 때)
|
|
279
|
+
useQuery({
|
|
280
|
+
queryKey: ['notifications'],
|
|
281
|
+
queryFn: fetchNotifications,
|
|
282
|
+
staleTime: 0,
|
|
283
|
+
refetchInterval: 1000 * 30, // 30초마다 재요청
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 10. 금지 사항
|
|
241
290
|
|
|
242
291
|
- `useEffect`로 데이터 페칭 금지 - TanStack Query를 사용한다
|
|
243
292
|
- queryKey 하드코딩 금지 - queryKey 팩토리 패턴을 사용한다
|