리액트(React) Formik / Yup

Alex Roh·2021년 1월 8일
14

React

목록 보기
1/1
post-thumbnail
post-custom-banner

인프런으로 리액트 쇼핑몰 강의를 듣다가 강의에서 복잡하다며
설명을 안하고 넘어가신 부분이 있었습니다.
바로 formikYup라이브러리를 사용해서 form양식을 핸들링 하는 방법이였는데요.

해당 라이브러리를 습득하려면 공식 문서를 찾아보는 것이 가장 현명하다고 느낍니다.
물론 저는 영알못이기 때문에 해당 문서를 일일히 번역기를 돌려가며 공부했습니다.
이번 포스트는 번역해서 얻은 저만의 결과물을 여기에 적어보려고 합니다.


개요#

Formik이 뭔가요?

  • Formik은 React Native에서 컴포넌트를 빌드하기 위한 React 구성 요소 및 hook들의 작은 그룹입니다.

Form양식을 제출(submit)할 때, 가장 성가신 3요소

  • 폼 상태에서 값 가져오기
  • 유효성 검사 및 오류 메세지
  • 폼 submit 핸들링

위에 3요소를 한 곳에 배치함으로서, Formik은 모든 것을 체계적으로 유지해서 리팩토링이 쉽다고 해요.


기초#

간단한 가입 양식을 작성 해보겠습니다.

<코드>

import React from 'react';
import { useFormik } from 'formik';
 
const SignupForm = () => {
  /*
  ueFormik() hook => initialValues에 
  <form> value값과 submit할 때 호출되는 
  submit함수를 파라미터로 전달
  */
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
    },
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2));
    },
  });
  return (
    <form onSubmit={formik.handleSubmit}>
    <label htmlFor="firstName">First Name</label>
    <input
      id="firstName"
      name="firstName"
      type="text"
      onChange={formik.handleChange}
      value={formik.values.firstName}
    />
    <label htmlFor="lastName">Last Name</label>
      <input
      id="lastName"
      name="lastName"
      type="text"
      onChange={formik.handleChange}
      value={formik.values.lastName}
    />
    <label htmlFor="email">Email Address</label>
      <input
      id="email"
      name="email"
      type="email"
      onChange={formik.handleChange}
      value={formik.values.email}
      />
    <button type="submit">Submit</button>
</form>
);
 };

useFormik hook은 formik변수에 폼의 상태와 여러가지의 helper 메소드를 반환합니다.
많은 helper 메소드가 있지만 신경 쓸 메소드는 다음과 같습니다.

  1. handleSubmit : onSubmit 핸들러
  2. handleChange : 각 <input>, <select> 또는 <textarea>에 전달하는 change핸들러
  3. values : 폼의 현재 value

위의 FormikhandleChangereact에 익숙하시다면 이렇게 동작하신다고 이해 하시면 됩니다.

<코드>

const [values, setValues] = React.useState({});
 
const handleChange = event => {
  setValues(prevValues => ({
    ...prevValues,
    // 업데이트 할 `values`의 키를 formik에게 알리기 위해 name prop을 사용
    [event.target.name]: event.target.value
  });
}

유효성 검사#

공식문서에서 Formik을 설명해주면서 HTML 유효성 검사에 대해서 회의적이더라고요.
그 이유를 세 가지로 설명하는데

첫째, 브라우저에서만 작동
둘째, 사용자에게 사용자 지정 오류 메세지를 표시하는 것은 어렵거나 불가능
셋째, 버벅거림

사실 세 가지 이유 전부 이해가 되지는 않습니다만, 일단은 그렇다고 생각하고 넘어갔습니다.

Formik은 폼들의 값 뿐만 아니라 오류 메세지 및 유효성 검사도 추적할 수 있다고 해요.

자바 스크립트로 유효성 검사를 추가하려면 사용자가 직접 유효성 검사를 수행하는 함수를 정의해서 useFormik hook한테 전달해야 됩니다.

사용자가 직접 정의한 유효성 함수를 validate 라고 정의하고 서술할 때도 사용하겠습니다.

validate함수는 values와 initialValues와 똑같은 모양의 오류 객체를 만들어 냅니다.

코드

//사용자 지정 유효성 검사 기능.
//이는 키가 우리의 values/initialValues와 대칭인 객체를 반환해야된다.
const validate = values => {
    const errors = {}; //에러를 반환할 빈 객체

    //firstName 값이 없다면
    if(!values.firstName) { 
        errors.firstName = 'Required'; //firstName키에 필수(Required)라는 문자열 저장
    }
    //firstName 값의 길이가 15보다 크면
    else if (values.firstName.length > 15) {
        errors.firstName = "Must be 15 characters or less"; //15글자 이하여야된다는 문자열 저장
    }

    //lastName 값이 없다면
    if (!values.lastName) {
        errors.lastName = 'Required'; //lastName키에 필수(Required)문자열 저장
    }
    //lastName 값의 길이가 20보다 크면
    else if(values.lastName.length > 20) {
        errors.lastName = "Must be 20 characters or less"; //20글자 이하여야된다는 문자열 저장
    }

    //email 값이 없다면
    if (!values.email) {
        errors.email = 'Required';
    }
    //email 값이 정규 표현식을 만족하지 못하면
    else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
        errors.email = 'Invalid email address'; //잘못된 이메일 형식
    }

    return errors;
}

