리액트 훅 폼

리액트 훅 폼 (React Hook Form)

웹 애플리케이션에서 가장 흔하게 만들면서도 가장 복잡한 것 중 하나가 바로 폼이에요. 이 가이드에서는 react-hook-form과 zod를 함께 써서 폼을 어떻게 구성하는지 살펴볼게요. Radix UI 컴포넌트를 조합해 접근성 좋은 폼을 만드는 FormField 컴포넌트도 사용할 거예요.

출처: 문서

본문

폼은 까다로워요. 웹 애플리케이션에서 가장 흔히 만들게 되는 것 중 하나이면서 가장 복잡한 것 중 하나이기도 하죠.

잘 설계된 HTML 폼은 이러해요:

  • 구조가 잘 잡혀 있고 의미적으로 올바르다.
  • (키보드로) 사용하고 탐색하기 쉽다.
  • ARIA 속성과 적절한 label로 접근성이 확보된다.
  • 클라이언트 및 서버 측 검증을 지원한다.
  • 스타일이 잘 정돈되어 있고 앱의 나머지 부분과 일관된다.

이 가이드에서는 react-hook-form과 zod로 폼을 만드는 법을 살펴볼게요. Radix UI 컴포넌트를 사용해 접근성 좋은 폼을 조합하기 위해 <FormField> 컴포넌트를 쓸 거예요.

기능 (Features)

<Form /> 컴포넌트는 react-hook-form 라이브러리를 감싸는 래퍼예요. 몇 가지를 제공하죠:

  • 폼을 만들기 위한 조합(composable) 컴포넌트.
  • 제어되는 폼 필드를 만들기 위한 <FormField /> 컴포넌트.
  • zod를 사용한 폼 검증.
  • 접근성과 오류 메시지 처리.
  • 고유한 ID 생성을 위한 React.useId() 사용.
  • 상태에 따라 폼 필드에 올바른 aria 속성을 적용.
  • 모든 Radix UI 컴포넌트와 함께 동작하도록 설계됨.
  • 원하는 스키마 라이브러리를 가져다 쓸 수 있어요. 우리는 zod를 사용하지만 원하는 거 아무거나 써도 돼요.
  • 마크업과 스타일을 완전히 제어할 수 있어요.

구조 (Anatomy)

<Form>
  <FormField
    control={...}
    name="..."
    render={() => (
      <FormItem>
        <FormLabel />
        <FormControl>
          { /* Your form field */}
        </FormControl>
        <FormDescription />
        <FormMessage />
      </FormItem>
    )}
  />
</Form>

예시 (Example)

const form = useForm()

<FormField
  control={form.control}
  name="username"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Username</FormLabel>
      <FormControl>
        <Input placeholder="shadcn" {...field} />
      </FormControl>
      <FormDescription>This is your public display name.</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

설치하기

CLI Manual

명령어 (Command)

npx shadcn@latest add form

다음 의존성을 설치해 주세요.

npm install @radix-ui/react-label @radix-ui/react-slot react-hook-form @hookform/resolvers zod

다음 코드를 프로젝트에 복사해서 붙여넣어 주세요.

import 경로를 프로젝트 설정에 맞게 바꿔 주세요.

사용하기

폼 스키마 만들기 (Create a form schema)

Zod 스키마를 사용해 폼의 형태를 정의해요. Zod 사용법을 더 자세히 알고 싶다면 Zod 문서를 참고하세요.

"use client"

import { z } from "zod"

const formSchema = z.object({
  username: z.string().min(2).max(50),
})

폼 정의하기 (Define a form)

react-hook-form의 useForm 훅을 사용해 폼을 만들어요.

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
})

export function ProfileForm() {
  // 1. Define your form.
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
    },
  })

  // 2. Define a submit handler.
  function onSubmit(values: z.infer<typeof formSchema>) {
    // Do something with the form values.
    // ✅ This will be type-safe and validated.
    console.log(values)
  }
}

FormField는 제어되는 컴포넌트를 사용하므로 필드에 기본값을 제공해야 해요. 제어되는 컴포넌트에 대해 더 알고 싶다면 React Hook Form 문서를 참고해 주세요.

폼 만들기 (Build your form)

이제 <Form /> 컴포넌트로 우리의 폼을 만들 수 있어요.

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
})

export function ProfileForm() {
  // ...

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

완료 (Done)

끝났어요. 이제 완전히 접근성이 확보되고, 타입 안전하며 클라이언트 측 검증이 되는 폼이 생겼어요.

예시 (Examples)

다른 컴포넌트와 함께 <Form /> 컴포넌트를 어떻게 사용하는지 더 많은 예시를 보려면 다음 링크를 확인해 주세요:

더 알아보기 (Learn more)

  • react-hook-form — 폼 상태와 검증을 관리하는 라이브러리예요.
  • Zod — 타입 안전한 스키마 검증 라이브러리예요.