Zod는 타입스크립트와 자바스크립트에서 사용되는 타입 검증 라이브러리이다.
Zod는 주로 스키마 기반의 데이터 검증 및 파싱에 사용되며, 매우 직관적이고 선언적인 방식으로 타입을 정의하고, 해당 타입에 대한 데이터를 검증할 수 있다.
보통 검증을 하려면 if(username !== ~~)이런식으로 if, else로 검증 코드를 작성했던 기억이 있다. 검증 관련 코드만 엄청 길었던 것 같다.
zod를 사용하면, 우리는 데이터가 어떤 형태여야 하는지를 정의하면 된다.
예를 들면 email은 string이자 email타입이여야 하는 등의 식으로 정의를 하게 되면
-> Zod가 나중에 if, else같은 것을 우리를 위해 해줄 것임.
zod에게 데이터의 형태나 타입을 설명할 때에는 Schema를 만든다.
-->데이터가 어떻게 생겨야 하는지, 타입은 무엇인지 알려주는 설계도 같은 것임을 알고 있으면 된다.
그럼 zod를 어떻게 작성하는지 한번 보러가보자
'use server';
import { z } from 'zod';
const usernameSchema = z.string().min(5).max(10);
export async function createAccount(prevState: any, formData: FormData) {
const data = {
username: formData.get('username'),
email: formData.get('email'),
password: formData.get('password'),
comfirmPassword: formData.get('comfirmPassword'),
};
console.log(data);
usernameSchema.parse(data.username);
}
간단하게 username을 검증해보는 코드를 작성해 보았다.
서버 측에서 사용자 계정을 생성하기 위한 함수 createAccount를 정의하고 있.
이 함수는 폼 데이터(FormData)를 받아서 데이터를 검증하는 과정에서 Zod 라이브러리를 사용하고 있다.
import { z } from 'zod'; : Zod 라이브러리를 가져온다.
--> Zod를 사용해 데이터 스키마를 정의하고, 데이터가 그 스키마에 맞는지 검증할 수 있다.
const usernameSchema = z.string().min(5).max(10); : usernameSchema라는 변수를 정의하고 있다.
--> 이 변수는 Zod의 string() 메서드를 호출해 문자열 스키마를 만들고, .min(5)와 .max(10) 메서드를 체인하여 그 문자열의 길이가 최소 5자에서 최대 10자 사이여야 한다는 규칙을 설정한다
const data = {} : data라는 객체를 만들어 폼 데이터에서 가져온 값을 할당해주었다.
usernameSchema.parse(data.username); : usernameSchema를 사용해 data.username 값을 검증한다.
usernameSchema.parse() 메서드는 데이터를 스키마와 비교하여 검증한다. 만약 username 값이 정의된 조건(min(5), max(10))을 충족하지 않으면, Zod는 오류를 발생시킨다.
이 부분은 폼 데이터에서 username 필드의 유효성을 검증하는 역할을 한다.
실제로 Username을 10글자 이상 작성해 보았더니, 친절한 오류 메시지를 보여주는 것을 확인할 수 있다.
그럼, data object(username, email, password 등등) 모두 개별 schema를 생성해주어야 할까?
--> 좋은 소식은 data object의 각 item마다 검사할 필요가 없다는 것! usernameSchema, emailSchema 이런식으로 하지 않아도 된다.
const formSchema = z.object({
username : z.string().min(3).max(10),
email : z.string().email(),
password : z.string().min(10),
comfirmPassword : z.string().min(10)
})
이렇게 formSchema를 정의한 후,
formSchema.parse(data)가 아닌 formSchema.safeParse(data)를 사용할 것이다.
왜 ? formSchema.parse를 실행하면서 유효하지 않은 data를 넘기면, zod가 에러를 발생시키게 된다.
parse는 데이터 유효성 검사가 실패하면 에러를 throw를 한다. --> parse를 사용하면 try catch를 사용해야 함
하지만, parse가 아니라 safeParse를 더 선호함 --> 에러를 throw를 하지 않기 때문!
parse 대신 safeParse를 쓰면 safeParse는 에러를 throw를 하지 않는다. 그대신 우리는 유효성 검사의 결과를 얻게 된다.
const result = formSchema.safeParse(data)로 result를 얻어올 수 있다.
result를 출력하면 에러를 볼 수 있는데 이 에러를 사용자에게 return 할 수도 있다.
우선, result를 console.log로 출력해보자

{ success : false, error: [Getter] }의 결과를 얻인 것을 확인할 수 있다.
사용자에게 보여주려면, 자세한 error 메시지가 출력되야 하는데?
if (!result.success) {
console.log(result.error);
}
이렇게 만약, result가 success가 아니라면, result의 error를 출력해주어라! 라고 명시해주었더니
에러가 발생한 것들에서 각각 에러 메시지를 자세하게 출력하는 것을 확인할 수 있다.
여기서 보통, 사용자 UI를 통해 에러메시지를 보여주기 위해서는
if (!result.success) {
console.log(result.error.flatten());
}
result.error.flatten()를 주로 사용한다.
이렇게 item 별로 각각 error message가 출력되는 것을 확인할 수 있다.
그럼 여기서, 사용자에게는 어떻게 보여주나?
return으로 result.error.flatten()
을 넘겨주면 된다.
이전에 배웠듯이 return을 하게 되면, useFormState() 의 state에 return 값이 저장되게 된다!

