React

 

 

 

공식 문서

https://www.react-hook-form.com/

 

 

1. React Hook Form 소개

React Hook Form은 리액트에서 폼 관리를 간편하고 효율적으로 할 수 있도록 도와주는 라이브러리입니다. 제어 컴포넌트(Control Components) 대신 비제어 컴포넌트(Uncontrolled Components) 방식을 사용하여 성능을 최적화하며, 간결한 API를 제공합니다.

주요 특징:

  • 성능 최적화: 최소한의 리렌더링으로 빠른 성능 제공
  • 간편한 API: 직관적인 훅 기반 API
  • 유효성 검사: 다양한 유효성 검사 옵션 및 Yup, Zod 등의 라이브러리와 통합
  • 타사 UI 라이브러리와의 호환성: Material-UI, Ant Design 등과 쉽게 통합

 

2. 설치

먼저, React 프로젝트에 React Hook Form을 설치합니다.

 

npm install react-hook-form

 

 

추가 설치 (유효성 검사 라이브러리와 통합 시):


npm install @hookform/resolvers yup

 

 

 

 

3. 기본 사용법

기본적인 폼을 React Hook Form을 사용하여 구현하는 방법을 알아보겠습니다.

예제: 간단한 로그인 폼

 

// src/components/LoginForm.jsx
import React from 'react';
import { useForm } from 'react-hook-form';

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

  const onSubmit = (data) => {
    console.log('로그인 데이터:', data);
    // 로그인 로직 추가
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>이메일</label>
        <input
          type="email"
          {...register('email', { required: '이메일을 입력해주세요.' })}
        />
        {errors.email && <p>{errors.email.message}</p>}
      </div>

      <div>
        <label>비밀번호</label>
        <input
          type="password"
          {...register('password', { required: '비밀번호를 입력해주세요.' })}
        />
        {errors.password && <p>{errors.password.message}</p>}
      </div>

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

export default LoginForm;

 

 

설명:

  • useForm 훅을 사용하여 폼 상태를 관리합니다.
  • register 메서드를 사용하여 각 입력 필드를 등록합니다.
  • handleSubmit 함수로 폼 제출 시 onSubmit 함수를 호출합니다.
  • errors 객체를 통해 유효성 검사 에러를 처리합니다.

 

 

 

4. 유효성 검사

React Hook Form은 기본적인 유효성 검사 옵션을 제공하며, 외부 라이브러리(Yup, Zod 등)와 통합하여 더 복잡한 검증도 가능합니다.

기본 유효성 검사 예제:

 

<input
  type="email"
  {...register('email', {
    required: '이메일을 입력해주세요.',
    pattern: {
      value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
      message: '유효한 이메일 형식이 아닙니다.',
    },
  })}
/>

 

 

Yup을 사용한 스키마 기반 유효성 검사:

  1. Yup 스키마 정의
// src/validationSchema.js
import * as Yup from 'yup';

export const loginSchema = Yup.object().shape({
  email: Yup.string()
    .email('유효한 이메일 주소를 입력하세요.')
    .required('이메일은 필수 항목입니다.'),
  password: Yup.string()
    .min(6, '비밀번호는 최소 6자 이상이어야 합니다.')
    .required('비밀번호는 필수 항목입니다.'),
});

 

 

React Hook Form과 통합

 

// src/components/LoginForm.jsx
import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { loginSchema } from '../validationSchema';

const LoginForm = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: yupResolver(loginSchema),
  });

  const onSubmit = (data) => {
    console.log('로그인 데이터:', data);
    // 로그인 로직 추가
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* 이메일 필드 */}
      <div>
        <label>이메일</label>
        <input type="email" {...register('email')} />
        {errors.email && <p>{errors.email.message}</p>}
      </div>

      {/* 비밀번호 필드 */}
      <div>
        <label>비밀번호</label>
        <input type="password" {...register('password')} />
        {errors.password && <p>{errors.password.message}</p>}
      </div>

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

export default LoginForm;

 

설명:

  • yupResolver를 사용하여 Yup 스키마를 React Hook Form에 통합합니다.
  • 스키마에서 정의한 유효성 검사가 자동으로 적용됩니다.

 

 

5. 폼 제출 처리

