๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
  • What would life be If we had no courage to attemp anything?
๐“๐จ๐๐š๐ฒ ๐ˆ ๐‹๐ž๐š๐ซ๐ง

[React] ํŒŒํŽธํ™”๋œ Form ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง, Zod๋กœ ์šฐ์•„ํ•˜๊ฒŒ ๊ฒฉ๋ฆฌํ•˜๊ธฐ

by DevIseo 2026. 1. 12.

1. ๋“ค์–ด๊ฐ€๋ฉฐ: ๊ฑฐ๋Œ€ํ•ด์ง„ Form์˜ ์Šต๊ฒฉ

ํ”„๋ŸฐํŠธ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ์ˆ˜์‹ญ ๊ฐœ์˜ ์ž…๋ ฅ ํ•„๋“œ๋ฅผ ๊ฐ€์ง„ ๊ฑฐ๋Œ€ํ•œ ์„ค์ • Form์„ ๋‹ค๋ค„์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.์ดˆ๊ธฐ์—๋Š” ๋‹จ์ˆœํ–ˆ๋˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง๋„ ์„œ๋น„์Šค๊ฐ€ ์„ฑ์žฅํ•˜๋ฉด์„œ ์ ์  ๋ณต์žกํ•ด์ง‘๋‹ˆ๋‹ค.

  • ์ •๊ทœ์‹ ๊ธฐ๋ฐ˜ ํฌ๋งท ๊ฒ€์ฆ
  • ํŒŒ์ผ ํฌ๊ธฐ ๋ฐ ํ™•์žฅ์ž ์ฒดํฌ
  • ํŠน์ • ์กฐ๊ฑด์ด ๋งŒ์กฑ๋  ๋•Œ๋งŒ ์ด์–ด์ง€๋Š” ์ˆœ์ฐจ ๊ฒ€์ฆ

์ด๋Ÿฐ ๋กœ์ง๋“ค์ด ํ•˜๋‚˜๋‘˜ ์ถ”๊ฐ€๋˜๋‹ค ๋ณด๋ฉด, ์–ด๋А ์ˆœ๊ฐ„ UI ์ปดํฌ๋„ŒํŠธ ์•ˆ์— ์ˆ˜๋ฐฑ ์ค„์˜ ๊ฒ€์ฆ ์ฝ”๋“œ๊ฐ€ ๋’ค์—‰์ผœ ์žˆ๋Š” ์ƒํƒœ๋ฅผ ๋งˆ์ฃผํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๋Š” ์ดˆ๊ธฐ์— ์•ฝ 3์ฃผ๋งŒ์— ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค๋ฅผ ์˜ฌ๋ ค์•ผ ํ–ˆ์œผ๋ฉฐ, MVP๊ฐ€ ๊ธฐ์กด์˜ ์˜จํ”„๋ ˆ๋ฏธ์Šค์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์„ ๋ชจ๋‘ ํ˜ผ์ž ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค๋Š” ํ•‘๊ณ„๋กœ UI๋กœ์ง๊ณผ ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์ด ๋’ค์„ž์ธ ์ƒํƒœ์˜€์Šต๋‹ˆ๋‹ค. ์ตœ๊ทผ์— ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋กœ์ง์ด ์ถ”๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญ์œผ๋กœ ๋“ค์–ด์˜ค๊ฒŒ ๋˜์–ด ๋ฎ์–ด๋‘์—ˆ๋˜ ์ธ๋ผ์ธ์œผ๋กœ ์ž‘์„ฑ๋œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์„ Zod ์Šคํ‚ค๋งˆ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝํ—˜์œผ๋กœ UI์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์–ผ๋งˆ๋‚˜ ์ค‘์š”ํ•œ์ง€ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ฒด๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธ€์—์„œ๋Š” ๊ทธ ๊ณผ์ •์—์„œ์˜ ๊ณ ๋ฏผ๊ณผ, ํ˜„์‹ค์ ์ธ ์ œ์•ฝ ์†์—์„œ ๋‚ด๋ฆฐ ์—”์ง€๋‹ˆ์–ด๋ง ์„ ํƒ์„ ๊ณต์œ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

 

