TypeScript

회원가입 폼 쉽게 만들기 : react-hook-form

코샵 2024. 11. 22. 10:56
반응형

소개

폼(Form) 관리는 웹 개발에서 매우 중요한 부분입니다. React에서 react-hook-form을 사용하면 복잡한 폼 상태 관리와 유효성 검사를 쉽게 구현할 수 있습니다. 이번 글에서는 회원가입 폼을 예제로 react-hook-form의 사용법을 자세히 알아보겠습니다.

react-hook-form 설치

npm install react-hook-form
// or
yarn add react-hook-form

기본 회원가입 폼 구현

import { useForm } from 'react-hook-form';
import { useState } from 'react';

// 폼 데이터 타입 정의
interface SignUpFormData {
  email: string;
  password: string;
  passwordConfirm: string;
  name: string;
  age: number;
  terms: boolean;
}

export default function SignUpForm() {
  const { 
    register, 
    handleSubmit,
    watch,
    formState: { errors }
  } = useForm<SignUpFormData>();

  const onSubmit = (data: SignUpFormData) => {
    console.log('폼 제출 데이터:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow">
      <h2 className="text-2xl font-bold mb-6">회원가입</h2>

      {/* 이메일 입력 필드 */}
      <div className="mb-4">
        <label className="block mb-2">이메일</label>
        <input
          {...register('email', {
            required: '이메일은 필수입니다',
            pattern: {
              value: /\S+@\S+\.\S+/,
              message: '이메일 형식이 올바르지 않습니다'
            }
          })}
          className="w-full p-2 border rounded"
        />
        {errors.email && <p className="text-red-500 mt-1">{errors.email.message}</p>}
      </div>

      {/* 비밀번호 입력 필드 */}
      <div className="mb-4">
        <label className="block mb-2">비밀번호</label>
        <input
          type="password"
          {...register('password', {
            required: '비밀번호는 필수입니다',
            minLength: {
              value: 8,
              message: '비밀번호는 8자 이상이어야 합니다'
            },
            pattern: {
              value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
              message: '비밀번호는 문자, 숫자, 특수문자를 포함해야 합니다'
            }
          })}
          className="w-full p-2 border rounded"
        />
        {errors.password && <p className="text-red-500 mt-1">{errors.password.message}</p>}
      </div>

      {/* 비밀번호 확인 필드 */}
      <div className="mb-4">
        <label className="block mb-2">비밀번호 확인</label>
        <input
          type="password"
          {...register('passwordConfirm', {
            required: '비밀번호 확인은 필수입니다',
            validate: (value) => 
              value === watch('password') || '비밀번호가 일치하지 않습니다'
          })}
          className="w-full p-2 border rounded"
        />
        {errors.passwordConfirm && (
          <p className="text-red-500 mt-1">{errors.passwordConfirm.message}</p>
        )}
      </div>

      {/* 이름 입력 필드 */}
      <div className="mb-4">
        <label className="block mb-2">이름</label>
        <input
          {...register('name', {
            required: '이름은 필수입니다',
            minLength: {
              value: 2,
              message: '이름은 2자 이상이어야 합니다'
            }
          })}
          className="w-full p-2 border rounded"
        />
        {errors.name && <p className="text-red-500 mt-1">{errors.name.message}</p>}
      </div>

      {/* 나이 입력 필드 */}
      <div className="mb-4">
        <label className="block mb-2">나이</label>
        <input
          type="number"
          {...register('age', {
            required: '나이는 필수입니다',
            min: {
              value: 14,
              message: '14세 이상만 가입 가능합니다'
            }
          })}
          className="w-full p-2 border rounded"
        />
        {errors.age && <p className="text-red-500 mt-1">{errors.age.message}</p>}
      </div>

      {/* 약관 동의 체크박스 */}
      <div className="mb-6">
        <label className="flex items-center">
          <input
            type="checkbox"
            {...register('terms', {
              required: '약관 동의는 필수입니다'
            })}
            className="mr-2"
          />
          <span>이용약관에 동의합니다</span>
        </label>
        {errors.terms && <p className="text-red-500 mt-1">{errors.terms.message}</p>}
      </div>

      {/* 제출 버튼 */}
      <button
        type="submit"
        className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
      >
        가입하기
      </button>
    </form>
  );
}

커스텀 유효성 검사 추가

const validatePassword = (password: string) => {
  const hasLower = /[a-z]/.test(password);
  const hasUpper = /[A-Z]/.test(password);
  const hasNumber = /\d/.test(password);
  const hasSpecial = /[@$!%*#?&]/.test(password);

  if (!hasLower) return '소문자를 포함해야 합니다';
  if (!hasUpper) return '대문자를 포함해야 합니다';
  if (!hasNumber) return '숫자를 포함해야 합니다';
  if (!hasSpecial) return '특수문자를 포함해야 합니다';

  return true;
};

// 비밀번호 필드에 적용
{...register('password', {
  validate: validatePassword
})}

비동기 유효성 검사 추가

// 이메일 중복 체크 함수
const checkEmailDuplicate = async (email: string) => {
  try {
    const response = await fetch(`/api/check-email?email=${email}`);
    const data = await response.json();
    return !data.exists || '이미 사용중인 이메일입니다';
  } catch (error) {
    console.error('이메일 체크 오류:', error);
    return '이메일 확인에 실패했습니다';
  }
};

// 이메일 필드에 적용
{...register('email', {
  validate: {
    duplicate: checkEmailDuplicate
  }
})}

폼 데이터 처리와 제출

const onSubmit = async (data: SignUpFormData) => {
  try {
    const response = await fetch('/api/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error('회원가입 실패');
    }

    // 성공 처리
    alert('회원가입이 완료되었습니다!');
    // 로그인 페이지로 이동
    router.push('/login');
  } catch (error) {
    console.error('회원가입 오류:', error);
    alert('회원가입 중 오류가 발생했습니다.');
  }
};

폼 상태 관리

const {
  formState: { isSubmitting, isDirty, isValid }
} = useForm<SignUpFormData>({
  mode: 'onChange' // 실시간 유효성 검사
});

// 제출 버튼 상태 처리
<button
  type="submit"
  disabled={!isDirty || !isValid || isSubmitting}
  className={`w-full p-2 rounded
    ${!isDirty || !isValid || isSubmitting
      ? 'bg-gray-300 cursor-not-allowed'
      : 'bg-blue-500 hover:bg-blue-600'}`}
>
  {isSubmitting ? '처리중...' : '가입하기'}
</button>

에러 메시지 커스터마이징

const errorMessages = {
  email: {
    required: '이메일을 입력해주세요',
    pattern: '올바른 이메일 형식이 아닙니다'
  },
  password: {
    required: '비밀번호를 입력해주세요',
    minLength: '비밀번호는 8자 이상이어야 합니다'
  }
};

// 에러 메시지 표시 컴포넌트
const ErrorMessage = ({ error }: { error: any }) => {
  if (!error) return null;

  return (
    <p className="text-red-500 text-sm mt-1">
      {error.message}
    </p>
  );
};

결론

react-hook-form을 사용하면 복잡한 폼 관리와 유효성 검사를 쉽게 구현할 수 있습니다. 특히 타입스크립트와 함께 사용하면 더욱 안전하고 견고한 폼을 만들 수 있습니다. 실시간 유효성 검사, 커스텀 검증 로직, 비동기 검증 등 다양한 기능을 활용하여 사용자 친화적인 회원가입 폼을 구현해보세요.