Post

πŸ₯œ [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>
  );
}

image-01 (server λ‚΄μ—μ„œ μ •μƒμ μœΌλ‘œ 값이 λ“€μ–΄μ˜€λŠ” λͺ¨μŠ΅)

μ—¬νƒœ 껏 state λ₯Ό 톡해 form 값을 κ΄€λ¦¬ν•΄μ„œ object 둜 μ „λ‹¬ν•΄μ™”μ—ˆλŠ”λ°

정말 μ‹ κΈ°ν•œ 방법이닀.. 기쑴의 방법인 api 보닀 훨씬 νŽΈν•˜κΈ°λ„ν•˜κ³ 

μ„œλ²„ μΈ‘μ—μ„œλ§Œ λ™μž‘μ΄ λœλ‹€λŠ” 점이 정말 큰 λ©”λ¦¬νŠΈ 인 것 κ°™λ‹€.

사싀 아직은 μ•ˆλ“œλ‘œμ΄λ“œ ν™˜κ²½μ΄λ‚˜ λ‹€λ₯Έ μ™ΈλΆ€ api μ—μ„œλŠ” 아직은 기쑴의

방법을 μ‚¬μš©ν•˜λŠ” 것 κ°™μ§€λ§Œ 맀우 μœ μš©ν•œ 방법인 것 κ°™λ‹€.



Zod λž€?

image-02 (곡식 ν™ˆνŽ˜μ΄μ§€ 링크)

λ“œλ””μ–΄ λŒ€λ§μ˜ λ‚΄κ°€ 이 포슀트λ₯Ό μž‘μ„±ν•˜λŠ” μ΄μœ κ°€ μ°Ύμ•„μ™”λ‹€.

일단 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 은 μ‹ μ€‘ν•˜κ³  μ •ν™•ν•΄μ•Ό ν•˜λŠ”λ° 컴파일 단계 뿐만이 μ•„λ‹Œ

λŸ°νƒ€μž„μ—μ„œλ„ λ™μž‘ν•œλ‹€λŠ” 것이 μ°Έ λ©”λ¦¬νŠΈκ°€ μ»Έλ‹€.

μ—­μ‹œ 기술이 있으면 찍먹은 ν•œλ²ˆ μ”© ν•΄λ΄μ•Όν•œλ‹€λŠ” κ±Έ λŠλ‚€λ‹€.

This post is licensed under CC BY 4.0 by the author.