회원 가입 페이지 Joi , react-hook-form 을 사용한 유효성 검증

2025. 8. 10. 00:06Coding Study/React

패키지 설치

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

 

 

입력시 실시간 유효성 검증

 - 유효성 검증 내용은 signupFormSchema 파일에 따로 적용하여 resolver 로 useForm 과 연결 시킨다.

 - cotrol 을 useForm 에서 반환 받아서 Controller 에 연결

 - Controller 내 render 로 input 을  넣어준다. (field에는 onChange, onBlur, value, ref, name이 모두 들어 있음.)

 - error={fieldState.error?.message} 를 통해 유효성 에러 메시지도 전달 가능

 - 버튼 활성화 조건 (isValid 활용)

      useForm에 mode: 'onChange' 설정 추가하면 isValid 상태를 실시간으로 가져올 수 있다.

      제출버튼에 비활성화 조건 적용

 - defaultValues를 넣지 않으면 Input 에서 오류가 발생된다

       ( https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable)

 

이 에러는 "uncontrolled → controlled" input 전환 문제입니다. 간단히 말해, input 요소가 처음에는 value가 undefined였는데, 이후 value가 명시되면서 React가 상태 관리 방식이 바뀌었다고 인식해서 경고를 발생시키는 겁니다.


✅ 원인 요약

<InputField>에 전달된 value 또는 내부적으로 사용하는 value가 undefined에서 문자열로 변하면서 발생합니다.

React에서는 input의 value가 undefined면 uncontrolled, 문자열이면 controlled로 인식합니다. 이 두 가지 상태가 컴포넌트 라이프사이클 중 섞이면 안 됩니다.

 

아래코드에서 InputField , CommonButton은 공통 컴포넌트.

InputFileld 는 input 태그이며 error 를 prorp으로 전달하면 input 아래에 에러메세지를 출력하도록 되어있다.

import InputField from '@/common/InputField'
import CommonButton from '@/common/CommonButton'
import { useNavigate } from 'react-router-dom'
import type { SignupForm } from '@/types/signupForm'
import { useForm, Controller } from 'react-hook-form'
import { joiResolver } from '@hookform/resolvers/joi'
import signupFormSchema from '@/schemas/signupFormSchema'

export default function SignupPage() {
  const navigate = useNavigate()

  const {
    control,
    handleSubmit,
    formState: { isValid },
  } = useForm<SignupForm>({
    resolver: joiResolver(signupFormSchema), // Joi를 React Hook Form과 연결
    defaultValues: {
      name: '',
      email: '',
      nickname: '',
      phone: '',
      password: '',
      gender: '남', // 또는 '남'/'여' 중 기본값
    },
    mode: 'onChange', // 입력값이 변경될 때마다 유효성 검사
  })

  const onSubmit = (_data: SignupForm) => {
    // console.log('Form Data:', data)
    // 여기에 회원가입 로직을 추가하세요
    navigate('/auth/signup/terms') // 회원가입 성공 페이지로 이동
  }

  return (
    <div className="flex flex-col items-center justify-center bg-white h-[856px]">
      <div className="flex flex-col items-center justify-center gap-[56px] w-[344px]">
        <div className="flex flex-col items-center justify-center gap-[24px]">
          <div className="text-[32px] font-semibold">계정찾기</div>
        </div>
        <form
          onSubmit={handleSubmit(onSubmit)}
          className="flex flex-col items-center justify-center gap-[40px]"
        >
          <div className="flex flex-col pace-y-3 gap-[16px]">
            <Controller
              name="name"
              control={control}
              rules={{ required: '이름은 필수입니다' }}
              render={({ field, fieldState }) => (
                <InputField
                  {...field}
                  className="w-full h-[48px] placeholder:text-text4"
                  placeholder="이름"
                  type="text"
                  error={fieldState.error?.message}
                />
              )}
            />
            <Controller
              name="email"
              control={control}
              rules={{ required: '이메일은 필수입니다' }}
              render={({ field, fieldState }) => (
                <InputField
                  {...field}
                  className="w-full h-[48px] placeholder:text-text4"
                  placeholder="이메일"
                  type="email"
                  error={fieldState.error?.message}
                />
              )}
            />
            <div className="flex w-full gap-[4px]">
              <Controller
                name="nickname"
                control={control}
                rules={{ required: '닉네임은 필수입니다' }}
                render={({ field, fieldState }) => (
                  <InputField
                    {...field}
                    className="w-[224px] h-[48px] placeholder:text-text4"
                    placeholder="닉네임"
                    type="text"
                    error={fieldState.error?.message}
                  />
                )}
              />
              <CommonButton
                type="button"
                variant="grayStyle"
                className="w-full text-[16px]"
              >
                중복확인
              </CommonButton>
            </div>
            <div className="flex w-full gap-[4px]">
              <Controller
                name="phone"
                control={control}
                rules={{ required: '휴대전화는 필수입니다' }}
                render={({ field, fieldState }) => (
                  <InputField
                    {...field}
                    className="w-[224px] h-[48px] placeholder:text-text4"
                    placeholder="휴대전화"
                    type="number"
                    error={fieldState.error?.message}
                  />
                )}
              />
              <CommonButton
                type="button"
                variant="grayStyle"
                className="w-full text-[16px]"
              >
                인증번호전송
              </CommonButton>
            </div>
            <Controller
              name="password"
              control={control}
              rules={{ required: '비밀번호는 필수입니다' }}
              render={({ field, fieldState }) => (
                <InputField
                  {...field}
                  className="w-full h-[48px] placeholder:text-text4"
                  placeholder="비밀번호"
                  type="password"
                  error={fieldState.error?.message}
                />
              )}
            />
            <div>
              <Controller
                name="gender"
                control={control}
                render={({ field }) => (
                  <div className="flex flex-col">
                    <div>성별*</div>
                    <div className="flex gap-[4px]">
                      {['남', '여'].map((genderOption) => (
                        <CommonButton
                          type="button"
                          key={genderOption}
                          variant={
                            field.value === genderOption
                              ? 'primary'
                              : 'secondary'
                          }
                          className="text-[16px] font-light border-primary border-[1px]"
                          onClick={() => field.onChange(genderOption)}
                        >
                          {genderOption === '남' ? '남자' : '여자'}
                        </CommonButton>
                      ))}
                    </div>
                  </div>
                )}
              />
            </div>
          </div>
          <div className="flex flex-col justify-center items-center gap-[24px] w-full">
            <CommonButton
              variant="disabled"
              disabled={!isValid}
              className={`text-[15px] w-full ${isValid ? 'cursor-pointer bg-primary text-text3 hover:bg-primary-hover' : ''}  `}
            >
              다음
            </CommonButton>
          </div>
        </form>
      </div>
    </div>
  )
}

'Coding Study > React' 카테고리의 다른 글

useEffectEvent  (0) 2025.10.13
React Hook Form - register  (1) 2025.08.13
Joi (유효성 검증 라이브러리)  (1) 2025.08.09
useActionstate  (0) 2025.05.15
react 어플리케이션 성능 최적화  (1) 2025.04.24