2. ๋ฌธ์ œ ์ธ์‹: UI ์†์— ์ˆจ์–ด๋ฒ„๋ฆฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง

๊ธฐ์กด ๊ตฌํ˜„์—์„œ๋Š” ๊ฐ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ์˜ rules ํ”„๋กœํผํ‹ฐ ์•ˆ์— ๋ชจ๋“  ๊ฒ€์ฆ ๋กœ์ง์ด ๋“ค์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” ๊ธฐ์กด ์ฝ”๋“œ์™€ ์œ ์‚ฌํ•œ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

// AS-IS: UI์™€ ๋กœ์ง์ด ๋’ค์„ž์ธ ๋ชจ์Šต
<TextField
  rules={[
    (value) => {
      if (!/https?:\/\/.+/.test(value)) {
        return Promise.reject("URL ํ˜•์‹์ด ํ‹€๋ฆฝ๋‹ˆ๋‹ค.");
      }
      return Promise.resolve();
    },
  ]}
/>

์ฒ˜์Œ์—๋Š” ๊ฐ„๋‹จํ•ด ๋ณด์ด์ง€๋งŒ, ์ด ๋ฐฉ์‹์—๋Š” ๋ช…ํ™•ํ•œ ํ•œ๊ณ„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ์ 

  • ๊ฐ€๋…์„ฑ ์ €ํ•˜
    JSX ๊ตฌ์กฐ์™€ ๊ฒ€์ฆ ๋กœ์ง์ด ๋’ค์„ž์ด๋ฉด์„œ UI์˜ ์˜๋„๊ฐ€ ํ•œ๋ˆˆ์— ๋“ค์–ด์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๋กœ์ง ์žฌ์‚ฌ์šฉ ๋ถˆ๊ฐ€
    ๋™์ผํ•œ URL ๊ฒ€์ฆ์ด ํ•„์š”ํ•œ ๊ณณ๋งˆ๋‹ค ์ •๊ทœ์‹์„ ๋ณต์‚ฌํ•ด ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ณต์žกํ•œ ๊ฒ€์ฆ์˜ ํ™•์žฅ์„ฑ ๋ถ€์กฑ
    “ํŒŒ์ผ์ด ์กด์žฌํ•  ๋•Œ๋งŒ ํฌ๊ธฐ๋ฅผ ์ฒดํฌํ•˜๊ณ , ํ†ต๊ณผํ•˜๋ฉด ํ™•์žฅ์ž๋ฅผ ๊ฒ€์‚ฌํ•œ๋‹ค” ๊ฐ™์€
    ์ˆœ์ฐจ์  ์กฐ๊ฑด์ด ๋“ค์–ด๊ฐ€๋Š” ์ˆœ๊ฐ„ ์ฝ”๋“œ๊ฐ€ ๊ธ‰๊ฒฉํžˆ ์ง€์ €๋ถ„ํ•ด์ง‘๋‹ˆ๋‹ค.

๊ฒฐ๊ตญ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ UI์˜ ์ฑ…์ž„์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ๋ช…๋ฐฑํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด๋ผ๋Š” ์ ์ด ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.

 

 

