Skip to content

useForm

폼 검증을 위한 React 훅

</> useForm: UseFormProps

useForm은 폼 관리를 쉽게 할 수 있도록 도와주는 커스텀 훅입니다. 이 훅은 선택적 인자로 하나의 객체를 받습니다. 아래 예제는 모든 속성과 기본값을 보여줍니다.

일반 속성:

옵션설명
mode제출 전 유효성 검사 전략.
reValidateMode제출 후 유효성 검사 전략.
defaultValues폼의 기본값.
values폼 값을 업데이트하는 반응형 값.
errors폼 오류를 업데이트하는 반응형 오류.
resetOptions새로운 폼 값을 업데이트할 때 폼 상태를 초기화하는 옵션.
criteriaMode모든 유효성 검사 오류를 한 번에 표시할지, 하나씩 표시할지 결정.
shouldFocusError내장된 포커스 관리 기능을 활성화 또는 비활성화.
delayError오류가 즉시 나타나지 않도록 지연.
shouldUseNativeValidation브라우저 내장 폼 제약 API 사용 여부.
shouldUnregister언마운트 후 입력 필드 등록 해제 여부.
disabled폼과 관련된 모든 입력 필드를 비활성화.

스키마 유효성 검사 속성:

옵션설명
resolver선호하는 스키마 유효성 검사 라이브러리와 통합.
context스키마 유효성 검사를 위한 컨텍스트 객체 제공.

Props


mode: onChange | onBlur | onSubmit | onTouched | all = 'onSubmit' React Native: Controller와 호환 가능


이 옵션은 사용자가 폼을 제출하기 전에 유효성 검사 전략을 설정할 수 있게 해줍니다. 유효성 검사는 handleSubmit 함수를 호출할 때 발생하는 onSubmit 이벤트 중에 이루어집니다.

이름타입설명
onSubmitstringsubmit 이벤트가 발생할 때 유효성 검사가 트리거되며, 입력 필드는 onChange 이벤트 리스너를 연결하여 스스로 재검증합니다.
onBlurstringblur 이벤트가 발생할 때 유효성 검사가 트리거됩니다.
onChangestring각 입력 필드의 change 이벤트가 발생할 때마다 유효성 검사가 트리거되어 여러 번의 리렌더링이 발생합니다. 경고: 이 옵션은 성능에 큰 영향을 미칠 수 있습니다.
onTouchedstring처음 blur 이벤트가 발생할 때 유효성 검사가 트리거됩니다. 이후에는 모든 change 이벤트가 발생할 때마다 유효성 검사가 트리거됩니다.

참고: Controller와 함께 사용할 때는 render prop에 onBlur를 연결해야 합니다.
allstringblurchange 이벤트 모두에서 유효성 검사가 트리거됩니다.

reValidateMode: onChange | onBlur | onSubmit = 'onChange' React Native: Custom register or using Controller


이 옵션은 폼 제출 후(onSubmit 이벤트와 handleSubmit 함수 실행 후) 오류가 있는 입력 필드가 다시 검증되는 전략을 설정할 수 있게 해줍니다. 기본적으로는 입력 변경 이벤트 중에 다시 검증이 이루어집니다.

defaultValues: FieldValues | () => Promise<FieldValues>


defaultValues prop은 폼 전체에 기본값을 채워줍니다. 동기 및 비동기 방식으로 기본값을 할당할 수 있습니다. defaultValuedefaultChecked를 사용해 입력 필드의 기본값을 설정할 수 있지만 (공식 React 문서 참조), 전체 폼에 대해서는 defaultValues를 사용하는 것을 권장합니다.

