@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,317 @@
1
+ # React Hook Form Skill - 폼 관리 규칙
2
+
3
+ React Hook Form + Zod를 사용한 폼 관리 규칙을 정의한다.
4
+ 공통 코딩 원칙은 `../Coding/SKILL.md`를 참고한다.
5
+
6
+ ---
7
+
8
+ ## 1. Zod 스키마 정의
9
+
10
+ ### 기본 스키마
11
+
12
+ ```typescript
13
+ import { z } from 'zod';
14
+
15
+ const createUserSchema = z.object({
16
+ name: z.string().min(1, '이름을 입력해주세요').max(50, '이름은 50자 이내로 입력해주세요'),
17
+ email: z.string().email('올바른 이메일 형식이 아닙니다'),
18
+ age: z.number().min(1, '나이는 1 이상이어야 합니다').max(150, '올바른 나이를 입력해주세요'),
19
+ role: z.enum(['admin', 'user', 'guest']),
20
+ });
21
+ ```
22
+
23
+ ### Refinement와 Transform
24
+
25
+ ```typescript
26
+ const signupSchema = z
27
+ .object({
28
+ password: z.string().min(8, '비밀번호는 8자 이상이어야 합니다'),
29
+ confirmPassword: z.string(),
30
+ phone: z.string().transform((val) => val.replace(/-/g, '')), // 하이픈 제거
31
+ })
32
+ .refine((data) => data.password === data.confirmPassword, {
33
+ message: '비밀번호가 일치하지 않습니다',
34
+ path: ['confirmPassword'], // 에러가 표시될 필드
35
+ });
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 2. 타입 추론
41
+
42
+ Zod 스키마에서 폼 타입을 자동으로 추론한다. 별도의 타입을 수동으로 정의하지 않는다.
43
+
44
+ ```typescript
45
+ // Good - 스키마에서 타입 추론
46
+ const createUserSchema = z.object({
47
+ name: z.string(),
48
+ email: z.string().email(),
49
+ });
50
+
51
+ type CreateUserForm = z.infer<typeof createUserSchema>;
52
+ // { name: string; email: string; }
53
+
54
+ // Bad - 수동으로 타입 정의 (스키마와 불일치 위험)
55
+ interface CreateUserForm {
56
+ name: string;
57
+ email: string;
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 3. zodResolver 연동
64
+
65
+ ```typescript
66
+ import { useForm } from 'react-hook-form';
67
+ import { zodResolver } from '@hookform/resolvers/zod';
68
+
69
+ function CreateUserForm() {
70
+ const {
71
+ register,
72
+ handleSubmit,
73
+ formState: { errors, isSubmitting },
74
+ } = useForm<CreateUserForm>({
75
+ resolver: zodResolver(createUserSchema),
76
+ defaultValues: {
77
+ name: '',
78
+ email: '',
79
+ },
80
+ });
81
+
82
+ const onSubmit = async (data: CreateUserForm) => {
83
+ // data는 스키마 검증을 통과한 안전한 데이터
84
+ await createUser(data);
85
+ };
86
+
87
+ return (
88
+ <form onSubmit={handleSubmit(onSubmit)}>
89
+ <input {...register('name')} />
90
+ {errors.name && <span>{errors.name.message}</span>}
91
+
92
+ <input {...register('email')} />
93
+ {errors.email && <span>{errors.email.message}</span>}
94
+
95
+ <button type="submit" disabled={isSubmitting}>
96
+ 생성
97
+ </button>
98
+ </form>
99
+ );
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 4. Controller 패턴
106
+
107
+ `register`를 사용할 수 없는 제어 컴포넌트 (Select, DatePicker, 커스텀 UI 라이브러리 등)에 사용한다.
108
+
109
+ ```typescript
110
+ import { Controller, useForm } from 'react-hook-form';
111
+
112
+ function UserForm() {
113
+ const { control, handleSubmit } = useForm<UserFormValues>({
114
+ resolver: zodResolver(userSchema),
115
+ });
116
+
117
+ return (
118
+ <form onSubmit={handleSubmit(onSubmit)}>
119
+ <Controller
120
+ name="role"
121
+ control={control}
122
+ render={({ field, fieldState: { error } }) => (
123
+ <>
124
+ <Select
125
+ value={field.value}
126
+ onChange={field.onChange}
127
+ options={roleOptions}
128
+ />
129
+ {error && <span>{error.message}</span>}
130
+ </>
131
+ )}
132
+ />
133
+
134
+ <Controller
135
+ name="birthDate"
136
+ control={control}
137
+ render={({ field, fieldState: { error } }) => (
138
+ <>
139
+ <DatePicker
140
+ selected={field.value}
141
+ onChange={field.onChange}
142
+ />
143
+ {error && <span>{error.message}</span>}
144
+ </>
145
+ )}
146
+ />
147
+ </form>
148
+ );
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## 5. 에러 핸들링
155
+
156
+ ### errors 객체로 직접 표시
157
+
158
+ ```typescript
159
+ // 간단한 에러 표시
160
+ {errors.email && <span className="error">{errors.email.message}</span>}
161
+ ```
162
+
163
+ ### 재사용 가능한 에러 컴포넌트
164
+
165
+ ```typescript
166
+ interface FieldErrorProps {
167
+ error?: FieldError;
168
+ }
169
+
170
+ function FieldError({ error }: FieldErrorProps) {
171
+ if (!error) return null;
172
+ return <span className="field-error">{error.message}</span>;
173
+ }
174
+
175
+ // 사용
176
+ <input {...register('email')} />
177
+ <FieldError error={errors.email} />
178
+ ```
179
+
180
+ ---
181
+
182
+ ## 6. 폼 검증 모드
183
+
184
+ ### mode 옵션
185
+
186
+ | mode | 동작 | 용도 |
187
+ |------|------|------|
188
+ | `onSubmit` (기본) | 제출 시에만 검증 | 대부분의 폼 |
189
+ | `onBlur` | 포커스를 벗어날 때 검증 | 긴 폼, 단계별 입력 |
190
+ | `onChange` | 입력할 때마다 검증 | 실시간 피드백이 필요한 필드 |
191
+ | `onTouched` | 첫 blur 이후부터 onChange로 검증 | 사용자 친화적 검증 |
192
+
193
+ ```typescript
194
+ const { register, handleSubmit } = useForm<FormValues>({
195
+ resolver: zodResolver(schema),
196
+ mode: 'onBlur', // 포커스를 벗어날 때 검증
197
+ });
198
+ ```
199
+
200
+ ---
201
+
202
+ ## 7. 동적 필드 (useFieldArray)
203
+
204
+ 반복되는 필드 그룹을 동적으로 추가/삭제한다.
205
+
206
+ ```typescript
207
+ const orderSchema = z.object({
208
+ items: z.array(
209
+ z.object({
210
+ productId: z.string().min(1, '상품을 선택해주세요'),
211
+ quantity: z.number().min(1, '수량은 1개 이상이어야 합니다'),
212
+ })
213
+ ).min(1, '최소 1개 이상의 상품을 추가해주세요'),
214
+ });
215
+
216
+ type OrderForm = z.infer<typeof orderSchema>;
217
+
218
+ function OrderForm() {
219
+ const { control, register, handleSubmit } = useForm<OrderForm>({
220
+ resolver: zodResolver(orderSchema),
221
+ defaultValues: { items: [{ productId: '', quantity: 1 }] },
222
+ });
223
+
224
+ const { fields, append, remove } = useFieldArray({
225
+ control,
226
+ name: 'items',
227
+ });
228
+
229
+ return (
230
+ <form onSubmit={handleSubmit(onSubmit)}>
231
+ {fields.map((field, index) => (
232
+ <div key={field.id}>
233
+ <input {...register(`items.${index}.productId`)} />
234
+ <input
235
+ type="number"
236
+ {...register(`items.${index}.quantity`, { valueAsNumber: true })}
237
+ />
238
+ <button type="button" onClick={() => remove(index)}>
239
+ 삭제
240
+ </button>
241
+ </div>
242
+ ))}
243
+ <button type="button" onClick={() => append({ productId: '', quantity: 1 })}>
244
+ 상품 추가
245
+ </button>
246
+ </form>
247
+ );
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## 8. 중첩 객체 / 배열 스키마
254
+
255
+ ### 중첩 객체
256
+
257
+ ```typescript
258
+ const addressSchema = z.object({
259
+ zipCode: z.string().length(5, '우편번호는 5자리입니다'),
260
+ city: z.string().min(1, '도시를 입력해주세요'),
261
+ detail: z.string().min(1, '상세 주소를 입력해주세요'),
262
+ });
263
+
264
+ const userSchema = z.object({
265
+ name: z.string().min(1),
266
+ address: addressSchema, // 중첩 객체
267
+ tags: z.array(z.string()), // 문자열 배열
268
+ });
269
+
270
+ type UserForm = z.infer<typeof userSchema>;
271
+
272
+ // register로 점 표기법 사용
273
+ <input {...register('address.zipCode')} />
274
+ <input {...register('address.city')} />
275
+ <input {...register('address.detail')} />
276
+
277
+ // 에러 접근도 점 표기법
278
+ {errors.address?.zipCode && <span>{errors.address.zipCode.message}</span>}
279
+ ```
280
+
281
+ ### 스키마 재사용
282
+
283
+ ```typescript
284
+ // 공통 스키마를 조합하여 재사용한다
285
+ const baseUserSchema = z.object({
286
+ name: z.string().min(1),
287
+ email: z.string().email(),
288
+ });
289
+
290
+ const createUserSchema = baseUserSchema.extend({
291
+ password: z.string().min(8),
292
+ });
293
+
294
+ const updateUserSchema = baseUserSchema.partial(); // 모든 필드 optional
295
+ ```
296
+
297
+ ---
298
+
299
+ ## 9. 네이밍 컨벤션
300
+
301
+ | 대상 | 규칙 | 예시 |
302
+ |------|------|------|
303
+ | 스키마 변수 | camelCase + `Schema` | `createUserSchema` |
304
+ | 폼 타입 | PascalCase + `Form` | `CreateUserForm` |
305
+ | 스키마 파일 | 도메인 + `.schema.ts` | `user.schema.ts` |
306
+ | 폼 컴포넌트 | PascalCase + `Form` | `CreateUserForm.tsx` |
307
+
308
+ ---
309
+
310
+ ## 10. 금지 사항
311
+
312
+ - 스키마 없이 수동 검증 금지 - 반드시 Zod 스키마를 정의하고 `zodResolver`를 사용한다
313
+ - `any` 타입 사용 금지 - `z.infer<typeof schema>`로 타입을 추론한다
314
+ - `register` 없이 `<input>` 사용 금지 - 모든 입력 필드는 `register` 또는 `Controller`로 연결한다
315
+ - `handleSubmit` 없이 `onSubmit` 직접 처리 금지 - `handleSubmit`이 검증을 수행한다
316
+ - 폼 타입과 스키마를 별도로 정의 금지 - 타입 불일치 위험이 있으므로 `z.infer`를 사용한다
317
+ - 에러 메시지 하드코딩 금지 - Zod 스키마에 메시지를 정의한다
@@ -0,0 +1,161 @@
1
+ # TDD Skill - 핵심 원칙
2
+
3
+ 이 문서는 모든 테스트 코드 작성 시 적용되는 공통 원칙을 정의한다.
4
+ FE/BE별 상세 규칙은 각각의 파일을 참고한다.
5
+ - `frontend.md` - React Testing Library 기반 프론트엔드 테스트 규칙
6
+ - `backend.md` - NestJS 백엔드 테스트 규칙
7
+
8
+ ---
9
+
10
+ ## 1. TDD 핵심 사이클
11
+
12
+ 모든 기능 구현은 Red-Green-Refactor 사이클을 따른다.
13
+
14
+ ### Red (실패하는 테스트 작성)
15
+ - 구현 코드보다 **테스트를 먼저** 작성한다
16
+ - 테스트가 실패하는 것을 확인한 뒤 다음 단계로 진행한다
17
+ - 실패 이유가 "기능이 없어서"여야 한다 (문법 에러가 아님)
18
+
19
+ ### Green (최소한의 코드로 통과)
20
+ - 테스트를 통과시키는 **가장 단순한** 코드를 작성한다
21
+ - 완벽한 설계나 최적화를 고려하지 않는다
22
+ - 하드코딩이라도 괜찮다 - 목표는 "초록불"이다
23
+
24
+ ### Refactor (리팩토링)
25
+ - 테스트가 통과하는 상태를 유지하면서 코드를 개선한다
26
+ - 중복을 제거하고, 네이밍을 개선하고, 구조를 정리한다
27
+ - 리팩토링 후 반드시 테스트를 다시 실행한다
28
+
29
+ ```
30
+ [Red] 실패하는 테스트 작성
31
+
32
+ [Green] 최소 구현으로 통과
33
+
34
+ [Refactor] 코드 개선 (테스트 유지)
35
+
36
+ [Red] 다음 테스트 작성 ...
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 2. FIRST 원칙
42
+
43
+ 좋은 테스트는 다음 5가지 특성을 갖는다.
44
+
45
+ | 원칙 | 설명 | 위반 예시 |
46
+ |------|------|-----------|
47
+ | **Fast** | 빠르게 실행된다 | DB 연결, 네트워크 호출이 포함된 단위 테스트 |
48
+ | **Isolated** | 다른 테스트에 의존하지 않는다 | 테스트 실행 순서에 따라 결과가 달라짐 |
49
+ | **Repeatable** | 어떤 환경에서도 동일한 결과를 낸다 | 현재 시각, 랜덤 값에 의존하는 테스트 |
50
+ | **Self-validating** | 성공/실패가 자동 판별된다 | console.log로 결과를 수동 확인 |
51
+ | **Timely** | 구현 코드와 함께(또는 먼저) 작성된다 | 구현 완료 후 나중에 테스트 추가 |
52
+
53
+ ---
54
+
55
+ ## 3. AAA 패턴 (Arrange-Act-Assert)
56
+
57
+ 모든 테스트는 3단계로 구성한다. 각 단계를 빈 줄로 구분하여 가독성을 높인다.
58
+
59
+ ```typescript
60
+ it('should calculate total price with discount', () => {
61
+ // Arrange - 테스트 전제 조건을 설정한다
62
+ const cart = new Cart();
63
+ cart.addItem({ name: 'Book', price: 10000 });
64
+ const discount = 0.1;
65
+
66
+ // Act - 테스트 대상을 실행한다
67
+ const totalPrice = cart.calculateTotal(discount);
68
+
69
+ // Assert - 기대 결과를 검증한다
70
+ expect(totalPrice).toBe(9000);
71
+ });
72
+ ```
73
+
74
+ ### 규칙
75
+ - **Arrange**: 테스트에 필요한 데이터, Mock, 환경을 준비한다
76
+ - **Act**: 테스트 대상 함수/메서드를 **한 번만** 호출한다
77
+ - **Assert**: 결과를 검증한다. 하나의 테스트에서 관련된 단언만 포함한다
78
+ - 빈 줄 또는 주석(`// Arrange`, `// Act`, `// Assert`)으로 구분한다
79
+
80
+ ---
81
+
82
+ ## 4. 테스트 네이밍
83
+
84
+ ### describe-it 패턴
85
+
86
+ ```typescript
87
+ describe('CartService', () => {
88
+ describe('calculateTotal', () => {
89
+ it('should return 0 when cart is empty', () => {});
90
+ it('should sum all item prices', () => {});
91
+ it('should apply discount rate to total', () => {});
92
+ it('should throw error when discount is negative', () => {});
93
+ });
94
+
95
+ describe('addItem', () => {
96
+ it('should add item to cart', () => {});
97
+ it('should increase quantity when same item is added', () => {});
98
+ });
99
+ });
100
+ ```
101
+
102
+ ### 규칙
103
+ - `describe`: 테스트 대상 (클래스명, 함수명, 컴포넌트명)
104
+ - `it`: `should` + 동작을 서술한다
105
+ - 중첩 `describe`로 메서드/시나리오를 그룹핑한다
106
+ - 테스트 이름만으로 **어떤 상황에서 어떤 결과가 나오는지** 파악할 수 있어야 한다
107
+
108
+ ---
109
+
110
+ ## 5. Mock / Stub / Spy 가이드라인
111
+
112
+ ### 용어 정의
113
+
114
+ | 종류 | 역할 | 사용 시점 |
115
+ |------|------|-----------|
116
+ | **Stub** | 정해진 값을 반환한다 | 외부 의존성의 반환값을 제어할 때 |
117
+ | **Mock** | 호출 여부/인자를 검증한다 | 특정 함수가 호출되었는지 확인할 때 |
118
+ | **Spy** | 실제 구현을 유지하면서 호출을 추적한다 | 실제 동작은 유지하되 호출 여부만 확인할 때 |
119
+
120
+ ### 사용해야 하는 경우
121
+ - 외부 시스템 호출 (API, DB, 파일 시스템)
122
+ - 비결정적 동작 (현재 시각, 랜덤 값)
123
+ - 느린 의존성 (네트워크, 디스크 I/O)
124
+ - 에러/예외 시나리오 재현
125
+
126
+ ### 피해야 하는 경우
127
+ - 테스트 대상 자체를 Mock하는 것 (의미 없는 테스트)
128
+ - 내부 구현 세부사항을 Mock하는 것 (리팩토링에 취약)
129
+ - 과도한 Mock으로 실제 동작과 괴리가 생기는 것
130
+
131
+ ```typescript
132
+ // Bad - 내부 구현을 Mock하여 리팩토링 시 테스트가 깨짐
133
+ jest.spyOn(service, 'privateHelper');
134
+ expect(service['privateHelper']).toHaveBeenCalled();
135
+
136
+ // Good - 공개 인터페이스의 결과를 검증
137
+ const result = service.processOrder(orderDto);
138
+ expect(result.status).toBe('COMPLETED');
139
+ ```
140
+
141
+ ### 원칙
142
+ - **Mock은 경계(boundary)에서만 사용한다** (외부 시스템과의 접점)
143
+ - **공개 인터페이스를 통해 검증한다** (구현이 아닌 행동을 테스트)
144
+ - Mock이 많아지면 설계를 의심한다 (의존성이 과도한 신호)
145
+
146
+ ---
147
+
148
+ ## 6. 테스트 품질 체크리스트
149
+
150
+ 테스트 작성/리뷰 시 다음을 확인한다:
151
+
152
+ - [ ] 테스트가 구현 없이 실패하는가? (Red 단계 확인)
153
+ - [ ] 하나의 테스트가 하나의 동작만 검증하는가?
154
+ - [ ] AAA 패턴이 명확하게 구분되는가?
155
+ - [ ] 테스트 이름만으로 시나리오를 이해할 수 있는가?
156
+ - [ ] Mock이 경계(외부 의존성)에서만 사용되었는가?
157
+ - [ ] 테스트 간 의존성이 없는가? (실행 순서 무관)
158
+ - [ ] 테스트가 구현 세부사항이 아닌 행동을 검증하는가?
159
+ - [ ] 엣지 케이스가 포함되어 있는가? (빈 값, null, 경계값)
160
+ - [ ] 에러/예외 케이스가 포함되어 있는가?
161
+ - [ ] 불필요한 테스트 코드 중복이 없는가? (`beforeEach` 활용)