UX Engineer 이야기

React-hook-form + Yup 라이브러리

seonju.lee 2024. 10. 11. 07:50

들어가며

최근 프로젝트에서 폼 요소로 구성된 문의하기 팝업을 구현하는 과정에서  react-hook-form을 사용하게 되었습니다. 작업이 끝나고 코드 리팩토링 작업을 하면서 유효성 및 에러 처리를 좀 더 편리하게 도와주는 라이브러리 중 yup 대해 소개하려고 합니다.

Yup 소개하기

yup는 데이터의 유효성 검증을 객체 스키마로 선언하여 검증하는 라이브러리입니다. yup 설치 시 react-hook-form을 연결해 주는 라이브러리도 함께 설치해줘야 합니다.

* 스키마란?
데이터베이스의 구조와 제약 조건에 관한 전반적인 명세를 기술한 메타데이터의 집합을 의미. 
// 1. 설치
npm install yup @hookform/resolvers
// or 
yarn add yup @hookform/resolvers


// 2. 선언
import { useForm } from 'react-hook-form';
import * as Yup from 'yup';

const resolver = useYupValidationResolver(validationSchema)
const { handleSubmit, register } = useForm({ resolver })
 
yup를 사용하려면 스키마  즉, 구문 작업과 입력값에 대한 테스트 코드에 대한 정의를 객체 정의를 해야 합니다. 스키마는 yup.object() 함수를 사용하여 정의할 수 있습니다.
 

object 스키마 정의

yup은 typescript를 지원하여 각 필드의 타입을 지정할 수 있고, 메서드의 나열형식으로 간결하고 확장할 수 있게 작성할 수 있습니다. 
 

타입 정의하기

  • string() / number() / date() / object() /boolean() / array()

데이터 구성하기

  • required() / optional() : 해당 필드가 반드시 존재해야 하는 필수 요소 인지 / 선택 요소인지 체크
  • nullable() : 필드의 데이터가 null 값을 허용하는지 체크
  • default() : 필드의 출력값이 undefined인 경우 기본값 설정
  • min / max : 필드의 최소 / 최대값을 설정

데이터 변환하기

  • trim() : 텍스트 필드의 양쪽 공백을 제거
  • lowercase() : 텍스트 필드의 경우, 영문을 모두 소문자로 변환
  • uppercase() : 텍스트 필드의 경우, 영문을 모두 대문자로 변환
  • ensure() : 텍스트 필드의 경우, undefined or null인 경우 빈 문자열로 변환
  • calmelCase() / constantCase() : object 필드의 경우, 객체의 key 값을 camelCase / CONSTANT_CASE 형식으로 변환 

데이터 검증하기

  • match() : react-hook-form의 rules에 들어가는 값으로, 정규식 패턴을 통해 값의 형식을 검사
  • email() / url() / uuid () : 정규식을 선언하지 않고, 내장된 정규식을 통해 유효성을 검증
  • positive() / negative() / interger() / moreThan() / lessThan() : 숫자 필드는 양수 / 음수 / 정수 /  최댓값 / 최솟값을 검증

데이터 에러 처리

선언된 데이터 구성 코드의 메서드의 첫 번째(입력 인자가 없는 경우) / 두 번째 인자에 커스텀 에러 메시지를 설정할 수 있습니다. 
required('이름은 필수 요소입니다.'),
min(3, '최솟값은 3입니다.')
matches('/^[0-9]+$/', '나이는 숫자만 입력 가능합니다.')
email('올바른 이메일 주소를 입력해주세요.')
positive('양수만 입력 가능합니다.')

Yup 적용하기

  • yup 적용 전