폼 제출 시 데이터를 서버로 전송하고, 응답을 처리하는 방법을 알아보겠습니다.

예제: 회원 가입 폼과 서버 통신

 

// src/components/SignupForm.jsx
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

const SignupForm = () => {
  const navigate = useNavigate();
  const [serverError, setServerError] = useState('');

  // Yup 스키마 정의
  const validationSchema = Yup.object().shape({
    username: Yup.string().required('아이디는 필수 항목입니다.'),
    name: Yup.string().required('이름은 필수 항목입니다.'),
    password: Yup.string()
      .min(6, '비밀번호는 최소 6자 이상이어야 합니다.')
      .required('비밀번호는 필수 항목입니다.'),
    confirmPassword: Yup.string()
      .oneOf([Yup.ref('password'), null], '비밀번호가 일치하지 않습니다.')
      .required('비밀번호 확인은 필수 항목입니다.'),
    birthDate: Yup.date().required('생일은 필수 항목입니다.'),
    email: Yup.string()
      .email('유효한 이메일 주소를 입력하세요.')
      .required('이메일은 필수 항목입니다.'),
  });

  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm({
    resolver: yupResolver(validationSchema),
  });

  const onSubmit = async (data) => {
    try {
      setServerError('');
      // API 호출
      const response = await axios.post('/api/users/signup', data);
      console.log('회원 가입 성공:', response.data);
      // 성공 시 로그인 페이지로 이동
      navigate('/login');
    } catch (error) {
      if (axios.isAxiosError(error)) {
        if (error.response && error.response.data.message) {
          setServerError(error.response.data.message);
        } else {
          setServerError('회원 가입 중 오류가 발생했습니다.');
        }
      } else {
        setServerError('알 수 없는 오류가 발생했습니다.');
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* 아이디 필드 */}
      <div>
        <label>아이디</label>
        <input type="text" {...register('username')} />
        {errors.username && <p>{errors.username.message}</p>}
      </div>

      {/* 이름 필드 */}
      <div>
        <label>이름</label>
        <input type="text" {...register('name')} />
        {errors.name && <p>{errors.name.message}</p>}
      </div>

      {/* 비밀번호 필드 */}
      <div>
        <label>비밀번호</label>
        <input type="password" {...register('password')} />
        {errors.password && <p>{errors.password.message}</p>}
      </div>

      {/* 비밀번호 확인 필드 */}
      <div>
        <label>비밀번호 확인</label>
        <input type="password" {...register('confirmPassword')} />
        {errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
      </div>

      {/* 생일 필드 */}
      <div>
        <label>생일</label>
        <input type="date" {...register('birthDate')} />
        {errors.birthDate && <p>{errors.birthDate.message}</p>}
      </div>

      {/* 이메일 필드 */}
      <div>
        <label>이메일</label>
        <input type="email" {...register('email')} />
        {errors.email && <p>{errors.email.message}</p>}
      </div>

      {/* 서버 에러 메시지 */}
      {serverError && <p style={{ color: 'red' }}>{serverError}</p>}

      {/* 제출 버튼 */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '회원 가입 중...' : '회원 가입'}
      </button>
    </form>
  );
};

export default SignupForm;

 

 

 

설명:

  • 폼 제출 처리: handleSubmit를 통해 onSubmit 함수를 호출합니다.
  • API 호출: axios를 사용하여 서버에 폼 데이터를 전송합니다.
  • 에러 처리: 서버에서 반환된 에러 메시지를 serverError 상태에 저장하여 사용자에게 표시합니다.
  • 로딩 상태: isSubmitting을 사용하여 폼 제출 중임을 표시하고, 버튼을 비활성화합니다.

 

 

 

6. 에러 처리

React Hook Form에서는 클라이언트 측 유효성 검사뿐만 아니라 서버 측 에러도 효과적으로 처리할 수 있습니다.

서버 측 에러 처리 예제:

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

const onSubmit = async (data) => {
  try {
    // API 호출
    const response = await axios.post('/api/users/signup', data);
    // 성공 처리
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.response && error.response.data.fieldErrors) {
        // 필드별 에러 설정
        Object.entries(error.response.data.fieldErrors).forEach(([field, message]) => {
          setError(field, { type: 'server', message });
        });
      }
      if (error.response && error.response.data.message) {
        // 일반 에러 메시지 설정
        setServerError(error.response.data.message);
      }
    }
  }
};

 

 