useForm({
defaultValues: {
firstName: '',
lastName: ''
}
})
// 비동기로 기본값 설정
useForm({
defaultValues: async () => fetch('/api-endpoint');
})
규칙
  • undefined를 기본값으로 제공하는 것은 피해야 합니다. 이는 제어 컴포넌트의 기본 상태와 충돌할 수 있습니다.

  • defaultValues는 캐시됩니다. 이를 초기화하려면 reset API를 사용하세요.

  • defaultValues는 기본적으로 제출 결과에 포함됩니다.

  • MomentLuxon과 같이 프로토타입 메서드를 포함한 커스텀 객체를 defaultValues로 사용하는 것은 권장하지 않습니다.

  • 폼 데이터를 포함하는 다른 방법도 있습니다:

    // 숨겨진 입력 필드 추가
    <input {...register("hidden", { value: "data" })} type="hidden" />
    // onSubmit 시 데이터 포함
    const onSubmit = (data) => {
    const output = {
    ...data,
    others: "others",
    }
    }

values: FieldValues


values prop은 변경에 반응하여 폼 값을 업데이트합니다. 이는 폼이 외부 상태나 서버 데이터에 의해 업데이트되어야 할 때 유용합니다. values prop은 useFormresetOptions: { keepDefaultValues: true }가 설정되지 않는 한, defaultValues prop을 덮어씁니다.

// 기본값을 동기적으로 설정
function App({ values }) {
useForm({
values, // values props가 업데이트될 때 함께 업데이트됨
})
}
function App() {
const values = useFetch("/api")
useForm({
defaultValues: {
firstName: "",
lastName: "",
},
values, // values가 반환되면 업데이트됨
})
}

errors: FieldErrors


errors props는 변경에 반응하고 서버 오류 상태를 업데이트합니다. 이는 폼이 외부 서버에서 반환된 오류로 업데이트되어야 할 때 유용합니다.

function App() {
const { errors, data } = useFetch("/api")
useForm({
errors, // errors가 반환되면 업데이트됩니다.
})
}

resetOptions: KeepStateOptions


이 속성은 값 업데이트 동작과 관련이 있습니다. values 또는 defaultValues가 업데이트되면 내부적으로 reset API가 호출됩니다. values 또는 defaultValues가 비동기적으로 업데이트된 후 원하는 동작을 지정하는 것이 중요합니다. 이 설정 옵션은 reset 메서드의 옵션을 참조합니다.

// 기본적으로 비동기적으로 값 또는 defaultValues가 업데이트되면 폼 값이 초기화됩니다.
useForm({ values })
useForm({ defaultValues: async () => await fetch() })
// 동작을 구성하는 옵션
// 예: 사용자가 상호작용한/변경된 값을 유지하고 사용자 오류를 제거하지 않으려는 경우
useForm({
values,
resetOptions: {
keepDirtyValues: true, // 사용자가 상호작용한 입력값이 유지됩니다.
keepErrors: true, // 값 업데이트 시 입력 오류가 유지됩니다.
},
})

context: object


이 context object는 변경 가능하며, resolver의 두 번째 인자나 Yup 검증의 context 객체에 주입됩니다. CodeSandbox

criteriaMode: firstError | all


  • firstError(기본값)로 설정하면 각 필드의 첫 번째 오류만 수집됩니다.
  • all로 설정하면 각 필드의 모든 오류가 수집됩니다.
CodeSandbox

shouldFocusError: boolean = true


이 값을 true로 설정하면(기본값), 사용자가 유효성 검사에 실패한 폼을 제출할 때 첫 번째 오류가 발생한 필드에 포커스가 자동으로 이동합니다.

NOTE
  • ref가 등록된 필드만 이 기능이 동작합니다. 커스텀으로 등록된 입력 필드는 적용되지 않습니다. 예를 들어: register('test') // 동작하지 않음
  • 포커스 순서는 register 순서를 기반으로 합니다.

delayError: number


이 설정은 지정된 밀리초(ms) 동안 오류 상태를 최종 사용자에게 표시하는 것을 지연시킵니다. 사용자가 오류 입력을 수정하면 오류가 즉시 제거되며, 지연 시간이 적용되지 않습니다. CodeSandbox

