#6 Validation
1. form의 유효성 검증을 위해 zod를 설치하자
npm i zod
[Object Schema]
z.object() 로 오브젝트 스키마를 만들 수 있습니다.
예시: const User = z.object( { username: z.string() } );
[.parse]
data의 타입이 유효한지 검사하기 위해 .parse 메소드를 사용할 수 있습니다. 유효한 경우 데이터 전체 정보가 포함된 값이 반환됩니다. 유효하지 않은 경우, 에러가 발생합니다. 보통 try-catch 문으로 감싸서 사용한다고 합니다.
[.safeParse]
.parse를 사용할 때 타입이 유효하지 않은 경우 Zod가 에러를 발생시키는 것을 원하지 않는다면, .safeParse를 사용하면 됩니다.
데이터가 유효한 경우 true값의 success와 데이터 정보가 담긴 data를 반환합니다.
유효하지 않은 경우에는 false값의 success와 에러 정보가 담긴 error를 반환합니다.
예시 : stringSchema.safeParse(12); // => { success: false; error: ZodError }
[.regax]
정규표현식으로 데이터 검증을 할 수 있습니다.
[.toLowerCase]
String 타입의 데이터를 모두 소문자로 변환해줍니다.
[.trim]
String 타입의 데이터에서 맨앞과 뒤에 붙은 공백을 제거해줍니다.
[.transform]
이 메서드를 이용하면 해당 데이터를 변환할 수 있습니다.
예시: .transform((username) => `🔥 ${username} 🔥`)
[zod 공식문서]
https://zod.dev/
///
Zod에는 몇 가지 문자열 관련 유효성 검사가 포함되어 있습니다.
(https://zod.dev/?id=strings)
문자열 스키마를 만들 때 몇 가지 오류 메시지를 지정할 수 있습니다.
const name = z.string({
required_error: "Name은 필수입니다.",
invalid_type_error: "Name은 문자열이어야 합니다.",
});
유효성 검사 메서드를 사용할 때 추가 인수를 전달하여 사용자 지정 오류 메시지를 제공할 수 있습니다.
z.string().min(5, { message: "5글자 이상 되어야합니다." });
.refine 메서드를 통해 사용자 지정 유효성 검사를 할 수 있습니다.
(https://zod.dev/?id=refine)
z.string().refine((val) ⇒ val.length ≤ 255, {message: “255이하의 문자열이어야 합니다.”});
.refine 은 2개의 인수를 받습니다.
1. 유효성 검사 함수
2. 몇가지 옵션
제공되는 옵션은 다음과 같습니다.
- message: 에러 메세지 지정
- path: 에러 경로 지정
- params: 에러시 메세지를 커스텀하기 위해 사용되는 객체
*create-account/actions.ts
"use server";
import { z } from "zod";
const passwordRegex = new RegExp(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*?[#?!@$%^&*-]).+$/
);
const formSchema = z
.object({
username: z
.string({
invalid_type_error: "Username must be a string!",
required_error: "Where is my username???",
})
.min(3, "Way too short!!!")
//.max(10, "That is too looooong!")
.trim()
.toLowerCase()
.transform((username) => `🔥 ${username}`)
.refine(
(username) => !username.includes("potato"),
"No potatoes allowed!"
),
email: z.string().email().toLowerCase(),
password: z
.string()
.min(4)
.regex(
passwordRegex,
"Passwords must contain at least one UPPERCASE, lowercase, number and special characters #?!@$%^&*-"
),
confirm_password: z.string().min(4),
})
.superRefine(({ password, confirm_password }, ctx) => {
if (password !== confirm_password) {
ctx.addIssue({
code: "custom",
message: "Two passwords should be equal",
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();
} else {
console.log(result.data);
}
}
1) 각 항목마다 세팅한 유효성 검사
2) zod 공식문서 보고 필요한건 다시 찾아보자
*components/input
import { InputHTMLAttributes } from "react";
interface InputProps {
name: string;
errors?: string[];
}
export default function Input({
name,
errors = [],
...rest
}: InputProps & InputHTMLAttributes<HTMLInputElement>) {
console.log(rest);
return (
<div className="flex flex-col gap-2">
<input
name={name}
className="bg-transparent rounded-md w-full h-10 focus:outline-none ring-2 focus:ring-4 transition ring-neutral-200 focus:ring-orange-500 border-none placeholder:text-neutral-400"
{...rest}
/>
{errors.map((error, index) => (
<span key={index} className="text-red-500 font-medium">
{error}
</span>
))}
</div>
);
}
1) input 속성을 다 받아오기 위해서 ...rest(이때 rest이름은 다른걸로 바꿔도됨)를 사용하고, 타입은 InputHTMLAttributes<HTMLInputElement> 를 적용시켜 주었다. 참고로 name은 별도 Props로 안빼줘도 되는데 props에서 까먹지 않기 위해서 한번 더 적어주었다.
*create-account/page.tsx
"use client";
import Button from "@/components/button";
import Input from "@/components/input";
import SocialLogin from "@/components/social-login";
import { useFormState } from "react-dom";
import { createAccount } from "./actions";
export default function CreateAccount() {
const [state, dispatch] = 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={dispatch} className="flex flex-col gap-3">
<Input
name="username"
type="text"
placeholder="Username"
required
errors={state?.fieldErrors.username}
minLength={3}
maxLength={10}
/>
<Input
name="email"
type="email"
placeholder="Email"
required
errors={state?.fieldErrors.email}
/>
<Input
name="password"
type="password"
placeholder="Password"
minLength={4}
required
errors={state?.fieldErrors.password}
/>
<Input
name="confirm_password"
type="password"
placeholder="Confirm Password"
required
minLength={4}
errors={state?.fieldErrors.confirm_password}
/>
<Button text="Create account" />
</form>
<SocialLogin />
</div>
);
}
1) zod에서 유효성 검증을 했지만, 프론트엔드 input 속성을 통해 해줄 수 있는 유효성 검증은 추가로 해주자.
2) 그리고 컴포넌트의 Button과 Input 이름 및 다른 부분도 일부 리팩토링 해주었다.(https://github.com/nomadcoders/carrot-market-reloaded/commit/95522558736d59e056b7e5a896a89d02a58b5a7e)
이와 같은 방법으로 login 페이지도 동일하게 적용시켜 주자.
*login/page.tsx
"use client";
import FormButton from "@/components/button";
import FormInput from "@/components/input";
import SocialLogin from "@/components/social-login";
import { useFormState } from "react-dom";
import { logIn } from "./actions";
import { PASSWORD_MIN_LENGTH } from "@/lib/constants";
export default function LogIn() {
const [state, dispatch] = useFormState(logIn, 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">Log in with email and password.</h2>
</div>
<form action={dispatch} className="flex flex-col gap-3">
<FormInput
name="email"
type="email"
placeholder="Email"
required
errors={state?.fieldErrors.email}
/>
<FormInput
name="password"
type="password"
placeholder="Password"
required
minLength={PASSWORD_MIN_LENGTH}
errors={state?.fieldErrors.password}
/>
<FormButton text="Log in" />
</form>
<SocialLogin />
</div>
);
}
*login/actions.ts
"use server";
import {
PASSWORD_MIN_LENGTH,
PASSWORD_REGEX,
PASSWORD_REGEX_ERROR,
} from "@/lib/constants";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email().toLowerCase(),
password: z
.string({
required_error: "Password is required",
})
.min(PASSWORD_MIN_LENGTH)
.regex(PASSWORD_REGEX, PASSWORD_REGEX_ERROR),
});
export async function logIn(prevState: any, formData: FormData) {
const data = {
email: formData.get("email"),
password: formData.get("password"),
};
const result = formSchema.safeParse(data);
if (!result.success) {
console.log(result.error.flatten());
return result.error.flatten();
} else {
console.log(result.data);
}
}
[Coerce]
Zod는 coerce를 이용하여 값의 타입을 강제할 수 있습니다.
모든 원시 타입이 지원되며, 아래와 같이 작동됩니다.
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
z.coerce.date(); // new Date(input)
[Validator]
JavaScript의 validator 모듈은 문자열 검증 및 살균(sanitization)을 위한 라이브러리입니다. 이 라이브러리는 다양한 유형의 문자열 입력을 검증하거나 살균하는 데 사용할 수 있는 여러 함수를 제공합니다. 예를 들어, 이메일 주소가 유효한 형식인지, 문자열이 특정 형식(예: URL, 날짜)에 맞는지 확인할 수 있습니다. 또한, 입력으로부터 HTML 태그를 제거하는 등의 살균 작업도 수행할 수 있습니다.
npm i validator
npm i @types/validator (*타입스크립트용)
2. sms 인증 부분
먼저 핸드폰 번호 유효성 검사를 위해 npm i --save-dev @types/validator 를 설치해주자.
useFormState의 prevState의 활용에 대해서 알아보자.
*sms/page.tsx
"use client";
import Button from "@/components/button";
import Input from "@/components/input";
import { useFormState } from "react-dom";
import { smsLogIn } from "./actions";
const initialState = {
token: false,
error: undefined,
};
export default function SMSLogin() {
const [state, dispatch] = useFormState(smsLogIn, initialState);
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">SMS Log in</h1>
<h2 className="text-xl">Verify your phone number.</h2>
</div>
<form action={dispatch} className="flex flex-col gap-3">
{state.token ? (
<Input
name="token"
type="number"
placeholder="Verification code"
required
min={100000}
max={999999}
/>
) : (
<Input
name="phone"
type="text"
placeholder="Phone number"
required
errors={state.error?.formErrors}
/>
)}
<Button text={state.token ? "Verify Token" : "Send Verification SMS"} />
</form>
</div>
);
}
1) 초기값 상태 변화에 따라서 화면 ui를 바꿔줄 수 있다.(핸드폰 번호 입력하면 다시 수정 못하게, 그 칸이 사라지고, 인증문자 넣는 칸 생기게 하기) 초기 폰넘버 값을 넘겨주면 state의 token값이 false에서 true로 변한다.
*sms/actions.ts
"use server";
import { z } from "zod";
import validator from "validator";
import { redirect } from "next/navigation";
const phoneSchema = z
.string()
.trim()
.refine(
(phone) => validator.isMobilePhone(phone, "ko-KR"),
"Wrong phone format"
);
const tokenSchema = z.coerce.number().min(100000).max(999999);
interface ActionState {
token: boolean;
}
export async function smsLogIn(prevState: ActionState, formData: FormData) {
const phone = formData.get("phone");
const token = formData.get("token");
if (!prevState.token) {
const result = phoneSchema.safeParse(phone);
if (!result.success) {
return {
token: false,
error: result.error.flatten(),
};
} else {
return {
token: true,
};
}
} else {
const result = tokenSchema.safeParse(token);
if (!result.success) {
return {
token: true,
error: result.error.flatten(),
};
} else {
redirect("/");
}
}
}
1) token 값 true가 리턴되면, page 화면에서는 다시 리렌더링이 된다. 그리고 page화면에서 그 다음단계를 거치면 token값을 받는 로직으로 넘어간다.