@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.
- package/.claude/.project-map-cache +1 -1
- package/.claude/CLAUDE.md +58 -18
- package/.claude/hooks/prompt-hook.sh +4 -4
- package/.claude/skills/Coding/SKILL.md +5 -4
- 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 +15 -10
- 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
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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` 등을 활용한다
|