설명:

  • setError를 사용하여 특정 필드에 서버 측 에러 메시지를 설정할 수 있습니다.
  • 일반 에러 메시지는 별도의 상태(serverError)에 저장하여 표시할 수 있습니다.

 

 

7. 동적 필드 관리

React Hook Form에서는 useFieldArray 훅을 사용하여 동적으로 필드를 추가하거나 제거할 수 있습니다.

예제: 동적으로 이메일 필드 추가

 

// src/components/DynamicEmailForm.jsx
import React from 'react';
import { useForm, useFieldArray } from 'react-hook-form';

const DynamicEmailForm = () => {
  const { register, control, handleSubmit, formState: { errors } } = useForm({
    defaultValues: {
      emails: [{ email: '' }],
    },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'emails',
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <label>이메일 {index + 1}</label>
          <input
            type="email"
            {...register(`emails.${index}.email`, { required: '이메일을 입력해주세요.' })}
          />
          {errors.emails?.[index]?.email && (
            <p>{errors.emails[index].email.message}</p>
          )}
          <button type="button" onClick={() => remove(index)}>
            삭제
          </button>
        </div>
      ))}

      <button type="button" onClick={() => append({ email: '' })}>
        이메일 추가
      </button>

      <button type="submit">제출</button>
    </form>
  );
};

export default DynamicEmailForm;

 

 

설명:

  • useFieldArray를 사용하여 동적 필드를 관리합니다.
  • append 함수로 새로운 필드를 추가하고, remove 함수로 필드를 제거할 수 있습니다.
  • 각 필드는 고유한 key를 가져야 합니다 (field.id 사용).

 

 

8. 커스텀 컴포넌트와 통합

타사 UI 라이브러리나 커스텀 컴포넌트와 React Hook Form을 통합하는 방법을 알아보겠습니다.

예제: Material-UI의 TextField와 통합

  1. 설치

 

npm install @mui/material @emotion/react @emotion/styled

 

통합 예제

// src/components/MaterialUITextFieldForm.jsx
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';

const MaterialUITextFieldForm = () => {
  const { control, handleSubmit, formState: { errors } } = useForm();

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* 이메일 필드 */}
      <Controller
        name="email"
        control={control}
        defaultValue=""
        rules={{
          required: '이메일을 입력해주세요.',
          pattern: {
            value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
            message: '유효한 이메일 형식이 아닙니다.',
          },
        }}
        render={({ field }) => (
          <TextField
            {...field}
            label="이메일"
            variant="outlined"
            error={!!errors.email}
            helperText={errors.email ? errors.email.message : ''}
            fullWidth
            margin="normal"
          />
        )}
      />

      {/* 비밀번호 필드 */}
      <Controller
        name="password"
        control={control}
        defaultValue=""
        rules={{
          required: '비밀번호를 입력해주세요.',
          minLength: {
            value: 6,
            message: '비밀번호는 최소 6자 이상이어야 합니다.',
          },
        }}
        render={({ field }) => (
          <TextField
            {...field}
            type="password"
            label="비밀번호"
            variant="outlined"
            error={!!errors.password}
            helperText={errors.password ? errors.password.message : ''}
            fullWidth
            margin="normal"
          />
        )}
      />

      <Button type="submit" variant="contained" color="primary">
        제출
      </Button>
    </form>
  );
};

export default MaterialUITextFieldForm;

 

설명:

  • Controller 컴포넌트를 사용하여 비제어 컴포넌트(예: Material-UI의 TextField)를 React Hook Form과 통합합니다.
  • rules를 사용하여 유효성 검사를 설정합니다.
  • render prop을 통해 커스텀 컴포넌트를 렌더링하면서 폼 필드를 연결합니다.

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

발난반정( 撥亂反正 ). 어지러운 세상을 바르게 다스려 바른 세상으로 돌린다. 조선왕조 때 인조반정(仁祖反正)은 여기에서 나온 말이다. -잡편

댓글 ( 0)

댓글 남기기

작성