[react-hook-form] 회원가입 Form 유효성 검사에 따른 조건부 클래스 작업 (clsx/useForm)

나라·2024년 1월 18일

[PROJECT] SOM

목록 보기
2/3

개요

  • react-hook-form 을 통한 회원가입 모달 내 유효성 검사 진행
  • 공식 문서에서 유효성 검사에 필요한 useForm 관련 각종 프로퍼티 및 메서드 설명 참고!

💁🏻‍♀️ 무엇을 할 것이냐면?
1. 각 입력값이 유효한 (검증을 마친) 상태면 input-success 클래스 부여 (tailwindCSS)
2. 사용자가 입력 중이거나 유효하지 않은 (잘못된) 값이라면 input-error 클래스 부여
3. 각 error message는 input 하단에 출력
4. 모든 form field 가 검증을 마친 상태라면 submit 버튼 활성화


과정

  • 일전에 다른 프로젝트에서는 유효성 검사를 submitHandler 함수 내에서 진행했었다
    (함수 최상단에서 유효성 검사 후 서버 통신 이전에 early return 하는 형식)
  • 이번에는 submit 요청 이전에 유효성 검사를 진행 / 각 에러 메세지를 클라이언트 측에 띄우기로 결정
  • register 관련 문서 참고**
//비밀번호 Input
<input
	type='password'
	placeholder='password'
	{...register('password', {
      required: '필수 입력 항목입니다',
      pattern: {
        value:
        /^.*(?=^.{8,16}$)(?=.*\d)(?=.*[a-zA-Z])(?=.*[!@#$%^&+=]).*$/,
        message: '8-16자 이내, 영문, 숫자, 특수 문자를 포함해주세요',
 	 },
})}
/>
  • pattern : 정규표현식을 이용한 유효성 검사 진행, message는 error.message 로 출력된다
  • required : 입력한 메세지는 error.message 로 출력된다

비밀번호 확인의 경우 비밀번호 입력값과 일치한 지 검증해야 하는데 이 때에는 validate 활용

  • validate

    (문서 번역) 콜백 함수를 인수로 전달하여 유효성을 검사할 수도 있고, 콜백 함수의 개체를 전달하여 모든 유효성을 검사할 수도 있습니다. 이 함수는 속성에 포함된 다른 유효성 검사 규칙에 의존하지 않고 자체적으로 실행됩니다 ... 중략

사실 처음에 공식 문서만 봐서는 정확히 감이 오지 않았지만, 활용해 검증한 많은 케이스들을 보면서 익혔다. 문자열을 return할 경우 이 역시 error.message 로 출력된다

//비밀번호 확인 Input
<input
  type='password'
  placeholder='verify password'
  {...register('verifyPwd', {
      required: '필수 입력 항목입니다',
      validate: {
        value: (value) => {
          const { password } = getValues()
          return password === value || '비밀번호가 일치하지 않습니다'
        },
      },
  })}
/>
  • getValues() 로 비밀번호 입력값 취득하여 인수로 전달된 값과 일치한지 검증
  • 일치하지 않을 경우 에러 메세지 return

유효한 (검증을 마친) 값인 지 여부 확인

getFieldState() 메서드에 인수로 name 전달 시 반환되는 객체에는 4가지 속성이 있다

이 중
1. 사용자가 입력을 시도했나
2. 값이 유효한가
두 상태를 알아야 하기 때문에 isTouched (해당 필드에 focus / blue 이벤트 발생 여부 확인) 와 invalid 활용

field 상태에 따른 클래스명 부여를 할 계획이었으므로 위의 두 조건을 검증하는 함수를 따로 하나 만들었다 (코드 중복 방지)

type Formvalues = {
  name: string
  email: string
  password: string
  verifyPwd: string
}
//중략
  const isItValidCSS = (name: keyof Formvalues) => {
    return !getFieldState(name).isTouched
      ? ''
      : getFieldState(name).invalid
        ? 'input-error'
        : 'input-success'
  }
  • isItValidCSS은 인자로 field name : string 을 받고 문자열을 리턴하는 함수다
  1. 입력 전이라면 빈 문자열 return
  2. 입력중이거나 유효하지 않은 상태라면 input-error
  3. 입력중이거나 유효한 상태라면 input-success

clsx

  • 조건부에 따른 class 병합은 clsx 라이브러리로 진행
  • 해당문서 참고해서 설치 진행하면 된다
<input
  type='password'
  placeholder='password'
  {...register('password', {
    required: '필수 입력 항목입니다',
    pattern: {
      value:
      /^.*(?=^.{8,16}$)(?=.*\d)(?=.*[a-zA-Z])(?=.*[!@#$%^&+=]).*$/,
      message: '8-16자 이내, 영문, 숫자, 특수 문자를 포함해주세요',
    },
  })}
  className={clsx(`input input-bordered mb-1.5`,
              isItValidCSS('password') )} // 문자열 return

/>

submit 버튼 활성화

  • 모든 field 값이 유효성 검증을 마치면 submit 버튼을 활성화 시킨다
  • 이는 formstate 객체의 속성 중 isValid 을 통해 확인이 가능하다
  const {
    register,
    getValues,
    handleSubmit,
    getFieldState,
    formState: { errors, isValid },
  } = useForm<Formvalues>({ mode: 'all' })
  • isValid : 모든 검증이 마치면 true 상태로 변환
//submit 버튼
<button type='submit' disabled={!isValid} className='btn btn-primary'>
  Create one
</button>

끝!

결과

완성코드

//signUpModal.tsx
import clsx from 'clsx'
import { SubmitHandler, useForm } from 'react-hook-form'
import WarningMsg from '../common/warningMsg'
type Formvalues = {
  name: string
  email: string
  password: string
  confirmPwd: string
}
const SignUpModal = () => {
  const {
    register,
    getValues,
    handleSubmit,
    getFieldState,
    formState: { errors, isValid },
  } = useForm<Formvalues>({ mode: 'all' })
  const onSubmitHandler: SubmitHandler<Formvalues> = (e: any) => {
    console.log(e)
  }
  const isItValidCSS = (type: keyof Formvalues) => {
    return !getFieldState(type).isDirty
      ? ''
      : getFieldState(type).invalid
        ? 'input-error'
        : 'input-success'
  }
  return (
    <form className='card-body' onSubmit={handleSubmit(onSubmitHandler)}>
      <h2 className='text-center text-xl'>회원가입</h2>
      <div className='form-control'>
        <label className='label'>
          <span className='label-text'>Name</span>
        </label>
        <input
          type='text'
          placeholder='name'
          {...register('name', {
            required: '필수 입력 항목입니다',
          })}
          className={clsx(`input input-bordered mb-1.5`, isItValidCSS('name'))}
          required
        />
        {errors.name && errors.name.message && (
          <WarningMsg message={errors.name.message} />
        )}
      </div>
      <div className='form-control'>
        <label className='label'>
          <span className='label-text'>Email</span>
        </label>
        <input
          type='email'
          placeholder='email'
          {...register('email', {
            required: '필수 입력 항목입니다',
            pattern: {
              value: /^[a-z0-9]+@[a-z]+\.[a-z]{2,3}$/i,
              message: '유효한 이메일 형식이 아닙니다',
            },
          })}
          className={clsx(`input input-bordered mb-1.5`, isItValidCSS('email'))}
        />
        {errors.email && errors.email.message && (
          <WarningMsg message={errors.email.message} />
        )}
      </div>
      <div className='form-control'>
        <label className='label'>
          <span className='label-text'>Password</span>
        </label>
        <input
          type='password'
          placeholder='password'
          {...register('password', {
            required: '필수 입력 항목입니다',
            pattern: {
              value:
                /^.*(?=^.{8,16}$)(?=.*\d)(?=.*[a-zA-Z])(?=.*[!@#$%^&+=]).*$/,
              message: '8-16자 이내, 영문, 숫자, 특수 문자를 포함해주세요',
            },
          })}
          className={clsx(
            `input input-bordered mb-1.5`,
            isItValidCSS('password')
          )}
        />
        {errors.password && errors.password.message && (
          <WarningMsg message={errors.password.message} />
        )}
      </div>
      <div className='form-control'>
        <label className='label'>
          <span className='label-text'>Confirm Password</span>
        </label>
        <input
          type='password'
          placeholder='confirm password'
          {...register('confirmPwd', {
            required: '필수 입력 항목입니다',
            validate: {
              value: (value) => {
                const { password } = getValues()
                return password === value || '비밀번호가 일치하지 않습니다'
              },
            },
          })}
          className={clsx(
            `input input-bordered mb-1.5`,
            isItValidCSS('confirmPwd')
          )}
        />
        {errors.confirmPwd && errors.confirmPwd.message && (
          <WarningMsg message={errors.confirmPwd.message} />
        )}
      </div>
      <div className='form-control mt-6'>
        <button disabled={!isValid} className='btn btn-primary'>
          Create one
        </button>
      </div>
    </form>
  )
}

export default SignUpModal
profile
FE Dev🔥🚀

0개의 댓글