3. ํ˜„์‹ค์ ์ธ ์ œ์•ฝ: “์™œ React-Hook-Form์„ ์“ฐ์ง€ ์•Š์•˜๋‚˜?”

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฆฌํŒฉํ† ๋ง์„ ๊ณ ๋ฏผํ•˜๋ฉด ๊ฐ€์žฅ ๋จผ์ € ๋– ์˜ค๋ฅด๋Š” ์„ ํƒ์ง€๋Š” React-Hook-Form + Zod ์กฐํ•ฉ์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค๋ฌด์—์„œ๋Š” ํ•ญ์ƒ ์ด์ƒ์ ์ธ ์„ ํƒ๋งŒ ํ•  ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ์˜ ์ œ์•ฝ ์‚ฌํ•ญ

  • ์ „์‚ฌ ๊ณตํ†ต UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊นŠ๊ฒŒ ์‚ฌ์šฉ ์ค‘
  • ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ref ๊ธฐ๋ฐ˜์˜ ๋ช…๋ นํ˜• ๊ฒ€์ฆ๊ณผ ์ž์ฒด rules ์‹œ์Šคํ…œ์— ์˜์กด
  • RHF๋กœ ์ „ํ™˜ํ•˜๋ ค๋ฉด ๊ธฐ์กด ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ „๋ฉด ์žฌ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ

์ด๋Š” ๋‹จ์ˆœํ•œ ๋ฆฌํŒฉํ† ๋ง์ด ์•„๋‹ˆ๋ผ ๋Œ€๊ทœ๋ชจ ๊ตฌ์กฐ ๋ณ€๊ฒฝ์— ํ•ด๋‹นํ–ˆ๊ณ , ๋ฆฌ์Šคํฌ์™€ ๋น„์šฉ ๋Œ€๋น„ ํ˜„์‹ค์ ์ธ ์„ ํƒ์€ ์•„๋‹ˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋‚ด๋ฆฐ ๊ฒฐ๋ก ์€ ํ•˜๋‚˜์˜€์Šต๋‹ˆ๋‹ค.

“ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์—†๋‹ค๋ฉด, ๊ฒ€์ฆ ์—”์ง„๋งŒ์ด๋ผ๋„ ๋ถ„๋ฆฌํ•˜์ž.”

UI ์‹œ์Šคํ…œ์€ ์œ ์ง€ํ•˜๋˜, ๊ทธ ์•ˆ์— ์ˆจ๊ฒจ์ง„ ํŒ๋‹จ ๋กœ์ง์„ ๋…๋ฆฝ์ ์ธ ๊ฒ€์ฆ ๋ ˆ์ด์–ด๋กœ ๊ฒฉ๋ฆฌํ•˜๋Š” ์ „๋žต์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

 

4. ์™œ Zod์˜€๋Š”๊ฐ€? (Yup / Joi์™€ ๋น„๊ต)

๊ฒ€์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„์ž…ํ•˜๊ธฐ ์ „, ๋‹ค์Œ ํ›„๋ณด๋“ค์„ ๊ฒ€ํ† ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • Yup
  • Joi
  • Zod

