뽁 서비스에는 많은 항목에 대해서 사용자로부터 입력을 받고 이를 제출해 일화를 생성하는 기능이 존재한다.
처음에는 각각의 항목들을 하나씩 상태를 생성하는 코드로 구현을 하였다.
그러면 상태를 생성하는 코드의 길이가 너무 길어지는 것 같아,
폼 작성의 입력에 대한 상태를 객체형태로 생성하여 객체의 key 값으로 접근해 value를 update 하는 함수와 initailData 를 인자로 전달하여 set 하는 함수를 아래와 같이 hook 형식으로 관리하였다.
const useHandleDiary = () => {
const [diary, setDiary] = useState<IDiaryRequestBody>({
badChecklist: [],
goodChecklist: [],
content: '',
date: '',
emoji: null,
sticker: '',
tags: [],
});
const onChangeDiary = (inputName: TDiaryKey, value: TDiaryValue) => {
setDiary({ ...diary, [inputName]: value });
};
const onSetDiary = (diary: IDiaryDetailResponse) => {
if (diary) {
setDiary({
badChecklist: diary.badChecklist,
goodChecklist: diary.goodChecklist
content: diary.content,
date: diary.date,
emoji: diary.emoji,
sticker: diary.sticker,
tags: diary.tags,
});
}
};
return {
diary,
onChangeDiary,
onSetDiary,
};
};
export default useHandleDiary;
<TextField input={diary.content} setInput={(value) => setDiary('content', value)} maxLength={1000} />
<DatePicker date={diary.date} setDate={(value) => setDiary('date', value)} />
폼을 다루기 위해서 상태를 하나하나 생성하고 핸들링 함수도 만들며 또 이에 따른 유효성 검사 함수도 만들었다.
😅 모든 값이 state 로 연결되어 있어 하나의 값이 변할 때 마다 여러 개의 컴포넌트들이 무수히 많은 리렌더링이 발생 문제가 발생하였다.
또 다른 문제로는
하나의 페이지에서 모든 항목을 입력받는 것이 아니었다.

