본문 바로가기
🔥Next 뽀개기

Zod + React Hook Form = 회원가입 뚝-딱!

by 짱돌보리 2024. 11. 27.
728x90

✅Zod, React Hook Form으로 회원가입 만들기

❓Zod

기존에 yup을 사용하다가 zod도 많이 쓰는 걸 봐서 사용해봤다. zod는 타입스크립트와의 호환성이 뛰어나다. zod는 타입 추론을 자동으로 지원하므로, 별도의 타입을 명시하지 않고도 스키마에서 바로 타입을 추론할 수 있다.

type FormData = z.infer<typeof signupSchema>; // signupSchema로부터 타입 자동 추론

0. zod 설치

npm install react-hook-form zod @hookform/resolvers

1. 회원가입 스키마 만들기

zodResolver를 이용해 zod 스키마로 유효성 검사를 해준다.

import { z } from 'zod'

export type SignupInput = z.infer<typeof signupSchema>

export const signupSchema = z
  .object({
    id: z.string().min(1, '아이디는 필수입니다.'),
    nickname: z
      .string()
      .max(10, '열 자 이하로 작성해주세요.')
      .min(1, '닉네임은 필수입니다.'),
    password: z
      .string()
      .min(8, '8자 이상 입력해주세요.')
      .regex(/[a-z]/, '소문자가 포함되어야 합니다.')
      .regex(/[0-9]/, '숫자가 포함되어야 합니다.')
      .regex(/[!@#$%^&*(),.?":{}|<>]/, '특수문자가 포함되어야 합니다.')
      .regex(/^\S*$/, '공백을 포함할 수 없습니다.')
      .min(1, '비밀번호는 필수입니다.'),
    confirmPassword: z.string().min(1, '비밀번호 확인은 필수입니다.'),
  })
  .refine((data) => data.password === data.confirmPassword, {
    path: ['confirmPassword'],
    message: '비밀번호가 일치하지 않습니다.',
  })

⚠️nonempty

ZodString 타입에서 nonempty 메서드를 사용할 수 없다는 오류 메시지다.
nonempty대신 min(1)을 사용하여 빈 값을 허용하지 않도록 설정할 수 있다!!

📍refine()

zod에서 .refine()는 조건을 정의하고, 그 조건이 충족되지 않으면 유효성 검사를 실패하도록 만든다.
이 메서드는 두 가지 인자를 받는다.

  • 첫 번째 인자: 조건을 나타내는 함수
  • 두 번째 인자: 오류 메시지 및 해당 조건에 관련된 세부 설정을 담은 객체
.refine((data) => data.password === data.confirmPassword, {
  path: ['confirmPassword'],
  message: '비밀번호가 일치하지 않습니다.',
})

첫 번째 인자 ((data) => data.password === data.confirmPassword)

  • 이 부분은 refine 메서드가 확인할 조건을 정의한다.
  • data는 zod 스키마에 의해 검사된 폼 데이터를 의미한다. 이 예시에서는 data는 password, confirmPassword를 포함한 전체 객체이다.
  • data.password === data.confirmPassword 조건은 비밀번호와 비밀번호 확인 값이 일치하는지 확인한다. 이 조건이 false이면 유효성 검사를 통과하지 못한 것으로 간주된다.

두 번째 인자 ({ path: ['confirmPassword'], message: '비밀번호가 일치하지 않습니다.' })

  • 이 객체는 유효성 검사가 실패했을 때, 어떤 항목에 오류 메시지를 표시할지를 설정한다.
  • path: ['confirmPassword']: 오류 메시지를 confirmPassword 필드에 연결한다. 즉, 비밀번호 확인 필드에서 오류가 발생할 때 이 메시지가 나타난다.
  • message: 조건이 실패할 때 표시될 오류 메시지를 정의한다.

2. 폼 만들기

/* eslint-disable @typescript-eslint/no-unused-vars */
'use client'
import { signupSchema } from '@/app/_utils/signupSchema'
import { zodResolver } from '@hookform/resolvers/zod'
import Image from 'next/image'
import Link from 'next/link'
import { useForm } from 'react-hook-form'

type FormData = {
  id: string
  nickname: string
  password: string
  confirmPassword: string
}

export default function Signup() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: zodResolver(signupSchema),
    mode: 'all',
  })

  const onSubmit = (data: FormData) => {
    const { confirmPassword, ...submitData } = data
    console.log('회원가입 data:', submitData)
  }

  return (
    <div className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b px-4 text-center">
      <Link href={'/'}>
        <Image
          src="/imgs/logo.png"
          alt="Habit Tracker Illustration"
          width={400}
          height={300}
          priority
        />
      </Link>
      <div className="w-full max-w-[400px]">
        <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-2">
          <input
            type="text"
            className="rounded-full border-2 border-green-30 bg-white px-4 py-2 outline-none"
            placeholder="아이디"
            {...register('id')}
          />
          {errors.id && (
            <p className="pl-4 text-left text-sm text-red-500">
              {errors.id.message}
            </p>
          )}

          <input
            type="text"
            className="rounded-full border-2 border-green-30 bg-white px-4 py-2 outline-none"
            placeholder="닉네임"
            {...register('nickname')}
          />
          {errors.nickname && (
            <p className="pl-4 text-left text-sm text-red-500">
              {errors.nickname.message}
            </p>
          )}

          <input
            type="password"
            className="rounded-full border-2 border-green-30 bg-white px-4 py-2 outline-none"
            placeholder="비밀번호"
            {...register('password')}
          />
          {errors.password && (
            <p className="pl-4 text-left text-sm text-red-500">
              {errors.password.message}
            </p>
          )}

          <input
            type="password"
            className="rounded-full border-2 border-green-30 bg-white px-4 py-2 outline-none"
            placeholder="비밀번호 확인"
            {...register('confirmPassword')}
          />
          {errors.confirmPassword && (
            <p className="pl-4 text-left text-sm text-red-500">
              {errors.confirmPassword.message}
            </p>
          )}

          <button
            type="submit"
            className="mt-4 rounded-full bg-green-30 px-6 py-2 text-white transition hover:bg-green-40"
          >
            회원가입하기
          </button>
        </form>
        <div className="text-md mt-20 flex items-center justify-center gap-4">
          <p>이미 회원이신가요?</p>
          <Link
            href="/login"
            className="text-green-40 underline decoration-green-40 underline-offset-2"
          >
            로그인하기
          </Link>
        </div>
      </div>
    </div>
  )
}

⚠️"use client" 쓰기

Next.js에서는 기본적으로 컴포넌트가 서버 컴포넌트로 처리되는데, 서버 컴포넌트는 클라이언트 상태를 관리하거나 이벤트를 처리할 수 없다. 그래서 React Hook Form과 같이 클라이언트 측에서 상태 관리가 필요한 라이브러리를 사용할 때는 'use client'를 명시적으로 지정해 해당 컴포넌트가 클라이언트에서 실행되도록 해야 한다.

구현 결과

비밀번호 확인 부분만 빼고 잘 출력되는 것을 볼 수 있돠. 🙂