@choblue/claude-code-toolkit 1.2.6 → 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.
@@ -0,0 +1,127 @@
1
+ # Metadata, 디렉토리 구조 패턴 & Image/Font 최적화
2
+
3
+ ## Metadata
4
+
5
+ ### 정적 Metadata
6
+ ```typescript
7
+ // app/about/page.tsx
8
+ import type { Metadata } from 'next';
9
+
10
+ export const metadata: Metadata = {
11
+ title: '소개 - 서비스명',
12
+ description: '서비스 소개 페이지입니다.',
13
+ };
14
+ ```
15
+
16
+ ### 동적 Metadata
17
+ ```typescript
18
+ // app/users/[id]/page.tsx
19
+ import type { Metadata } from 'next';
20
+
21
+ export async function generateMetadata({
22
+ params,
23
+ }: {
24
+ params: { id: string };
25
+ }): Promise<Metadata> {
26
+ const user = await getUser(params.id);
27
+
28
+ return {
29
+ title: `${user.name} - 프로필`,
30
+ description: `${user.name}의 프로필 페이지입니다.`,
31
+ };
32
+ }
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 디렉토리 구조 패턴
38
+
39
+ ### Route Groups - `(group)`
40
+ - URL에 영향을 주지 않고 라우트를 그룹화한다
41
+
42
+ ```
43
+ app/
44
+ (marketing)/
45
+ about/page.tsx # /about
46
+ blog/page.tsx # /blog
47
+ (dashboard)/
48
+ layout.tsx # dashboard 전용 레이아웃
49
+ settings/page.tsx # /settings
50
+ profile/page.tsx # /profile
51
+ ```
52
+
53
+ ### Private Folders - `_folder`
54
+ - 라우팅에서 제외되는 내부 폴더
55
+
56
+ ```
57
+ app/
58
+ _components/ # 라우팅에 포함되지 않음
59
+ Header.tsx
60
+ Footer.tsx
61
+ _lib/ # 내부 유틸리티
62
+ utils.ts
63
+ ```
64
+
65
+ ### Parallel Routes - `@slot`
66
+ - 동일 레이아웃에서 여러 페이지를 동시에 렌더링한다
67
+
68
+ ```
69
+ app/
70
+ layout.tsx # children + @analytics + @team 동시 렌더링
71
+ @analytics/
72
+ page.tsx
73
+ @team/
74
+ page.tsx
75
+ ```
76
+
77
+ ### Intercepting Routes - `(.)`, `(..)`, `(...)`
78
+ - 현재 레이아웃 내에서 다른 라우트를 가로채서 표시한다
79
+
80
+ ```
81
+ app/
82
+ feed/
83
+ page.tsx
84
+ (..)photo/[id]/ # /photo/:id를 모달로 가로챔
85
+ page.tsx
86
+ photo/[id]/
87
+ page.tsx # 직접 접근 시 전체 페이지
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Image/Font 최적화
93
+
94
+ ### next/image
95
+ - 모든 이미지는 `next/image`를 사용한다
96
+ - `width`, `height`를 명시하거나 `fill` 속성을 사용한다
97
+
98
+ ```typescript
99
+ import Image from 'next/image';
100
+
101
+ // 크기 지정
102
+ <Image src="/hero.png" alt="히어로 이미지" width={800} height={400} />
103
+
104
+ // fill 모드 (부모 기준 채움)
105
+ <div className="relative h-64 w-full">
106
+ <Image src="/banner.png" alt="배너" fill className="object-cover" />
107
+ </div>
108
+ ```
109
+
110
+ ### next/font
111
+ - Google Fonts는 `next/font/google`을 사용한다
112
+ - 커스텀 폰트는 `next/font/local`을 사용한다
113
+
114
+ ```typescript
115
+ // app/layout.tsx
116
+ import { Inter } from 'next/font/google';
117
+
118
+ const inter = Inter({ subsets: ['latin'] });
119
+
120
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
121
+ return (
122
+ <html lang="ko" className={inter.className}>
123
+ <body>{children}</body>
124
+ </html>
125
+ );
126
+ }
127
+ ```
@@ -32,6 +32,22 @@ L 티어는 채팅이 아닌 **파일 기반**으로 설계를 진행한다. 세
32
32
  - [기술적 제약, 호환성 이슈 등]
33
33
  ```
34
34
 
35
+ ### Step 1.5: Task Header 출력
36
+ research 후, plan 작성 전에 Task Header를 출력한다:
37
+ ```
38
+ 📋 [작업명]
39
+ ⚡ L — [판단 근거 한 줄]
40
+ 📚 [사용할 스킬 목록]
41
+ 🔄 [에이전트 흐름]
42
+ 📁 [예상 파일 수와 대상]
43
+ 📌 Plan:
44
+ ○ Step 1: [작업 단위]
45
+ ○ Step 2: [작업 단위]
46
+ ...
47
+ ```
48
+ 각 Step 진행에 따라 상태를 갱신한다:
49
+ - `○` 대기 → `▶ ⏳` 진행 중 → `✔` 완료
50
+
35
51
  ### Step 2: plan.md 작성
36
52
  research 기반으로 구현 계획을 작성한다.
37
53
  ```markdown
@@ -57,17 +73,24 @@ research 기반으로 구현 계획을 작성한다.
57
73
  3. **사용자가 승인할 때까지 구현하지 않는다**
58
74
  4. 승인 후 implementer 에이전트에 plan.md를 전달하여 구현한다
59
75
 
60
- ## 3. M 티어 Planning: 채팅 기반
76
+ ## 3. M 티어 Planning: Task Header + 채팅 기반
61
77
 
62
- M 티어는 파일 없이 채팅에서 간단히 계획을 제시한다:
78
+ M 티어는 Task Header를 출력한 바로 구현을 진행한다:
63
79
  ```
64
- **티어**: M
65
- **변경 범위**: [레이어 목록]
66
- **작업 목록**:
67
- 1. [작업] → [담당 에이전트]
68
- 2. [작업] [담당 에이전트]
80
+ 📋 [작업명]
81
+ M [판단 근거 한 줄]
82
+ 📚 [사용할 스킬 목록]
83
+ 🔄 [에이전트 흐름]
84
+ 📁 [예상 파일 수와 대상]
69
85
  ```
70
86
 
87
+ ### Task Header 작성 규칙
88
+ - **📋**: 사용자 요청을 한 줄로 요약
89
+ - **⚡**: 티어 + 판단 근거 (예: `M — 단일 API 엔드포인트 추가`)
90
+ - **📚**: 이 작업에서 참조할 스킬 (예: `Coding, TypeORM`)
91
+ - **🔄**: 에이전트 실행 순서 (예: `code-writer-be → git-manager`)
92
+ - **📁**: 변경 파일 수와 대상 (예: `3 files (service, controller, dto)`)
93
+
71
94
  ## 4. 작업 분해 원칙
72
95
 
73
96
  **"한 번에 하나, 완전하게"** — 전체를 한꺼번에 80%로 끝내지 말고, 단위별로 100% 완성한다.
@@ -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` 등을 활용한다