useForm은 폼을 쉽게 관리할 수 있는 커스텀 훅입니다. 선택적 인수로 하나의 객체를 받습니다.
onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'
이 옵션을 사용하면 사용자가 양식을 제출하기 전에 유효성 검사를 할 수 있습니다.
FieldValues | () => Promise<FieldValues>
defaultValues 프로퍼티는 전체 폼을 기본값으로 채웁니다. 기본값의 동기식 및 비동기식 할당을 모두 지원합니다. 공식 React 문서에 자세히 설명된 대로 defaultValue 또는 defaultChecked를 사용하여 입력의 기본값을 설정할 수 있지만, 전체 양식에 defaultValues를 사용하는 것이 좋습니다.
useForm({
defaultValues: {
firstName: '',
lastName: ''
}
})
// set default value async
useForm({
defaultValues: async () => fetch('/api-endpoint');
})
🛑 주의점
defaultValues를 undefined로 설정하는 것을 피해야합니다. 제어 컴포넌트의 기본값과 충돌할 수 있기 때문입니다. defaultValues는 캐시됩니다. 초기화하기 위해서는 reset을 사용합니다.defaultValues 는 기본적으로 submit 결과에 포함됩니다.Moment 또는 Luxon과 같은 프로토타입 메서드가 포함된 커스텀 객체를 defaultValues로 사용하지 않는 것이 좋습니다. values prop은 변경 사항에 반응하여 폼을 업데이트하므로 외부 상태 또는 서버 데이터에 의해 양식을 업데이트해야 할 때 유용합니다. resetOptions: { keepDefaultValues: true } 를 설정하지 않는 한, values prop은 defaultValues prop을 덮어씁니다.
errors prop은 변경 사항에 반응하여 서버 errors 상태를 업데이트하므로 외부 서버에서 반환된 오류로 인해 양식을 업데이트해야 할 때 유용합니다. (ex. 이메일 양식을 확인해주세요.)
이 속성은 값 update 동작과 관련이 있습니다. values 또는 defaultValues가 업데이트되면 내부적으로 reset API가 호출됩니다. values 또는 defaultValues가 비동기적으로 업데이트된 후 원하는 동작을 지정하는 것이 중요합니다.
=> react-hook-form은 values나 defaultValues가 비동기적으로 업데이트되거나 명시적으로 변경될 때 내부적으로 reset API를 호출하여 폼을 새로운 값으로 초기화합니다. 이때 어떤 동작이 수행될지 제어하기 위해 resetOptions가 사용됩니다.
이 기능을 사용하면 Yup, Zod, Joi, Vest, Ajv 등 모든 외부 유효성 검사 라이브러리를 사용할 수 있습니다. 원하는 유효성 검사 라이브러리를 원활하게 통합할 수 있도록 합니다. 라이브러리를 사용하지 않는 경우에는 언제든지 자체 로직을 작성하여 양식의 유효성을 검사할 수 있습니다.
register에서 직접 유효성 검증하는 것과 resolver에서 하는 것?react-hook-form은 기본적으로 비제어 컴포넌트와 input을 활용합니다. input이 react-hook-form에 의해 비제어 컴포넌트로 동작하기 위해서는 register 함수가 반환하는 값을 컴포넌트의 prop으로 전달받아야 합니다.
register 함수가 반환하는 값에는 ref object도 포함되어 있는데, 따로 컴포넌트를 사용하고 있다면 ref를 object를 전달하기 위해서는 forwardRef로 감싸는 작업이 필요합니다.
다만 문제가 발생하는 경우는, 이미 전달받은 ref를 자체적으로 사용하고 있는 컴포넌트이거나, 제어 방식으로 value를 관리하는 컴포넌트입니다.
이를 해결하는 방법 중 하나는, 유저가 입력한 값의 업데이트를 직접 setValue 를 통해서 진행하는 것입니다.
react-hook-form에서는 이를 더 수월하게 하기 위한 Controller를 제공합니다.
Controller를 이용해 해당 요소를 감싼다면, register를 적용한 것처럼 react-hook-form에서 form 요소를 제어할 수 있습니다.
<Controller
name="memo"
render={({ field }) => (
<TextInput {...field} />
)}
/>
React Hook Form은 비제어 컴포넌트를 사용하는 라이브러리입니다. 반면, MUI는 React를 기반으로 한 UI 라이브러리이기 때문에 대부분 제어 컴포넌트로 구현되어있습니다. 이 둘을 같이 사용하기 위해서는 react-hook-form의 Controller를 사용하면 됩니다. Controller 컴포넌트를 사용하여 MUI 컴포넌트를 래핑하면, react-hook-form이 해당 필드의 상태를 추적하고 필요한 경우에만 업데이트하는 장점을 그대로 이용할 수 있습니다.