다른 많은 페이지에서 사용자로 부터 입력 받은 데이터들을 body 로 api 를 마지막 페이지에서 제출해야 했기에,
위의 코드로는 효율적으로 상태를 관리하기에 어려움이 있었다.
따라서,
다음과 같은 문제로 부터 위의 코드를 개선해보고자 한다.
일단 리액트의 제어 컴포넌트와 비제어 컴포넌트를 살펴보자
HTML 에서 <input>, <textarea>, <select> 와 같은 form element 는 일반적으로 사용자의 입력을 기반으로 자신의 state 를 관리하고 업데이트를 한다.
React 에서 변경할 수 있는 state 가 일반적으로 컴포넌트의 state 속성에 유지되며 setState() 에 의해 업데이트가 된다.
폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어한다. 이러한 방식으로 React 에 의해 값이 제어되는 입력 폼 element 를 제어 컴포넌트라고 한다.
간단히 말하면, state 가 렌더링을 제어하는 것을 제어 컴포넌트라고 하는 데, onChange 방식이 제어 컴포넌트라고 할 수 있다.
예를 들면,
const UseInput = () => {
const [input, setInput] = useState("");
const onChangeValue = (e) => {
setInput(e.target.value);
};
return (
<div>
<input onChange={onChangeValue} />
</div>
);
}
export default UseInput;
사용자가 입력한 값과 저장되는 값이 실시간으로 동기화된다.
이러한 방식으로 데이터를 전부 받아올 수 있어 유효성 검사에 탁월하지만,
데이터를 하나하나 다 받아오므로 비효율적이거나 속도가 느릴 수 있다는 단점이 있다.
또한 state 값이 변함 입력할 때마다 렌더링을 하기 때문에 불필요하게 렌더링되거나 API를 호출할 수 있다.
제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다루어 진다.
비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다루어 진다.
모든 state 업데이트에 대한 이벤트 핸들러를 작성하는 대신 비제어 컴포넌트를 만들려면 ref 를 사용하여 DOM에서 폼 값을 가져올 수 있다.
import React, { useRef } from 'react';
const UseRefInput = () => {
const inputRef = useRef(null);
const onSubmit = () => {
console.log(inputRef.current.value);
};
return (
<div>
<input ref={inputRef} />
<button type="submit" onClick={onSubmit}>
로그인
</button>
</div>
);
}
export default UseRefInput;
ref 는 값을 업데이트하여도 리랜더링 되지 않는 특성으로, 입력이 모두 되고 난 후 ref 를 통해 값을 한번에 가져와서 활용한다.
state 로 값을 관리하지 않기 때문에 값이 바뀔 때마다 리렌더링을 하지 않고 값을 한 번에 가져올 수 있는 성능상에 이점이 있으나, 데이터를 완벽하게 가져올 수 없는 단점이 있다.
🔍 useRef() => heap 영역에 저장되는 자바스크립트 객체
- 렌더링 할 때마다 동일한 객체를 제공한다. heap 에 저장을 하므로 어플리케이션이 종류되거나 가비지 컬렉팅이 되기 전까지 참조시에는 같은 메모리 값을 가진다.
- 값이 변경이 되어도 리렌더링 되지 않는다. 같은 메모리 값을 항상 반환하므로 변경사항을 감지할 수 없어서 리렌더링을 하지 않는다.
React Hook Form 은 비제어 컴포넌트로 렌더링을 최적화할 수 있는 라이브러리 이다.
단순히 form을 처리하기 위해 state 로 모든 값을 검사하여 리랜더링 하는 것 보다 입력이 끝난 후 유효성 검사를 보여주어도 되고 더 빠른 검사를 할 수 있어 비제어 컴포넌트 방식인 React Hook Form을 많이 사용한다.
1. 간결한 API
React Hook Form 은 사용하기 쉽고 직관적인 API를 제공하여 복잡한 폼 로직을 단순화 한다.
기본적으로 제공하는 Hook 함수들과 컴포넌트들을 사용하여 폼을 쉽게 생성하고 관리할 수 있다.
2. 높은 성능
React Hook Form 은 성능에 중점을 두어 최적화되어 있습니다.
입력 필드의 값 변화를 추적하는 상태 대신 각 입력 필드의 참조를 사용하여 불필요한 리렌더링을 방지하고, 가상 DOM의 업데이트를 최소화한다.
3. 유효성 검사
React Hook Form 은 내장된 유효성 검사를 지원하며, Yup, Joi 외부 유효성 검사 라이브러리와 통합할 수 있다.
입력 필드의 값에 대한 유효성 검사를 수행하고, 에러 메시지를 표시할 수 있다.
4. 커스텀 훅
React Hook Form 은 커스텀 훅을 사용하여 개발자가 필요한 로직을 쉽게 작성하고 재사용할 수 있도록 지원한다.
커스텀 훅을 사용하면 폼 상태, 에러 처리, 폼 제출 등의 로직을 캡슐화할 수 있다.
=> 개발하고 있는 서비스에는 많은 항목에 대한 입력을 받고 있기에 렌더링을 고려해 react hook form 을 활용해보고자 한다.
yarn add react-hook-form
const {
register,
formState: { errors },
handleSubmit,
setError,
} = useForm<IAuthForm>({mode: 'onBlur'});
const onSubmit = (data) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)>
...
input 요소를 React Hook Form 과 연결시켜 검증 규칙을 적용할 수 있게 하는 메소드
const { register } = useForm();
const { name, ref, onChange, onBlur } = register("username");
<input
name={name}
ref={ref}
onChange={onChange}
onBlur={onBlur}
/>
// 객체 안에 value 를 일일이 하기 너무 많다
<input {...register("username")} />
register 가 리턴하는 객체 항목을 input 항목에 연결시키면 된다.
form state 에 관한 정보를 담고 있는 객체
form 을 submit 했을 때 실행할 함수
Validation을 통과했을 때 실행할 콜백함수가 반드시 필요하다. 실패했을 때의 콜백함수(submitErrorHandler)는 optional
error 관련 설정에 사용되는 함수
사용자가 submit 버튼을 누르기 전에 form 에 입력한 값이 유효한 값이 안라는 것을 미리 표시해주고 싶을 때 사용하는 것이 mode!
mode 는 useForm() 에 넘겨줄 수 있는 다양한 optional arguments 중 하나로 사용자가 form 을 submit 하기 전에 validation이 실행될 수 있게 해준다.
// mode에 사용 가능한 값
mode: onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'
React Hook Form 은 Devtool 을 제공한다.
Form 관리를 일일이 콘솔 창에 출력하지 않고 현재 일어 나고 있는 상태를 쉽게 파악할 수 있다.
yarn add -D @hookform/devtools
FormProvider 은 React Hook Form 에서 제공하는 컴포넌트로, React의 Context API를 기반으로 구현되었다.
FormProvider 은 상위 컴포넌트에서 하위 컴포넌트로 폼 데이터와 관련된 상태와 로직을 전달하기 위해 사용한다.
useFormContext 를 사용하면 컨텍스트를 prop으로 전달하는 것이 불편한 깊은 중첩 구조에서도 컨텍스트에 접근할 수 있다.
이 훅을 사용하면 useForm 에서 반환하는 모든 메서드와 속성을 가져올 수 있다. 즉, useForm 의 반환값을 그대로 사용할 수 있다.

