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.tsx1. 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)
}검증 흐름:
- 요청 검증:
LoginFormSchema.parse()로 전송 데이터 검증 - API 호출: 검증된 데이터로 서버 요청
- 응답 검증:
LoginResponseSchema.parse()로 응답 데이터 검증 - 타입 안전: 검증 실패 시
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
rulesprop으로 에러 메시지 전달value와getValues로 실시간 값 추적- 일관된 폼 필드 UI
5. 접근성
aria-invalid로 에러 상태 표시disabled상태로 중복 제출 방지- 명확한 에러 메시지 제공
관련 패턴
- Authentication - 인증 폼 구현
- Error Handling - 폼 에러 처리
- Accessibility Patterns - 접근 가능한 폼
Last updated on