프로젝트에서 useForm을 사용하면서 느꼈던 이점과 간단한 사용법, 그 외 React Hook Form의 기본 기능에 대해 공유해 보고자 한다.
useForm이란?
공식 홈페이지에서는 useForm
에 대해 form을 쉽게 관리하기 위한 custom hook이라 설명하고 있다.
단순히 form을 관리한다면 사용자 입력에 대한 form의 수만큼 상태(state)를 만들고 또 그 수만큼 onChange()와 같은 이벤트 처리가 필요하다. 만일 사용자 입력 값, 즉 form이 많다면 이에 대한 효율적 처리 방법에 대해 생각해 봐야 한다.
기존 사용법
InputField란 interface가 있고 name, age 필드가 있다.
export interface InputField {
name: string;
age: number;
}
이 필드들을 useState로 초깃값을 설정한 후 onChange 이벤트를 이용해 setField로 수정된 값을 업데이트해주었다.
function App() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const handleChangeName = (event: ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
}
const handleChangeAge = (event: ChangeEvent<HTMLInputElement>) => {
setAge(parseFloat(event.target.value));
}
const handleClickRegister = () => {
const params = { name, age }
console.log(params)
}
return (
<div className="App">
<header className="App-header">
<div>
<input onChange={handleChangeName} />
<input type='number' onChange={handleChangeAge} />
<button onClick={handleClickRegister}>register</button>
</div>
</header>
</div>
);
}
useState를 사용하여 상태를 관리하면 해당 상태가 변경될 때마다 컴포넌트가 다시 렌더링 된다.
useState를 사용한 상태 관리는 해당 상태와 연관된 컴포넌트만 다시 렌더링 되는 것이 아니라 컴포넌트 전체가 다시 렌더링 될 수도 있다. 이는 다른 컴포넌트 부분에도 영향을 미쳐 예상치 못한 동작이 발생해 오류가 생길 수도 있다. 결국 불필요하게 렌더링 되면 원치 않는 계산이 발생해 성능 저하를 초래한다.
useForm 사용해보기
useForm을 사용해 보기 전에 useForm props 중 일부를 알아보자.
register
: 입력값을 등록하거나 유효성 검사 규칙을 React Hook Form에 적용할 수 있다.handleSubmit
: form 유효성 검사가 성공하면 form 데이터를 처리해준다.formState
: 전체 form 상태(유효성, 변경 여부, 에러 메시지 등)에 대한 정보가 포함되어 있어 form 응용 프로그램과 사용자의 상호 작용을 추적하는 데 도움이 된다.
이제 useForm을 이용해 상태 값들을 변경해 보자.
객체 필드들의 default 값을 defaultValues{} 객체를 이용해 설정한다.
const {
register,
handleSubmit,
formState: { errors },
} = useForm<InputField>({
defaultValues: {
name: '',
age: 0,
}
});
그리고 input component에서 register 함수를 호출하여 사용자의 입력값을 form에 등록하며 formstate를 활용해 필드 유효성 검사가 실패하는 경우 오류를 반환하도록 설정한다. 마지막으로 유효성 검사를 통과한 최종 데이터는 handleSubmit 함수를 통해 console에 출력되게 한다.
const onSubmit: SubmitHandler<InputField> = (data) => console.log(data);
return (
<div className="App">
<header className="App-header">
{/* form 데이터 처리 */}
<form onSubmit={handleSubmit(onSubmit)}>
{/* 입력값 등록 */}
<input {...register('name', { required: true })} />
{/* 유효성 검사 */}
{errors.name && <span>Name is required</span>}
<input type='number' {...register('age', { required: true })} />
{errors?.age && <span>Age is required</span>}
<button onSubmit={() => onSubmit}>register</button>
</form>
</header>
</div>
);
화면상에서 확인해 보자.
name 필드를 입력하지 않은 상태에서 register 버튼을 클릭하면 필드 유효성 검사가 실패하여 아래와 같은 메시지가 표시된다.
이번에는 모든 필드를 제대로 기입한 후 register 버튼을 클릭해 보았다.
이처럼 useForm은
전체 양식을 다시 렌더링하지 않고 변경된 필드만 다시 렌더링하여 최적의 성능을 제공한다. 더불어 내장된 유효성 검사 기능을 활용하여 필수 입력 항목을 간편하게 검사할 수 있다.
Controller
프로젝트에서 useForm과 Material-UI 라이브러리를 사용하여 form을 관리하면서 일부 컴포넌트들에서 상태 값이 제대로 처리되지 않거나 다양한 오류가 발생했다. 왜 이런 상황들이 발생하는 걸까?
Controller Component
이때 해결한 방법이 Controller
다. React Hook Form
은 비제어(uncontrolled) 컴포넌트를 사용하는 라이브러리인 반면, Material-UI(MUI)
는 React 기반의 UI 라이브러리로서 제어 컴포넌트(controlled component) 방식으로 구현되어 있다.
두 라이브러리를 함께 사용하려면 React Hook Form의 Controller가 필요하다. Controller를 사용하면 외부 라이브러리와의 상호작용을 훨씬 간편하게 할 수 있다. 따라서 MUI 컴포넌트를 Controller로 감싸면 React Hook Form은 해당 필드의 상태를 추적하고 필요한 경우에만 업데이트하는 장점을 그대로 이용할 수 있다.
Controller 컴포넌트를 사용해 보자. 먼저 useForm의 props에 control을 추가해야 한다.
control
: 구성 요소를 React Hook Form에 등록하고 수정하는데 사용되는 메서드를 포함하고 있다. 단, control을 사용할 때에는 객체 내부의 속성에 직접 접근하면 안 된다.
useFrom의 props에 control을 추가하고 Controller 컴포넌트 내부에 render 메서드를 사용하여 Rating 컴포넌트를 넣어준다.
function App() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<StarRate>({
defaultValues: {
starRate: ''
}
});
const onSubmit: SubmitHandler<StarRate> = (value) => console.log(value);
return(
<>
form<br/>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name={'starRate'}
control={control}
render={({ field: { onChange, value } }) => <Rating value={parseInt(value)} onChange={onChange} />}
/>
<button type="submit">register</button>
</form>
</>
);
}
Controller 컴포넌트를 사용하여 변경된 상태 값이 업데이트되는 것을 확인했다.
useController
또 다른 방법으로 useController
라는 custom hook을 사용할 수도 있다.
useController도 Controller 컴포넌트와 마찬가지로 React Hook Form에서 제공하는 제어 컴포넌트를 사용하기 위한 방법이다. 이 hook은 Controller 컴포넌트와 동일한 속성과 메서드를 공유한다.
useController나 Controller 컴포넌트 둘 다 control 객체와 연결되어 있다. 따라서 Controller 컴포넌트를 사용했을 때와 같이 useForm의 props에 control을 추가해 줘야 한다.
function App() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<StarRate>({
defaultValues: {
starRate: ''
}
});
const onSubmit: SubmitHandler<StarRate> = (data) => console.log(data);
return (
<div className="App">
<header className="App-header">
form
<br/>
<form onSubmit={handleSubmit(onSubmit)}>
<RatingInput control={control} name="starRate" />
<button type="submit">register</button>
</form>
</header>
</div>
);
}
하위 컴포넌트에서 control과 name을 props로 받아오고 useController hook을 사용한다. name 속성은 이 control이 어떤 필드를 제어하는지를 식별하는 데 사용된다.
useController를 통해 field 객체를 가져올 수 있는데 이 객체에는 입력 필드를 제어하는 데 필요한 여러 속성과 메서드가 포함되어 있다. field.onChange 함수를 호출하여 상태를 업데이트하고 field.value를 통해 현재 필드의 값을 나타내준다.
function RatingInput({ control, name }: any) {
const {
field
} = useController({
name,
control,
rules: { required: true },
});
return (
<Rating
onChange={field.onChange}
value={parseInt(field.value)}
name={field.name}
/>
);
}
또한 useController은 재사용 가능한 제어 입력을 생성하는데 유용하다.
재사용 가능한 input 컴포넌트를 만들어준다. field 객체를 통해 입력값을 추출하고 이 값을 input 요소의 value에 바인딩하여 컴포넌트를 구성한다.
export interface InputField {
id: string;
pwd: string;
}
function ControllerInput(props: UseControllerProps<InputField>) {
const {
field
} = useController(props);
return (
<div>
<input {...field} placeholder={props.name} />
</div>
);
}
만든 input 컴포넌트 사용할 때 각각의 입력 필드에 대해 다른 name 값을 부여하여 form 입력값들을 지정해 준다. 또한 mode를 onChange로 설정하여 입력 필드의 변경이 발생할 때마다 유효성 검사를 수행하도록 설정한다.
mode
: React Hook Form 라이브러리의 동작 방식을 제어하는 옵션 중 하나이다. mode의 다양한 옵션(onChange, onSubmit 등)은 어떤 조건에서 form의 유효성 검사를 수행할지 결정한다.rules
: register와 동일한 형식의 유효성 검사 규칙을 정의하는 데 사용된다. 이 규칙을 통해 필수 입력, 최대/최소, 특정 패턴 검증 등과 같은 다양한 유효성 검사를 수행할 수 있다.
function App() {
const {
control,
handleSubmit
} = useForm<InputField>({
defaultValues: {
id: '',
pwd: '',
},
mode: 'onChange'
});
const onSubmit: SubmitHandler<InputField> = (data) => console.log(data);
return (
<div className="App">
<header className="App-header">
form
<br/>
<form onSubmit={handleSubmit(onSubmit)}>
<ControllerInput control={control} name="id" rules={{ required: true }} />
<ControllerInput control={control} name="pwd" rules={{ required: true }} />
<button type="submit">login</button>
</form>
</header>
</div>
);
}
Controller 컴포넌트
는 render prop을 사용하여 필드를 래핑하고 control 정보를 설정하는 방식으로 동작한다. 이 컴포넌트는 자체적으로 렌더링 및 갱신 로직을 갖고 있어 필드가 변경될 때마다 상위 컴포넌트가 다시 렌더링 될 수 있다. 반면 useController
는 hook을 사용하므로 함수 내에서 필드 정보를 가져와 직접 control 한다. 이를 통해 필드의 렌더링 및 업데이트를 더 세밀하게 제어할 수 있으며 성능 최적화에 더 많은 유연성을 제공한다.
일반적으로 Controller 컴포넌트
는 간단한 form 필드와 함께 사용되며 빠르게 구성할 때 유용하다. useController
hook은 더 복잡하거나 커스텀 된 form 필드를 다룰 때 더 유용하다. 어떤 방식을 선택할지는 프로젝트의 요구 사항과 성능 최적화에 따라 다르다.
그 외 기능들
프로젝트에서는 사용하지 않았지만 추가적인 기능들에 대해 알아보았다.
useFieldArray
useFieldArray
는 동적으로 필드 배열을 관리하기 위한 custom hook이다.
입력 필드를 동적으로 추가하는 데 사용되는append
및 추가된 필드를 삭제하는데 활용되는 remove
등의 메서드가 props로 포함되어 있으며 name은 필드 배열을 식별하는 데 사용되는 이름이다.
const {
register,
control,
handleSubmit
} = useForm();
const {
fields,
append,
remove
} = useFieldArray({
control,
name: 'user'
});
const onSubmit = (data: any) => console.log(data);
fields.map을 사용하여 필드를 ui 요소에 매핑하고 각 user 필드를 li 요소로 렌더링한다. 각 li 요소에는 key 속성을 부여하여 각각의 user 필드를 식별한다. input 요소에는 register 함수를 사용하여 각 user의 id 입력 필드를 등록한다. 또한 Controller 컴포넌트를 사용하여 pwd 입력 필드를 생성한다.
return (
<div className='App'>
<header className='App-header'>
<form onSubmit={handleSubmit(onSubmit)}>
<button
type="button"
onClick={() => append({id: '', pwd: ''})}
>
+
</button>
<ul>
{fields.map((user, index) => (
<li key={user.id}>
<input {...register(`user.${index}.id`)} />
<Controller
render={({ field }) => <input {...field} />}
name={`user.${index}.pwd`}
control={control}
/>
<button type="button" onClick={() => remove(index)}>-</button>
</li>
))}
</ul>
<button type="submit">✓</button>
</form>
</header>
</div>
);
필드들을 추가해 보았다.
추가한 필드를 삭제해 보았다.
이렇듯 useFieldArray는 form 내에서 반복적으로 나타나는 필드나 입력 요소들을 배열 형태로 관리하고 추가, 삭제, 수정하는 데 사용된다. 이를 통해 동적인 필드 배열을 쉽게 다룰 수 있다.
useFormContext
useFormContext
를 사용하면 form context에 접근할 수 있다. 깊게 중첩된 구조에서 useForm의 반환값을 prop으로 전달하는 게 불편할 때 사용된다.
useForm의 반환값을 그대로 가져와서 사용할 수 있으며 이를 통해 form 관련 메서드와 속성을 쉽게 활용할 수 있다.
FormProvider
: form context를 하위 컴포넌트에 전달하고 form 상태와 method에 쉽게 접근할 수 있게 해준다.
useForm 함수를 호출하여 methods라는 객체에 form 관련 메서드를 할당한다. 그리고 FormProvider 컴포넌트를 사용해 useForm의 모든 method를 context에 전달한다. 이렇게 하면 하위 컴포넌트에서 form 관련 기능에 쉽게 접근할 수 있다.
function App() {
const methods = useForm()
const onSubmit = (data: any) => console.log(data)
return (
<div className="App">
<header className="App-header">
form
<br/>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<ReviewInput />
<button type="submit">✓</button>
</form>
</FormProvider>
</header>
</div>
);
}
useFormContext를 호출하여 모든 form의 context를 가져온다. 이 context를 통해 form에서 사용된 모든 hook method에 접근할 수 있다.
export const ReviewInput = () => {
const { register } = useFormContext()
return (
<>
<StarRateInput/>
<input {...register("review")} />
</>
);
}
export const StarRateInput = () => {
const { control } = useFormContext();
return (
<>
<Controller
name={'starRate'}
control={control}
render={({ field: { onChange, value } }) => <Rating value={parseInt(value)} onChange={onChange} />}
/>
</>
);
}
useFormContext를 이용하면 복잡한 form을 효과적으로 구축하고 form의 상태와 동작을 효과적으로 관리할 수 있다. 이를 통해 form 관련 로직을 더 간결하게 구현하고 여러 컴포넌트에서 form에 접근할 수 있게 된다.
React Hook Form은 간결한 API를 제공하여 복잡한 form 관리를 간단하게 만들어주며 내장된 유효성 검사 기능을 활용하여 데이터 처리 시 코드 작성과 유지 관리를 더욱 편리하게 해준다. 그뿐만 아니라 입력 필드가 변경될 때만 렌더링되므로 성능 최적화도 가능하다. 이러한 이점들을 통해 React Hook Form은 form 관리를 간편하고 효율적으로 수행할 수 있는 강력한 도구로 자리 잡고 있다.
이제부터 React Hook Form을 사용해 코드를 간소화하여 빠른 개발을 해보도록 하자!!!!
Hippo