이렇게 errors에 각각 state?.fieldErrors.각각의 내용물를 통해 error메시지를 UI에 보여줄 수 있게 되었다!
화면에 이렇게 오류 메시지가 출력되는 것을 확인할 수 있다!
먼저 위의 에러메시지를 살펴보면, 딱히 이쁘지도 않고, 에러 메시지의 내용이 직관적이지 않다는 것을 확인할 수 있다.
그럼, 내가 원하는 대로 에러 메시지를 커스텀할 수 있을 까?
이렇게 각 필드에 대해 정의한 에러 메시지는 해당 타입 검증이 실패했을 때 출력되게 된다.
짠 , 정상적으로 작동하는 것을 확인할 수 있다!
를 검증하려면 어떻게 해야할 까?
--> .refine()함수를 사용하면 된다.
function checkUsername(username: string) {
return username.includes('potato') ? false : true
}
.refine((username) => checkUsername(username), 'No Potato allowed'),
refine에 작성해야 할 함수의 첫 번째 argument(인자)는 현재 검증 중인 값으로 username을 의미한다.
refine 안에 작성한 함수가 true를 리턴하면 문제가 없음. false를 return 해야 함.
false를 리턴하면 문제가 있다는 뜻이고 유저에게 이 에러메시지가 표시되게 된다.
--> username에 potato가 포함되어 있으면 ? false이고, 아니면 true인데, false일 때 No Potato라는 에러메시지를 출력하는 거라고 이해하면 된다.
이 또한 refine() 함수를 사용하면 되는데,
password: z.string().min(10, 'Way too short!').refine()
이렇게 refine() 함수를 사용하게 되면, 이는 password에 관해서만 사용할 수 있다.
그럼 password와 confirmPassword이 값이 같은지 비교가 되지 않는다.
그럼 어떻게 해야할까?
formSchema 전체에 .refine()함수를 이용하는 것이다. 그럼 password, comfirmPassword 둘다 사용할 수 있게 된다!
이렇게 .refine(({password, comfirmPassword}) => password === comfirmPassword, "Both passwords should be the same!") 작성하게 되면,
password와 comfirmPassword의 값이 같은지 비교하고, false라면, 오른쪽에 정의한 에러 메시지를 출력하게 된다.
그런데 password와 comfirmPassword를 다르게 입력했음에도 불구하고 에러메시지가 화면에 나타나지 않는다.
에러메시지는 comfirmPassword아래에 나타나면 좋을 것 같은데 왜 나타나지 않는 것일까?
에러메시지를 출력해보니, 내가 정의했던 에러메시지가 formErrors에 뜨는 것을 확인할 수 있다.
흠... 아마 password나 comfirmPassword에 정의하지 않고 formSchema에 정의한 검증(에러메시지)이기 때문에 해당 필드에 직접 연결되지 않고, 폼 전체의 에러로 판단하게 된다.
그럼 내가 어디에 에러메시지를 출력해줄 지 지정해주면 된다.
이렇게 message (에러 메시지)와 path (어디에 출력할 것인지) ['comfirmPassword']를 지정해두게 되면,
path에 지정한 경로로 에러메시지가 출력되는 것을 확인할 수 있다!
'use server';
import { z } from 'zod';
const checkUsername = (username: string) => {
return username.includes('potato') ? false : true;
};
const checkPassword = ({
password,
comfirmPassword,
}: {
password: string;
comfirmPassword: string;
}) => password === comfirmPassword;
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 Looooooong!')
.refine((username) => checkUsername(username), 'No potato allowed'),
email: z
.string({
invalid_type_error: 'Email must be a string!',
required_error: 'Where is my email',
})
.email(),
password: z.string().min(10, 'Way too short!'),
comfirmPassword: z.string().min(10, 'Way too short!'),
})
.refine(checkPassword, {
message: 'Both passwords should be the same!',
path: ['comfirmPassword'],
});
export async function createAccount(prevState: any, formData: FormData) {
const data = {
username: formData.get('username'),
email: formData.get('email'),
password: formData.get('password'),
comfirmPassword: formData.get('comfirmPassword'),
};
console.log(data);
const result = formSchema.safeParse(data);
console.log(result.error?.flatten());
if (!result.success) {
return result.error.flatten();
}
}
보통의 비밀번호는 소문자, 대문자, 숫자, 문자 등이 다 포함되어 있도록 설정해 둔다
const passwordRegex = new RegExp(
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).+$/
);
요렇게 대문자, 소문자, 숫자, 문자기호 등이 포함되어 있도록 new RegExp()를 통해서 passwordRegex를 정의해 둔 뒤,
이렇게 .regex()를 통해서 passwordRegex를 검증할 수 있게 된다. 이 함수 또한 마찬가지로 두번째 인자에는 error message를 정의할 수 있게 된다!
데이터 변환에 간단한 예를 들면 toLowercase(), toUppercase()이런것들을 의미한다.
.trim(): 공백을 제거시켜준다.
.toLowerCase() : 소문자로 변경시켜준다.
username과 email에 각각 적용시키고 데이터를 출력시켜보면,
공백이 제거되고, 대문자를 소문자로 변경시켜준 것을 확인할 수 있다.
.transform() : return 값을 변환시켜준다.
.transform((username) => `⭐️${username}⭐️`)
transform 또한 refine 과 마찬가지로 transform 하려는 대상을 넘겨주게 된다.
여기서는 true, false가 아니라 변환된 값을 return 하면 된다. (transform 함수는 반드시 무언가를 return 해주어야 한다.)
username을 ⭐️${username}⭐️ 로 변환시킨 것을 확인할 수 있다!
input 에서 기본적으로 제공하는 속성들에 대해서 우리는 알고 있다.
이와 같이 min, max, minLength, maxLength 등등 다양한 속성을 제공해준다.
이를 통해서 프론트 단에서도 간단한 검증을 할 수 있게 된다.
그럼 필요한 속성을 모두 불러와서 사용해야 할까?
아니다! InputHTMLAttributes<HTMLInputElement>를 사용하면 된다.
InputHTMLAttributes<HTMLInputElement> 는 React에서 HTML <input> 요소에 적용할 수 있는 모든 속성을 정의한 타입이다.
이 타입은 TypeScript를 사용할 때 주로 활용되며, HTML <input> 요소에 대해 사용할 수 있는 속성들을 정확하게 정의하고 타입 안전성을 제공하는 역할을 하게 된다.
즉, InputHTMLAttributes<HTMLInputElement> 를 통해서 우리는 min, max와 같은 input에 적용할 수 있는 요소들을 불러올 수 있는 것이다! 
...rest 는 InputHTMLAttributes<HTMLInputElement>에 포함된 속성들을 의미한다
...rest 객체는 type, placeholder, onChange, value, min, max 등 <input> 요소에 적용될 수 있는 모든 HTML 속성을 포함되어 있다고 생각하면된다.
그렇기 떄문에 interface에 정의한 Input에 type, placeholder 등등을 제거해도 오류가 발생하지 않는다. 왜 ? ...rest 에 정의되어 있기 때문!
이렇게 {...rest} 를 <input/> 태그에 쏙 넣어주면,
이렇게 다양하게 사용할 수 있음을 확인할 수 있다!

