2025. 8. 10. 00:06ㆍCoding 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 |