๊ทธ๋ฆฌ๊ณ  ์ตœ์ข…์ ์œผ๋กœ ๊ตฌ์กฐ์ ์œผ๋กœ ๊ฐ€์žฅ ์ž˜ ๋งž๋Š” Zod๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ญ๋ชฉ Zod Yup Joi
TypeScript ์นœํ™”์„ฑ ๋งค์šฐ ์šฐ์ˆ˜ (z.infer๋กœ ํƒ€์ž… ์ž๋™ ์ถ”๋ก ) ์ œํ•œ์  (ํƒ€์ž… ๋ณด์กฐ ํ•„์š”) ๋ถˆํŽธํ•จ (ํƒ€์ž… ์ •์˜๊ฐ€ ๋ฒˆ๊ฑฐ๋กœ์›€)
์Šคํ‚ค๋งˆ ๊ฐ€๋…์„ฑ ์„ ์–ธ์ ์ด๊ณ  ์ง๊ด€์  ๋น„๊ต์  ๋ช…ํ™• ๋ฌธ๋ฒ•์ด ๋‹ค์†Œ ๋ฌด๊ฑฐ์›€
๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ safeParse ๊ธฐ๋ฐ˜, ์˜ˆ์™ธ ์—†์ด ์•ˆ์ „ ๊ฐ€๋Šฅ ๊ฐ€๋Šฅ
๋ณตํ•ฉ ๊ฒ€์ฆ superRefine์œผ๋กœ ์œ ์—ฐํ•˜๊ฒŒ ํ‘œํ˜„ ๊ฐ€๋Šฅ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๊ฐ€๋…์„ฑ ๋–จ์–ด์ง ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์ฝ”๋“œ ๋ณต์žก
ํ”„๋ ˆ์ž„์›Œํฌ ์ข…์†์„ฑ ์—†์Œ (๋…๋ฆฝ ์‚ฌ์šฉ ๊ฐ€๋Šฅ) ์—†์Œ ์—†์Œ
RHF ์™ธ ๋‹จ๋… ์‚ฌ์šฉ ๋งค์šฐ ์ ํ•ฉ ๊ฐ€๋Šฅํ•˜๋‚˜ ๋ถˆํŽธ ๊ฐ€๋Šฅํ•˜๋‚˜ ๊ณผํ•จ
๋Ÿฌ๋‹ ์ปค๋ธŒ ๋‚ฎ์Œ ๋‚ฎ์Œ ์ƒ๋Œ€์ ์œผ๋กœ ๋†’์Œ
์‹ค๋ฌด ์ฒด๊ฐ ํƒ€์ž… + ๊ฒ€์ฆ์„ ํ•˜๋‚˜๋กœ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ ํƒ€์ž… ๋ถ„๋ฆฌ ๊ด€๋ฆฌ ํ•„์š” ์„œ๋ฒ„ ๊ฒ€์ฆ์šฉ ๋А๋‚Œ์ด ๊ฐ•ํ•จ

 

์ตœ๊ทผ npm trends ๋น„๊ต - ์ถœ์ฒ˜ : https://npmtrends.com/joi-vs-yup-vs-zod

 

Zod๋ž€?
- Zod๋Š” TypeScript-first ์Šคํ‚ค๋งˆ ์„ ์–ธ ๋ฐ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ์˜ ๊ตฌ์กฐ์™€ ์ œ์•ฝ ์กฐ๊ฑด์„ ์ฝ”๋“œ๋กœ ์„ ์–ธํ•˜๊ณ , ๋Ÿฐํƒ€์ž„์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

์„ค์น˜ ๋ฐฉ๋ฒ•
- npm
npm install zod
- yarn
yarn add zod
- pnpm
pnpm add zod

Zod๋ฅผ ์„ ํƒํ•œ ๊ฒฐ์ •์  ์ด์œ 

1. ํƒ€์ž…๊ณผ ๊ฒ€์ฆ์„ ํ•˜๋‚˜์˜ ์†Œ์Šค๋กœ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ

const schema = z.object({ url: z.string(), }); 
type Schema = z.infer<typeof schema>;

์Šคํ‚ค๋งˆ ์ •์˜ ์ž์ฒด๊ฐ€ ๊ณง ํƒ€์ž…์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์—,
๊ฒ€์ฆ ๊ทœ์น™๊ณผ ํƒ€์ž… ์ •์˜๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋‹จ์ผ ์†Œ์Šค(Single Source of Truth)๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

Yup์ด๋‚˜ Joi์—์„œ๋Š” ์ด ์ผ๊ด€์„ฑ์ด ์ƒ๋Œ€์ ์œผ๋กœ ์•ฝํ–ˆ์Šต๋‹ˆ๋‹ค.


2. superRefine์„ ํ†ตํ•œ ์‹ค๋ฌดํ˜• ๊ฒ€์ฆ ํ‘œํ˜„๋ ฅ

์‹ค๋ฌด์—์„œ ํ•„์š”ํ•œ ๊ฒ€์ฆ์€ ๋‹จ์ˆœ required/min/max๊ฐ€ ์•„๋‹ˆ๋ผ,
๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์— ๊ฐ€๊นŒ์šด ๋กœ์ง์ž…๋‹ˆ๋‹ค.