Zod에 대해서 정리해보려고 한다.
우선 createAccount는 사용자가 form을 제출하면, form에 입력한 데이터인 formData와 함께 호출되게 된다. 그 다음에, form에서 받아온 데이터를 data object안에 넣어준다.
(이때 formData.get() 을 이용해서 값을 가져오는데 괄호 안에 들어가는 'username'은 input에 적어둔 name으로 찾아서 데이터 값을 가져온다)
그 다음에 formSchema.safeParse 메서드에 data를 넘겨주었다.
safeParse는 formSchema의 형태에 따라서 데이터를 parse, validate, transform 등을 진행시켜준다.
여기서 formSchema는 우리가 zod에 작성해둔 zod object를 나타낸다.
(데이터의 모양, 타입, 제약 등을 기입해둔다)
다양한 validator 등이 있는데(min, max, string 등등) 가장 좋은 점은 에러메시지를 각각 정할 수 있다는 것이다.
또한, 우리가 직접 validator를 만들 수 있다 ---> refind() (데이터를 직접 refind()를 통해서 우리가 필요한 작업을 확인할 수 있었다)
refind 안에 함수를 작성하고, false -> validation 실패 --> 에러메시지 출력, true면 문제 없음!
password의 경우 password와 comfirmPassword를 같은지 비교해야 하는데, 각각 refind()함수를 사용하게 되면, 같은지 비교가 되지 않는다(각각 사용하게 되면, 해당 속성만 적용되기 때문)
둘다 엑세스 하기 위해서 formSchema object 전체를 refind()를 하게 되었다.
object 형식으로 { password, comfirmPassword }를 받아오고, 같은지를 확인시켜 주었다.
이렇게 검증을 하게 된 후에, 
formSchema.safeParse(data) 를 통해서 result를 받아올 수 있다.
만약, !result.success즉, validation에 실패하면, return result.error.flatten() 에러메시지를 반환시켜준다.
flatten() 를 사용하게 되면, input에 정의한 name을 key로 간단한 error message를 반환 받을 수 있게 된다!
만약, result가 success라면, 검증 결과인 result안에 data에 접근할 수가 있게 된다.
바로 이 result.data를 database에 사용할 것이다!