컴포넌트에 필요한 UI 정보와 유효성 검사 관련 코드를 한곳에서 관리
// 스키마 정의
const formArray = [
    {
      name: "name",
      label: "이름",
      type: "text",
      placeholder: "이름을 입력해 주세요.",
      required: {
        value: true,
        message: "필수 입력 항목입니다."
      },
      regex: {
        value: (/^[a-zA-Zㄱ-ㅎㅏ-ㅣ가-힣]{1,30}$/),
        message: "한글, 영어만 입력할 수 있습니다.",
      },
      maxLength: {
        value: 30,
        message: "최대 30자 이내로 작성할 수 있습니다."
     }
    },
    {
      name: "phoneNumber",
      label: "전화번호",
      type: "text",
      placeholder: "전화번호를 입력해 주세요.",
      regex: {
        value: (/^\d{1,11}$/g),
        message: "숫자만 입력할 수 있습니다."
      }, 
       maxLength: {
        value: 11,
        message: "최대 11자 이내로 작성할 수 있습니다."
     }
    },
    {
      name: "email",
      label: "이메일",
      type: "text",
      required: {
        value: true,
        message: "필수 입력 항목입니다."
      },
      placeholder: "이메일 주소를 입력해 주세요.",
      regex: {
        value: (/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g),
        message: "올바른 값을 입력해주세요."
      },
    },
    {
      name: "url",
      label: "관련 URL",
      type: "text",
      placeholder: "URL을 입력하세요."
    },
    {
      name: "message",
      label: "기타 메시지",
      type: "textarea",
      placeholder: "기타 메시지를 입력해 주세요.",
      regex: {
        value: (/^(?=.*[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9!@#$%^&*])(?=.{1,200}$)/),
        message: "올바른 값을 입력해주세요."
      },
      maxLength: {
        value: 200,
        message: "최대 200자 이내로 작성할 수 있습니다."
     }
    },
    {
      name: "attachment",
      label: "Upload",
      type: "attachment"
    }
  ];
  • yup 적용 후

컴포넌트에 필요한 ui 정보와 유효성 검사 관련 코드를 분리하여 관리

// 스키마 정의
const schema = yup.object().shape({
  name: yup
    .string()
    .trim()
    .required('필수 입력 항목입니다.')
    .max(30, '최대 30자 이내로 작성할 수 있습니다.')
    .matches(/^[a-zA-Zㄱ-ㅎㅏ-ㅣ가-힣]{1,30}$/, '한글, 영어만 입력할 수 있습니다.'),
  phoneNumber: yup
    .string()
    .trim()
    .test(
      "empty-or-validation",
      '숫자만 입력할 수 있습니다.' as yup.Message<any>,
      (value) => !value || (/^\d{1,11}$/g).test(value)
    )
    .max(11, '최대 11자 이내로 작성할 수 있습니다.'),
  email: yup
    .string()
    .trim()
    .required('필수 입력 항목입니다.')
    .max(30, '최대 30자 이내로 작성할 수 있습니다.')
    .email('올바른 값을 입력해주세요.')
  url: yup
    .string()
    .url('올바른 url 주소를 입력해주세요.'),
message: yup
    .string()
    .trim()
    .max(200, '기타 의견은 최대 200자 이내로 작성할 수 있습니다.')
    .test(
      "empty-or-validation",
      '올바른 값을 입력해주세요.' as yup.Message<any>,
      (value) => !value || (/^(?=.*[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9!@#$%^&*])(?=.{1,200}$)/).test(value)
    )
});

// react-hook-form와 yup 연결
const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<any>({ resolver: yupResolver(schema), mode: "onChange" });

const formArrayData = {
  name: {
    label: "이름",
    type: "text",
    placeholder: "이름을 입력해 주세요.",
  },
  phoneNumber: {
    label: "전화번호",
    type: "text",
    placeholder: "전화번호를 입력해 주세요.",
  },
  email: {
    label: "이메일",
    type: "text",
    placeholder: "이메일 주소를 입력해 주세요.",
  },
  url: {
    label: "관련 URL",
    type: "text",
    placeholder: "URL을 입력하세요."
  },
  message: {
    label: "기타 메시지",
    type: "textarea",
    placeholder: "기타 메시지를 입력해 주세요.",
  },
};

마치며

yup 라이브러리를 사용함으로써 사용자의 입력 요소를 서비스에 알맞게 입력하게끔 적절한 피드백을 전달하는 과정을 스키마 정의하면서, 개발자 측면에서 코드는 간결해지고, 한눈에 인지할 수 있어 유지보수 측면에서 더 좋은 코드를 만들 수 있겠다는 생각이 들었습니다. 

읽어주셔서 감사합니다.

이 글은 pxd XE Group Blog에서도 보실 수 있습니다.

참고문서