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 ์ธ ๋จ๋ ์ฌ์ฉ | ๋งค์ฐ ์ ํฉ | ๊ฐ๋ฅํ๋ ๋ถํธ | ๊ฐ๋ฅํ๋ ๊ณผํจ |
| ๋ฌ๋ ์ปค๋ธ | ๋ฎ์ | ๋ฎ์ | ์๋์ ์ผ๋ก ๋์ |
| ์ค๋ฌด ์ฒด๊ฐ | ํ์ + ๊ฒ์ฆ์ ํ๋๋ก ๊ด๋ฆฌ ๊ฐ๋ฅ | ํ์ ๋ถ๋ฆฌ ๊ด๋ฆฌ ํ์ | ์๋ฒ ๊ฒ์ฆ์ฉ ๋๋์ด ๊ฐํจ |

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)]}
/>
๋ฆฌํฉํ ๋ง์ ํตํด ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ ํ๋ณดํ์ต๋๋ค.
- ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ (Separation of Concerns)
- ๊ฒ์ฆ ๊ท์น์ ์คํค๋ง์, UI๋ ํ๋ฉด ๊ตฌ์ฑ์๋ง ์ง์ค
- ์ ์ง๋ณด์์ฑ ํฅ์
- ๊ท์น ๋ณ๊ฒฝ ์ UI ํ์ผ์ ์ด ํ์๊ฐ ์์
- ์คํค๋ง ํ ๊ณณ๋ง ์์ ํ๋ฉด ์ ์ฒด ๋ฐ์
- UI ์ผ๊ด์ฑ ์ ์ง
- ๊ธฐ์กด UX์ ์ปดํฌ๋ํธ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋๋ก ์ ์ง
- ๋ฎ์ ์ธ์ง ๋ถํ
- JSX ์ฌ์ด์ ์จ์ด ์๋ ์ ๊ท์๊ณผ if/else ์ ๊ฑฐ
- ์ฝ๋ ๊ฐ๋ ์ฑ ๋ํญ ๊ฐ์
6. ๋ง์น๋ฉฐ
์ด๋ฒ ๋ฆฌํฉํ ๋ง์ “๋๊ตฌ๋ฅผ ๋ฐ๊ฟจ๋ค”๊ธฐ๋ณด๋ค๋, ์ฑ ์์ ๊ฒฝ๊ณ๋ฅผ ๋ค์ ์ ์ํ ๊ฒฝํ์ ๊ฐ๊น์ ์ต๋๋ค. ํน์ ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์์๋ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ด JSX ์ฌ์ด์ ์จ์ด ์์ง๋ ์๋์? ํ๋ ์์ํฌ๋ฅผ ๊ฐ์์์ง ์์๋, ์ด๋ํฐ ํจํด๊ณผ ์คํค๋ง ๊ธฐ๋ฐ ๊ฒ์ฆ๋ง์ผ๋ก ์ถฉ๋ถํ ์ฐ์ํ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
๋๊ธ