useFormContext 를 사용하기 위해서는 폼을 FormProvider 컴포넌트로 감싸줘야 한다.
FormProvider 컴포넌트에 useForm에서 반환한 메서드와 속성을 전달하면 된다.
그런 다음 useFormContext를 호출하면 해당 메서드와 속성을 가져올 수 있다.
import { useForm, FormProvider } from 'react-hook-form';
...
const methods = useForm();
...
<FormProvider {...methods}>
<FormProvider {...methods}>
//폼 컴포넌트가 들어있는 childComponent로
<ChildComponent />
</FormProvider>
const ChildComponent = () => {
const { register, handleSubmit } = useFormContext();
const onSubmit = (data) => console.log(data)
return(
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("test")} />
<input type="submit" />
</form>
)
}
import React from 'react';
import { useForm, FormProvider, useFormContext } from 'react-hook-form';
const ParentForm = () => {
const methods = useForm();
return (
<FormProvider {...methods}>
<ChildForm />
</FormProvider>
);
};
const ChildForm = () => {
const { register, handleSubmit } = useFormContext();
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={methods.handleSubmit(onSubmit)}>
<input type="text" name="firstName" {...methods.register('firstName')} />
<input type="text" name="lastName" {...methods.register('lastName')} />
<button type="submit">Submit</button>
</form>
)
}
const App = () => {
return (
<div>
<h1>My Form</h1>
<ParentForm />
</div>
);
};
export default App;
useFormContext 를 사용한다고 가정했을 때, FormProvider 의 자식 컴포넌트의 내부의 컴포넌트들의 예를 들어 input 이 수정하면 부모 컴포넌트들도 재렌더링이 될까? 아니다!
그 이유는 FormProvider 과 useFormContext 를 사용하는 핵심 개념인 로컬 폼 상태를 활용하여 최적화하였기 때문이다.
로컬 폼 상태는 해당 컴포넌트가 직접적으로 의존하는 폼 필드에 대해서만 관리하고 업데이트하므로, 다른 컴포넌트의 리렌더링에 영향을 받지 않는다.
=> 자식의 자식 컴포넌트들의 성능 이슈는 해당 필드의 변경에 따라 발생하며, 다른 부모나 형제 컴포넌트 들의 리렌더링 과는 독립적이다.
만약 부모 컴포넌트가 FormProvider 로 감싸져 있고, 그 하위에 여러개의 자식 컴포넌트가 있고, 그 자식 컴포넌트 중에서 useFormContext 를 사용하는 컴포넌트가 있다면, 해당 useFormContext 를 사용하는 컴포넌트의 로컬 폼 상태 변경이 발생하면 그 컴포넌트와 그의 하위 컴포넌트 들이 리렌더링 될 수 있다.
=> React 의 컴포넌트 트리에서 상위로의 리렌더링 전파로 인해 발생한다.
따라서, 깊은 계층 구조를 가지는 컴포넌트에서는 useFormContext 를 사용하는 컴포넌트의 성능에 영향을 주는 요소들을 최적화하는 것이 중요하다.
뽁에서는 많은 페이지에서 form data 를 다루기 때문에 FormProvider 을 활용해 context 로 데이터를 다른 컴포넌트에서도 prop전달없이 접근할 수 있도록 할 것이다.
import { FormProvider, useForm } from 'react-hook-form';
import { DevTool } from '@hookform/devtools';
const WritingDiaryFormProvier = ({ children }: PropsWithChildren) => {
const methods = useForm<IDiaryContextBody>();
const onSubmit = (data: IDiaryContextBody) => {
console.log(data);
};
const isMounted = useIsMounted();
return (
<FormProvider {...methods}>
{isMounted && <DevTool control={methods.control} />}
<form className="flex size-full flex-col" onSubmit={methods.handleSubmit(onSubmit)}>
{children}
</form>
</FormProvider>
);
};
export default WritingDiaryFormProvier;
사용자로 부터 입력을 받는 받는 페이지의 layout 으로 다음과 같이 컴포넌트로 감싸준다.
'use client';
import { WritingDiaryFormProvider } from '@features/diary/contexts';
import usePreventLeave from '@hooks/usePreventLeave';
import { type PropsWithChildren } from 'react';
const WritingLayout = ({ children }: PropsWithChildren) => {
usePreventLeave();
return <WritingDiaryFormProvider>{children}</WritingDiaryFormProvider>;
};
export default WritingLayout;
여기서 페이지를 이탈하거나 혹은 새로고침을 할 경우 폼 데이터가 초기화하기에 사용자에게 이탈 전 아래와 같이 alert 창을 띄워주는 hook인 usePreventLeaver 를 추가해주었다.
훅은 weindow.onbeforeunload 에 beforeUnloadHandler 를 할당하여 페이지를 떠날 때마다 사용자에게 경고를 표시해준다.

