π₯ [NextJS] Zod λ₯Ό νμ©ν validation check
library μ€
Zod
λ₯Ό νμ©ν validation μ λν΄ μμ보λ ν¬μ€νΈ μ λλ€.
κ°μ
μμ¦ νκ°λ‘μ΄ NextJS 14
μ λν΄ λ°°μλκ°λ μ€β¦
NextJS 14
μμλ Server Action
κ³Ό Zod
λ₯Ό νμ©ν΄ API μ°κ²°μ
λ³΄λ€ λ κ°κ²°νκ² νλ€λ λ§μ λ£κ³ μμλ³΄κ² λμλ€.
λ¨Όμ Server Action
μ λν΄ μμ보μ
Server Action μ΄λ
κ°λ¨νκ² λ§ν΄ μλ²μμ μλνκ²λ νλ κ²
κ·Έλ λ€. λ»νμ΄ κ·Έλλ‘ μλ²μμ ꡬλ κ°λ₯νκ² νλ κ²μ΄λ€.
μ¬ν νλ μΉμ API ν΅μ μ 보면
1
2
3
4
//api.ts
export default async function getData() {
return axios.get()...
}
μ΄λ° μμΌλ‘ api
λ₯Ό λ°λ‘ κ΄λ¦¬νλ νμΌμ μμ±ν΄
ν¨μλ€μ μ§μ΄λ£κ³€ νλ€.
κ·Έλ¬λ€ 보λ api
ν¨μλ₯Ό μΈλΆλ‘λΆν° κ΄λ¦¬νκΈ°λ μ΄λ €μμ§λ€.
νμ§λ§ Next 14
μμλ μλ‘μ΄ κΈ°λ₯μ μ 곡νλ€κ³ νλ€.
μ μ΄μ api μ μ κ·Όνλ κ²μ Server
μμλ§ μλνκ² νλ κ²μ΄λ€.
βuse serverβ | βuse clientβ |
---|---|
μλ² μΈ‘μμ ꡬλ | ν΄λΌμ΄μΈνΈ μΈ‘μμ ꡬλ |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"use server";
/**
* preState λ μ΄κΈ°κ°, formData λ μ μ‘ν λ°μ΄ν°
*/
export default async function onSubmit(prevState: any, formData: FormData) {
"use server";
// μλ² λ΄μμλ§ μλλλ ν¨μ
console.log("SEND FORM");
await new Promise((resolve) => setTimeout(resolve, 5000));
// μμλ‘ λ§λ api ν¨μ λμ©. ν΅μ λλ μ΄λ₯Ό 5μ΄λ₯Ό μ€λ€
// redirect("/");
return {
errors: ["wrong password", "password too short"],
// μμ μλ¬ λͺ©λ‘ λ°ν
};
}
κ·Έ ν μμ²μ΄ μ§ν μ€μΈμ§, μμ²μ΄ λ§λ£λμλμ§ μ¬λΆλ₯Ό μκΈ°μν΄
useFormState
μ useFormStatus
λ₯Ό νμ©νλ€.
useFormState
- νν μλ
useState
μ μ¬μ© λ°©λ²μ΄ λΉμ·νλ€. action
λ°μ μ μλν ν¨μ νλμ κ°μ κ°μ Έμ¬ μ΄κΈ° κ°μ λ§€κ°λ³μλ‘ κ°μ ΈμΌ νλ€.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// μΈν°λν°λΈ ν λμμ΄λ―λ‘
"use client";
/* μ λ΅ */
/*
1. "use client"
2. form μ μΆ μ μλν action κ³Ό μ΄κΈ°κ° state λ±λ‘
3. form action μ κ±Έμ΄μ£Όλ©΄ λ!
*/
export default function Login() {
const [state, action] = useFormState(onSubmit, null);
return (
<div>
<form action={action} className="flex flex-col gap-3">
// form μ action μ useFormState action μ κ±Έμ΄μ€λ€!
<Input
type={"email"}
placeholder={"Email"}
required={true}
name={"email"}
/>
<Input
type={"password"}
placeholder={"Password"}
required={true}
name={"password"}
/>
<FormButton text={"Login"} />
</form>
<SocialLogin />
</div>
);
}
useFormStatus
- μ£Όλ‘
<form>
λ΄<button>
μ κ±Έλ©° νμ¬ form μ μ μ‘ μνλ₯Ό μμλ³Ό μ μλ€. pending
μ΄λΌλboolean
νμ λ³μλ₯Ό νμ©ν΄ κ°μ μ μ΄νλ€.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"use client";
/*
1. "use client"
2. const { pending } = useFormStatus();
3. pending μΌλ‘ μνλμμ 체ν¬ν μ μλ€.
*/
interface IFormButtonProps {
text: string;
}
export default function FormButton({ text }: IFormButtonProps) {
const { pending } = useFormStatus();
// action μ΄ λλ¬λμ§ μ¬λΆλ₯Ό μ μ μμ
return (
<button
disabled={pending}
>
{pending ? "λ‘λ© μ€ ..." : text}
</button>
);
}
(server λ΄μμ μ μμ μΌλ‘ κ°μ΄ λ€μ΄μ€λ λͺ¨μ΅)
μ¬ν κ» state
λ₯Ό ν΅ν΄ form κ°μ κ΄λ¦¬ν΄μ object λ‘ μ λ¬ν΄μμλλ°
μ λ§ μ κΈ°ν λ°©λ²μ΄λ€.. κΈ°μ‘΄μ λ°©λ²μΈ api λ³΄λ€ ν¨μ¬ νΈνκΈ°λνκ³
μλ² μΈ‘μμλ§ λμμ΄ λλ€λ μ μ΄ μ λ§ ν° λ©λ¦¬νΈ μΈ κ² κ°λ€.
μ¬μ€ μμ§μ μλλ‘μ΄λ νκ²½μ΄λ λ€λ₯Έ μΈλΆ api
μμλ μμ§μ κΈ°μ‘΄μ
λ°©λ²μ μ¬μ©νλ κ² κ°μ§λ§ λ§€μ° μ μ©ν λ°©λ²μΈ κ² κ°λ€.
Zod λ?
λλμ΄ λλ§μ λ΄κ° μ΄ ν¬μ€νΈλ₯Ό μμ±νλ μ΄μ κ° μ°Ύμμλ€.
μΌλ¨ Zod
κ° λ¬΄μμ΄λλ©΄
μ€ν€λ§ μ μΈμ ν΅ν μ ν¨μ± κ²μ¬ λΌμ΄λΈλ¬λ¦¬
λΌκ³ νλ€. λ μμΈν μμ보μ.
μ¬μ©νλ μ΄μ
zod
λ₯Ό μμ£Ό νμ©νλ μ΄μ λ TypeScript
μ νκ³ λλ¬Έμ΄λΌκ³ νλ€
TypeScript
λ μ»΄νμΌ μμ μμμ νμ
μλ¬λ§ μ‘μλΌ μ μκ³
λ°νμ λ¨κ³μμμ νμ μλ¬λ μ΄μ© λ°©λκ° μλ€.
μλλ©΄ λ°νμ λ¨κ³μμ μλλλ κ²μ JavaScript
μ΄κΈ° λλ¬Έμ΄λ€.
λν μνλ λ¬Έμμ΄μ΄λ μνλ μ«μ λ²μλ₯Ό κ°μ νκ±°λ number
νμ μ μ μ/μ€μ ꡬλΆμ λΆκ°λ₯νκΈ° λλ¬Έμ TypeScript μμμ
νκ³λ₯Ό 극볡νκΈ° μν΄ μμ¦μ Zod
λ₯Ό μμ£Ό νμ©νλ€κ³ νλ€.
μ€μΉ λ°©λ²
1
npm install zod
μ€ν€λ§ ννλ‘ μ μ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"use server";
import { z } from "zod";
// zod λ validation μ schema νμμΌλ‘ κ²μ¦ν¨
const formSchema = z
.object({
username: z
.string()
.min(3)
.max(10)
.toLowerCase()
.trim(),
email: z
.string()
.email()
.toLowerCase()
.trim(),
password: z
.string()
.min(10),
confirmPassword: z.string(),
});
Zod
λ λ°μ΄ν° νμμ Schema ννλ‘ μ μ₯ λ° κ΄λ¦¬νλ€.
λ§€μ° μ§κ΄μ μ΄λ€. ν΄λΉνλ κ°μ΄ Schema
μ μ ν©νμ§ μ¬λΆλ₯Ό νμΈν λ
parse()
μ safeParse()
λ₯Ό μ¬μ©νλ€.
parse() | safeParse() |
---|---|
μλ¬λ₯Ό λ°ν | μλ¬λ₯Ό λ°ννμ§ μμ |
μ΄λΌλ μ°¨μ΄μ μ΄ μ‘΄μ¬νλλ°, λκ°λ κ±°μ§ μ·¨ν₯ μ°¨μ΄μΈ κ² κ°λ€.
λλ μ΄λ²μ κΈ°μ‘΄μ api
ꡬ문μ²λΌ try-catch
λ¬Έμ μ΅μν ν΄λ³΄κ³ μΆμ΄
safeParse()
λ₯Ό νμ©νλ€.
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
1. ν΄λΉ μ€ν€λ§μ μ ν©νμ§ parsing
2. μ±κ³΅νλ©΄ success μ΄λ―λ‘ μλ¬ νΈλ€λ§
3. flatten ν¨μλ‘ μλ¬λ₯Ό 보기νΈνκ² λ¦¬νΌ
*/
let result = formSchema.safeParse(data);
// error λ₯Ό λ°ννμ§ μμ
if (!result.success) {
console.log(result.error.flatten());
return result.error.flatten();
} else {
console.log(data);
}
ν΄λΉ μ€ν€λ§μ λ§μ§ μλ κ΅¬λ¬Έμ΄ μκΈ°λ©΄ μλ¬κ° μ½μμ μ°νλ€.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
issues: [
{
code: 'invalid_type',
expected: 'string',
received: 'null',
path: [Array],
message: 'Expected string, received null'
},
<!-- μλ΅ -->
{
formErrors: [],
fieldErrors: {
username: [ 'Expected string, received null' ],
password: [ 'String must contain at least 10 character(s)' ],
confirmPassword: [ 'String must contain at least 10 character(s)' ]
}
}
<!-- flatten() μΌλ‘ reform ν κ° -->
μ°λ¦¬λ μ¬κΈ°μ μΆκ°λ‘ validation
μ μμΈνκ² μ μ μ μλ€.
μ½λ μμ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"use server";
import { z } from "zod";
const passwordRegex = new RegExp(
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).+$/
);
// zod λ validation μ schema νμμΌλ‘ κ²μ¦ν¨
const formSchema = z
.object({
username: z
.string({
invalid_type_error: "νμμ΄ ν립λλ€.",
required_error: "μ μ λͺ
μ μ
λ ₯ν΄μ£ΌμΈμ.",
})
.min(3, "μ μ λͺ
μ΄ λ무 μ§§μ΅λλ€.")
.max(10, "μ μ λͺ
μ΄ λ무 κΉλλ€.")
.toLowerCase()
.trim(),
email: z
.string({
invalid_type_error: "νμμ΄ ν립λλ€.",
required_error: "μ΄λ©μΌμ μ
λ ₯ν΄μ£ΌμΈμ.",
})
.email({
message: "μ΄λ©μΌ νμμ΄ μλλλ€.",
})
.toLowerCase()
.trim(),
password: z
.string({
invalid_type_error: "νμμ΄ ν립λλ€.",
required_error: "λΉλ°λ²νΈλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.",
})
.min(10, {
message: "λΉλ°λ²νΈκ° λ무 μ§§μ΅λλ€.",
})
.regex(passwordRegex, {
message:
"λΉλ°λ²νΈλ μ«μ, λλ¬Έμ, μλ¬Έμ, νΉμκΈ°νΈλ₯Ό ν¬ν¨νμ¬μΌ ν©λλ€.",
}),
confirmPassword: z.string(),
})
.refine((data) => data.confirmPassword === data.password, {
message: "λΉλ°λ²νΈκ° μλ‘ λ€λ¦
λλ€.",
path: ["confirmPassword"],
});
// refine() μΌλ‘ μ ν¨μ± κ²μ¬λ₯Ό μ 체μ μΌλ‘ μ§νμ΄ κ°λ₯νλ©°
// path λ‘ μλ¬μ μμΉλ₯Ό λ°νν΄μ€ μ μμ
export default async function createAccount(preState: any, formData: FormData) {
const data = {
username: formData.get("username"),
email: formData.get("email"),
password: formData.get("password"),
confirmPassword: formData.get("confirmPassword"),
};
let result = formSchema.safeParse(data);
// error λ₯Ό λ°ννμ§ μμ
if (!result.success) {
console.log(result.error.flatten());
return result.error.flatten();
} else {
console.log(data);
}
}
λ§μΉλ©°
μ§μ§ κ΅μ₯ν νΈνλ€ Next μ°λ μ΄μ κ° μλ€
Zod λ₯Ό μ²μ λ³Όλ μμνκΈ°λ νκ³ κ΅³μ΄ μ¨μΌνλ μΆμλλ°,
Validation
μ μ μ€νκ³ μ νν΄μΌ νλλ° μ»΄νμΌ λ¨κ³ λΏλ§μ΄ μλ
λ°νμμμλ λμνλ€λ κ²μ΄ μ°Έ λ©λ¦¬νΈκ° μ»Έλ€.
μμ κΈ°μ μ΄ μμΌλ©΄ μ°λ¨Ήμ νλ² μ© ν΄λ΄μΌνλ€λ κ±Έ λλλ€.