React Hook Form과 Zod를 사용한 회원가입 폼 구현하기

하영·2024년 10월 21일
1

Next.js

목록 보기
15/19

팀프로젝트를 하면서 로그인, 회원가입, 마이페이지 부분을 구현해야하는데 소셜로그인 뿐만 아니라 유효성 검사를 할 수 있는 로직이 들어간 코드를 작성해야 프로젝트에 의미가 있다고 판단되었다.

그동안은 캠프에서 제공하는 api로 유효성 검사가 됐었던거라 이걸 직접 작성하려면 어떻게 해야하나 걱정이 되었다.

생각해보니 Next.js에서 React Hook FormZod를 활용해 회원가입 폼을 구현하는 방법을 배웠던게 생각났고 이 라이브러리를 사용하면 효율적으로 폼 입력을 관리하고, 유효성 검사를 검사를 간단하게 처리할 수 있었다!

실제 프로젝트에 내가 로그인을 맡게 될지는 모르겠지만 간단하게 연습해보려고 한다!

React Hook Form과 Zod를 사용한 회원가입 폼 구현

01. 라이브러리 설치 및 기본 화면 UI ✅

패키지 설치

yarn add react-hook-form
yarn add @hookform/resolvers
yarn add zod

02. 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 메서드의 첫 번째 인자로 검증 로직(함수)을 전달, 두 번째 인자로는 옵션 객체를 전달해서 실패 시 메시지를 지정하거나 특정 필드에 에러를 표시할 수 있다.


아이디 유효성 검사 ✅

비밀번호 유효성 검사 ✅


03. 폼 데이터 처리하고 상태 관리하기

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);
  };

이 코드 부분은 폼 데이터를 처리하고 상태를 관리하는 데 관련된 코드이다. 하나씩 설명해보면,

  1. z.infer<typeof signupSchema>Zod에서 제공하는 기능으로, signupSchema에서 유추된 타입을 SignupFormData 타입으로 정의한다.

  2. signupSchema에서 정의한 유효성 검사 규칙에 따라 TypeScript가 해당 스키마로부터 데이터 구조를 추론할 수 있다. 이렇게 하면 Zod 스키마에 정의된 이메일, 비밀번호, 비밀번호 확인 필드가 SignupFormData 타입에 반영되어 자동으로 타입이 유추되기 때문에 우리가 별도로 string, number 이렇게 부여하지 않아도 된다!

  3. React Hook FormuseForm으로 통해 폼의 입력 상태와 유효성 검사를 관리한다.

  • register: 입력 필드를 React Hook Form과 연결해주는 함수
  • handleSubmit: 폼이 제출될 때 호출되는 함수, onSubmit 함수에 데이터를 전달
  • errors: 폼의 유효성 검사 오류를 담고 있는 객체

회원가입 전체 코드 👩🏻‍💻

"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;
profile
왕쪼랩 탈출 목표자의 코딩 공부기록

0개의 댓글