Zod를 활용한 유효성 검증 에러 메시지 처리

Odyssey·2025년 2월 10일
0

Next.js_study

목록 보기
31/58
post-thumbnail

2025.2.10 월요일의 공부기록

Zod로 데이터 유효성 검증하기

Zod는 객체의 스키마(구조)를 정의하고, 데이터를 검증하며 유효하지 않은 경우 오류를 던지거나 안전하게 처리할 수 있다.
이를 활용하면 사용자 입력 데이터를 검증하고, 에러 메시지를 사용자에게 반환하는 기능을 쉽게 구현할 수 있다.

📌 공식 문서 참고:


Zod 기본 개념: parse() vs safeParse()

📌 parse(): 유효성 검증 실패 시 오류 발생

import { z } from "zod";

const usernameSchema = z.string().min(5).max(10);

console.log(usernameSchema.parse("validUser")); // ✅ 유효 → "validUser"
console.log(usernameSchema.parse("a"));         // ❌ 오류 발생 (5자 미만)

📌 특징:

  • .parse()데이터가 유효하면 그대로 반환.
  • 유효하지 않으면 오류를 던짐(throw error) → try-catch로 감싸야 함.

📌 safeParse(): 오류 발생 없이 검증 결과 반환

const result = usernameSchema.safeParse("a");

console.log(result);
// => { success: false, error: ZodError }

📌 특징:

  • .safeParse()는 오류를 발생시키지 않고 검증 결과를 객체로 반환.
  • success: false일 경우 error 속성에 ZodError 객체 포함.

Zod를 활용한 Next.js 데이터 검증

📌 createAccount 함수에서 유효성 검증 추가

"use server";

import { z } from "zod";

// **Zod 스키마 정의**
const registerSchema = z.object({
  username: z.string().min(5, "사용자 이름은 최소 5자 이상이어야 합니다.").max(10, "사용자 이름은 최대 10자 이하이어야 합니다."),
  email: z.string().email("유효한 이메일 주소를 입력하세요."),
  password: z.string().min(6, "비밀번호는 최소 6자 이상이어야 합니다."),
  confirm_password: z.string(),
}).refine((data) => data.password === data.confirm_password, {
  message: "비밀번호가 일치하지 않습니다.",
  path: ["confirm_password"],
});

export async function createAccount(prevState: any, formData: FormData) {
  const data = {
    username: formData.get("username")?.toString(),
    email: formData.get("email")?.toString(),
    password: formData.get("password")?.toString(),
    confirm_password: formData.get("confirm_password")?.toString(),
  };

  // **유효성 검사 실행**
  const validationResult = registerSchema.safeParse(data);

  if (!validationResult.success) {
    return {
      success: false,
      message: validationResult.error.issues[0].message, // 첫 번째 오류 메시지 반환
    };
  }

  return { success: true, message: "회원가입 성공!" };
}

📌 설명:

  • .safeParse()를 사용하여 유효성 검사 실패 시 오류 발생 없이 처리.
  • validationResult.error.issues[0].message를 사용하여 첫 번째 에러 메시지만 반환.

클라이언트에서 useActionState로 검증 결과 처리

"use client";

import { useActionState } from "react";
import { createAccount } from "@/server-actions";

export default function RegisterForm() {
  const [state, formAction, isPending] = useActionState(createAccount, { success: null, message: "" });

  return (
    <div className="flex flex-col gap-10 py-8 px-6">
      <h1 className="text-2xl">회원가입</h1>
      <form action={formAction} className="flex flex-col gap-3">
        <input name="username" type="text" placeholder="Username" className="border p-2" required />
        <input name="email" type="email" placeholder="Email" className="border p-2" required />
        <input name="password" type="password" placeholder="Password" className="border p-2" required />
        <input name="confirm_password" type="password" placeholder="Confirm Password" className="border p-2" required />
        <button type="submit" disabled={isPending} className="bg-blue-500 text-white px-4 py-2 rounded-md">
          {isPending ? "가입 중..." : "가입하기"}
        </button>
      </form>
      {state.message && (
        <p className={state.success ? "text-green-500" : "text-red-500"}>{state.message}</p>
      )}
    </div>
  );
}

📌 설명:

  • useActionState()를 사용하여 서버 액션(createAccount)의 상태를 자동으로 관리.
  • isPending을 활용하여 폼 제출 중 버튼을 비활성화.
  • state.message를 출력하여 회원가입 성공/실패 메시지를 표시.