shouldUnregister: boolean = false


기본적으로 입력 필드가 제거되더라도 해당 입력 값은 유지됩니다. 하지만 shouldUnregistertrue로 설정하면 언마운트 시 입력 필드를 unregister할 수 있습니다.

  • 이 설정은 전역적으로 적용되며, 하위 레벨의 설정을 덮어씁니다. 개별적인 동작을 원한다면 useForm이 아닌 컴포넌트나 훅 레벨에서 설정해야 합니다.

  • 기본값인 shouldUnregister: false는 언마운트된 필드가 내장된 유효성 검사를 받지 않음을 의미합니다.

  • useForm 레벨에서 shouldUnregistertrue로 설정하면, defaultValues가 제출 결과와 병합되지 않습니다.

  • shouldUnregister: true로 설정하면 폼이 네이티브 폼과 더 유사하게 동작합니다.

    • 폼 값은 입력 필드 자체에 저장됩니다.

    • 입력 필드가 언마운트되면 해당 값도 제거됩니다.

    • 숨겨진 데이터를 저장하려면 hidden 속성을 사용해야 합니다.

    • 제출 데이터에는 등록된 입력 필드만 포함됩니다.

    • 언마운트된 입력 필드는 useForm 또는 useWatchuseEffect에서 알림을 받아야 훅 폼이 DOM에서 해당 입력 필드가 언마운트되었음을 확인할 수 있습니다.

      const NotWork = () => {
      const [show, setShow] = React.useState(false)
      // ❌ 알림을 받지 못함, unregister를 호출해야 함
      return show && <input {...register("test")} />
      }
      const Work = ({ control }) => {
      const { show } = useWatch({ control })
      // ✅ useEffect에서 알림을 받음
      return show && <input {...register("test1")} />
      }
      const App = () => {
      const [show, setShow] = React.useState(false)
      const { control } = useForm({ shouldUnregister: true })
      return (
      <div>
      // ✅ useForm의 useEffect에서 알림을 받음
      {show && <input {...register("test2")} />}
      <NotWork />
      <Work control={control} />
      </div>
      )
      }

shouldUseNativeValidation: boolean = false


이 설정은 브라우저의 기본 유효성 검사를 활성화합니다. 또한 :valid:invalid CSS 선택자를 사용할 수 있게 해 입력 필드 스타일링을 더 쉽게 만듭니다. 클라이언트 측 유효성 검사가 비활성화된 경우에도 이 선택자들을 사용할 수 있습니다.

  • onSubmitonChange 모드에서만 작동합니다. reportValidity 실행 시 오류가 발생한 입력 필드에 포커스가 이동하기 때문입니다.
  • 각 등록된 필드의 유효성 검사 메시지는 기본적으로 표시되기 위해 문자열이어야 합니다.
  • 이 기능은 실제 DOM 참조와 연결된 register API 및 useController/Controller에서만 작동합니다.

예제:


import { useForm } from "react-hook-form"
export default function App() {
const { register, handleSubmit } = useForm({
shouldUseNativeValidation: true,
})
const onSubmit = async (data) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("firstName", {
required: "이름을 입력해주세요.",
})} // 커스텀 메시지
/>
<input type="submit" />
</form>
)
}

disabled: boolean = false


이 설정을 true로 설정하면 폼 전체와 관련된 모든 입력 필드를 비활성화할 수 있습니다.
이 기능은 비동기 작업 중이나 입력 필드가 일시적으로 반응하지 않아야 하는 상황에서 사용자 상호작용을 방지하는 데 유용합니다.

예제:


