2025.3.24 월요일의 공부기록
이 글에서는 Zod 라이브러리에서 제공하는 고급 유효성 검사 메서드인 superRefine
의 활용법을 설명하고, Zod의 다양한 오류 처리 방식을 깊이 있게 다룬다.
Zod의 superRefine
메서드는 정교한 유효성 검사를 할 때 유용하게 쓰인다. 기존의 refine
메서드보다 더 세부적이고 정밀한 검증이 가능하며, 한 번의 유효성 검사에서 여러 개의 오류 메시지를 동시에 반환할 수도 있다.
superRefine
메서드는 함수 실행 중에 ctx.addIssue()
메서드를 호출하여 유효성 검사에 문제가 있음을 알릴 수 있다. 만약 실행 과정에서 단 한 번이라도 ctx.addIssue()
가 호출되지 않는다면, 유효성 검사는 통과된다.
import { z } from 'zod';
const schema = z.object({
username: z.string(),
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "비밀번호가 일치하지 않습니다.",
path: ['confirmPassword'],
});
}
if (data.password.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 8,
message: "비밀번호는 최소 8글자 이상이어야 합니다.",
path: ['password'],
});
}
});
이렇게 작성하면 여러 조건을 동시에 검사할 수 있고, 각각의 오류 메시지를 개별적으로 전달할 수 있다.
fatal: true
)superRefine에서 오류 발생 시 더 이상의 검증이 의미 없다고 판단할 때, fatal
옵션을 설정하여 추가적인 검사를 중단할 수 있다.
schema.superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "비밀번호가 일치하지 않습니다.",
path: ['confirmPassword'],
fatal: true, // 이 에러가 발생하면 이후의 refine 작업을 중단함
});
}
// 위에서 fatal 오류가 발생했다면 이 검사는 실행되지 않음
if (data.password.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 8,
message: "비밀번호는 최소 8글자 이상이어야 합니다.",
path: ['password'],
});
}
});
fatal: true
설정이 있으면 해당 조건에서 오류가 발생하는 순간 즉시 이후 검증 로직을 중단한다.z.NEVER
는 반환 값 자체를 활용하지 않으면서 타입 시스템을 맞추기 위해 사용하는 특별한 용도의 Zod 스키마이다. 주로 유효성 검사는 수행하지만 반환 값을 절대 사용하지 않을 때 유용하게 사용된다.
예시:
const neverSchema = z.NEVER;
const mySchema = z.object({
action: z.literal('delete'),
result: neverSchema, // result 값은 사용되지 않음
});
이러한 설정은 반환 값 없이 타입 검증만을 위한 설정으로 사용된다.
Zod는 기본적으로 유효성 검사가 실패하면 예외를 던지거나 오류를 반환한다. Zod의 오류는 상세한 정보를 포함하고 있으며, 다양한 형태로 커스터마이징 할 수 있다.
parse
메서드 사용 시)try {
schema.parse(input);
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.flatten());
}
}
safeParse
메서드 사용 시)const result = schema.safeParse(input);
if (!result.success) {
console.log(result.error.flatten());
}
flatten()
메서드 활용flatten()
메서드를 사용하면 Zod 오류 객체에서 필드별로 정리된 오류 메시지를 얻을 수 있다.
const result = schema.safeParse(input);
if (!result.success) {
const errors = result.error.flatten();
console.log(errors.fieldErrors);
/*
{
password: ["비밀번호는 최소 8글자 이상이어야 합니다."],
confirmPassword: ["비밀번호가 일치하지 않습니다."]
}
*/
}
iron-session
)actions.ts
"use server";
import {
PASSWORD_MIN_LENGTH,
PASSWORD_REGEX,
PASSWORD_REGEX_ERROR,
} from "@/lib/constants";
import db from "@/lib/db";
import { z } from "zod";
import bcrypt from "bcrypt";
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";
// username에서 특정 단어 제한 (예: "admin")
function checkUsername(username: string) {
return !username.includes("admin");
}
// 비밀번호 확인 검사 함수
const checkPasswords = ({
password,
confirm_password,
}: {
password: string;
confirm_password: string;
}) => password === confirm_password;
// Zod 스키마 정의
const formSchema = z
.object({
username: z
.string({
invalid_type_error: "Username must be a string",
required_error: "Username is required",
})
.toLowerCase()
.trim()
.refine(checkUsername, "Username cannot contain 'admin'"),
email: z.string().email().toLowerCase(),
password: z
.string()
.min(PASSWORD_MIN_LENGTH, `Password must be at least ${PASSWORD_MIN_LENGTH} characters.`)
.regex(PASSWORD_REGEX, PASSWORD_REGEX_ERROR),
confirm_password: z.string().min(PASSWORD_MIN_LENGTH),
})
.superRefine(async ({ username }, ctx) => {
// 사용자 이름 중복 검사
const user = await db.user.findUnique({
where: { username },
select: { id: true },
});
if (user) {
ctx.addIssue({
code: "custom",
message: "This username is already taken",
path: ["username"],
fatal: true,
});
return z.NEVER; // 추가 검증 중단
}
})
.superRefine(async ({ email }, ctx) => {
// 이메일 중복 검사
const user = await db.user.findUnique({
where: { email },
select: { id: true },
});
if (user) {
ctx.addIssue({
code: "custom",
message: "This email is already taken",
path: ["email"],
fatal: true,
});
return z.NEVER; // 추가 검증 중단
}
})
.refine(checkPasswords, {
message: "Passwords do not match",
path: ["confirm_password"],
});
export async function createAccount(prevState: unknown, formData: FormData) {
const data = {
username: formData.get("username"),
email: formData.get("email"),
password: formData.get("password"),
confirm_password: formData.get("confirm_password"),
};
// Zod 유효성 검사 수행 (safeParseAsync 사용)
const result = await formSchema.safeParseAsync(data);
if (!result.success) {
// 에러가 있을 경우 에러 객체 반환
return result.error.flatten();
} else {
// bcrypt를 통해 비밀번호 해시 처리
const hashedPassword = await bcrypt.hash(result.data.password, 12);
// 데이터베이스에 사용자 정보 저장
const user = await db.user.create({
data: {
username: result.data.username,
email: result.data.email,
password: hashedPassword, // 안전한 비밀번호 저장
},
select: {
id: true,
},
});
// 세션 생성 및 사용자 자동 로그인 처리
const session = await getSession();
session.userId = user.id;
await session.save();
// 프로필 페이지로 자동 리다이렉트
redirect("/profile");
}
}
username
은 공백 제거 및 소문자로 변환하며, "admin"과 같은 금지 단어 포함 여부를 검사한다.email
은 이메일 형식을 엄격히 검증한다.password
는 길이 및 복잡성(정규 표현식)을 검증한다.confirm_password
는 별도의 refine
을 통해 password
와 일치하는지 검사한다.superRefine
활용)fatal: true
), 사용자에게 정확한 오류 메시지를 제공한다.saltRounds
는 12
로 설정하여 보안성과 성능을 적절히 유지한다.getSession()
은 서버 측에서 안전한 세션을 생성하고 관리한다./profile
페이지로 리다이렉트된다.superRefine
메서드는 더 세부적인 조건을 검사하고 여러 개의 오류를 동시에 전달할 수 있다.fatal: true
옵션으로 중요한 오류 발생 시 즉시 검증을 중단할 수 있다.z.NEVER
는 타입 검사만을 목적으로 사용할 때 유용하다.parse
, safeParse
, flatten()
등의 메서드를 통해 다양하게 활용할 수 있다.