Zod의 다양한 검증 기능

📌 1️⃣ .strict(): 예상치 못한 값이 포함된 경우 오류 발생

const userSchema = z.object({
  name: z.string(),
  age: z.number(),
}).strict();

userSchema.parse({ name: "John", age: 25, extra: "unexpected" }); // ❌ 오류 발생

📌 설명:

  • .strict()을 사용하면 정의되지 않은 값이 포함될 경우 오류 발생.

📌 2️⃣ .transform(): 데이터 변환

const nameSchema = z.string().transform((val) => val.toUpperCase());

console.log(nameSchema.parse("john")); // "JOHN"

📌 설명:

  • .transform()을 사용하여 데이터를 자동 변환 가능.

📌 3️⃣ .refine(): 커스텀 검증 로직 추가

const passwordSchema = z.string().min(6).refine((val) => /[A-Z]/.test(val), {
  message: "비밀번호에는 최소 하나의 대문자가 포함되어야 합니다.",
});

console.log(passwordSchema.safeParse("abc123")); // ❌ 오류 발생
console.log(passwordSchema.safeParse("Abc123")); // ✅ 통과

📌 설명:

  • .refine()을 사용하여 추가적인 맞춤 검증 로직 적용 가능.

Zod를 활용한 Next.js 데이터 검증 정리

✅ 기능🛠 설명
객체 스키마 정의z.object({ username: z.string() })
데이터 검증 (parse).parse(data)
데이터 검증 (safeParse).safeParse(data) (오류 발생 없이 결과 반환)
에러 메시지 출력error.issues[0].message
추가 검증 (refine).refine((val) => 조건, { message })
데이터 변환 (transform).transform((val) => val.toUpperCase())
추가 필드 제한 (strict).strict()

💻 실습 코드

action.ts

"use server";

import { z } from "zod";

function checkUsername(username: string) {
  return !username.includes("admin");
}

const checkPasswords = ({
  password,
  confirm_password,
}: {
  password: string;
  confirm_password: string;
}) => password === confirm_password;

const formSchema = z
  .object({
    username: z
      .string({
        invalid_type_error: "Username must be a string",
        required_error: "Username is required",
      })
      .min(3, "Way too short!!")
      .max(10, " Way too long!!")
      .refine(checkUsername, "Username cannot contain 'admin'"),
    email: z.string().email(),
    password: z.string().min(10),
    confirm_password: z.string().min(10),
  })
  .refine(checkPasswords, {
    message: "Passwords do not match",
    path: ["confirm_password"],
  });

export async function createAccount(prevState: any, formData: FormData) {
  const data = {
    username: formData.get("username"),
    email: formData.get("email"),
    password: formData.get("password"),
    confirm_password: formData.get("confirm_password"),
  };
  const result = formSchema.safeParse(data);

  if (!result.success) {
    return result.error.flatten();
  }
}

page.tsx

"use client";

import FormButton from "@/components/form-btn";
import FormInput from "@/components/form-input";
import SocialLogin from "@/components/social-login";
import { createAccount } from "./actions";
import { useFormState } from "react-dom";

export default function CreateAccount() {
  const [state, action] = useFormState(createAccount, null);
  return (
    <div className="flex flex-col gap-10 py-8 px-6">
      <div className="flex flex-col gap-2 *:font-medium">
        <h1 className="text-2xl">안녕하세요!</h1>
        <h2 className="text-xl">Fill in the form below to join!</h2>
      </div>
      <form action={action} className="flex flex-col gap-3">
        <FormInput
          name="username"
          type="text"
          placeholder="Username"
          required={true}
          errors={state?.fieldErrors?.username}
        />
        <FormInput
          name="email"
          type="email"
          placeholder="Email"
          required={true}
          errors={state?.fieldErrors?.email}
        />
        <FormInput
          name="password"
          type="password"
          placeholder="Password"
          required={true}
          errors={state?.fieldErrors?.password}
        />
        <FormInput
          name="confirm_password"
          type="password"
          placeholder="Confirm password"
          required={true}
          errors={state?.fieldErrors?.confirm_password}
        />
        <FormButton text="Create account" />
      </form>
      <SocialLogin />
    </div>
  );
}

0개의 댓글