import { useForm, Controller } from "react-hook-form"
const App = () => {
const [disabled, setDisabled] = useState(false)
const { register, handleSubmit, control } = useForm({
disabled,
})
return (
<form
onSubmit={handleSubmit(async () => {
setDisabled(true)
await sleep(100)
setDisabled(false)
})}
>
<input
type={"checkbox"}
{...register("checkbox")}
data-testid={"checkbox"}
/>
<select {...register("select")} data-testid={"select"} />
<Controller
control={control}
render={({ field }) => <input disabled={field.disabled} />}
name="test"
/>
<button type="submit">Submit</button>
</form>
)
}

resolver: Resolver


이 함수를 사용하면 Yup, Zod, Joi, Vest, Ajv 등 다양한 외부 검증 라이브러리를 활용할 수 있습니다. 목표는 여러분이 선호하는 검증 라이브러리를 원활하게 통합할 수 있도록 하는 것입니다. 라이브러리를 사용하지 않는다면, 폼을 검증하기 위해 직접 로직을 작성할 수도 있습니다.

npm install @hookform/resolvers
Props

이름타입설명
valuesobject이 객체는 폼의 모든 값을 포함합니다.
contextobjectuseForm 설정에서 제공할 수 있는 context 객체입니다. 이 객체는 리렌더링마다 변경 가능한 가변 객체입니다.
options
{
  "criteriaMode": "string",
  "fields": "object",
  "names": "string[]"
}
이 옵션 객체는 useForm에서 검증된 필드, 이름 및 criteriaMode에 대한 정보를 포함합니다.
규칙
  • 스키마 검증은 필드 수준의 오류 보고에 중점을 둡니다. 부모 수준의 오류 검사는 그룹 체크박스와 같은 컴포넌트에 적용되는 직접적인 부모 수준으로 제한됩니다.
  • 이 함수는 캐시됩니다.
  • 사용자 상호작용 중에 입력 필드의 재검증은 한 번에 하나의 필드씩만 발생합니다. 라이브러리 자체가 error 객체를 평가하여 리렌더링을 적절히 트리거합니다.
  • 리졸버는 내장된 검증기(예: required, min 등)와 함께 사용할 수 없습니다.
  • 커스텀 리졸버를 만들 때:
    • valueserrors 속성을 모두 가진 객체를 반환해야 합니다. 기본값은 빈 객체여야 합니다. 예: {}.
    • error 객체의 키는 필드의 name 값과 일치해야 합니다.

예제:


