Skip to Content
PatternsForm Validation

Form Validation

폼 검증 및 에러 처리 구현 패턴입니다.

개요

Form Validation 패턴은 사용자 입력을 검증하고, 명확한 피드백을 제공하며, 접근 가능한 에러 메시지를 표시하는 검증된 구현 방법을 제공합니다. 실시간 검증, 서버 검증 통합, 다국어 에러 메시지, 복잡한 비즈니스 규칙 검증 등 프로덕션 환경에서 필요한 모든 폼 검증 시나리오를 다룹니다.

사용 사례:

  • 회원가입/로그인 폼
  • 주문/결제 폼
  • 프로필 업데이트 폼
  • 데이터 입력 폼
  • 복잡한 다단계 폼

사용하지 말아야 할 때:

  • 검색창 (실시간 제안만 필요)
  • 단순 필터 (즉시 적용)
  • 읽기 전용 폼

Best Practices

로그인 폼 (React Hook Form + Zod)

Feature 기반 구조

src/features/auth/ ├── api/ │ └── login.ts # API 호출 함수 ├── hooks/ │ └── mutations/ │ └── use-login.ts # React Query Mutation ├── schemas/ │ └── form.ts # 폼 검증 스키마 └── components/ └── login-form.tsx

1. Zod 스키마 정의 (schemas/form.ts)

폼 검증 및 API 요청/응답 스키마를 정의합니다.

import { z } from "zod" // 폼 검증 스키마 export const LoginFormSchema = z.object({ email: z .string() .min(1, "이메일을 입력해주세요") .email("올바른 이메일 형식이 아닙니다") .max(255, "이메일은 255자를 초과할 수 없습니다") .toLowerCase() .trim(), password: z .string() .min(1, "비밀번호를 입력해주세요") .min(6, "비밀번호는 최소 6자 이상이어야 합니다") .max(100, "비밀번호는 100자를 초과할 수 없습니다") .regex( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, "비밀번호는 대문자, 소문자, 숫자를 각각 최소 1개 이상 포함해야 합니다", ) .regex( /^[A-Za-z\d@$!%*?&#^()_+\-=\[\]{};':"\\|,.<>\/~`]+$/, "비밀번호에 허용되지 않는 특수문자가 포함되어 있습니다", ), }) export type LoginFormValues = z.infer<typeof LoginFormSchema> // API 응답 스키마 export const LoginResponseSchema = z.object({ token: z.string(), user: z.object({ id: z.number(), email: z.string(), name: z.string(), }), }) export type LoginResponse = z.infer<typeof LoginResponseSchema>

2. API 함수 (api/login.ts)

요청/응답 데이터를 Zod로 검증하여 타입 안전성을 보장합니다.

import { api } from "@/lib/axios" import { LoginFormSchema, LoginFormValues, LoginResponseSchema, LoginResponse, } from "../schemas/form" export async function login(data: LoginFormValues): Promise<LoginResponse> { // 요청 데이터 검증 const validatedData = LoginFormSchema.parse(data) // API 호출 const response = await api.$post<LoginResponse>("/auth/login", validatedData) // 응답 데이터 검증 return LoginResponseSchema.parse(response) }

검증 흐름:

  1. 요청 검증: LoginFormSchema.parse()로 전송 데이터 검증
  2. API 호출: 검증된 데이터로 서버 요청
  3. 응답 검증: LoginResponseSchema.parse()로 응답 데이터 검증
  4. 타입 안전: 검증 실패 시 ZodError 발생

3. React Query Mutation Hook (hooks/mutations/use-login.ts)

import { useMutation } from "@tanstack/react-query" import { login } from "../../api/login" import type { LoginFormValues } from "../../schemas/form" export function useLogin() { return useMutation({ mutationFn: login, onError: (error) => { console.error("로그인 실패:", error) }, }) }

4. 로그인 폼 컴포넌트 (components/login-form.tsx)

"use client" import { useCallback } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { useRouter } from "next/navigation" import { Input, FormItem } from "@vortex/ui-foundation" import { LoginFormSchema, LoginFormValues } from "../schemas/form" import { useLogin } from "../hooks/mutations/use-login" import { useAuthStore } from "@/stores/auth" export function UserLoginForm() { const router = useRouter() const { login: authLogin } = useAuthStore() const { mutateAsync: login, isPending, error } = useLogin() const { register, handleSubmit, getValues, formState: { errors }, } = useForm<LoginFormValues>({ resolver: zodResolver(LoginFormSchema), defaultValues: { email: "", password: "", }, }) const onSubmit = useCallback( async (data: LoginFormValues) => { try { const response = await login(data) authLogin(response.token, response.user) router.push("/dashboard") } catch (error) { // 에러는 useLogin의 onError에서 처리됨 console.error("로그인 실패:", error) } }, [login, authLogin, router], ) return ( <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4 max-w-md" > <div> <FormItem label="이메일" value={getValues("email") || ""} rules={[() => errors.email?.message || true]} > <Input {...register("email")} type="email" placeholder="이메일을 입력하세요" className="w-full" aria-invalid={errors.email ? "true" : "false"} /> </FormItem> </div> <div> <FormItem label="비밀번호" value={getValues("password") || ""} rules={[() => errors.password?.message || true]} > <Input {...register("password")} type="password" placeholder="비밀번호를 입력하세요" className="w-full" aria-invalid={errors.password ? "true" : "false"} /> </FormItem> </div> {error && ( <p className="text-red-500 text-sm"> {error.message || "로그인에 실패했습니다."} </p> )} <button type="submit" disabled={isPending} className="bg-blue-500 text-white p-3 rounded hover:bg-blue-600 disabled:opacity-50" > {isPending ? "로그인 중..." : "로그인"} </button> </form> ) }

주요 패턴 정리

1. 계층 분리

  • Schemas: Zod 스키마로 검증 로직 정의
  • API: HTTP 요청 로직
  • Hooks: React Query Mutation으로 상태 관리
  • Components: UI 렌더링 및 폼 제출

2. React Hook Form + Zod

  • zodResolver로 Zod 스키마 연결
  • formState.errors로 클라이언트 검증 에러
  • 타입 안전한 폼 데이터 (LoginFormValues)

3. React Query Mutation

  • mutate로 비동기 로그인 처리
  • isPending으로 로딩 상태 관리
  • onSuccess 콜백으로 성공 후 처리 (인증 상태 저장, 리다이렉트)
  • error로 서버 에러 표시

4. Vortex UI FormItem

  • rules prop으로 에러 메시지 전달
  • valuegetValues로 실시간 값 추적
  • 일관된 폼 필드 UI

5. 접근성

  • aria-invalid로 에러 상태 표시
  • disabled 상태로 중복 제출 방지
  • 명확한 에러 메시지 제공

관련 패턴

Last updated on