☁️ goormTIL | Next.js #65

매루·2025년 12월 10일

goormTIL

목록 보기
63/67
post-thumbnail

📅 2025-12-10

➡️ React Hook Form, Zod에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리


🔎 학습 리마인드

📌 React Hook Form (RHF)

💡 제어 컴포넌트와 비제어 컴포넌트

제어 컴포넌트

  • 입력값이 변경될 때마다 setState 호출 → 리렌더링 발생
  • 입력값을 항상 React state로 관리
  • 실시간 반응이 필요한 UI에 적합 (유효성 검사, 버튼 disabled, 입력값 수정 제어 등)
import { useState } from "react";

function ControlledInput() {
  const [value, setValue] = useState("");

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  return (
    <div>
      <input value={value} onChange={handleChange} />
      <p>현재 값: {value}</p>
    </div>
  );
}

비제어 컴포넌트

  • 입력 중에는 React가 관여하지 않음 (DOM이 값을 보관)
  • 특정 시점에만 값이 필요할 때 적합 (ex. 제출)
  • 입력 시 리렌더링이 일어나지 않아 성능상 유리
import { useRef } from "react";

function UncontrolledInput() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  const handleSubmit = () => {
    if (!inputRef.current) return;
    alert(`입력한 값: ${inputRef.current.value}`);
  };

  return (
    <div>
      <input ref={inputRef} />
      <button type="button" onClick={handleSubmit}>
        값 확인
      </button>
    </div>
  );
}

비교

기능제어 컴포넌트비제어 컴포넌트
일회성 정보 검색 (예: 제출)OO
제출 시 값 검증OO
실시간으로 필드 값의 유효성 검사OX
조건부로 제출 버튼 비활성화 (disabled)OX
실시간으로 입력 형식 적용하기 (숫자만 가능하게 등)OX
동적 입력OX
  • 실시간 피드백, 조건부 렌더링 ⇒ 제어 컴포넌트가 유리
  • 제출 시점에 한 번 값만 필요, 성능이 중요 ⇒ 비제어 컴포넌트가 유리

💡 React Hook Form

React Hook Form - performant, flexible and extensible form library

  • React 애플리케이션에서 폼을 효율적으로 관리하고 검증하기 위한 비제어 컴포넌트 기반의 라이브러리

특징

  • 리렌더링 최소화
    • 각 필드는 가능하면 독립적으로 동작하도록 설계되어 있어 큰 폼에서도 성능이 좋음
  • 적은 코드량
    • register, handleSubmit 같은 훅만으로도 꽤 복잡한 폼을 구현할 수 있음
  • 검증 라이브러리 연동 용이
    • Zod와 쉽게 통합하여 타입 기반 검증을 구현할 수 있음

설치

yarn add react-hook-form
#또는
npm install react-hook-form
  • 기존 방식
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    
    return (
      <form>
        <input
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <input
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </form>
    );
  • React Hook Form 방식
    import { useForm } from "react-hook-form";
    
    function LoginForm() {
      const { register, handleSubmit } = useForm();
    
      const onSubmit = (data) => console.log(data);
    
      return (
        <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("email")} />
          <input {...register("password")} />
          <button type="submit">로그인</button>
        </form>
      );
    }

💡 React Hook Form 옵션

  • useFrom 옵션
    const {
      register,
      handleSubmit,
      formState: { errors, isValid, isDirty, isSubmitting },
      watch,
      reset,
      control,
    } = useForm({
      mode: "onChange",
      defaultValues: {
        email: "",
        password: "",
      },
    });
    1. mode

      • 유효성 검사 시점 설정
      mode설명
      onSubmit (기본값)제출할 때만 검사
      onChange입력이 변경될 때마다 검사
      onBlur포커스 벗어날 때 검사
      onTouched한 번이라도 건드린 후 검사
      all모든 상황에서 검사
    2. defaultValues

      • 폼의 초기 입력값
      • API 호출 후 데이터를 채워 넣어 초기값 설정하는 데 유용

  • register 옵션 (Validation)
    <input
      {...register("email", {
        required: "이메일은 필수입니다.",
        minLength: { value: 3, message: "3자 이상 입력해주세요" },
        maxLength: { value: 30, message: "30자 이하로 입력해주세요" },
        pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "이메일 형식이 아닙니다" },
        validate: (value) => value !== "test" || "test는 사용할 수 없습니다",
      })}
    />
    1. required

      • 필수 입력 여부 판단
      • 문자열을 넣으면 에러 메시지로 자동 사용
      required: "이 필드는 필수입니다"
    2. minLength / maxLength

      • 문자열 길이 검증
      minLength: { value: 3, message: "3자 이상 입력" }
      maxLength: { value: 20, message: "20자 이하로 입력" }
    3. pattern

      • 정규식 기반 유효성 검사
      pattern: {
        value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
        message: "이메일 형식이 올바르지 않습니다.",
      }
    4. validate

      • 직접 커스텀 검사 로직 작성 가능
      • true 반환 → 통과
      • false / string 반환 → 실패(문자열이면 에러 메시지)
      validate: (v) => v === "admin" ? "admin은 불가" : true

  • formState 속성

    const {
      formState: { errors, isDirty, isValid, isSubmitting },
    } = useForm({ mode: "onChange" });
    • 폼 상태 정보
    1. errors

      • 각 필드의 검증 에러 정보 객체
    2. isDirty

      • 입력값이 변경됐는지 여부
    3. isValid

      • 모든 필드가 유효성 검사를 통과했는지 여부
    4. isSubmitting

      • 현재 제출 중인지 여부

