팀프로젝트를 하면서 로그인, 회원가입, 마이페이지 부분을 구현해야하는데 소셜로그인 뿐만 아니라 유효성 검사를 할 수 있는 로직이 들어간 코드를 작성해야 프로젝트에 의미가 있다고 판단되었다.
그동안은 캠프에서 제공하는 api
로 유효성 검사가 됐었던거라 이걸 직접 작성하려면 어떻게 해야하나 걱정이 되었다.
생각해보니 Next.js에서 React Hook Form
과 Zod
를 활용해 회원가입 폼을 구현하는 방법을 배웠던게 생각났고 이 라이브러리를 사용하면 효율적으로 폼 입력을 관리하고, 유효성 검사를 검사를 간단하게 처리할 수 있었다!
실제 프로젝트에 내가 로그인을 맡게 될지는 모르겠지만 간단하게 연습해보려고 한다!
React Hook Form과 Zod를 사용한 회원가입 폼 구현
패키지 설치
yarn add react-hook-form
yarn add @hookform/resolvers
yarn add zod
const signupSchema = z
.object({
email: z.string().email("유효한 이메일 주소를 입력해주세요."),
password: z.string().min(8, "비밀번호는 최소 8자리 이상이어야 합니다."),
confirmPassword: z
.string()
.min(8, "비밀번호 확인란도 최소 8자리 이상이어야 합니다."),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "비밀번호가 일치하지 않습니다.",
});
z.object()
를 이용해 이메일, 비밀번호, 비밀번호 확인 필드에 대한 스키마를 정의하고, 비밀번호와 비밀번호 확인이 일치하는지 추가로 검사하는 로직을 작성했다.
refine
: 특정 조건을 추가로 검증할 때 사용하는 zod 메서드이다. 위 예제에서는 password와 confirmPassword가 일치하는지 확인하는 추가 조건으로 사용했다. refine
메서드의 첫 번째 인자로 검증 로직(함수)을 전달, 두 번째 인자로는 옵션 객체를 전달해서 실패 시 메시지를 지정하거나 특정 필드에 에러를 표시할 수 있다.
아이디 유효성 검사 ✅
비밀번호 유효성 검사 ✅
type SignupFormData = z.infer<typeof signupSchema>;
const [submittedData, setSubmittedData] = useState<SignupFormData | null>(
null
);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignupFormData>({
resolver: zodResolver(signupSchema),
});
const onSubmit = (data: SignupFormData) => {
setSubmittedData(data);
console.log("회원가입 성공:", data);
};
이 코드 부분은 폼 데이터를 처리하고 상태를 관리하는 데 관련된 코드이다. 하나씩 설명해보면,
z.infer<typeof signupSchema>
는 Zod
에서 제공하는 기능으로, signupSchema
에서 유추된 타입을 SignupFormData
타입으로 정의한다.
signupSchema
에서 정의한 유효성 검사 규칙에 따라 TypeScript가 해당 스키마로부터 데이터 구조를 추론할 수 있다. 이렇게 하면 Zod
스키마에 정의된 이메일, 비밀번호, 비밀번호 확인 필드가 SignupFormData
타입에 반영되어 자동으로 타입이 유추되기 때문에 우리가 별도로 string
, number
이렇게 부여하지 않아도 된다!
React Hook Form
의 useForm
으로 통해 폼의 입력 상태와 유효성 검사를 관리한다.
"use client";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
// Zod 유효성 검사 스키마
const signupSchema = z
.object({
email: z.string().email("유효한 이메일 주소를 입력해주세요."),
password: z.string().min(8, "비밀번호는 최소 8자리 이상이어야 합니다."),
confirmPassword: z
.string()
.min(8, "비밀번호 확인란도 최소 8자리 이상이어야 합니다."),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "비밀번호가 일치하지 않습니다.",
});
type SignupFormData = z.infer<typeof signupSchema>;
const SignupForm = () => {
const [submittedData, setSubmittedData] = useState<SignupFormData | null>(
null
);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignupFormData>({
resolver: zodResolver(signupSchema),
});
const onSubmit = (data: SignupFormData) => {
setSubmittedData(data);
console.log("회원가입 성공:", data);
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold text-center mb-6">회원가입</h2>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
이메일
</label>
<input
type="email"
{...register("email")}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
/>
{errors.email && (
<p className="mt-2 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
비밀번호
</label>
<input
type="password"
{...register("password")}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
/>
{errors.password && (
<p className="mt-2 text-sm text-red-600">
{errors.password.message}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
비밀번호 확인
</label>
<input
type="password"
{...register("confirmPassword")}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
/>
{errors.confirmPassword && (
<p className="mt-2 text-sm text-red-600">
{errors.confirmPassword.message}
</p>
)}
</div>
<button
type="submit"
className="w-full py-2 px-4 bg-indigo-600 text-white font-semibold rounded-md shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
회원가입
</button>
</form>
{submittedData && (
<div className="mt-6 bg-green-50 p-4 rounded-md">
<h3 className="text-lg font-medium text-green-800">제출된 데이터:</h3>
<pre className="text-sm text-gray-700 mt-2">
{JSON.stringify(submittedData, null, 2)}
</pre>
</div>
)}
</div>
);
};
export default SignupForm;