用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/puk0806/gugbab-claude --skill form-handling命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
DDD(Domain-Driven Design) 아키텍처 핵심 패턴 - 유비쿼터스 언어, 서브도메인, 바운디드 컨텍스트, Aggregate, Entity/VO, 도메인 서비스/이벤트, 레이어드 아키텍처
대규모 React/Next.js 프로젝트를 layer-first(types/·utils/·hooks/·api/·components/ 밑에 도메인이 반복되는 구조)에서 domain-first(feature/도메인 우선) 구조로 전환하는 설계 기준과 절차. Feature-Sliced Design 2.1 정본(layers 6종·slices·segments·import 규칙·@x 크로스임포트·public API), FSD를 쓰지 않는 경량 대안(features + shared 2~3계층 + ESLint import/no-restricted-paths), Next.js App Router 공존 전략(route group `()`·private folder `_`·colocation), Turborepo/Nx 모노레포에서 폴더↔패키지 승격 기준, colocation과 배럴 파일 성능 트레이드오프, 도메인 경계 역추출(import 그래프·change coupling·용어 클러스터), 전환 실패 패턴(shared 비대화·entities 남용·순환 의존·도메인=라우트 착각·조기 추상화). 도메인 개념 자체(바운디드 컨텍스트·유비쿼터스 언어)는 `architecture/ddd` 스킬을 참조한다.
소스 파일 수천 개 규모 프론트엔드 코드베이스를 멈추지 않고 점진 재구조화하는 실행 전략 - Strangler Fig / Branch by Abstraction / Parallel Change, ts-morph·jscodeshift codemod, PR 분할·검증 게이트·되돌리기, 테스트 없는 코드의 안전망, 작업 순서 설계와 위반 수 기반 진행 추적
基于 SOC 职业分类
正在显示 SKILL.md
| name | form-handling |
| description | React Hook Form + Zod 유효성 검증, 제어/비제어 폼 패턴, 서버 액션 연동 |
소스: https://react-hook-form.com/docs | https://zod.dev/ 검증일: 2026-04-01
| 일반 useState 폼 | React Hook Form | |
|---|---|---|
| 리렌더링 | 입력마다 발생 | 최소화 (비제어) |
| 유효성 검증 | 수동 구현 | 스키마 기반 자동 |
| 에러 처리 | 직접 관리 | 자동 |
| 코드량 | 많음 | 적음 |
pnpm add react-hook-form zod @hookform/resolvers
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
// 1. 스키마 정의 (단일 진실 공급원)
const SignupSchema = z.object({
email: z.string().email('올바른 이메일을 입력해주세요'),
password: z
.string()
.min(8, '비밀번호는 8자 이상이어야 합니다')
.regex(/[A-Z]/, '대문자를 포함해야 합니다'),
passwordConfirm: z.string(),
age: z.number().min(14, '14세 이상만 가입 가능합니다').optional(),
}).refine(
data => data.password === data.passwordConfirm,
{ message: '비밀번호가 일치하지 않습니다', path: ['passwordConfirm'] }
)
// 2. 타입 자동 추론
type SignupFormData = z.infer<typeof SignupSchema>
// 3. 폼 컴포넌트
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setError,
} = useForm<SignupFormData>({
resolver: zodResolver(SignupSchema),
defaultValues: { email: '', password: '', passwordConfirm: '' },
})
async function onSubmit(data: SignupFormData) {
try {
await signup(data)
} catch (err) {
// 서버 에러를 특정 필드에 연결
setError('email', { message: '이미 사용 중인 이메일입니다' })
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('email')} placeholder="이메일" />
{errors.email && <p role="alert">{errors.email.message}</p>}
</div>
<div>
<input type="password" {...register('password')} />
{errors.password && <p role="alert">{errors.password.message}</p>}
</div>
<div>
<input type="password" {...register('passwordConfirm')} />
{errors.passwordConfirm && <p role="alert">{errors.passwordConfirm.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '처리 중...' : '가입하기'}
</>
)
}
// FormField.tsx — 레이블, 입력, 에러를 캡슐화
import { type FieldError, type UseFormRegisterReturn } from 'react-hook-form'
import styles from './FormField.module.scss'
interface FormFieldProps {
label: string
registration: UseFormRegisterReturn
error?: FieldError
type?: string
placeholder?: string
}
function FormField({ label, registration, error, type = 'text', placeholder }: FormFieldProps) {
const id = registration.name
return (
<div className={styles.field}>
<label htmlFor={id} className={styles.label}>{label}</label>
<input
id={id}
type={type}
placeholder={placeholder}
className={error ? }
=
= ? `${}` }
{}
/>
{error && (
{error.message}
)}
)
}
<
label=
registration={()}
error={errors.}
=
/>
import { useFieldArray } from 'react-hook-form'
const Schema = z.object({
members: z.array(z.object({
name: z.string().min(1, '이름을 입력해주세요'),
role: z.string(),
})).min(1, '최소 1명이 필요합니다'),
})
function TeamForm() {
const { control, register, formState: { errors } } = useForm({
resolver: zodResolver(Schema),
defaultValues: { members: [{ name: '', role: '' }] },
})
const { fields, append, remove } = useFieldArray({ control, name: 'members' })
return (
<form>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`members.${index}.name`)} placeholder="이름" />
{errors.members?.[index]?.name && (
{errors.members[index].name.message}
)}
remove(index)}>삭제
))}
append({ name: '', role: '' })}>
멤버 추가
)
}
// app/actions.ts (Next.js Server Action)
'use server'
import { z } from 'zod'
const Schema = z.object({ email: z.string().email() })
export async function submitForm(formData: FormData) {
const result = Schema.safeParse({ email: formData.get('email') })
if (!result.success) {
return { error: result.error.flatten() }
}
// DB 저장 등...
return { success: true }
}
// 클라이언트에서 RHF + Server Action 연동
function ContactForm() {
const [serverError, setServerError] = useState<string | null>(null)
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(Schema),
})
// RHF handleSubmit은 스키마 타입 객체를 전달 (브라우저 FormData 아님)
async function () {
formData = ()
.(data).( formData.(k, (v)))
result = (formData)
(result.) {
(result..[] ?? result...?.[] ?? )
}
}
(
)
}
// 선택적 필드 (비어있으면 undefined)
z.string().optional()
// 빈 문자열을 undefined로 변환
z.string().transform(v => v === '' ? undefined : v).optional()
// 숫자 입력 (input은 string으로 옴)
z.coerce.number().min(0).max(100)
// 날짜
z.string().datetime()
z.coerce.date()
// enum
z.enum(['admin', 'user', 'guest'])
// 조건부 유효성
z.discriminatedUnion('type', [
z.object({ type: z.literal('email'), email: z.string().email() }),
z.object({ type: z.literal('phone'), phone: z.string().min(10) }),
])