💡 예제

import { useForm } from "react-hook-form";

type LoginFormValues = {
  email: string;
  password: string;
};

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<LoginFormValues>();

  const onSubmit = (data: LoginFormValues) => {
    console.log("폼 데이터:", data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* 이메일 */}
      <div>
        <input
          type="email"
          placeholder="이메일"
          {...register("email", { required: "이메일은 필수입니다." })}
        />
        {errors.email && <p>{errors.email.message}</p>}
      </div>

      {/* 비밀번호 */}
      <div>
        <input
          type="password"
          placeholder="비밀번호"
          {...register("password", {
            required: "비밀번호는 필수입니다.",
            minLength: {
              value: 6,
              message: "비밀번호는 최소 6자 이상이어야 합니다.",
            },
          })}
        />
        {errors.password && <p>{errors.password.message}</p>}
      </div>

      <button type="submit">로그인</button>
    </form>
  );
}

export default LoginForm;

📌 Zod

🔗 Intro | Zod

  • 스키마 기반 타입 검증 라이브러리
  • 입력 데이터의 형식 + 규칙 + 에러 메시지를 한 곳에서 선언
  • React Hook Form과 함께 쓰면 아래를 한 번에 해결할 수 있음
    • 폼 구조 정의
    • 검증 규칙
    • 타입 추론

설치

npm install zod @hookform/resolvers
# 또는
yarn add zod @hookform/resolvers

💡 예제

Zod 스키마 정의

const loginSchema = z.object({
  email: z
    .string()
    .min(1, "이메일을 입력해주세요.")
    .email("이메일 형식이 올바르지 않습니다."),
  password: z
    .string()
    .min(8, "비밀번호는 최소 8자 이상이어야 합니다."),
});
  • loginSchema: 폼 필드를 Zod로 정의하고, 동시에 검증 규칙과 메시지를 함께 선언

스키마 기반 타입 생성

type LoginFormValues = z.infer<typeof loginSchema>;
  • z.infer<typeof loginSchema>: 스키마로부터 폼 값 타입을 자동으로 추출

React Hook Form과 연동

const { register, handleSubmit, formState: { errors } } =
  useForm<LoginFormValues>({
    resolver: zodResolver(loginSchema),
});
  • resolver: zodResolver(loginSchema): React Hook Form이 Zod 검증을 사용하도록 설정
  • errors.email?.message: Zod 메시지를 그대로 UI에서 사용

💡 비동기 검증

  • Zod는 기본적으로 동기 스타일, 하지만 실제 앱에서는 “이메일 중복 확인”처럼 비동기 검증이 필요
  • 추천 방식
    • 형식, 길이, 정규식 → Zod
    • 중복 확인 같은 서버 검증 → onSubmit에서 처리

예시

const onSubmit = async (values: SignUpFormValues) => {
  const isDuplicated = await checkEmailDuplicated(values.email);

  if (isDuplicated) {
    // 서버 응답 기반으로 RHF 에러 수동 등록
    setError("email", {
      type: "server",
      message: "이미 사용 중인 이메일입니다.",
    });
    return;
  }

  console.log("회원가입 진행:", values);
};
  • 형식/길이 검증 → Zod 스키마
  • 중복 여부 검증 → onSubmit에서 서버 호출 후 setError로 RHF 에러 등록

💡 커스텀 에러 메시지 처리

  • Zod는 정규식 / 길이 / 패턴 검증에 대해 명확한 메시지를 함께 선언할 수 있음.

예시

const passwordSchema = z.object({
  password: z
    .string()
    .min(8, "비밀번호는 8자 이상이어야 합니다.")
    .regex(/[0-9]/, "숫자를 최소 1개 포함해야 합니다.")
    .regex(/[A-Z]/, "대문자를 최소 1개 포함해야 합니다."),
});
return (
  <form onSubmit={handleSubmit(onSubmit)}>
    <input
      type="password"
      placeholder="새 비밀번호"
      {...register("password")}
    />
    {errors.password && <p>{errors.password.message}</p>}

    <button type="submit">저장</button>
  </form>
);
  • 검증 규칙과 메시지가 스키마에 집중
  • UI는 에러 메시지 표시만 담당 → 코드 매우 깔끔
  • 스키마를 재활용하면, 서버·클라이언트에서 같은 규칙을 공유하는 것도 가능

0개의 댓글