.superRefine((value, ctx) => {
  if (!value) return;

  if (value.size > LIMIT) {
    ctx.addIssue({ message: "์šฉ๋Ÿ‰ ์ดˆ๊ณผ" });
    return;
  }

  if (!isValidExt(value)) {
    ctx.addIssue({ message: "ํ™•์žฅ์ž ์˜ค๋ฅ˜" });
  }
});

์ด ๋ฐฉ์‹์€ ๋‹จ์ˆœํžˆ “๊ฒ€์ฆ ๋„๊ตฌ”๋ผ๊ธฐ๋ณด๋‹ค,
๊ฒ€์ฆ ๊ทœ์น™์„ ์„ ์–ธ์ ์œผ๋กœ ๋ชจ๋ธ๋งํ•˜๋Š” ๋„๊ตฌ๋ผ๋Š” ์ธ์ƒ์„ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.


3. ํŠน์ • Form ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ข…์†๋˜์ง€ ์•Š์Œ

Zod๋Š” RHF์™€ ํ•จ๊ป˜ ์ž์ฃผ ์‚ฌ์šฉ๋˜์ง€๋งŒ,
sageParse๋ผ๋Š” ๋ช…ํ™•ํ•œ ์ธํ„ฐํŽ˜์ด์Šค ๋•๋ถ„์— ์™„์ „ํžˆ ๋…๋ฆฝ์ ์ธ ๊ฒ€์ฆ ์—”์ง„์ฒ˜๋Ÿผ ํ™œ์šฉ ๊ฐ€๋Šฅํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ์  ๋•๋ถ„์— ๊ธฐ์กด UI ์‹œ์Šคํ…œ ์œ„์— ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

 

5. ํ•ด๊ฒฐ์ฑ…: Zod + ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด(Adapter Pattern)

1) ์ค‘์•™ ์ง‘์ค‘์‹ ์Šคํ‚ค๋งˆ ์ •์˜

๋จผ์ € ๋ชจ๋“  ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ทœ์น™์„ Zod ์Šคํ‚ค๋งˆ๋กœ ํ•œ ๊ณณ์— ๋ชจ์•˜์Šต๋‹ˆ๋‹ค.
ํŠนํžˆ ๋ณต์žกํ•œ ์ˆœ์ฐจ ๊ฒ€์ฆ์€ superRefine์„ ํ™œ์šฉํ•ด ์„ ์–ธ์ ์œผ๋กœ ํ‘œํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

// webSettingSchema.ts ์˜ˆ์ œ
export const webSettingSchema = z.object({
  url: z.string().regex(URL_REGEX, "์˜ฌ๋ฐ”๋ฅธ URL์ด ์•„๋‹™๋‹ˆ๋‹ค."),
  loginRecords: z.any().superRefine((val, ctx) => {
    if (!val?.[0]) return;

    if (val[0].size > LIMIT) {
      ctx.addIssue({
        code: "custom",
        message: "ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.",
      });
      return; // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ›„์† ๊ฒ€์ฆ ์ค‘๋‹จ
    }

    if (ext !== "ecl") {
      ctx.addIssue({
        code: "custom",
        message: "ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ํ™•์žฅ์ž์ž…๋‹ˆ๋‹ค.",
      });
    }
  }),
});

 

2) validateWith: ์ž‘์€ ์–ด๋Œ‘ํ„ฐ์˜ ํž˜

๋ฌธ์ œ๋Š” ๊ธฐ์กด UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ (value) => Promise ํ˜•ํƒœ์˜ rules๋ฅผ ์š”๊ตฌํ•œ๋‹ค๋Š” ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Zod ์Šคํ‚ค๋งˆ๋ฅผ ๊ธฐ์กด ์ธํ„ฐํŽ˜์ด์Šค์— ๋งž์ถฐ์ฃผ๋Š” ์ž‘์€ ์–ด๋Œ‘ํ„ฐ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