import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
const usePreventLeave = () => {
const router = useRouter();
useEffect(() => {
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue = '';
};
window.onbeforeunload = beforeUnloadHandler;
return () => {
window.onbeforeunload = null;
};
}, [router]);
};
export default usePreventLeave;
const WritingEmojiForm = () => {
const [selectEmoji, setSelectEmoji] = useState<TEmoji | null>(null);
const { register, getValues, setValue, control } = useFormContext<IDiaryContextBody>();
const { field } = useController({
name: 'isChecked',
control,
defaultValue: true,
});
useEffect(() => {
setSelectEmoji(getValues('emoji'));
}, [getValues]);
const handleSelectEmoji = (emoji: TEmoji) => {
setSelectEmoji(emoji);
setValue('emoji', emoji);
};
return (
<>
<h2 className="mb-3 text-base font-medium text-gray-65">감정</h2>
<div className="flex justify-center gap-3" {...register('emoji')}>
{DIARY_EMOJI_ARRAY.map((emoji) => (
<Image
className="cursor-pointer"
loader={ImageLoader}
width={40}
height={40}
key={emoji}
src={emoji === selectEmoji ? DIARY_EMOJI[emoji].smallSelect : DIARY_EMOJI[emoji].smallNotSelect}
onClick={() => handleSelectEmoji(emoji)}
alt=""
/>
))}
</div>
const WritingDateForm = ({ defaultValue }: IWritingDateFormProp) => {
const today = moment().format('YYYY-MM-DD');
const { register, control } = useFormContext<IDiaryContextBody>();
const { field } = useController({ name: 'date', control, defaultValue: defaultValue || today });
return (
<>
<h2 className="mb-3 mt-8 text-base font-medium text-gray-65">날짜</h2>
<DatePicker {...register('date')} date={field.value} setDate={field.onChange} />
</>
);
};
출처
https://velog.io/@yesoryeseul/react-hook-form-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90
https://velog.io/@boyeon_jeong/React-Hook-Form
https://velog.io/@boyeon_jeong/React-Hook-Form-Controller-useController