2025.2.10 월요일의 공부기록
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()
는 데이터가 유효하면 그대로 반환.safeParse()
: 오류 발생 없이 검증 결과 반환const result = usernameSchema.safeParse("a");
console.log(result);
// => { success: false, error: ZodError }
📌 특징:
.safeParse()
는 오류를 발생시키지 않고 검증 결과를 객체로 반환.success: false
일 경우 error
속성에 ZodError 객체 포함.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
를 출력하여 회원가입 성공/실패 메시지를 표시..strict()
: 예상치 못한 값이 포함된 경우 오류 발생const userSchema = z.object({
name: z.string(),
age: z.number(),
}).strict();
userSchema.parse({ name: "John", age: 25, extra: "unexpected" }); // ❌ 오류 발생
📌 설명:
.strict()
을 사용하면 정의되지 않은 값이 포함될 경우 오류 발생..transform()
: 데이터 변환const nameSchema = z.string().transform((val) => val.toUpperCase());
console.log(nameSchema.parse("john")); // "JOHN"
📌 설명:
.transform()
을 사용하여 데이터를 자동 변환 가능..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()
을 사용하여 추가적인 맞춤 검증 로직 적용 가능.✅ 기능 | 🛠 설명 |
---|---|
객체 스키마 정의 | 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() |
"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();
}
}
"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>
);
}