2025.2.15 토요일의 공부기록
Zod의 coerce
(강제 변환) 기능을 사용하면, 입력된 값을 원하는 타입으로 변환할 수 있다.
또한, validator
모듈을 활용하면 이메일, URL, 날짜 등의 데이터 유효성을 더욱 강화할 수 있다.
📌 공식 문서 참고:
z.coerce
를 활용한 데이터 타입 변환coerce
는 다양한 원시 타입의 변환을 지원하며, 입력값을 자동으로 변환하여 검증할 수 있도록 도와준다.
coerce
를 사용한 타입 변환import { z } from "zod";
const numberSchema = z.coerce.number();
const booleanSchema = z.coerce.boolean();
const dateSchema = z.coerce.date();
console.log(numberSchema.parse("123")); // ✅ 123 (문자열 → 숫자 변환)
console.log(booleanSchema.parse("true")); // ✅ true (문자열 → 불리언 변환)
console.log(dateSchema.parse("2024-01-01")); // ✅ 2024-01-01T00:00:00.000Z (문자열 → Date 변환)
📌 설명:
.coerce.number()
→ "123"
(문자열)을 123
(숫자)로 변환..coerce.boolean()
→ "true"
(문자열)을 true
(불리언)로 변환..coerce.date()
→ "2024-01-01"
(문자열)을 Date 객체
로 변환.const positiveNumberSchema = z.coerce.number().refine((val) => val > 0, {
message: "숫자는 0보다 커야 합니다.",
});
console.log(positiveNumberSchema.parse("10")); // ✅ 10
console.log(positiveNumberSchema.safeParse("-5")); // ❌ 실패 (음수는 허용되지 않음)
📌 설명:
.coerce.number()
→ 입력값을 숫자로 변환 후, 0보다 큰 값인지 검증
.Validator.js는 이메일, URL, 날짜 형식 검증 및 XSS 방지(sanitization) 기능을 제공하는 라이브러리이다.
npm i validator
npm i --save-dev @types/validator # TypeScript 지원
isEmail()
)import validator from "validator";
console.log(validator.isEmail("test@example.com")); // ✅ true
console.log(validator.isEmail("invalid-email")); // ❌ false
📌 설명:
validator.isEmail()
→ 이메일 형식 검증.isURL()
)console.log(validator.isURL("https://example.com")); // ✅ true
console.log(validator.isURL("invalid-url")); // ❌ false
📌 설명:
validator.isURL()
→ 올바른 URL 형식인지 검증.stripLow()
)console.log(validator.stripLow("<script>alert('XSS')</script>"));
📌 설명:
stripLow()
를 사용하여 XSS 공격 방지.coerce
+ validator
를 활용한 Next.js 데이터 검증createAccount
서버 액션 (회원가입 검증)"use server";
import { z } from "zod";
import validator from "validator";
// **회원가입 스키마 정의**
const registerSchema = z.object({
username: z.string().trim().min(5, "사용자 이름은 최소 5자 이상이어야 합니다.").max(10, "사용자 이름은 최대 10자 이하이어야 합니다."),
email: z.string().email("올바른 이메일 형식을 입력하세요.").toLowerCase(),
password: z.string().min(8, "비밀번호는 최소 8자 이상이어야 합니다.")
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*?[#?!@$%^&*-]).+$/, { message: "비밀번호는 대문자, 소문자, 숫자, 특수문자를 포함해야 합니다." }),
birthdate: z.coerce.date(),
phone: z.string().refine((val) => validator.isMobilePhone(val, "ko-KR"), {
message: "올바른 전화번호 형식이 아닙니다. (예: 010-1234-5678)",
}),
}).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(),
birthdate: formData.get("birthdate")?.toString(),
phone: formData.get("phone")?.toString(),
};
// **유효성 검사 실행**
const validationResult = registerSchema.safeParse(data);
if (!validationResult.success) {
return {
success: false,
message: validationResult.error.issues[0].message, // 첫 번째 오류 메시지 반환
};
}
return { success: true, message: "회원가입 성공!" };
}
📌 설명:
.coerce.date()
→ 문자열 생년월일을 Date
타입으로 변환..refine((val) => validator.isMobilePhone(val, "ko-KR"))
→ 한국 전화번호 형식 검증..trim()
, .toLowerCase()
→ 공백 제거, 이메일 소문자 변환.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 />
<input name="birthdate" type="date" placeholder="Birthdate" className="border p-2" required />
<input name="phone" type="text" placeholder="Phone Number" 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>
);
}
📌 더 자세한 내용은 Zod 공식 문서 & Validator.js를 참고하자! 🚀