필수 props
name : 요소를 구분하기 위한 값으로, 같은 폼으로 묶여 있는 요소끼리 고유해야합니다.
control : useForm 훅의 반환값
render : 화면에 노출될 컴포넌트를 반환하는 함수
render prop을 통해 실제로 렌더링할 컴포넌트를 정의하고 field 객체를 통해 필요한 props( value, onChange, onBlur, ref)를 전달합니다.
사용하기 간편하고 제어 컴포넌트와 함께 직관적으로 사용할 수 있습니다.
<Controller
control={control}
name="ReactDatepicker"
render={({ field: { onChange, onBlur, value, ref } }) => (
<ReactDatePicker
onChange={onChange} // send value to hook form
onBlur={onBlur} // notify when input is touched/blur
selected={value}
/>
)}
/>
useController 훅은 Controller 컴포넌트와 같은 역할을 하지만, 더 세밀한 제어가 가능합니다. 이 방법은 Controller 컴포넌트보다 코드가 더 유연하게 작동할 수 있으며, 특히 컴포넌트 로직 내에서 더 많은 제어가 필요할 때 유용합니다.
→ 재사용 가능한 Controller Checkbox 컴포넌트를 만들 수 있다.
import * as React from "react";
import { useController, useForm } from "react-hook-form";
const Checkboxes = ({ options, control, name }) => {
const { field } = useController({
control,
name
});
const [value, setValue] = React.useState(field.value || []);
return (
<>
{options.map((option, index) => (
<input
onChange={(e) => {
const valueCopy = [...value];
// update checkbox value
valueCopy[index] = e.target.checked ? e.target.value : null;
// send data to react hook form
field.onChange(valueCopy);
// update local state
setValue(valueCopy);
}}
key={option}
checked={value.includes(option)}
type="checkbox"
value={option}
/>
))}
</>
);
};
export default function App() {
const { register, handleSubmit, control } = useForm({
defaultValues: {
controlled: []
}
});
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Checkboxes
options={["a", "b", "c"]}
control={control}
name="controlled"
/>
<input type="submit" />
</form>
);
}
onChange : 입력의 값을 라이브러리로 보내는 함수입니다. 입력의 onChange prop에 할당되어야 하며 value가 undefined가 되면 안됩니다. 이 prop은 formState를 업데이트하며 setValue 또는 필드 업데이트와 관련된 다른 API를 수동으로 호출하지 않아야 합니다.
onBlur
value : 제어 컴포넌트의 현재 값
name
ref : hook form을 input에 연결하는 데 사용되는 ref입니다. 컴포넌트의 input ref에 이 ref를 할당하여 hook form이 error input에 초점을 맞출 수 있도록 합니다.
..등등
isDirty
사용자가 입력을 변경하면 true로 설정합니다.
중요: defaultValues를 정확히 설정하는 것이 (폼이 더러워졌는지) isDirty를 판단하는데 중요한 역할을 합니다.
즉, defaultValues와 비교하여 isDirty 상태를 판단합니다.
dirtyFields
사용자가 수정한 필드가 있는 객체입니다. 라이브러리가 defaultValues와 비교할 수 있도록 모든 입력의 기본값을 useForm을 통해 제공해야 합니다.
dirty fields는 전체 폼이 아닌 필드 수준에서 필드 더티로 표시되므로 더티 필드는 isDirty formState로 표시되지 않습니다. 전체 양식 상태를 확인하려면 isDirty를 대신 사용하세요.
💡 isDirty와 dirtyFields
isDirty와 dirtyFields 싱크가 맞지 않는 경우가 간혹 있습니다.
Issue #7970 : Form isDirty doesn’t always match dirtyFields
Issue #7845 : isDirty and dirtyFields are not in sync
isDirty는 form level에서 defaultValues 값과 현재 form의 값을 깊은 비교를 해서 나오는 상태값
dirtyFields는 각 input의 상태값입니다. (사용자로부터 값의 변경이 있었는지를 나타내는 상태값)으로, input의 값에 대한 상태 값이 아닙니다.
react-hook-form에서 필드의 값을 변경하는 방법은 크게 2가지입니다.
input의 onChange 이벤트에 register 함수를 사용하여 등록하는 방법
setValue 함수를 사용하여 수동으로 설정하는 방법
위 두가지 방법 모두 필드 값이 변경될 때 내부적으로 updateTouchAndDirty 함수를 실행해서 isDirty 와 dirtyFields를 결정합니다. 단, setValue는 option으로 shouldTouch 또는 shouldDirty를 true로 설정하지 않으면 updateTouchAndDirty 함수가 실행되지 않습니다.
// isDirty가 결정되는 과정
if (_proxyFormState.isDirty) {
isPreviousDirty = _formState.isDirty;
_formState.isDirty = output.isDirty = _getDirty();
shouldUpdateField = isPreviousDirty !== output.isDirty;
}
// _getDirty 함수
const _getDirty: GetIsDirty = (name, data) => (
name && data && set(_formValues, name, data),
!deepEqual(getValues(), _defaultValues) // 깊은 비교
);
// dirtyFields가 결정되는 과정
if (!isBlurEvent || shouldDirty) {
const isCurrentFieldPristine =
disabledField || deepEqual(get(_defaultValues, name), fieldValue);
isCurrentFieldPristine || disabledField
? unset(_formState.dirtyFields, name)
: set(_formState.dirtyFields, name, true);
output.dirtyFields = _formState.dirtyFields;
}
isDirty는 value의 깊은 비교를 통해서 상태 값이 결정됩니다.
dirtyFields도 개별 필드 value의 깊은 비교를 통해서 최종적인 상태 값이 결정되기는 하지만, isBlurEvent와 shouldDirty 라는 옵션에 의해서 dirtyFields에 필드를 넣을지 말지 결정할 수 있습니다.
즉 현재 폼의 값이 defaultValue와 같아지더라도 isDirty가 false가 되지 않는 이유는 setValue로 직접 필드의 값을 변경하면서 shouldDirty 옵션을 true로 설정하지 않았기 때문입니다. true로 설정하지 않으면 updateTouchAndDirty 함수가 실행되지 않으므로 isDirty를 다시 계산하지 않습니다.
setValue > shouldDirty 옵션

isDirty는 폼의 어느 한 필드라도 변경되면, true가 됩니다.
shouldDirty는 해당 필드가 초기값(defaultValues)과 비교하여 변경되었는지 여부를 판단할 수 있습니다.
shouldDirty 옵션을 사용하면 해당 필드가 dirtyFields에 포함되지만, 폼 전체의 dirty 상태(isDirty)는 모든 필드의 상태를 종합하여 결정됩니다.
🛑) 실수했던 부분: 현재 사용자게에 입력받는 폼에서 초기값을 undefined로 정의하고, 각 인풋 컴포넌트에서 useEffect로 서버에서 받아온 값으로 초기값을 설정해줬었습니다.
그러다보니, 첫 진입 시 isDirty가 false > true로 찍히게 됩니다.
defaultValues: {
patient_relationship: undefined,
status: undefined,
cancer: undefined,
goals: [],
},

🟢) 개선 사항
defaultValues가 아닌 values로 서버에서 받아온 값으로 설정 values: {
patient_relationship: data.patient_relationship ?? '',
status: data?.status ?? '',
cancer: data?.cancer ?? '',
goals: data?.goals ?? [],
},