์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽฏ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋กœ ํผ ์ฒ˜๋ฆฌ ์™„๋ฒฝ ์ •๋ณตํ•˜๊ธฐ: Validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํƒ€์ž… ์‹œ์Šคํ…œ์˜ ํ™˜์ƒ์ ์ธ ๋งŒ๋‚จ

๐ŸŽฏ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋กœ ํผ ์ฒ˜๋ฆฌ ์™„๋ฒฝ ์ •๋ณตํ•˜๊ธฐ: Validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํƒ€์ž… ์‹œ์Šคํ…œ์˜ ํ™˜์ƒ์ ์ธ ๋งŒ๋‚จ

์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ ์•Œ๋ ค์ฃผ๋Š” ์‹ค์ „ ํผ ์ฒ˜๋ฆฌ ๊ฐ€์ด๋“œ

์•ˆ๋…•! ๐Ÿ‘‹ ์˜ค๋Š˜์€ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋กœ ํผ์„ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ ๊ฑฐ์•ผ. ์†”์งํžˆ ๋งํ•˜๋ฉด, ํผ ์ฒ˜๋ฆฌ๋Š” ์›น ๊ฐœ๋ฐœ์—์„œ ๊ฐ€์žฅ ๊ท€์ฐฎ์œผ๋ฉด์„œ๋„ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์ž–์•„? ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ , ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , ํƒ€์ž… ์•ˆ์ •์„ฑ๊นŒ์ง€ ์ฑ™๊ฒจ์•ผ ํ•˜๋‹ˆ๊นŒ ๋ง์ด์•ผ.

๊ทผ๋ฐ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ ์ตœ์‹  validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ฉด, ์ด ๋ชจ๋“  ๊ฒŒ ์ •๋ง ์šฐ์•„ํ•˜๊ฒŒ ํ•ด๊ฒฐ๋ผ. ๋งˆ์น˜ ํผ์ฆ ์กฐ๊ฐ๋“ค์ด ๋”ฑ๋”ฑ ๋งž์•„๋–จ์–ด์ง€๋Š” ๋А๋‚Œ์ด๋ž„๊นŒ? ๐Ÿงฉ

๐Ÿ’ก ์ด ๊ธ€์—์„œ ๋‹ค๋ฃฐ ๋‚ด์šฉ

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์˜ ํƒ€์ž… ์‹œ์Šคํ…œ๊ณผ validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•, ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ํŒจํ„ด๋“ค, ๊ทธ๋ฆฌ๊ณ  ๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์žฅ๋‹จ์ ๊นŒ์ง€ ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•˜๋“ฏ ์‰ฝ๊ฒŒ ํ’€์–ด๋ณผ๊ฒŒ!

Form Input name: string email: string age: number โœ“ Validated Data Type Safe Validated Error Handled Auto Complete TypeScript + Validation = ๐Ÿ’ช

๐ŸŽช ์™œ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ Validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ•จ๊ป˜ ์จ์•ผ ํ• ๊นŒ?

์ž, ๋จผ์ € ๊ทผ๋ณธ์ ์ธ ์งˆ๋ฌธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์ž. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋งŒ ์žˆ์œผ๋ฉด ๋˜๋Š” ๊ฑฐ ์•„๋‹ˆ์•ผ? ์™œ ๊ตณ์ด validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊นŒ์ง€ ์จ์•ผ ํ•˜๋Š” ๊ฑธ๊นŒ? ๐Ÿค”

๋‹ต์€ ๊ฐ„๋‹จํ•ด. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ์ปดํŒŒ์ผ ํƒ€์ž„์—๋งŒ ์ž‘๋™ํ•˜๊ฑฐ๋“ . ์ฆ‰, ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๋นŒ๋“œํ•  ๋•Œ๋Š” ํƒ€์ž… ์ฒดํฌ๋ฅผ ํ•ด์ฃผ์ง€๋งŒ, ์‹ค์ œ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํผ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ž…๋ ฅํ•  ๋•Œ๋Š” ์•„๋ฌด๋Ÿฐ ๋ณดํ˜ธ๋ง‰์ด ์—†๋‹ค๋Š” ๊ฑฐ์•ผ.

๐Ÿ“Œ ํ•ต์‹ฌ ํฌ์ธํŠธ

์ปดํŒŒ์ผ ํƒ€์ž„ vs ๋Ÿฐํƒ€์ž„

โ€ข ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ: ์ปดํŒŒ์ผ ํƒ€์ž„์— ํƒ€์ž… ์ฒดํฌ (๊ฐœ๋ฐœ ์ค‘)
โ€ข Validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: ๋Ÿฐํƒ€์ž„์— ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ (์‹คํ–‰ ์ค‘)
โ€ข ๋‘˜ ๋‹ค ํ•„์š”ํ•œ ์ด์œ : ์™„๋ฒฝํ•œ ํƒ€์ž… ์•ˆ์ •์„ฑ + ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ์ด๋Ÿฐ ํƒ€์ž…์„ ์ •์˜ํ–ˆ๋‹ค๊ณ  ํ•ด๋ณด์ž:

interface UserForm {
  name: string;
  email: string;
  age: number;
}

function submitForm(data: UserForm) {
  // ์—ฌ๊ธฐ์„œ data๋Š” UserForm ํƒ€์ž…์ด๋ผ๊ณ  ๋ณด์žฅ๋˜์ง€๋งŒ...
  // ์‹ค์ œ๋กœ ๋Ÿฐํƒ€์ž„์— ๋“ค์–ด์˜ค๋Š” ๊ฐ’์€ ๋ญ๊ฐ€ ๋ ์ง€ ๋ชฐ๋ผ!
}

์‚ฌ์šฉ์ž๊ฐ€ ๋‚˜์ด์— "์Šค๋ฌผ๋‹ค์„ฏ"์ด๋ผ๊ณ  ์ž…๋ ฅํ•˜๋ฉด? ์ด๋ฉ”์ผ์— "์ด๋ฉ”์ผ์•„๋‹˜"์ด๋ผ๊ณ  ์“ฐ๋ฉด? ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ์ด๊ฑธ ๋ง‰์•„์ฃผ์ง€ ๋ชปํ•ด. ์™œ๋ƒํ•˜๋ฉด ์‹คํ–‰ ์‹œ์ ์—๋Š” ํƒ€์ž… ์ •๋ณด๊ฐ€ ์‚ฌ๋ผ์ง€๊ฑฐ๋“ . ๐Ÿ˜ฑ

๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฑฐ์•ผ. ๊ทธ๋ฆฌ๊ณ  ๋” ๋‚˜์•„๊ฐ€์„œ, ์ด validation ์Šคํ‚ค๋งˆ์—์„œ ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์ถ”๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด? ์™„๋ฒฝํ•˜์ง€ ์•Š์•„? ๐ŸŽฏ

๐Ÿ› ๏ธ ์ฃผ์š” Validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์†Œ๊ฐœ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์–ด๋–ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์žˆ๋Š”์ง€ ์•Œ์•„๋ณด์ž. ์š”์ฆ˜ ๊ฐ€์žฅ ํ•ซํ•œ ์นœ๊ตฌ๋“ค์„ ์†Œ๊ฐœํ• ๊ฒŒ!

๐Ÿ”ท Zod

ํŠน์ง•: ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์šฐ์„  ์„ค๊ณ„

์žฅ์ : ์ง๊ด€์ ์ธ API, ๋›ฐ์–ด๋‚œ ํƒ€์ž… ์ถ”๋ก , ์ž‘์€ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ

๋‹จ์ : ์ƒ๋Œ€์ ์œผ๋กœ ์ตœ์‹  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์ธ๊ธฐ๋„: โญโญโญโญโญ

๐Ÿ”ถ Yup

ํŠน์ง•: ์˜ค๋ž˜๋œ ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์žฅ์ : ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„, ๋งŽ์€ ๋ ˆํผ๋Ÿฐ์Šค

๋‹จ์ : ํƒ€์ž… ์ถ”๋ก ์ด Zod๋ณด๋‹ค ์•ฝํ•จ

์ธ๊ธฐ๋„: โญโญโญโญ

๐Ÿ”น Joi

ํŠน์ง•: Node.js ํ™˜๊ฒฝ์— ์ตœ์ ํ™”

์žฅ์ : ๊ฐ•๋ ฅํ•œ ๊ฒ€์ฆ ๊ธฐ๋Šฅ

๋‹จ์ : ๋ธŒ๋ผ์šฐ์ € ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๊ฐ€ ํผ

์ธ๊ธฐ๋„: โญโญโญโญ

๐Ÿ”ธ io-ts

ํŠน์ง•: ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ

์žฅ์ : ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์•ˆ์ •์„ฑ

๋‹จ์ : ๋Ÿฌ๋‹ ์ปค๋ธŒ๊ฐ€ ๋†’์Œ

์ธ๊ธฐ๋„: โญโญโญ

์ด ์ค‘์—์„œ ์˜ค๋Š˜์€ Zod์™€ Yup์„ ์ค‘์‹ฌ์œผ๋กœ ์„ค๋ช…ํ• ๊ฒŒ. ์ด ๋‘˜์ด ํ˜„์žฌ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๊ณ , ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€์˜ ํ†ตํ•ฉ๋„ ํ›Œ๋ฅญํ•˜๊ฑฐ๋“ ! ๐Ÿ’ช

๐ŸŽจ Zod๋กœ ์‹œ์ž‘ํ•˜๋Š” ํƒ€์ž… ์•ˆ์ „ํ•œ ํผ ์ฒ˜๋ฆฌ

Zod๋Š” ์ •๋ง ๋ฉ‹์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. "Schema to Type"์ด ์•„๋‹ˆ๋ผ "Type from Schema"๋ผ๋Š” ์ฒ ํ•™์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด. ๋ฌด์Šจ ๋ง์ด๋ƒ๋ฉด, ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๋ฉด ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ๋”ฐ๋ผ์˜จ๋‹ค๋Š” ๊ฑฐ์ง€! ๐ŸŽช

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

๋จผ์ € ์„ค์น˜๋ถ€ํ„ฐ ํ•ด๋ณด์ž:

npm install zod
# ๋˜๋Š”
yarn add zod

์ด์ œ ๊ฐ„๋‹จํ•œ ํผ ์Šคํ‚ค๋งˆ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ:

import { z } from 'zod';