//html의 <label>태그의 for는 <input>태그의 id속성에 연계되는데
//React에서는 for대신 htmlFor을 사용합니다.
return (
    <form onSubmit={formik.handleSubmit}>

      {/* 성 */}
      <label htmlFor="firstName">First Name</label>
      <input id="firstName" type="text"
      {...formik.getFieldProps('firstName')}
      />

      {/* 유효성 메세지 */}
      {formik.touched.firstName && formik.errors.firstName ? (
      <div>{formik.errors.firstName}</div>
      ) : null}

      {/* 이름 */}
      <label htmlFor="lastName">Last Name</label>
      <input id="lastName" type="text"
      {...formik.getFieldProps('lastName')}
      />

      {/* 유효성 메세지 */}
      {formik.touched.lastName && formik.errors.lastName ? (
      <div>{formik.errors.lastName}</div>
      ) : null}

      {/* 이메일 */}
      <label htmlFor="email">Email Address</label>
      <input id="email" type="email"
      {...formik.getFieldProps('email')}
      />

      {/* 유효성 메세지 */}
      {formik.touched.email && formik.errors.email ? (
      <div>{formik.errors.email}</div>
      ) : null}

      <br />

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

해설

  • formik.errors는 validate 함수에 의해서 값이 채워집니다.
  • 기본적으로 Fromik은 onSubmit전에 onChange, onBlur이벤트 후에 유효성을 검사합니다.
  • 오류가 없을 때 (즉, velidate 함수가 빈 객체{}를 반환하면) useFormik에 전달한 onSubmit 함수만 실행됩니다.

개선

폼이 작동하고 사용자에게 오류를 표시하지만 이는 좋지 못합니다.

그 이유는 validate 함수가 전체 폼의 값에 대해서 하나의 태그에 입력 이벤트에도 호출돼서 주어진 순간에 모든 유효성 검사를 거치기 때문입니다.

쉽게 말해서 이메일 태그에 값을 입력했는데 입력하자마자 성과 이름은 입력하지 않은 상태로 validate 함수가 호출되면서 성과 이름 태그에 Required라는 문자열을 뱉어낸다는 거죠.

이는 오류가 있는지 확인한 다음에 사용자에게 직접 나타내야지 입력하는 도중에 함수가 호출돼서
error 객체를 뱉어내면 안된다는 겁니다.

대부분 사이트의 가입 폼 같은 경우도 그렇습니다.

errors를 만들어내는 것과 values를 설정하는 것과 마찬가지로 Formik은 방문한 필드(태그)를 추적할 수 있습니다.

touched라는 객체에 values 와 initialValues를 미러링해서 저장합니다.

touched객체를 활용하려면 formik.handleBlur를 각 입력의 onBlur prop에 전달해야됩니다.

이 함수는 업데이트할 필드를 파악하기 위해 name prop을 사용하는 점에서 formik.handleChange와 유사하게 동작합니다.

touched를 추적하고 있기때문에 이제 필드의 오류 메세지가 있고 사용자가 주어진 필드를 방문한 경우에만 필드의 올 메세지를 표시하도록 오류 메세지 렌더링의 논리를 변경한 겁니다.

Formik을 공부하기로 마음먹었을 때, Yup은 언제 또 공부하나... 생각했었는데 Yup도 다른 라이브러리 지만 Formik과 같이 사용하면서 서로 같이 묶여 다녀서 공부하는 시간을 절약할 수 있었다. 만세.


Yup을 이용한 유효성 검사#

위에서 서술한 것들은 사용자가 직접 유효성 검사 기능을 수행하는 validate 함수를 정의하고 호출해서 유효성 검사를 진행했습니다.

Formik은 그런 수고스러움을 덜어주기 위해 Yup이라는 라이브러리를 사용합니다.

Formik에서 validationSchema라는 특별한 옵션(prop)이 있는데요.

이 옵션은 Yup의 유효성 검사 오류 메세지를 키가 values / initialValues / touched와 일치하는 객체로 자동 변환 됩니다. ( 사용자가 직접 유효성 검사를 구현할 때, validate라는 함수가 필요했던 것 처럼)

아무튼 해당 라이브러리는 npm install yup --save로 설치할 수 있습니다.

Yup이 어떻게 작동하는지 확인하기 위해 validate함수는 제거하고, Yup 및 validateShema를 사용해서 유효성 검사를 다시 작성하겠습니다.

코드

import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
 
const SignupForm = () => {
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: '',
    },
    validationSchema: Yup.object({
      firstName: Yup.string()
      .max(15, 'Must be 15 characters or less')
      .required('Required'),
      lastName: Yup.string()
      .max(20, 'Must be 20 characters or less')
      .required('Required'),
      email: Yup.string().email('Invalid email address').required('Required'),
    }),
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2));
    },
});
return (
    <form onSubmit={formik.handleSubmit}>

      {/* 성 */}
      <label htmlFor="firstName">First Name</label>
      <input id="firstName" type="text"
      {...formik.getFieldProps('firstName')}
      />

      {/* 유효성 메세지 */}
      {formik.touched.firstName && formik.errors.firstName ? (
      <div>{formik.errors.firstName}</div>
      ) : null}

      {/* 이름 */}
      <label htmlFor="lastName">Last Name</label>
      <input id="lastName" type="text"
      {...formik.getFieldProps('lastName')}
      />

      {/* 유효성 메세지 */}
      {formik.touched.lastName && formik.errors.lastName ? (
      <div>{formik.errors.lastName}</div>
      ) : null}

      {/* 이메일 */}
      <label htmlFor="email">Email Address</label>
      <input id="email" type="email"
      {...formik.getFieldProps('email')}
      />

      {/* 유효성 메세지 */}
      {formik.touched.email && formik.errors.email ? (
      <div>{formik.errors.email}</div>
      ) : null}

      <br />

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

위에서 볼 수 있듯이 직접 validate함수를 구현한 것 보다 코드가 간결해졌습니다.

Formik의 핵심 디자인 원칙 중 하나는 조직화를 유지하는 것이다.

Yup은 확실히 이 작업에 많은 도움을 줍니다.
스키마는 매우 표현력이 풍부하고 직관적이고 재사용이 가능합니다.


리팩토링#

위의 코드는 Formik이 수행하는 작업에 대해서 매우 명확합니다.

시간을 절약하기 위해 useFormik은 formik.getFieldProps()라는 helper메소드를 리턴해서 입력을 더 빠르게 연결 시킵니다.

일부 필드 수준 정보가 주어지면 주어진 필드에 대해 확인된 onChange, onBlur, value의 정확한 그룹을 리턴합니다.

그런 다음에 input, select, textarea등에 분산할 수 있습니다.

getFieldProps에 name속성에 넣었던 문자열을 넣으면 알아서 onChange, onBlur, value를 그룹화해서 리턴한다는 것 같습니다.

코드

return (
  <form onSubmit={formik.handleSubmit}>
       <label htmlFor="firstName">First Name</label>
       <input
         id="firstName"
         type="text"
         {...formik.getFieldProps('firstName')}
       />

       {formik.touched.firstName && formik.errors.firstName ? (
         <div>{formik.errors.firstName}</div>
       ) : null}

       <label htmlFor="lastName">Last Name</label>
       <input id="lastName" type="text" {...formik.getFieldProps('lastName')} />
       {formik.touched.lastName && formik.errors.lastName ? (
         <div>{formik.errors.lastName}</div>
       ) : null}

       <label htmlFor="email">Email Address</label>
       <input id="email" type="email" {...formik.getFieldProps('email')} />

       {formik.touched.email && formik.errors.email ? (
         <div>{formik.errors.email}</div>
       ) : null}

       <button type="submit">Submit</button>
  </form>
);

name, onChange, onBlur, value부분이 다 사라져서 또 한번 코드가 간결해 졌습니다.


React Context 활용#

공식문서에서는 getFieldProps()로 하는 것 조차 수동으로 전달함과 코드의 중복으로 좋지 않다고 합니다.

이를 개선하기위해 Formik은 React Context 기반 API/컴포넌트와 함께 제공돼서 코드를 간결하게 만듭니다.

<Fromik />, <Form />, <Field /> 및 <ErrorMessage />

React Context를 암시적으로 사용해서 부모 state/메소드에 연결합니다.

이러한 컴포넌트는 React Context를 사용하기 때문에
우리의 폼 상태와 helper들을 보유하는 React Context Provider를 렌더링해야된다.

import React from 'react';
import { Formik } from 'formik';
import * as Yup from 'yup';
 
const SignupForm = () => {
   return (
     <Formik
       initialValues={{ firstName: '', lastName: '', email: '' }}
       validationSchema={Yup.object({
         firstName: Yup.string()
           .max(15, 'Must be 15 characters or less')
           .required('Required'),
         lastName: Yup.string()
           .max(20, 'Must be 20 characters or less')
           .required('Required'),
         email: Yup.string().email('Invalid email address').required('Required'),
       })}
       onSubmit={(values, { setSubmitting }) => {
         setTimeout(() => {
           alert(JSON.stringify(values, null, 2));
           setSubmitting(false);
         }, 400);
       }}
     >
       {formik => (
         <form onSubmit={formik.handleSubmit}>
           <label htmlFor="firstName">First Name</label>
           <input
             id="firstName"
             type="text"
             {...formik.getFieldProps('firstName')}
           />
           {formik.touched.firstName && formik.errors.firstName ? (
             <div>{formik.errors.firstName}</div>
           ) : null}
           <label htmlFor="lastName">Last Name</label>
           <input
             id="lastName"
             type="text"
             {...formik.getFieldProps('lastName')}
           />
           {formik.touched.lastName && formik.errors.lastName ? (
             <div>{formik.errors.lastName}</div>
           ) : null}
           <label htmlFor="email">Email Address</label>
           <input id="email" type="email" {...formik.getFieldProps('email')} />
           {formik.touched.email && formik.errors.email ? (
             <div>{formik.errors.email}</div>
           ) : null}
           <button type="submit">Submit</button>
         </form>
     )}
     </Formik>
  );
};

<h1> {value} </h1> 이런 형식과 유사하게 <Formik> {formik => ()} </Formik>으로 되어있다.

<Formik>의 prop으로 기존과 같이 initialValues, validationSchema, onSubmit 핸들러를 전달해주는데 컴포넌트이기 때문에 useFormik()에 전달된 객체를 JSX로 변환하고
각 키는 prop이 됩니다.


마무리#

사실 이 뒤에도 더 간결하게 <Field><ErrorMessage>등으로 더욱 더 간결하게 리팩토링 하지만 쇼핑몰 강의의 소스코드를 온전히 내 것으로 만드는 데는 여기까지의 이론이면 충분하다고 생각해서 포스팅을 마칠려고 한다.

처음 소스코드에 내던져 졌을 때 이게 무슨 외계 언어인가 지레 겁을 먹고 역시 나는 프로그래밍과 맞지 않는건가 하고 좌절했지만 끈기를 가지고 공부를 하고 보니 생각보다 별거 아니였고 오히려 onSubmit이 실행되고 나서 리덕스를 통해 데이터가 오고 가는 부분이 더 중요했다고 느꼈다.

추후에 습득한 지식들이 더 생긴다면 포스팅을 수정해서 업데이트 하도록 하겠다.
리액트는 정말 할 수록 너무 재밌는 것 같다.

그런데 스프링을 점점 까먹고 있어.. 큰일이다...

긴 글 읽어주셔서 감사합니다.(_ _)

profile
남들이 보기 편한 코드를 만드는 개발자가 되고싶어요
post-custom-banner

2개의 댓글

comment-user-thumbnail
2021년 5월 1일

개인 프로젝트에서 사용해보고 싶었다가 찾는 도중 좋은 발견했네요😊 감사합니다👏

답글 달기
comment-user-thumbnail
2022년 7월 9일

정리 잘하셨네요 잘 읽었습니다 다만 Fomik이 뭔가요에 정의할때 ReactNative에서만 쓰는걸로 오해 할수 있어서(처음 써보시는분들은) React, ReactNative 둘다 사용한다고 추가 해주시면 더 좋을것 같네요

답글 달기