// validation.ts
export const validateWith = (schema) => (value) => {
  const result = schema.safeParse(value);

  return result.success
    ? Promise.resolve()
    : Promise.reject(result.error.issues[0].message);
};

์ด ์–ด๋Œ‘ํ„ฐ ๋•๋ถ„์—:

  • ๊ธฐ์กด UI ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹จ ํ•œ ์ค„๋„ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ 
  • ๋‚ด๋ถ€ ๊ฒ€์ฆ ๋กœ์ง๋งŒ Zod ๊ธฐ๋ฐ˜์œผ๋กœ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

5. ๋ฆฌํŒฉํ† ๋ง ๊ฒฐ๊ณผ: UI๋Š” ๋” ๊ฐ€๋ณ๊ฒŒ, ๋กœ์ง์€ ๋” ๊ฒฌ๊ณ ํ•˜๊ฒŒ

๋ฆฌํŒฉํ† ๋ง ํ›„ UI ์ปดํฌ๋„ŒํŠธ๋Š” ํ›จ์”ฌ ๋‹จ์ˆœํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

// AS-IS: UI์™€ ๋กœ์ง์ด ๋’ค์„ž์ธ ๋ชจ์Šต
<TextField
  rules={[
    (value) => {
      if (!/https?:\/\/.+/.test(value)) {
        return Promise.reject("URL ํ˜•์‹์ด ํ‹€๋ฆฝ๋‹ˆ๋‹ค.");
      }
      return Promise.resolve();
    },
  ]}
/>

// TO-BE: ์„ ์–ธ์ ์ธ ์—ฐ๊ฒฐ
<TextField
  name="url"
  rules={[validateWith(schema.shape.url)]}
/>

๋ฆฌํŒฉํ† ๋ง์„ ํ†ตํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ (Separation of Concerns)
    • ๊ฒ€์ฆ ๊ทœ์น™์€ ์Šคํ‚ค๋งˆ์—, UI๋Š” ํ™”๋ฉด ๊ตฌ์„ฑ์—๋งŒ ์ง‘์ค‘
  2. ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ
    • ๊ทœ์น™ ๋ณ€๊ฒฝ ์‹œ UI ํŒŒ์ผ์„ ์—ด ํ•„์š”๊ฐ€ ์—†์Œ
    • ์Šคํ‚ค๋งˆ ํ•œ ๊ณณ๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ์ „์ฒด ๋ฐ˜์˜
  3. UI ์ผ๊ด€์„ฑ ์œ ์ง€
    • ๊ธฐ์กด UX์™€ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€
  4. ๋‚ฎ์€ ์ธ์ง€ ๋ถ€ํ•˜
    • JSX ์‚ฌ์ด์— ์ˆจ์–ด ์žˆ๋˜ ์ •๊ทœ์‹๊ณผ if/else ์ œ๊ฑฐ
    • ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ๋Œ€ํญ ๊ฐœ์„ 

 

6. ๋งˆ์น˜๋ฉฐ

์ด๋ฒˆ ๋ฆฌํŒฉํ† ๋ง์€ “๋„๊ตฌ๋ฅผ ๋ฐ”๊ฟจ๋‹ค”๊ธฐ๋ณด๋‹ค๋Š”, ์ฑ…์ž„์˜ ๊ฒฝ๊ณ„๋ฅผ ๋‹ค์‹œ ์ •์˜ํ•œ ๊ฒฝํ—˜์— ๊ฐ€๊นŒ์› ์Šต๋‹ˆ๋‹ค. ํ˜น์‹œ ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์—์„œ๋„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์ด JSX ์‚ฌ์ด์— ์ˆจ์–ด ์žˆ์ง€๋Š” ์•Š๋‚˜์š”? ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๊ฐˆ์•„์—Ž์ง€ ์•Š์•„๋„, ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด๊ณผ ์Šคํ‚ค๋งˆ ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํžˆ ์šฐ์•„ํ•œ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ“๊ธ€