// ์Šคํ‚ค๋งˆ ์ •์˜
const userFormSchema = z.object({
  name: z.string().min(2, '์ด๋ฆ„์€ ์ตœ์†Œ 2๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”'),
  email: z.string().email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”'),
  age: z.number().min(18, '18์„ธ ์ด์ƒ๋งŒ ๊ฐ€์ž…ํ•  ์ˆ˜ ์žˆ์–ด์š”').max(100),
  password: z.string()
    .min(8, '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ตœ์†Œ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .regex(/[A-Z]/, '๋Œ€๋ฌธ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .regex(/[0-9]/, '์ˆซ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”'),
  agreeToTerms: z.boolean().refine(val => val === true, {
    message: '์•ฝ๊ด€์— ๋™์˜ํ•ด์•ผ ํ•ด์š”'
  })
});

// ํƒ€์ž… ์ž๋™ ์ถ”๋ก ! ๐ŸŽ‰
type UserForm = z.infer<typeof userFormSchema>;

// UserForm ํƒ€์ž…์€ ์ด๋ ‡๊ฒŒ ์ถ”๋ก ๋ผ:
// {
//   name: string;
//   email: string;
//   age: number;
//   password: string;
//   agreeToTerms: boolean;
// }

๐ŸŽฏ Pro Tip

z.infer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์Šคํ‚ค๋งˆ์—์„œ ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์ถ”๋ก ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๊ฒŒ Zod์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์ด์•ผ! ์Šคํ‚ค๋งˆ๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ํƒ€์ž…๋„ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋‹ˆ๊นŒ ์ค‘๋ณต ์ž‘์—…์ด ์—†์–ด์ ธ. ๐Ÿ‘

์‹ค์ œ ํผ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ ํผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•ด๋ณด์ž:

function handleFormSubmit(formData: unknown) {
  try {
    // ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ํŒŒ์‹ฑ
    const validatedData = userFormSchema.parse(formData);
    
    // ์—ฌ๊ธฐ์„œ validatedData๋Š” UserForm ํƒ€์ž…์œผ๋กœ ๋ณด์žฅ๋ผ!
    console.log('๊ฒ€์ฆ ์„ฑ๊ณต:', validatedData);
    
    // API ํ˜ธ์ถœ ๋“ฑ์˜ ์ž‘์—… ์ˆ˜ํ–‰
    await submitToServer(validatedData);
    
  } catch (error) {
    if (error instanceof z.ZodError) {
      // ๊ฒ€์ฆ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
      console.error('๊ฒ€์ฆ ์‹คํŒจ:', error.errors);
      
      // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํฌ๋งทํŒ…
      const formattedErrors = error.errors.map(err => ({
        field: err.path.join('.'),
        message: err.message
      }));
      
      displayErrors(formattedErrors);
    }
  }
}

๋ณด๋‹ค์‹œํ”ผ ์ •๋ง ๊ฐ„๋‹จํ•˜์ง€? parse ๋ฉ”์„œ๋“œ ํ•˜๋‚˜๋กœ ๊ฒ€์ฆ๊ณผ ํƒ€์ž… ๋ณ€ํ™˜์ด ๋™์‹œ์— ์ด๋ฃจ์–ด์ ธ. ๊ทธ๋ฆฌ๊ณ  ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ZodError ๊ฐ์ฒด์— ๋ชจ๋“  ๊ฒ€์ฆ ์‹คํŒจ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ์–ด. ๐Ÿ˜Š

๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค

Zod๋Š” ์ •๋ง ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด. ๋ช‡ ๊ฐ€์ง€ ์œ ์šฉํ•œ ํŒจํ„ด๋“ค์„ ์†Œ๊ฐœํ• ๊ฒŒ:

// 1. ์„ ํƒ์  ํ•„๋“œ์™€ ๊ธฐ๋ณธ๊ฐ’
const profileSchema = z.object({
  username: z.string(),
  bio: z.string().optional(), // ์„ ํƒ์  ํ•„๋“œ
  theme: z.enum(['light', 'dark']).default('light'), // ๊ธฐ๋ณธ๊ฐ’
  notifications: z.boolean().default(true)
});

// 2. ์กฐ๊ฑด๋ถ€ ๊ฒ€์ฆ
const conditionalSchema = z.object({
  accountType: z.enum(['personal', 'business']),
  companyName: z.string().optional()
}).refine(data => {
  // business ํƒ€์ž…์ด๋ฉด companyName์ด ํ•„์ˆ˜
  if (data.accountType === 'business') {
    return data.companyName !== undefined && data.companyName.length > 0;
  }
  return true;
}, {
  message: '์‚ฌ์—…์ž ๊ณ„์ •์€ ํšŒ์‚ฌ๋ช…์ด ํ•„์ˆ˜์˜ˆ์š”',
  path: ['companyName']
});

// 3. ๋ฐฐ์—ด๊ณผ ์ค‘์ฒฉ ๊ฐ์ฒด
const orderSchema = z.object({
  orderId: z.string().uuid(),
  items: z.array(
    z.object({
      productId: z.string(),
      quantity: z.number().positive(),
      price: z.number().positive()
    })
  ).min(1, '์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ์ƒํ’ˆ์ด ํ•„์š”ํ•ด์š”'),
  shippingAddress: z.object({
    street: z.string(),
    city: z.string(),
    zipCode: z.string().regex(/^\d{5}$/, '์šฐํŽธ๋ฒˆํ˜ธ๋Š” 5์ž๋ฆฌ ์ˆซ์ž์—ฌ์•ผ ํ•ด์š”')
  }),
  totalAmount: z.number()
}).refine(data => {
  // ์ด์•ก์ด ์•„์ดํ…œ ๊ฐ€๊ฒฉ์˜ ํ•ฉ๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆ
  const calculatedTotal = data.items.reduce(
    (sum, item) => sum + (item.price * item.quantity), 
    0
  );
  return Math.abs(calculatedTotal - data.totalAmount) < 0.01;
}, {
  message: '์ด์•ก์ด ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”'
});

// 4. ๋ณ€ํ™˜(Transform)
const dateSchema = z.object({
  birthDate: z.string().transform(str => new Date(str)),
  registeredAt: z.string().datetime().transform(str => new Date(str))
});

// 5. ์œ ๋‹ˆ์˜จ ํƒ€์ž…
const paymentSchema = z.discriminatedUnion('method', [
  z.object({
    method: z.literal('card'),
    cardNumber: z.string().length(16),
    cvv: z.string().length(3)
  }),
  z.object({
    method: z.literal('bank'),
    accountNumber: z.string(),
    bankCode: z.string()
  }),
  z.object({
    method: z.literal('paypal'),
    email: z.string().email()
  })
]);

๐Ÿ’ก ์‹ค์ „ ํŒ: safeParse ์‚ฌ์šฉํ•˜๊ธฐ

parse ๋Œ€์‹  safeParse๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์˜ˆ์™ธ๋ฅผ ๋˜์ง€์ง€ ์•Š๊ณ  ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด. ์ด๊ฒŒ ๋” ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ์ด๊ณ , ์—๋Ÿฌ ํ•ธ๋“ค๋ง๋„ ๊น”๋”ํ•ด์ ธ:

const result = userFormSchema.safeParse(formData);

if (result.success) {
  // ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ขํ˜€์ ธ!
  const data = result.data; // UserForm ํƒ€์ž…
  console.log('์„ฑ๊ณต:', data);
} else {
  // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  const errors = result.error.errors;
  console.log('์‹คํŒจ:', errors);
}

๐ŸŽญ Yup์œผ๋กœ ํผ ๊ฒ€์ฆํ•˜๊ธฐ

Yup์€ Zod๋ณด๋‹ค ๋จผ์ € ๋‚˜์˜จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. Formik์ด๋‚˜ React Hook Form ๊ฐ™์€ ํผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค๊ณผ ๊ถํ•ฉ์ด ์ •๋ง ์ข‹์•„์„œ ์•„์ง๋„ ๋งŽ์ด ์“ฐ์ด๊ณ  ์žˆ์–ด. ๐ŸŽช

Yup ์‹œ์ž‘ํ•˜๊ธฐ

npm install yup
# ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ํƒ€์ž… ์ •์˜๋„ ํ•จ๊ป˜ ์„ค์น˜๋ผ

Yup์˜ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•์„ ๋ณผ๊นŒ?

import * as yup from 'yup';

// ์Šคํ‚ค๋งˆ ์ •์˜
const userFormSchema = yup.object({
  name: yup.string()
    .min(2, '์ด๋ฆ„์€ ์ตœ์†Œ 2๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .required('์ด๋ฆ„์€ ํ•„์ˆ˜์˜ˆ์š”'),
  email: yup.string()
    .email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”')
    .required('์ด๋ฉ”์ผ์€ ํ•„์ˆ˜์˜ˆ์š”'),
  age: yup.number()
    .min(18, '18์„ธ ์ด์ƒ๋งŒ ๊ฐ€์ž…ํ•  ์ˆ˜ ์žˆ์–ด์š”')
    .max(100)
    .required('๋‚˜์ด๋Š” ํ•„์ˆ˜์˜ˆ์š”'),
  password: yup.string()
    .min(8, '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ตœ์†Œ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .matches(/[A-Z]/, '๋Œ€๋ฌธ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .matches(/[0-9]/, '์ˆซ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .required('๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜์˜ˆ์š”'),
  agreeToTerms: yup.boolean()
    .oneOf([true], '์•ฝ๊ด€์— ๋™์˜ํ•ด์•ผ ํ•ด์š”')
    .required()
});

// ํƒ€์ž… ์ถ”๋ก 
type UserForm = yup.InferType<typeof userFormSchema>;

// ๊ฒ€์ฆ
async function validateForm(data: unknown) {
  try {
    const validatedData = await userFormSchema.validate(data, {
      abortEarly: false // ๋ชจ๋“  ์—๋Ÿฌ๋ฅผ ํ•œ๋ฒˆ์— ์ˆ˜์ง‘
    });
    
    console.log('๊ฒ€์ฆ ์„ฑ๊ณต:', validatedData);
    return validatedData;
    
  } catch (error) {
    if (error instanceof yup.ValidationError) {
      console.error('๊ฒ€์ฆ ์‹คํŒจ:', error.errors);
      // error.inner์— ๋ชจ๋“  ๊ฒ€์ฆ ์—๋Ÿฌ๊ฐ€ ๋‹ด๊ฒจ์žˆ์–ด
      const formattedErrors = error.inner.map(err => ({
        field: err.path,
        message: err.message
      }));
      return formattedErrors;
    }
  }
}

Zod์™€ ๋น„์Šทํ•˜์ง€? ํ•˜์ง€๋งŒ ๋ช‡ ๊ฐ€์ง€ ์ฐจ์ด์ ์ด ์žˆ์–ด:

๐Ÿ“Š Zod vs Yup ์ฃผ์š” ์ฐจ์ด์ 

1. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
โ€ข Yup: validate ๋ฉ”์„œ๋“œ๊ฐ€ Promise๋ฅผ ๋ฐ˜ํ™˜ (async/await ํ•„์š”)
โ€ข Zod: ๋™๊ธฐ์ ์œผ๋กœ ์ž‘๋™ (๋” ๋น ๋ฆ„)

2. ํƒ€์ž… ์ถ”๋ก 
โ€ข Yup: InferType ์‚ฌ์šฉ, ํƒ€์ž… ์ถ”๋ก ์ด ์•ฝ๊ฐ„ ๋œ ์ •ํ™•ํ•  ์ˆ˜ ์žˆ์Œ
โ€ข Zod: z.infer ์‚ฌ์šฉ, ๋” ์ •ํ™•ํ•œ ํƒ€์ž… ์ถ”๋ก 

3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ
โ€ข Yup: abortEarly ์˜ต์…˜์œผ๋กœ ์ œ์–ด
โ€ข Zod: ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ์—๋Ÿฌ ์ˆ˜์ง‘

4. ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ
โ€ข Yup: ์•ฝ๊ฐ„ ๋” ํผ (~15KB)
โ€ข Zod: ๋” ์ž‘์Œ (~8KB)

Yup์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค

// 1. ์กฐ๊ฑด๋ถ€ ๊ฒ€์ฆ (when)
const conditionalSchema = yup.object({
  accountType: yup.string().oneOf(['personal', 'business']).required(),
  companyName: yup.string().when('accountType', {
    is: 'business',
    then: schema => schema.required('์‚ฌ์—…์ž๋Š” ํšŒ์‚ฌ๋ช…์ด ํ•„์ˆ˜์˜ˆ์š”'),
    otherwise: schema => schema.notRequired()
  })
});

// 2. ์ปค์Šคํ…€ ๊ฒ€์ฆ
const customSchema = yup.object({
  username: yup.string()
    .test('unique-username', '์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์•„์ด๋””์˜ˆ์š”', async (value) => {
      if (!value) return false;
      // API ํ˜ธ์ถœ๋กœ ์ค‘๋ณต ์ฒดํฌ
      const isAvailable = await checkUsernameAvailability(value);
      return isAvailable;
    })
    .required()
});

// 3. ์ฐธ์กฐ(Reference)๋ฅผ ์ด์šฉํ•œ ๊ฒ€์ฆ
const passwordSchema = yup.object({
  password: yup.string()
    .min(8)
    .required('๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”')
    .required('๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
});

// 4. ๋ฐฐ์—ด ๊ฒ€์ฆ
const tagsSchema = yup.object({
  tags: yup.array()
    .of(yup.string().min(2).max(20))
    .min(1, '์ตœ์†Œ 1๊ฐœ์˜ ํƒœ๊ทธ๊ฐ€ ํ•„์š”ํ•ด์š”')
    .max(5, '์ตœ๋Œ€ 5๊ฐœ๊นŒ์ง€๋งŒ ๊ฐ€๋Šฅํ•ด์š”')
    .required()
});

// 5. ์ค‘์ฒฉ ๊ฐ์ฒด์™€ ๋ฐฐ์—ด
const complexSchema = yup.object({
  user: yup.object({
    profile: yup.object({
      firstName: yup.string().required(),
      lastName: yup.string().required()
    })
  }),
  posts: yup.array().of(
    yup.object({
      title: yup.string().required(),
      content: yup.string().required(),
      tags: yup.array().of(yup.string())
    })
  )
});
Zod ์žฅ์  ๋›ฐ์–ด๋‚œ ํƒ€์ž… ์ถ”๋ก  ์ž‘์€ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ๋™๊ธฐ์  ๊ฒ€์ฆ ์ง๊ด€์ ์ธ API ๋‹จ์  ์ƒ๋Œ€์ ์œผ๋กœ ์ตœ์‹  ์ƒํƒœ๊ณ„๊ฐ€ ์ž‘์Œ Yup ์žฅ์  ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„ ๋งŽ์€ ๋ ˆํผ๋Ÿฐ์Šค ๋น„๋™๊ธฐ ๊ฒ€์ฆ ์ง€์› ๋‹จ์  ํƒ€์ž… ์ถ”๋ก ์ด ์•ฝํ•จ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๊ฐ€ ํผ ๋น„๋™๊ธฐ ์˜ค๋ฒ„ํ—ค๋“œ

๐ŸŽฏ React Hook Form๊ณผ์˜ ํ†ตํ•ฉ

์ž, ์ด์ œ ์‹ค์ „์ด์•ผ! React์—์„œ ํผ์„ ๋‹ค๋ฃฐ ๋•Œ ๊ฐ€์žฅ ์ธ๊ธฐ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ React Hook Form๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ์ด ์กฐํ•ฉ์€ ์ •๋ง ํ™˜์ƒ์ ์ด์•ผ! ๐Ÿš€

Zod + React Hook Form

๋จผ์ € ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค์„ ์„ค์น˜ํ•˜์ž:

npm install react-hook-form @hookform/resolvers zod

์ด์ œ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// ์Šคํ‚ค๋งˆ ์ •์˜
const signupSchema = z.object({
  username: z.string()
    .min(3, '์•„์ด๋””๋Š” 3๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .max(20, '์•„์ด๋””๋Š” 20๊ธ€์ž ์ดํ•˜์—ฌ์•ผ ํ•ด์š”')
    .regex(/^[a-zA-Z0-9_]+$/, '์˜๋ฌธ, ์ˆซ์ž, ์–ธ๋”์Šค์ฝ”์–ด๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์š”'),
  email: z.string()
    .email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”'),
  password: z.string()
    .min(8, '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .regex(/[A-Z]/, '๋Œ€๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .regex(/[a-z]/, '์†Œ๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .regex(/[0-9]/, '์ˆซ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”'),
  confirmPassword: z.string(),
  age: z.number()
    .min(18, '18์„ธ ์ด์ƒ๋งŒ ๊ฐ€์ž… ๊ฐ€๋Šฅํ•ด์š”')
    .max(120, '์˜ฌ๋ฐ”๋ฅธ ๋‚˜์ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  terms: z.boolean()
    .refine(val => val === true, '์•ฝ๊ด€์— ๋™์˜ํ•ด์ฃผ์„ธ์š”')
}).refine(data => data.password === data.confirmPassword, {
  message: '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”',
  path: ['confirmPassword']
});

// ํƒ€์ž… ์ถ”๋ก 
type SignupForm = z.infer<typeof signupSchema>;

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset
  } = useForm<SignupForm>({
    resolver: zodResolver(signupSchema),
    mode: 'onBlur' // ํฌ์ปค์Šค๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ ๊ฒ€์ฆ
  });

  const onSubmit = async (data: SignupForm) => {
    try {
      console.log('ํผ ๋ฐ์ดํ„ฐ:', data);
      // API ํ˜ธ์ถœ
      await fetch('/api/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });
      
      alert('ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต! ๐ŸŽ‰');
      reset(); // ํผ ์ดˆ๊ธฐํ™”
      
    } catch (error) {
      console.error('ํšŒ์›๊ฐ€์ž… ์‹คํŒจ:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="username">์•„์ด๋””</label>
        <input
          id="username"
          type="text"
          {...register('username')}
          placeholder="์•„์ด๋””๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
        />
        {errors.username && (
          <span className="error">{errors.username.message}</span>
        )}
      </div>

      <div>
        <label htmlFor="email">์ด๋ฉ”์ผ</label>
        <input
          id="email"
          type="email"
          {...register('email')}
          placeholder="email@example.com"
        />
        {errors.email && (
          <span className="error">{errors.email.message}</span>
        )}
      </div>

      <div>
        <label htmlFor="password">๋น„๋ฐ€๋ฒˆํ˜ธ</label>
        <input
          id="password"
          type="password"
          {...register('password')}
          placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
        />
        {errors.password && (
          <span className="error">{errors.password.message}</span>
        )}
      </div>

      <div>
        <label htmlFor="confirmPassword">๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ</label>
        <input
          id="confirmPassword"
          type="password"
          {...register('confirmPassword')}
          placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ์ž…๋ ฅํ•˜์„ธ์š”"
        />
        {errors.confirmPassword && (
          <span className="error">{errors.confirmPassword.message}</span>
        )}
      </div>

      <div>
        <label htmlFor="age">๋‚˜์ด</label>
        <input
          id="age"
          type="number"
          {...register('age', { valueAsNumber: true })}
          placeholder="๋‚˜์ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
        />
        {errors.age && (
          <span className="error">{errors.age.message}</span>
        )}
      </div>

      <div>
        <label>
          <input
            type="checkbox"
            {...register('terms')}
          />
          ์ด์šฉ์•ฝ๊ด€์— ๋™์˜ํ•ฉ๋‹ˆ๋‹ค
        </label>
        {errors.terms && (
          <span className="error">{errors.terms.message}</span>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '์ฒ˜๋ฆฌ์ค‘...' : 'ํšŒ์›๊ฐ€์ž…'}
      </button>
    </form>
  );
}

๐ŸŽจ ์ด ์ฝ”๋“œ์˜ ๋ฉ‹์ง„ ์ ๋“ค

1. ์™„๋ฒฝํ•œ ํƒ€์ž… ์•ˆ์ •์„ฑ: register ํ•จ์ˆ˜์— ์ž˜๋ชป๋œ ํ•„๋“œ๋ช…์„ ๋„ฃ์œผ๋ฉด ํƒ€์ž… ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด
2. ์ž๋™ ์™„์„ฑ: errors ๊ฐ์ฒด์—์„œ ํ•„๋“œ๋ช…์„ ์ž…๋ ฅํ•˜๋ฉด ์ž๋™์™„์„ฑ์ด ๋ผ
3. ์‹ค์‹œ๊ฐ„ ๊ฒ€์ฆ: mode ์˜ต์…˜์œผ๋กœ ๊ฒ€์ฆ ์‹œ์ ์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด
4. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€: ์Šคํ‚ค๋งˆ์— ์ •์˜ํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ๊ทธ๋Œ€๋กœ ํ‘œ์‹œ๋ผ

Yup + React Hook Form

Yup๋„ ๋˜‘๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const signupSchema = yup.object({
  username: yup.string()
    .min(3, '์•„์ด๋””๋Š” 3๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .max(20, '์•„์ด๋””๋Š” 20๊ธ€์ž ์ดํ•˜์—ฌ์•ผ ํ•ด์š”')
    .matches(/^[a-zA-Z0-9_]+$/, '์˜๋ฌธ, ์ˆซ์ž, ์–ธ๋”์Šค์ฝ”์–ด๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์š”')
    .required('์•„์ด๋””๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  email: yup.string()
    .email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”')
    .required('์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  password: yup.string()
    .min(8, '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .matches(/[A-Z]/, '๋Œ€๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .matches(/[a-z]/, '์†Œ๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .matches(/[0-9]/, '์ˆซ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .required('๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”')
    .required('๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  age: yup.number()
    .min(18, '18์„ธ ์ด์ƒ๋งŒ ๊ฐ€์ž… ๊ฐ€๋Šฅํ•ด์š”')
    .max(120, '์˜ฌ๋ฐ”๋ฅธ ๋‚˜์ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
    .required('๋‚˜์ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  terms: yup.boolean()
    .oneOf([true], '์•ฝ๊ด€์— ๋™์˜ํ•ด์ฃผ์„ธ์š”')
    .required()
});

type SignupForm = yup.InferType<typeof signupSchema>;

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<SignupForm>({
    resolver: yupResolver(signupSchema)
  });

  // ๋‚˜๋จธ์ง€๋Š” Zod์™€ ๋™์ผ!
}

๋ณด๋‹ค์‹œํ”ผ ์‚ฌ์šฉ๋ฒ•์ด ๊ฑฐ์˜ ๋˜‘๊ฐ™์•„. resolver๋งŒ ๋ฐ”๊ฟ”์ฃผ๋ฉด ๋ผ! ๐Ÿ˜Š

๐Ÿ”ฅ ๊ณ ๊ธ‰ ํŒจํ„ด๊ณผ ์‹ค์ „ ํŒ

์ด์ œ ์ข€ ๋” ๊ณ ๊ธ‰ ๊ธฐ์ˆ ๋“ค์„ ์•Œ์•„๋ณด์ž. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์ •๋ง ์œ ์šฉํ•œ ํŒจํ„ด๋“ค์ด์•ผ! ๐Ÿ’ช

1. ๋™์  ํผ ์Šคํ‚ค๋งˆ

๋•Œ๋กœ๋Š” ์‚ฌ์šฉ์ž์˜ ์„ ํƒ์— ๋”ฐ๋ผ ํผ ํ•„๋“œ๊ฐ€ ๋‹ฌ๋ผ์ ธ์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด. ์ด๋Ÿด ๋•Œ๋Š” ๋™์ ์œผ๋กœ ์Šคํ‚ค๋งˆ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋ผ:

import { z } from 'zod';

type AccountType = 'personal' | 'business';

function createAccountSchema(accountType: AccountType) {
  const baseSchema = z.object({
    accountType: z.enum(['personal', 'business']),
    email: z.string().email(),
    password: z.string().min(8)
  });

  if (accountType === 'business') {
    return baseSchema.extend({
      companyName: z.string().min(2, 'ํšŒ์‚ฌ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
      businessNumber: z.string()
        .regex(/^\d{10}$/, '์‚ฌ์—…์ž๋“ฑ๋ก๋ฒˆํ˜ธ๋Š” 10์ž๋ฆฌ ์ˆซ์ž์˜ˆ์š”'),
      companyAddress: z.string().min(5, 'ํšŒ์‚ฌ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
    });
  }

  return baseSchema.extend({
    firstName: z.string().min(1, '์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
    lastName: z.string().min(1, '์„ฑ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
    birthDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
  });
}

// ์‚ฌ์šฉ
function DynamicForm() {
  const [accountType, setAccountType] = useState<AccountType>('personal');
  const schema = useMemo(() => createAccountSchema(accountType), [accountType]);
  
  const { register, handleSubmit } = useForm({
    resolver: zodResolver(schema)
  });
  
  // ...
}

2. ๋น„๋™๊ธฐ ๊ฒ€์ฆ (์„œ๋ฒ„ ์ค‘๋ณต ์ฒดํฌ)

์•„์ด๋””๋‚˜ ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ์ฒ˜๋Ÿผ ์„œ๋ฒ„์— ํ™•์ธํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์ง€? ์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด:

// Zod ๋ฐฉ์‹
const usernameSchema = z.string()
  .min(3)
  .refine(async (username) => {
    // ๋””๋ฐ”์šด์‹ฑ์„ ์œ„ํ•ด ์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด
    await new Promise(resolve => setTimeout(resolve, 300));
    
    const response = await fetch(`/api/check-username?username=${username}`);
    const { available } = await response.json();
    return available;
  }, {
    message: '์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์•„์ด๋””์˜ˆ์š”'
  });

// React Hook Form์—์„œ ์‚ฌ์šฉ
function UsernameInput() {
  const { register, formState: { errors } } = useForm({
    resolver: zodResolver(z.object({ username: usernameSchema })),
    mode: 'onBlur' // ํฌ์ปค์Šค๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ๋งŒ ๊ฒ€์ฆ
  });
  
  return (
    <div>
      <input {...register('username')} />
      {errors.username && <span>{errors.username.message}</span>}
    </div>
  );
}

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

๋น„๋™๊ธฐ ๊ฒ€์ฆ์€ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์–ด. ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ๋””๋ฐ”์šด์‹ฑ์„ ์ ์šฉํ•ด์„œ ๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ์„ ์ค„์ด์ž!

3. ๋‹ค๋‹จ๊ณ„ ํผ (Multi-step Form)

ํšŒ์›๊ฐ€์ž…์ด๋‚˜ ์„ค๋ฌธ์กฐ์‚ฌ์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋กœ ๋‚˜๋‰œ ํผ์„ ๋งŒ๋“ค ๋•Œ๋Š” ์ด๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด:

import { z } from 'zod';

// ๊ฐ ๋‹จ๊ณ„๋ณ„ ์Šคํ‚ค๋งˆ
const step1Schema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

const step2Schema = z.object({
  firstName: z.string().min(1),
  lastName: z.string().min(1),
  birthDate: z.string()
});

const step3Schema = z.object({
  address: z.string().min(5),
  city: z.string().min(2),
  zipCode: z.string().regex(/^\d{5}$/)
});

// ์ „์ฒด ์Šคํ‚ค๋งˆ
const fullSchema = step1Schema.merge(step2Schema).merge(step3Schema);

type FullForm = z.infer<typeof fullSchema>;

function MultiStepForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState<Partial<FullForm>>({});

  const getCurrentSchema = () => {
    switch(step) {
      case 1: return step1Schema;
      case 2: return step2Schema;
      case 3: return step3Schema;
      default: return step1Schema;
    }
  };

  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(getCurrentSchema()),
    defaultValues: formData
  });

  const onSubmit = (data: any) => {
    const updatedData = { ...formData, ...data };
    setFormData(updatedData);

    if (step < 3) {
      setStep(step + 1);
    } else {
      // ์ตœ์ข… ์ œ์ถœ
      console.log('์ตœ์ข… ๋ฐ์ดํ„ฐ:', updatedData);
      submitToServer(updatedData);
    }
  };

  return (
    <div>
      <h2>๋‹จ๊ณ„ {step} / 3</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        {step === 1 && (
          <>
            <input {...register('email')} placeholder="์ด๋ฉ”์ผ" />
            {errors.email && <span>{errors.email.message}</span>}
            
            <input {...register('password')} type="password" placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ" />
            {errors.password && <span>{errors.password.message}</span>}
          </>
        )}

        {step === 2 && (
          <>
            <input {...register('firstName')} placeholder="์ด๋ฆ„" />
            {errors.firstName && <span>{errors.firstName.message}</span>}
            
            <input {...register('lastName')} placeholder="์„ฑ" />
            {errors.lastName && <span>{errors.lastName.message}</span>}
            
            <input {...register('birthDate')} type="date" />
            {errors.birthDate && <span>{errors.birthDate.message}</span>}
          </>
        )}

        {step === 3 && (
          <>
            <input {...register('address')} placeholder="์ฃผ์†Œ" />
            {errors.address && <span>{errors.address.message}</span>}
            
            <input {...register('city')} placeholder="๋„์‹œ" />
            {errors.city && <span>{errors.city.message}</span>}
            
            <input {...register('zipCode')} placeholder="์šฐํŽธ๋ฒˆํ˜ธ" />
            {errors.zipCode && <span>{errors.zipCode.message}</span>}
          </>
        )}

        <div>
          {step > 1 && (
            <button type="button" onClick={() => setStep(step - 1)}>
              ์ด์ „
            </button>
          )}
          <button type="submit">
            {step === 3 ? '์™„๋ฃŒ' : '๋‹ค์Œ'}
          </button>
        </div>
      </form>
    </div>
  );
}

4. ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฒ€์ฆ

ํŒŒ์ผ ์—…๋กœ๋“œ๋„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด:

const fileSchema = z.object({
  avatar: z.custom<FileList>()
    .refine(files => files?.length === 1, 'ํŒŒ์ผ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”')
    .refine(
      files => files?.[0]?.size <= 5 * 1024 * 1024,
      'ํŒŒ์ผ ํฌ๊ธฐ๋Š” 5MB ์ดํ•˜์—ฌ์•ผ ํ•ด์š”'
    )
    .refine(
      files => ['image/jpeg', 'image/png', 'image/webp'].includes(files?.[0]?.type),
      'JPG, PNG, WEBP ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ด์š”'
    )
    .transform(files => files?.[0])
});

function FileUploadForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(z.object({ avatar: fileSchema }))
  });

  const onSubmit = async (data: any) => {
    const formData = new FormData();
    formData.append('avatar', data.avatar);
    
    await fetch('/api/upload', {
      method: 'POST',
      body: formData
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        type="file"
        accept="image/*"
        {...register('avatar')}
      />
      {errors.avatar && <span>{errors.avatar.message}</span>}
      <button type="submit">์—…๋กœ๋“œ</button>
    </form>
  );
}

๐ŸŽจ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

์ด์ œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์–ด๋–ป๊ฒŒ ๊ตฌ์กฐํ™”ํ•˜๋ฉด ์ข‹์„์ง€ ์•Œ์•„๋ณด์ž. ๊น”๋”ํ•œ ๊ตฌ์กฐ๋Š” ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜! ๐Ÿ—๏ธ

ํด๋” ๊ตฌ์กฐ

src/
โ”œโ”€โ”€ schemas/
โ”‚   โ”œโ”€โ”€ auth.schema.ts        # ์ธ์ฆ ๊ด€๋ จ ์Šคํ‚ค๋งˆ
โ”‚   โ”œโ”€โ”€ user.schema.ts        # ์‚ฌ์šฉ์ž ๊ด€๋ จ ์Šคํ‚ค๋งˆ
โ”‚   โ”œโ”€โ”€ product.schema.ts     # ์ƒํ’ˆ ๊ด€๋ จ ์Šคํ‚ค๋งˆ
โ”‚   โ””โ”€โ”€ index.ts              # ์Šคํ‚ค๋งˆ export
โ”œโ”€โ”€ types/
โ”‚   โ”œโ”€โ”€ auth.types.ts         # ์ธ์ฆ ํƒ€์ž…
โ”‚   โ”œโ”€โ”€ user.types.ts         # ์‚ฌ์šฉ์ž ํƒ€์ž…
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ forms/
โ”‚   โ”‚   โ”œโ”€โ”€ LoginForm.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ SignupForm.tsx
โ”‚   โ”‚   โ””โ”€โ”€ ProfileForm.tsx
โ”‚   โ””โ”€โ”€ common/
โ”‚       โ”œโ”€โ”€ FormInput.tsx     # ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ
โ”‚       โ””โ”€โ”€ FormError.tsx     # ์—๋Ÿฌ ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ
โ””โ”€โ”€ utils/
    โ””โ”€โ”€ validation.ts         # ๊ฒ€์ฆ ์œ ํ‹ธ๋ฆฌํ‹ฐ

์Šคํ‚ค๋งˆ ํŒŒ์ผ ์˜ˆ์‹œ

// schemas/auth.schema.ts
import { z } from 'zod';

export const loginSchema = z.object({
  email: z.string()
    .email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”')
    .min(1, '์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  password: z.string()
    .min(1, '๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  rememberMe: z.boolean().optional()
});

export const signupSchema = z.object({
  email: z.string().email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”'),
  password: z.string()
    .min(8, '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .regex(/[A-Z]/, '๋Œ€๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .regex(/[0-9]/, '์ˆซ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ด์š”'),
  confirmPassword: z.string(),
  username: z.string()
    .min(3, '์•„์ด๋””๋Š” 3๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .regex(/^[a-zA-Z0-9_]+$/, '์˜๋ฌธ, ์ˆซ์ž, ์–ธ๋”์Šค์ฝ”์–ด๋งŒ ๊ฐ€๋Šฅํ•ด์š”'),
  agreeToTerms: z.boolean()
    .refine(val => val === true, '์•ฝ๊ด€์— ๋™์˜ํ•ด์ฃผ์„ธ์š”')
}).refine(data => data.password === data.confirmPassword, {
  message: '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”',
  path: ['confirmPassword']
});

export const resetPasswordSchema = z.object({
  email: z.string().email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”')
});

// schemas/index.ts
export * from './auth.schema';
export * from './user.schema';
export * from './product.schema';

ํƒ€์ž… ํŒŒ์ผ ์˜ˆ์‹œ

// types/auth.types.ts
import { z } from 'zod';
import { loginSchema, signupSchema, resetPasswordSchema } from '@/schemas';

export type LoginForm = z.infer<typeof loginSchema>;
export type SignupForm = z.infer<typeof signupSchema>;
export type ResetPasswordForm = z.infer<typeof resetPasswordSchema>;

// API ์‘๋‹ต ํƒ€์ž…
export interface AuthResponse {
  token: string;
  user: {
    id: string;
    email: string;
    username: string;
  };
}

export interface AuthError {
  message: string;
  field?: string;
}

์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํผ ์ปดํฌ๋„ŒํŠธ

// components/common/FormInput.tsx
import { UseFormRegister, FieldError } from 'react-hook-form';

interface FormInputProps {
  label: string;
  name: string;
  type?: string;
  placeholder?: string;
  register: UseFormRegister<any>;
  error?: FieldError;
  required?: boolean;
}

export function FormInput({
  label,
  name,
  type = 'text',
  placeholder,
  register,
  error,
  required = false
}: FormInputProps) {
  return (
    <div className="form-group">
      <label htmlFor={name}>
        {label}
        {required && <span className="required">*</span>}
      </label>
      <input
        id={name}
        type={type}
        placeholder={placeholder}
        {...register(name)}
        className={error ? 'error' : ''}
      />
      {error && (
        <span className="error-message">{error.message}</span>
      )}
    </div>
  );
}

// components/common/FormError.tsx
interface FormErrorProps {
  message?: string;
}

export function FormError({ message }: FormErrorProps) {
  if (!message) return null;
  
  return (
    <div className="form-error">
      <span>โš ๏ธ {message}</span>
    </div>
  );
}

์‹ค์ œ ํผ ์ปดํฌ๋„ŒํŠธ

// components/forms/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema } from '@/schemas';
import { LoginForm as LoginFormType } from '@/types';
import { FormInput, FormError } from '@/components/common';

export function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError
  } = useForm<LoginFormType>({
    resolver: zodResolver(loginSchema)
  });

  const onSubmit = async (data: LoginFormType) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });

      if (!response.ok) {
        const error = await response.json();
        setError('root', {
          message: error.message || '๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์–ด์š”'
        });
        return;
      }

      const result = await response.json();
      // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
      console.log('๋กœ๊ทธ์ธ ์„ฑ๊ณต:', result);
      
    } catch (error) {
      setError('root', {
        message: '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”'
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="login-form">
      <h2>๋กœ๊ทธ์ธ</h2>
      
      <FormError message={errors.root?.message} />
      
      <FormInput
        label="์ด๋ฉ”์ผ"
        name="email"
        type="email"
        placeholder="email@example.com"
        register={register}
        error={errors.email}
        required
      />
      
      <FormInput
        label="๋น„๋ฐ€๋ฒˆํ˜ธ"
        name="password"
        type="password"
        placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
        register={register}
        error={errors.password}
        required
      />
      
      <div className="form-group">
        <label>
          <input type="checkbox" {...register('rememberMe')} />
          ๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€
        </label>
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '๋กœ๊ทธ์ธ ์ค‘...' : '๋กœ๊ทธ์ธ'}
      </button>
    </form>
  );
}

๐Ÿ’ก ๊ตฌ์กฐํ™”์˜ ์žฅ์ 

โ€ข ์žฌ์‚ฌ์šฉ์„ฑ: ์Šคํ‚ค๋งˆ์™€ ํƒ€์ž…์„ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์žฌ์‚ฌ์šฉ
โ€ข ์œ ์ง€๋ณด์ˆ˜: ๊ฒ€์ฆ ๋กœ์ง์ด ํ•œ ๊ณณ์— ๋ชจ์—ฌ์žˆ์–ด ์ˆ˜์ •์ด ์‰ฌ์›€
โ€ข ์ผ๊ด€์„ฑ: ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ๋™์ผํ•œ ํŒจํ„ด ์‚ฌ์šฉ
โ€ข ํ…Œ์ŠคํŠธ: ์Šคํ‚ค๋งˆ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

ํผ ๊ฒ€์ฆ์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์ณ. ๊ทธ๋ž˜์„œ ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ์ •๋ง ์ค‘์š”ํ•ด! ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ๊ณต์œ ํ• ๊ฒŒ. ๐ŸŽฏ

1. ๊ฒ€์ฆ ๋ชจ๋“œ ์„ ํƒ

// ์˜ต์…˜ 1: onChange - ์ž…๋ ฅํ•  ๋•Œ๋งˆ๋‹ค ๊ฒ€์ฆ (๊ฐ€์žฅ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ, ์„ฑ๋Šฅ ๋ถ€๋‹ด)
useForm({
  mode: 'onChange'
});

// ์˜ต์…˜ 2: onBlur - ํฌ์ปค์Šค๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ ๊ฒ€์ฆ (๊ท ํ˜•์žกํžŒ ์„ ํƒ)
useForm({
  mode: 'onBlur'
});

// ์˜ต์…˜ 3: onSubmit - ์ œ์ถœํ•  ๋•Œ๋งŒ ๊ฒ€์ฆ (๊ฐ€์žฅ ๊ฐ€๋ฒผ์›€)
useForm({
  mode: 'onSubmit'
});

// ์˜ต์…˜ 4: ํ˜ผํ•ฉ - ์ฒซ ์ œ์ถœ ํ›„์—๋Š” onChange๋กœ ์ „ํ™˜
useForm({
  mode: 'onSubmit',
  reValidateMode: 'onChange'
});

๐Ÿ“Š ๊ฐ ๋ชจ๋“œ์˜ ํŠน์ง•

onChange: ์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ๋ฐฑ, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ตœ๊ณ , ์„ฑ๋Šฅ ๋ถ€๋‹ด ์žˆ์Œ
onBlur: ์ ์ ˆํ•œ ๊ท ํ˜•, ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ถ”์ฒœ
onSubmit: ๊ฐ€์žฅ ๊ฐ€๋ฒผ์›€, ๊ฐ„๋‹จํ•œ ํผ์— ์ ํ•ฉ
ํ˜ผํ•ฉ: ์ตœ์ดˆ์—๋Š” ๊ฐ€๋ณ๊ฒŒ, ์—๋Ÿฌ ๋ฐœ์ƒ ํ›„ ์ ๊ทน์ ์œผ๋กœ

2. ๋””๋ฐ”์šด์‹ฑ ์ ์šฉ

ํŠนํžˆ ๋น„๋™๊ธฐ ๊ฒ€์ฆ์ด๋‚˜ ๋ณต์žกํ•œ ๊ฒ€์ฆ์—๋Š” ๋””๋ฐ”์šด์‹ฑ์ด ํ•„์ˆ˜์•ผ:

import { useDebounce } from 'use-debounce';

function SearchForm() {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedSearchTerm] = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // ๋””๋ฐ”์šด์‹ฑ๋œ ๊ฐ’์œผ๋กœ ๊ฒ€์ƒ‰
      performSearch(debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

  return (
    <input
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
    />
  );
}

// ๋˜๋Š” React Hook Form์˜ watch์™€ ํ•จ๊ป˜
function DebouncedValidation() {
  const { watch, setValue } = useForm();
  const username = watch('username');
  const [debouncedUsername] = useDebounce(username, 500);

  useEffect(() => {
    if (debouncedUsername) {
      checkUsernameAvailability(debouncedUsername);
    }
  }, [debouncedUsername]);
}

3. ์Šคํ‚ค๋งˆ ๋ฉ”๋ชจ์ด์ œ์ด์…˜

๋™์  ์Šคํ‚ค๋งˆ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” useMemo๋กœ ์ตœ์ ํ™”ํ•˜์ž:

function DynamicForm({ userType }: { userType: string }) {
  // โŒ ๋‚˜์œ ์˜ˆ: ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ
  const schema = createSchema(userType);

  // โœ… ์ข‹์€ ์˜ˆ: userType์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ์ƒˆ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ
  const schema = useMemo(() => createSchema(userType), [userType]);

  const { register, handleSubmit } = useForm({
    resolver: zodResolver(schema)
  });

  // ...
}

4. ์กฐ๊ฑด๋ถ€ ํ•„๋“œ ์ตœ์ ํ™”

// โŒ ๋น„ํšจ์œจ์ : ๋ชจ๋“  ํ•„๋“œ๋ฅผ ํ•ญ์ƒ ๋ Œ๋”๋ง
function IneffcientForm() {
  const { register, watch } = useForm();
  const accountType = watch('accountType');

  return (
    <>
      <select {...register('accountType')}>
        <option value="personal">๊ฐœ์ธ</option>
        <option value="business">์‚ฌ์—…์ž</option>
      </select>

      {/* ํ•ญ์ƒ ๋ Œ๋”๋ง๋˜์ง€๋งŒ ์ˆจ๊ฒจ์ง */}
      <div style={{ display: accountType === 'business' ? 'block' : 'none' }}>
        <input {...register('companyName')} />
      </div>
    </>
  );
}

// โœ… ํšจ์œจ์ : ํ•„์š”ํ•  ๋•Œ๋งŒ ๋ Œ๋”๋ง
function EfficientForm() {
  const { register, watch } = useForm();
  const accountType = watch('accountType');

  return (
    <>
      <select {...register('accountType')}>
        <option value="personal">๊ฐœ์ธ</option>
        <option value="business">์‚ฌ์—…์ž</option>
      </select>

      {/* ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง */}
      {accountType === 'business' && (
        <input {...register('companyName')} />
      )}
    </>
  );
}

5. ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™”

// โŒ ์ „์ฒด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ import
import * as yup from 'yup';

// โœ… ํ•„์š”ํ•œ ๊ฒƒ๋งŒ import (tree-shaking ๊ฐ€๋Šฅ)
import { object, string, number } from 'yup';

// Zod๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ tree-shaking์ด ์ž˜ ๋ผ
import { z } from 'zod'; // ์ด๋ฏธ ์ตœ์ ํ™”๋˜์–ด ์žˆ์Œ

๐ŸŽ“ ์‹ค์ „ ์˜ˆ์ œ: ์™„์ „ํ•œ ํšŒ์›๊ฐ€์ž… ํผ

์ž, ์ด์ œ ๋ฐฐ์šด ๊ฑธ ๋ชจ๋‘ ํ•ฉ์ณ์„œ ์™„์ „ํ•œ ํšŒ์›๊ฐ€์ž… ํผ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์œผ๋กœ ๋งŒ๋“ค ๊ฑฐ์•ผ. ๐Ÿ’ช

// schemas/signup.schema.ts
import { z } from 'zod';

export const signupSchema = z.object({
  // ๊ธฐ๋ณธ ์ •๋ณด
  email: z.string()
    .min(1, '์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
    .email('์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”')
    .refine(async (email) => {
      // ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ
      const response = await fetch(`/api/check-email?email=${email}`);
      const { available } = await response.json();
      return available;
    }, '์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์ด๋ฉ”์ผ์ด์—์š”'),

  username: z.string()
    .min(3, '์•„์ด๋””๋Š” 3๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .max(20, '์•„์ด๋””๋Š” 20๊ธ€์ž ์ดํ•˜์—ฌ์•ผ ํ•ด์š”')
    .regex(/^[a-zA-Z0-9_]+$/, '์˜๋ฌธ, ์ˆซ์ž, ์–ธ๋”์Šค์ฝ”์–ด๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์š”')
    .refine(async (username) => {
      const response = await fetch(`/api/check-username?username=${username}`);
      const { available } = await response.json();
      return available;
    }, '์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์•„์ด๋””์˜ˆ์š”'),

  password: z.string()
    .min(8, '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”')
    .max(100, '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ์–ด์š”')
    .regex(/[A-Z]/, '๋Œ€๋ฌธ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .regex(/[a-z]/, '์†Œ๋ฌธ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .regex(/[0-9]/, '์ˆซ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”')
    .regex(/[^A-Za-z0-9]/, 'ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ์ตœ์†Œ 1๊ฐœ ํฌํ•จํ•ด์•ผ ํ•ด์š”'),

  confirmPassword: z.string()
    .min(1, '๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),

  // ๊ฐœ์ธ ์ •๋ณด
  firstName: z.string()
    .min(1, '์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
    .max(50, '์ด๋ฆ„์ด ๋„ˆ๋ฌด ๊ธธ์–ด์š”'),

  lastName: z.string()
    .min(1, '์„ฑ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
    .max(50, '์„ฑ์ด ๋„ˆ๋ฌด ๊ธธ์–ด์š”'),

  birthDate: z.string()
    .regex(/^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
    .refine((date) => {
      const birthDate = new Date(date);
      const today = new Date();
      const age = today.getFullYear() - birthDate.getFullYear();
      return age >= 18;
    }, '18์„ธ ์ด์ƒ๋งŒ ๊ฐ€์ž…ํ•  ์ˆ˜ ์žˆ์–ด์š”'),

  phone: z.string()
    .regex(/^010-\d{4}-\d{4}$/, '010-0000-0000 ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”')
    .optional(),

  // ์•ฝ๊ด€ ๋™์˜
  agreeToTerms: z.boolean()
    .refine(val => val === true, '์ด์šฉ์•ฝ๊ด€์— ๋™์˜ํ•ด์ฃผ์„ธ์š”'),

  agreeToPrivacy: z.boolean()
    .refine(val => val === true, '๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ์— ๋™์˜ํ•ด์ฃผ์„ธ์š”'),

  agreeToMarketing: z.boolean()
    .optional()

}).refine(data => data.password === data.confirmPassword, {
  message: '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”',
  path: ['confirmPassword']
});

export type SignupFormData = z.infer<typeof signupSchema>;

// components/SignupForm.tsx
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { signupSchema, SignupFormData } from '@/schemas/signup.schema';

export function SignupForm() {
  const [step, setStep] = useState(1);
  const [isSuccess, setIsSuccess] = useState(false);

  const {
    register,
    handleSubmit,
    watch,
    formState: { errors, isSubmitting, isValid },
    trigger
  } = useForm<SignupFormData>({
    resolver: zodResolver(signupSchema),
    mode: 'onBlur'
  });

  const password = watch('password');

  // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„ ๊ณ„์‚ฐ
  const getPasswordStrength = (pwd: string): number => {
    if (!pwd) return 0;
    let strength = 0;
    if (pwd.length >= 8) strength++;
    if (/[A-Z]/.test(pwd)) strength++;
    if (/[a-z]/.test(pwd)) strength++;
    if (/[0-9]/.test(pwd)) strength++;
    if (/[^A-Za-z0-9]/.test(pwd)) strength++;
    return strength;
  };

  const passwordStrength = getPasswordStrength(password);

  const onSubmit = async (data: SignupFormData) => {
    try {
      const response = await fetch('/api/auth/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });

      if (!response.ok) {
        throw new Error('ํšŒ์›๊ฐ€์ž…์— ์‹คํŒจํ–ˆ์–ด์š”');
      }

      setIsSuccess(true);
      
    } catch (error) {
      console.error('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜:', error);
      alert('ํšŒ์›๊ฐ€์ž… ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
    }
  };

  const handleNextStep = async () => {
    const fieldsToValidate = step === 1 
      ? ['email', 'username', 'password', 'confirmPassword']
      : ['firstName', 'lastName', 'birthDate', 'phone'];

    const isStepValid = await trigger(fieldsToValidate as any);
    
    if (isStepValid) {
      setStep(step + 1);
    }
  };

  if (isSuccess) {
    return (
      <div className="success-message">
        <h2>๐ŸŽ‰ ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ!</h2>
        <p>ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! ์ด๋ฉ”์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.</p>
      </div>
    );
  }

  return (
    <div className="signup-container">
      <div className="progress-bar">
        <div 
          className="progress" 
          style={{ width: `${(step / 3) * 100}%` }}
        />
      </div>

      <h2>ํšŒ์›๊ฐ€์ž… - {step}/3</h2>

      <form onSubmit={handleSubmit(onSubmit)}>
        {/* Step 1: ๊ณ„์ • ์ •๋ณด */}
        {step === 1 && (
          <div className="form-step">
            <h3>๊ณ„์ • ์ •๋ณด</h3>

            <div className="form-group">
              <label htmlFor="email">์ด๋ฉ”์ผ *</label>
              <input
                id="email"
                type="email"
                {...register('email')}
                placeholder="email@example.com"
              />
              {errors.email && (
                <span className="error">{errors.email.message}</span>
              )}
            </div>

            <div className="form-group">
              <label htmlFor="username">์•„์ด๋”” *</label>
              <input
                id="username"
                type="text"
                {...register('username')}
                placeholder="3-20์ž์˜ ์˜๋ฌธ, ์ˆซ์ž, ์–ธ๋”์Šค์ฝ”์–ด"
              />
              {errors.username && (
                <span className="error">{errors.username.message}</span>
              )}
            </div>

            <div className="form-group">
              <label htmlFor="password">๋น„๋ฐ€๋ฒˆํ˜ธ *</label>
              <input
                id="password"
                type="password"
                {...register('password')}
                placeholder="8์ž ์ด์ƒ, ๋Œ€์†Œ๋ฌธ์ž, ์ˆซ์ž, ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ"
              />
              {password && (
                <div className="password-strength">
                  <div className="strength-bar">
                    <div 
                      className={`strength-fill strength-${passwordStrength}`}
                      style={{ width: `${(passwordStrength / 5) * 100}%` }}
                    />
                  </div>
                  <span className="strength-text">
                    {passwordStrength <= 2 && '์•ฝํ•จ'}
                    {passwordStrength === 3 && '๋ณดํ†ต'}
                    {passwordStrength === 4 && '๊ฐ•ํ•จ'}
                    {passwordStrength === 5 && '๋งค์šฐ ๊ฐ•ํ•จ'}
                  </span>
                </div>
              )}
              {errors.password && (
                <span className="error">{errors.password.message}</span>
              )}
            </div>

            <div className="form-group">
              <label htmlFor="confirmPassword">๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ *</label>
              <input
                id="confirmPassword"
                type="password"
                {...register('confirmPassword')}
                placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ์ž…๋ ฅํ•˜์„ธ์š”"
              />
              {errors.confirmPassword && (
                <span className="error">{errors.confirmPassword.message}</span>
              )}
            </div>
          </div>
        )}

        {/* Step 2: ๊ฐœ์ธ ์ •๋ณด */}
        {step === 2 && (
          <div className="form-step">
            <h3>๊ฐœ์ธ ์ •๋ณด</h3>

            <div className="form-row">
              <div className="form-group">
                <label htmlFor="lastName">์„ฑ *</label>
                <input
                  id="lastName"
                  type="text"
                  {...register('lastName')}
                  placeholder="ํ™"
                />
                {errors.lastName && (
                  <span className="error">{errors.lastName.message}</span>
                )}
              </div>

              <div className="form-group">
                <label htmlFor="firstName">์ด๋ฆ„ *</label>
                <input
                  id="firstName"
                  type="text"
                  {...register('firstName')}
                  placeholder="๊ธธ๋™"
                />
                {errors.firstName && (
                  <span className="error">{errors.firstName.message}</span>
                )}
              </div>
            </div>

            <div className="form-group">
              <label htmlFor="birthDate">์ƒ๋…„์›”์ผ *</label>
              <input
                id="birthDate"
                type="date"
                {...register('birthDate')}
              />
              {errors.birthDate && (
                <span className="error">{errors.birthDate.message}</span>
              )}
            </div>

            <div className="form-group">
              <label htmlFor="phone">ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ</label>
              <input
                id="phone"
                type="tel"
                {...register('phone')}
                placeholder="010-0000-0000"
              />
              {errors.phone && (
                <span className="error">{errors.phone.message}</span>
              )}
            </div>
          </div>
        )}

        {/* Step 3: ์•ฝ๊ด€ ๋™์˜ */}
        {step === 3 && (
          <div className="form-step">
            <h3>์•ฝ๊ด€ ๋™์˜</h3>

            <div className="terms-group">
              <label className="checkbox-label">
                <input
                  type="checkbox"
                  {...register('agreeToTerms')}
                />
                <span>[ํ•„์ˆ˜] ์ด์šฉ์•ฝ๊ด€์— ๋™์˜ํ•ฉ๋‹ˆ๋‹ค</span>
              </label>
              {errors.agreeToTerms && (
                <span className="error">{errors.agreeToTerms.message}</span>
              )}
            </div>

            <div className="terms-group">
              <label className="checkbox-label">
                <input
                  type="checkbox"
                  {...register('agreeToPrivacy')}
                />
                <span>[ํ•„์ˆ˜] ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ์— ๋™์˜ํ•ฉ๋‹ˆ๋‹ค</span>
              </label>
              {errors.agreeToPrivacy && (
                <span className="error">{errors.agreeToPrivacy.message}</span>
              )}
            </div>

            <div className="terms-group">
              <label className="checkbox-label">
                <input
                  type="checkbox"
                  {...register('agreeToMarketing')}
                />
                <span>[์„ ํƒ] ๋งˆ์ผ€ํŒ… ์ •๋ณด ์ˆ˜์‹ ์— ๋™์˜ํ•ฉ๋‹ˆ๋‹ค</span>
              </label>
            </div>
          </div>
        )}

        {/* ๋ฒ„ํŠผ */}
        <div className="form-actions">
          {step > 1 && (
            <button
              type="button"
              onClick={() => setStep(step - 1)}
              className="btn-secondary"
            >
              ์ด์ „
            </button>
          )}

          {step < 3 ? (
            <button
              type="button"
              onClick={handleNextStep}
              className="btn-primary"
            >
              ๋‹ค์Œ
            </button>
          ) : (
            <button
              type="submit"
              disabled={isSubmitting}
              className="btn-primary"
            >
              {isSubmitting ? '์ฒ˜๋ฆฌ์ค‘...' : '๊ฐ€์ž…ํ•˜๊ธฐ'}
            </button>
          )}
        </div>
      </form>
    </div>
  );
}

์ด ์˜ˆ์ œ๋Š” ์‹ค์ „์—์„œ ํ•„์š”ํ•œ ๊ฑฐ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด:

โœจ ํฌํ•จ๋œ ๊ธฐ๋Šฅ๋“ค

โ€ข ๋‹ค๋‹จ๊ณ„ ํผ (3๋‹จ๊ณ„)
โ€ข ์‹ค์‹œ๊ฐ„ ๊ฒ€์ฆ (onBlur ๋ชจ๋“œ)
โ€ข ๋น„๋™๊ธฐ ์ค‘๋ณต ์ฒดํฌ (์ด๋ฉ”์ผ, ์•„์ด๋””)
โ€ข ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„ ํ‘œ์‹œ
โ€ข ๋น„๋ฐ€๋ฒˆํ˜ธ ์ผ์น˜ ํ™•์ธ
โ€ข ์ง„ํ–‰ ์ƒํƒœ ํ‘œ์‹œ
โ€ข ๋‹จ๊ณ„๋ณ„ ๊ฒ€์ฆ
โ€ข ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
โ€ข ๋กœ๋”ฉ ์ƒํƒœ ๊ด€๋ฆฌ
โ€ข ์„ฑ๊ณต ๋ฉ”์‹œ์ง€

๐ŸŽช ์žฌ๋Šฅ๋„ท์—์„œ ๋ฐฐ์šฐ๋Š” ์‹ค์ „ ํŒ

์—ฌ๊ธฐ์„œ ์ž ๊น! ํ˜น์‹œ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋‚˜ ํผ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•ด ๋” ๊นŠ์ด ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋“ค์˜ ๊ฐ•์˜๋ฅผ ๋“ค์–ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ์‹ค์ „ ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ž์‹ ๋งŒ์˜ ๋…ธํ•˜์šฐ๋ฅผ ๊ณต์œ ํ•˜๊ฑฐ๋“ ! ๐ŸŽ“

ํŠนํžˆ ๋ณต์žกํ•œ ํผ ์ฒ˜๋ฆฌ๋‚˜ ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ์˜ ํƒ€์ž… ์‹œ์Šคํ…œ ์„ค๊ณ„ ๊ฐ™์€ ๊ณ ๊ธ‰ ์ฃผ์ œ๋“ค์€ ๊ฒฝํ—˜์ž์—๊ฒŒ ์ง์ ‘ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ํ›จ์”ฌ ํšจ์œจ์ ์ด์•ผ. ์žฌ๋Šฅ๋„ท์˜ '์ง€์‹์ธ์˜ ์ˆฒ' ๊ฐ™์€ ๊ณณ์—์„œ ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ์ง€์‹์„ ์–ป์„ ์ˆ˜ ์žˆ์–ด!

๐Ÿ› ํ”ํ•œ ์‹ค์ˆ˜์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

์ด์ œ ์‹ค์ „์—์„œ ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ๋‚˜๋„ ์ด๋Ÿฐ ์‹ค์ˆ˜๋“ค์„ ๋งŽ์ด ํ–ˆ์—ˆ์–ด! ๐Ÿ˜…

1. ํƒ€์ž… ๋ถˆ์ผ์น˜ ๋ฌธ์ œ

// โŒ ๋ฌธ์ œ: ํผ ๋ฐ์ดํ„ฐ์™€ ์Šคํ‚ค๋งˆ ํƒ€์ž…์ด ๋ถˆ์ผ์น˜
const schema = z.object({
  age: z.number()
});

// HTML input์€ ํ•ญ์ƒ string์„ ๋ฐ˜ํ™˜!
<input type="number" {...register('age')} />

// โœ… ํ•ด๊ฒฐ: valueAsNumber ์˜ต์…˜ ์‚ฌ์šฉ
<input 
  type="number" 
  {...register('age', { valueAsNumber: true })} 
/>

// ๋˜๋Š” ์Šคํ‚ค๋งˆ์—์„œ ๋ณ€ํ™˜
const schema = z.object({
  age: z.string().transform(val => parseInt(val, 10))
});

2. ๋น„๋™๊ธฐ ๊ฒ€์ฆ ํƒ€์ด๋ฐ

// โŒ ๋ฌธ์ œ: ๋งค ์ž…๋ ฅ๋งˆ๋‹ค API ํ˜ธ์ถœ
const schema = z.object({
  username: z.string().refine(async (val) => {
    const response = await fetch(`/api/check/${val}`);
    return response.ok;
  })
});

// โœ… ํ•ด๊ฒฐ: ๋””๋ฐ”์šด์‹ฑ + onBlur ๋ชจ๋“œ
useForm({
  resolver: zodResolver(schema),
  mode: 'onBlur' // ํฌ์ปค์Šค๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ๋งŒ ๊ฒ€์ฆ
});

// ๋˜๋Š” ๋ณ„๋„์˜ ๊ฒ€์ฆ ๋กœ์ง์œผ๋กœ ๋ถ„๋ฆฌ
const { setError, clearErrors } = useForm();
const username = watch('username');
const [debouncedUsername] = useDebounce(username, 500);

useEffect(() => {
  if (debouncedUsername) {
    checkUsername(debouncedUsername).then(available => {
      if (!available) {
        setError('username', {
          message: '์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์•„์ด๋””์˜ˆ์š”'
        });
      } else {
        clearErrors('username');
      }
    });
  }
}, [debouncedUsername]);

3. ์ค‘์ฒฉ ๊ฐ์ฒด ์ฒ˜๋ฆฌ

// โŒ ๋ฌธ์ œ: ์ค‘์ฒฉ ๊ฐ์ฒด์˜ ์—๋Ÿฌ ์ ‘๊ทผ
const schema = z.object({
  address: z.object({
    street: z.string(),
    city: z.string()
  })
});

// ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํƒ€์ž… ์—๋Ÿฌ!
{errors.address.street?.message}

// โœ… ํ•ด๊ฒฐ: ์˜ต์…”๋„ ์ฒด์ด๋‹ ์ œ๋Œ€๋กœ ์‚ฌ์šฉ
{errors.address?.street?.message}

// ๋˜๋Š” ๋” ์•ˆ์ „ํ•˜๊ฒŒ
{errors.address && errors.address.street && (
  <span>{errors.address.street.message}</span>
)}

4. ๋ฐฐ์—ด ํ•„๋“œ ์ฒ˜๋ฆฌ

// โŒ ๋ฌธ์ œ: ๋ฐฐ์—ด ํ•„๋“œ๋ฅผ ์ผ๋ฐ˜ ํ•„๋“œ์ฒ˜๋Ÿผ ์ฒ˜๋ฆฌ
const schema = z.object({
  tags: z.array(z.string())
});

<input {...register('tags')} /> // ์ž‘๋™ ์•ˆ ํ•จ!

// โœ… ํ•ด๊ฒฐ: useFieldArray ์‚ฌ์šฉ
import { useFieldArray } from 'react-hook-form';

function TagsInput() {
  const { control, register } = useForm({
    defaultValues: {
      tags: ['']
    }
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'tags'
  });

  return (
    <div>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`tags.${index}`)} />
          <button type="button" onClick={() => remove(index)}>
            ์‚ญ์ œ
          </button>
        </div>
      ))}
      <button type="button" onClick={() => append('')}>
        ํƒœ๊ทธ ์ถ”๊ฐ€
      </button>
    </div>
  );
}

5. ์กฐ๊ฑด๋ถ€ ํ•„๋“œ ๊ฒ€์ฆ

// โŒ ๋ฌธ์ œ: ์กฐ๊ฑด๋ถ€ ํ•„๋“œ๊ฐ€ ํ•ญ์ƒ ๊ฒ€์ฆ๋จ
const schema = z.object({
  hasCompany: z.boolean(),
  companyName: z.string().min(1) // ํ•ญ์ƒ ํ•„์ˆ˜!
});

// โœ… ํ•ด๊ฒฐ: ์กฐ๊ฑด๋ถ€ ๊ฒ€์ฆ ์ ์šฉ
const schema = z.object({
  hasCompany: z.boolean(),
  companyName: z.string().optional()
}).refine(data => {
  if (data.hasCompany) {
    return data.companyName && data.companyName.length > 0;
  }
  return true;
}, {
  message: 'ํšŒ์‚ฌ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”',
  path: ['companyName']
});

// ๋˜๋Š” Yup์˜ when ์‚ฌ์šฉ
const schema = yup.object({
  hasCompany: yup.boolean(),
  companyName: yup.string().when('hasCompany', {
    is: true,
    then: schema => schema.required('ํšŒ์‚ฌ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
    otherwise: schema => schema.notRequired()
  })
});

โš ๏ธ ์ฃผ์˜ํ•  ์ 

โ€ข ๋น„๋™๊ธฐ ๊ฒ€์ฆ์€ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜์ž
โ€ข ๋ณต์žกํ•œ ์กฐ๊ฑด๋ถ€ ๊ฒ€์ฆ์€ ์ฝ”๋“œ๋ฅผ ์ฝ๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ์ฃผ์„์„ ์ž˜ ๋‹ฌ์•„๋‘์ž
โ€ข ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ์‚ฌ์šฉ์ž ์นœํ™”์ ์œผ๋กœ ์ž‘์„ฑํ•˜์ž. "Invalid input"๋ณด๋‹ค๋Š” "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์•„์š”"๊ฐ€ ํ›จ์”ฌ ์ข‹์•„!

๐Ÿงช ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

ํผ ๊ฒ€์ฆ ๋กœ์ง๋„ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•ด! ํŠนํžˆ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋“ค์–ด๊ฐ„ ๊ฒฝ์šฐ์—๋Š” ๋”์šฑ ๊ทธ๋ž˜. ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ์˜ˆ์ œ๋ฅผ ๋ณด์ž. ๐Ÿงช

// schemas/__tests__/signup.schema.test.ts
import { describe, it, expect } from 'vitest';
import { signupSchema } from '../signup.schema';

describe('Signup Schema', () => {
  it('์œ ํšจํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ต๊ณผ์‹œ์ผœ์•ผ ํ•จ', () => {
    const validData = {
      email: 'test@example.com',
      username: 'testuser',
      password: 'Test1234!',
      confirmPassword: 'Test1234!',
      firstName: '๊ธธ๋™',
      lastName: 'ํ™',
      birthDate: '1990-01-01',
      agreeToTerms: true,
      agreeToPrivacy: true
    };

    const result = signupSchema.safeParse(validData);
    expect(result.success).toBe(true);
  });

  it('์ž˜๋ชป๋œ ์ด๋ฉ”์ผ์„ ๊ฑฐ๋ถ€ํ•ด์•ผ ํ•จ', () => {
    const invalidData = {
      email: 'invalid-email',
      // ... ๋‚˜๋จธ์ง€ ํ•„๋“œ
    };

    const result = signupSchema.safeParse(invalidData);
    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.issues[0].path).toContain('email');
    }
  });

  it('์งง์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ฑฐ๋ถ€ํ•ด์•ผ ํ•จ', () => {
    const invalidData = {
      password: 'short',
      // ... ๋‚˜๋จธ์ง€ ํ•„๋“œ
    };

    const result = signupSchema.safeParse(invalidData);
    expect(result.success).toBe(false);
  });

  it('๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜๋ฅผ ๊ฐ์ง€ํ•ด์•ผ ํ•จ', () => {
    const invalidData = {
      password: 'Test1234!',
      confirmPassword: 'Different1234!',
      // ... ๋‚˜๋จธ์ง€ ํ•„๋“œ
    };

    const result = signupSchema.safeParse(invalidData);
    expect(result.success).toBe(false);
    if (!result.success) {
      const error = result.error.issues.find(
        issue => issue.path.includes('confirmPassword')
      );
      expect(error).toBeDefined();
    }
  });

  it('18์„ธ ๋ฏธ๋งŒ์„ ๊ฑฐ๋ถ€ํ•ด์•ผ ํ•จ', () => {
    const today = new Date();
    const underageDate = new Date(
      today.getFullYear() - 17,
      today.getMonth(),
      today.getDate()
    );

    const invalidData = {
      birthDate: underageDate.toISOString().split('T')[0],
      // ... ๋‚˜๋จธ์ง€ ํ•„๋“œ
    };

    const result = signupSchema.safeParse(invalidData);
    expect(result.success).toBe(false);
  });
});

// ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SignupForm } from '../SignupForm';

describe('SignupForm Component', () => {
  it('ํผ์„ ๋ Œ๋”๋งํ•ด์•ผ ํ•จ', () => {
    render(<SignupForm />);
    expect(screen.getByLabelText(/์ด๋ฉ”์ผ/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/์•„์ด๋””/i)).toBeInTheDocument();
  });

  it('์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์— ์—๋Ÿฌ๋ฅผ ํ‘œ์‹œํ•ด์•ผ ํ•จ', async () => {
    const user = userEvent.setup();
    render(<SignupForm />);

    const emailInput = screen.getByLabelText(/์ด๋ฉ”์ผ/i);
    await user.type(emailInput, 'invalid-email');
    await user.tab(); // blur ์ด๋ฒคํŠธ ๋ฐœ์ƒ

    await waitFor(() => {
      expect(screen.getByText(/์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹ˆ์—์š”/i))
        .toBeInTheDocument();
    });
  });

  it('๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„๋ฅผ ํ‘œ์‹œํ•ด์•ผ ํ•จ', async () => {
    const user = userEvent.setup();
    render(<SignupForm />);

    const passwordInput = screen.getByLabelText(/^๋น„๋ฐ€๋ฒˆํ˜ธ/i);
    await user.type(passwordInput, 'Test1234!');

    await waitFor(() => {
      expect(screen.getByText(/๊ฐ•ํ•จ|๋งค์šฐ ๊ฐ•ํ•จ/i)).toBeInTheDocument();
    });
  });

  it('ํผ ์ œ์ถœ์ด ์ž‘๋™ํ•ด์•ผ ํ•จ', async () => {
    const user = userEvent.setup();
    render(<SignupForm />);

    // ๋ชจ๋“  ํ•„๋“œ ์ž…๋ ฅ
    await user.type(screen.getByLabelText(/์ด๋ฉ”์ผ/i), 'test@example.com');
    await user.type(screen.getByLabelText(/์•„์ด๋””/i), 'testuser');
    await user.type(screen.getByLabelText(/^๋น„๋ฐ€๋ฒˆํ˜ธ/i), 'Test1234!');
    await user.type(screen.getByLabelText(/๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ/i), 'Test1234!');

    // ๋‹ค์Œ ๋‹จ๊ณ„๋กœ
    await user.click(screen.getByText(/๋‹ค์Œ/i));

    // 2๋‹จ๊ณ„ ํ•„๋“œ ์ž…๋ ฅ
    await user.type(screen.getByLabelText(/์„ฑ/i), 'ํ™');
    await user.type(screen.getByLabelText(/์ด๋ฆ„/i), '๊ธธ๋™');
    // ... ๋‚˜๋จธ์ง€ ํ•„๋“œ

    // ์ œ์ถœ
    await user.click(screen.getByText(/๊ฐ€์ž…ํ•˜๊ธฐ/i));

    await waitFor(() => {
      expect(screen.getByText(/ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ/i)).toBeInTheDocument();
    });
  });
});

ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ๋ฆฌํŒฉํ† ๋งํ•  ๋•Œ๋„ ์•ˆ์‹ฌํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐ŸŽ‰ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ •๋ง ๋งŽ์€ ๊ฑธ ๋‹ค๋ค˜์ง€?

ํ•ต์‹ฌ์„ ์ •๋ฆฌํ•˜์ž๋ฉด:

๐ŸŽ“ ํ•ต์‹ฌ ์š”์•ฝ

1. ํƒ€์ž… ์•ˆ์ •์„ฑ
โ€ข ์Šคํ‚ค๋งˆ์—์„œ ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์ถ”๋ก ํ•˜์ž
โ€ข z.infer๋‚˜ InferType์„ ํ™œ์šฉํ•˜์ž
โ€ข ์ปดํŒŒ์ผ ํƒ€์ž„๊ณผ ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ์„ ๋ชจ๋‘ ์ฑ™๊ธฐ์ž

2. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ํƒ
โ€ข Zod: ์ตœ์‹  ํ”„๋กœ์ ํŠธ, ๋›ฐ์–ด๋‚œ ํƒ€์ž… ์ถ”๋ก 
โ€ข Yup: ์•ˆ์ •์„ฑ, ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„
โ€ข ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๊ฒŒ ์„ ํƒํ•˜์ž

3. ์„ฑ๋Šฅ ์ตœ์ ํ™”
โ€ข ์ ์ ˆํ•œ ๊ฒ€์ฆ ๋ชจ๋“œ ์„ ํƒ
โ€ข ๋””๋ฐ”์šด์‹ฑ ํ™œ์šฉ
โ€ข ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง ๋ฐฉ์ง€

4. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜
โ€ข ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
โ€ข ์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ๋ฐฑ
โ€ข ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ

5. ์ฝ”๋“œ ๊ตฌ์กฐํ™”
โ€ข ์Šคํ‚ค๋งˆ์™€ ํƒ€์ž… ๋ถ„๋ฆฌ
โ€ข ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ
โ€ข ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

ํผ ์ฒ˜๋ฆฌ๋Š” ์›น ๊ฐœ๋ฐœ์˜ ๊ธฐ๋ณธ์ด๋ฉด์„œ๋„ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ. ์‚ฌ์šฉ์ž์™€ ์ง์ ‘ ์†Œํ†ตํ•˜๋Š” ์ฐฝ๊ตฌ๋‹ˆ๊นŒ! ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ฉด, ์•ˆ์ „ํ•˜๊ณ  ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ํผ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๐Ÿš€

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•œ ๋ฒˆ ์ต์ˆ™ํ•ด์ง€๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋ผ. ํŠนํžˆ ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ด๋Ÿฐ ์ฒด๊ณ„์ ์ธ ์ ‘๊ทผ์ด ํ•„์ˆ˜์ ์ด์•ผ.

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ์กฐ์–ธ

โ€ข ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ๋ณต์žกํ•œ ํผ์„ ๋‹ค๋ค„๋ณด์ž
โ€ข ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ž์ฃผ ์ฐธ๊ณ ํ•˜์ž (Zod, Yup, React Hook Form ๋ชจ๋‘ ๋ฌธ์„œ๊ฐ€ ํ›Œ๋ฅญํ•ด)
โ€ข ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค์˜ ํŒจํ„ด์„ ๋ฐฐ์šฐ์ž
โ€ข ์‹ค์ˆ˜๋ฅผ ๋‘๋ ค์›Œํ•˜์ง€ ๋ง์ž. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์นœ์ ˆํ•˜๊ฒŒ ์•Œ๋ ค์ค„ ๊ฑฐ์•ผ!
โ€ข ํ•„์š”ํ•˜๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ

์ž, ์ด์ œ ๋‹น์‹ ๋„ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋กœ ์™„๋ฒฝํ•œ ํผ์„ ๋งŒ๋“ค ์ค€๋น„๊ฐ€ ๋์–ด! ํ™”์ดํŒ…! ๐Ÿ’ชโœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.