λ€λ₯Έ νμμ λ§λ νμκ°μ νΌμ 리ν©ν°λ§ν΄λ¬λΌλ μμ²μ λ°μλ€.
μ½λλ₯Ό λΆμν΄λ³΄λ λ€μκ³Ό κ°μ μ΄μκ° μμλ€:
.regex()λ‘ μ€μ²©λμ΄ μ μ§λ³΄μ μ΄λ €μerror: stringλ§ μ§μ β λ°°μ΄μ μ§μ λ£μ μ μμimport { z } from 'zod'
const passwordSchema = z
.string({
error: (issue) =>
issue.input === undefined
? 'λΉλ°λ²νΈλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ'
: 'μ¬λ°λ₯Έ λΉλ°λ²νΈ νμμΌλ‘ μ
λ ₯ν΄μ£ΌμΈμ',
})
.min(8, { error: 'λΉλ°λ²νΈλ μ΅μ 8μ μ΄μμ΄μ΄μΌ ν©λλ€.' })
.max(50, { error: 'λΉλ°λ²νΈλ 50μ μ΄νλ‘ μ
λ ₯ν΄μ£ΌμΈμ.' })
export const registerSchema = z
.object({
email: z.string().trim(),
password: passwordSchema
.regex(/[a-z]/, { error: 'λΉλ°λ²νΈλ μ΅μ νλμ μλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€.' })
.regex(/[A-Z]/, { error: 'λΉλ°λ²νΈλ μ΅μ νλμ λλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€.' })
.regex(/\d/, { error: 'λΉλ°λ²νΈλ μ΅μ νλμ μ«μλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€.' })
.regex(/[!@#$%^&*(),.?":{}|<>]/, { error: 'λΉλ°λ²νΈλ μ΅μ νλμ νΉμλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€.' }),
passwordConfirm: z.string({
error: (issue) =>
issue.input === undefined ? 'λΉλ°λ²νΈ νμΈ κ°μ μ
λ €ν΄μ£ΌμΈμ.' : 'λ¬Έμμ΄μ΄μ΄μΌ ν©λλ€',
}),
nickname: z.union([z.literal(''), z.string().trim().min(2).max(12)]).optional(),
})
.refine((data) => data.password === data.passwordConfirm, {
message: 'λΉλ°λ²νΈ κ°μ΄ μΌμΉνμ§ μμ΅λλ€',
path: ['passwordConfirm'],
})
// RegisterForm.tsx (Before)
<Input
label="λΉλ°λ²νΈ"
type="password"
name="password"
value={formData.password || ''}
onChange={handleChange}
placeholder="μ΅μ 8μ μ΄μ (μλ¬Έ, μ«μ, νΉμλ¬Έμ ν¬ν¨)"
error={error.includes('λΉλ°λ²νΈ') && !error.includes('νμΈ') ? error : undefined}
required
/>
error: string)μ΄λΌ μ¬λ¬ 쑰건μ λμμ 보μ¬μ€ μ μμ.regex() λμ΄ λμ superRefineλ‘ μλ°λ νλͺ©λ§ λμ superRefineλ‘ μλ°λ§ λμ import { z } from 'zod'
export const registerSchema = z
.object({
email: z.string().min(1, { message: 'μ΄λ©μΌμ μ
λ ₯ν΄μ£ΌμΈμ.' }).email(),
password: z.string().min(8).max(50),
passwordConfirm: z.string().min(1, { message: 'λΉλ°λ²νΈ νμΈ κ°μ μ
λ ₯ν΄μ£ΌμΈμ.' }),
nickname: z.union([z.literal(''), z.string().trim().min(2).max(12)]).optional(),
})
.superRefine((data, ctx) => {
const pwd = data.password ?? ''
if (pwd.length < 8)
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['password'], message: 'λΉλ°λ²νΈλ μ΅μ 8μ μ΄μμ΄μ΄μΌ ν©λλ€.' })
if (!/[A-Z]/.test(pwd))
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['password'], message: 'λΉλ°λ²νΈλ μ΅μ νλμ λλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€.' })
if (!/\d/.test(pwd))
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['password'], message: 'λΉλ°λ²νΈλ μ΅μ νλμ μ«μλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€.' })
if (!/[!@#$%^&*(),.?":{}|<>]/.test(pwd))
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['password'], message: 'λΉλ°λ²νΈλ μ΅μ νλμ νΉμλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€.' })
if (data.password !== data.passwordConfirm)
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['passwordConfirm'], message: 'λΉλ°λ²νΈ κ°μ΄ μΌμΉνμ§ μμ΅λλ€' })
})
type FormDataState = z.infer<typeof registerSchema>
type FieldErrors = Partial<Record<keyof FormDataState, string[]>>
const [errors, setErrors] = useState<FieldErrors>({})
const parsed = registerSchema.safeParse(formData)
if (!parsed.success) {
const { fieldErrors } = parsed.error.flatten()
setErrors(fieldErrors as FieldErrors) // β
κ° νλμ μλ¬κ° string[] νν
return
}
fieldErrors.passwordλ string[]interface InputProps extends ComponentPropsWithRef<'input'> {
label?: string
error?: string // β stringλ§ νμ©
helperText?: string
}
errorκ° stringμΌλ‘ κ³ μ λμ΄ μμ΄ string[]μ κ·Έλλ‘ λ£μΌλ©΄ νμ
μλ¬<p>{error}</p> κ΅¬μ‘°λΌ λ¦¬μ€νΈ μΆλ ₯ λΆκ°FieldErrorsλ‘ λ λλ§function FieldErrors({ messages }: { messages?: string[] }) {
if (!messages?.length) return null
return (
<ul className="mt-1 space-y-1 text-sm text-destructive" role="alert">
{messages.map((m, i) => <li key={i}>β’ {m}</li>)}
</ul>
)
}
<Input
label="λΉλ°λ²νΈ"
type="password"
name="password"
value={formData.password || ''}
onChange={handleChange}
placeholder="μ΅μ 8μ μ΄μ (μλ¬Έ, μ«μ, νΉμλ¬Έμ ν¬ν¨)"
// error propμ μλ΅νκ±°λ 첫 μλ¬λ§ λ£μ μλ μμ
required
/>
<FieldErrors messages={errors.password} />
μ°Έκ³ : μ₯κΈ°μ μΌλ‘λ κ³΅ν΅ Inputμ error νμ μ string | string[]λ‘ νμ₯νκ³ , λ΄λΆμμ
<ul>μ λ λλ§νλλ‘ κ°μ νλ©΄ νΌ μͺ½ μ½λκ° λ κΉλν΄μ§λ€. λ¨, μ΄λ μ»΄ν¬λνΈ μ¬μ©νλ μ 체μ λ―ΈμΉλ μν₯ 체ν¬νκ³ μ§νν΄μΌ νλ€.
| νλͺ© | Before | After |
|---|---|---|
| κ²μ¦ λ°©μ | .regex() μ€μ²© | superRefineλ‘ ν΅ν© |
| μλ¬ κ΅¬μ‘° | error: string (λ¨μΌ) | errors[field]: string[] (λ€μ€) |
| μλ¬ νμ | ν μ€ | <ul><li> 리μ€νΈ |
| Input μ¬μ© | errorμ λ¬Έμμ΄ | FieldErrorsλ‘ λ°°μ΄ μ§μ λ λ |
| κ³΅ν΅ μ»΄ν¬λνΈ μν₯ | μμ | μμ (μΈλΆ νμ₯ λ°©μ) |
| UX | μ€λ¬΄κ³ κ°ν | μλ° νλͺ© λμ λ ΈμΆ |
νμΈμ μ½λλ₯Ό 리ν©ν°λ§ν λ λΆμμ΄ λ¨Όμ .
μμ ν λΆλΆκ³Ό κ°μΈμ ν΄κ²°ν λΆλΆμ ꡬλΆν΄μΌ νλ€.
κ²μ¦μ λ‘μ§μ΄ μλλΌ UX.
μ νλ Έλμ§ νλμ 보μ΄κ² ν΄μΌ νλ€.
κ³΅ν΅ μ»΄ν¬λνΈλ λ³κ²½ μν₯λλ₯Ό κ³ λ €.
μ°μ μ μΈλΆμμ νμ₯ν΄ μμ νκ² κ°μ νκ³ ,
νμν λ ν ν©μλ‘ κ³΅ν΅ μ»΄ν¬λνΈλ₯Ό λ°μ μν€μ.
Zod superRefineμ κ°λ ₯ν λꡬ.
볡μ‘ν 쑰건μ ν λ²μ μ μ΄νκ³ , βμλ°λ§β λͺ¨μμ 보μ¬μ€ μ μλ€.
μ΄λ² 리ν©ν°λ§μ λ¨μν μλ¬ λ¬Έκ΅¬λ₯Ό κ³ μΉλ μμ μ΄ μλλΌ,
λ€λ₯Έ νμ΄ λ§λ κ³΅ν΅ μ»΄ν¬λνΈμ μ μ½μ μ΄ν΄νκ³ ,
κ·Έ μμμ UXλ₯Ό μ΅λν λμ΄μ¬λ¦¬λ ꡬ쑰μ κ°μ μ΄μλ€.
νμνλ€λ©΄ κ³΅ν΅ Inputμ error: string | string[]λ‘ νμ₯νλ λ€μ λ¨κ³λ κ°λ₯νμ§λ§,
μ΄λ²μλ μμ νκ³ μν₯μ΄ μ μ μ°ν μ λ΅(FieldErrors) μΌλ‘ λͺ©νλ₯Ό λ¬μ±νλ€.