import React from "react"
import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import * as yup from "yup"
type Inputs = {
name: string
age: string
}
const schema = yup
.object()
.shape({
name: yup.string().required(),
age: yup.number().required(),
})
.required()
const App = () => {
const { register, handleSubmit } = useForm<Inputs>({
resolver: yupResolver(schema), // yup, joi 및 커스텀 리졸버 사용 가능
})
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<input {...register("name")} />
<input type="number" {...register("age")} />
<input type="submit" />
</form>
)
}
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
const schema = z.object({
name: z.string(),
age: z.number(),
})
type Schema = z.infer<typeof schema>
const App = () => {
const { register, handleSubmit } = useForm<Schema>({
resolver: zodResolver(schema),
})
const onSubmit = (data: Schema) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
<input {...register("age", { valueAsNumber: true })} type="number" />
<input type="submit" />
</form>
)
}
import React from "react";
import { useForm } from "react-hook-form";
import { joiResolver } from "@hookform/resolvers/joi";
import Joi from "joi";
interface IFormInput {
name: string;
age: number;
}
const schema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required()
});
const App = () => {
const { register, handleSubmit, formState: { errors } } = useForm<IFormInput>({
resolver: joiResolver(schema)
});
const onSubmit = (data: IFormInput) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name"} />
<input type="number" {...register("age"} />
<input type="submit" />
</form>
);
}
import { useForm } from "react-hook-form"
import { ajvResolver } from "@hookform/resolvers/ajv"
// 필수 필드를 구현하려면 `minLength: 1`을 사용해야 함
const schema = {
type: "object",
properties: {
username: {
type: "string",
minLength: 1,
errorMessage: { minLength: "username field is required" },
},
password: {
type: "string",
minLength: 1,
errorMessage: { minLength: "password field is required" },
},
},
required: ["username", "password"],
additionalProperties: false,
}
const App = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: ajvResolver(schema),
})
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("username")} />
{errors.username && <p>{errors.username.message}</p>}
<input {...register("password")} />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">submit</button>
</form>
)
}
import * as React from "react"
import { useForm } from "react-hook-form"
import { vestResolver } from "@hookform/resolvers/vest"
import vest, { test, enforce } from "vest"
const validationSuite = vest.create((data = {}) => {
test("username", "Username is required", () => {
enforce(data.username).isNotEmpty()
})
test("username", "Must be longer than 3 chars", () => {
enforce(data.username).longerThan(3)
})
test("password", "Password is required", () => {
enforce(data.password).isNotEmpty()
})
test("password", "Password must be at least 5 chars", () => {
enforce(data.password).longerThanOrEquals(5)
})
test("password", "Password must contain a digit", () => {
enforce(data.password).matches(/[0-9]/)
})
test("password", "Password must contain a symbol", () => {
enforce(data.password).matches(/[^A-Za-z0-9]/)
})
})
const App = () => {
const { register, handleSubmit } = useForm({
resolver: vestResolver(validationSuite),
})
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("username")} />
<input {...register("password")} />
<input type="submit" />
</form>
)
}
import * as React from "react"
import { useForm } from "react-hook-form"
import * as Joi from "joi"
interface IFormInputs {
username: string
}
const validationSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
})
const App = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<IFormInputs>({
resolver: async (data) => {
const { error, value: values } = validationSchema.validate(data, {
abortEarly: false,
})
return {
values: error ? {} : values,
errors: error
? error.details.reduce((previous, currentError) => {
return {
...previous,
[currentError.path[0]]: currentError,
}
}, {})
: {},
}
},
})
const onSubmit = (data: IFormInputs) => console.log(data)
return (
<div className="App">
<h1>resolver</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<label>Username</label>
<input {...register("username")} />
{errors.username && <p>errors.username.message</p>}
<input type="submit" />
</form>
</div>
)
}

더 많은 정보가 필요하다면 리졸버 문서를 참고하세요.

TIP

다음 코드 스니펫을 통해 스키마를 디버깅할 수 있습니다:

resolver: async (data, context, options) => {
// 여기서 검증 스키마를 디버깅할 수 있습니다
console.log("formData", data)
console.log(
"validation result",
await anyResolver(schema)(data, context, options)
)
return anyResolver(schema)(data, context, options)
}

useForm 반환값과 useEffect 의존성

향후 주요 릴리스에서 useForm 반환값은 성능 최적화를 위해 메모이제이션되고, formState의 변경 사항을 반영할 예정입니다.
이로 인해 useForm의 전체 반환값을 useEffect 의존성 배열에 추가하면 무한 루프가 발생할 수 있습니다.

WARNING

다음 코드는 이러한 상황을 초래할 가능성이 높습니다:

const methods = useForm()
useEffect(() => {
methods.reset({ ... })
}, [methods])

아래와 같이 관련 메서드만 전달하면 이러한 문제를 피할 수 있습니다:

const methods = useForm()
useEffect(() => {
methods.reset({ ... })
}, [methods.reset])
TIP

추천하는 방법은 구조 분해 할당을 통해 메서드를 useEffect 의존성에 전달하는 것입니다.

const { reset } = useForm()
useEffect(() => {
reset({ ... })
}, [reset])

자세한 내용은 이 이슈에서 확인할 수 있습니다

반환 값


아래 목록은 useForm이 반환하는 프로퍼티들을 참조합니다.

여러분의 지원에 감사드립니다

React Hook Form이 프로젝트에서 유용하다면, GitHub에서 스타를 눌러 지원해 주세요.