Front-End(Web)/React - 라이브러리들

[Form 라이브러리] formik

ttaeng_99 2023. 3. 15. 05:26
반응형

🧐 개요

프로젝트들을 진행하면서, 입력 form 컴포넌트를 제작해야 하는 경우가 종종 존재하였다. 특히, 폼 제어를 위한 다양한 기능들이 요구된다.

(입력값 관리, 유효성 검사 및 에러상태 관리, 유효성 검사 및 입력값 제출 API 연동, 성공/실패 시의 fallback 로직 등..)

 

이전 회사의 멤버십 프로젝트에서도, 이러한 기능들을 템플릿화 하기 위해 컴포넌트 커스터마이징 등 많은 시도를 했다.

또, 최근 파트너 프로젝트를 지원하면서, 투어상품을 판매하는 파트너로 가입하기 위해 진행되는 다양한 입력 폼에 대한 제어가 필요함을 다시금 느꼈다.

 

앞으로도, 입력 폼들에 대한 신규개발 혹은 리팩토링을 대비해,

React 환경에서 이를 좀 더 심플하게 구현할 방법들에 대해 리서치한 내용들을 간단히 공유하고자 한다!

 


🤦 입력폼 API 직접구현 시도

이전 회사 팀장님께서 폼의 각 아이템들과 전체에 대한 유효성 검사를 커스텀하신 적이 있다. (Vue, vee-validate 기반)

이를 비슷하게 구현해보고자 했고, 결국 중도포기를 하게 되긴 했다 😂😂

// FormerItem.tsx
// InputText, InputPassword 등 폼아이템들의 공통설정을 위한 랩핑 컴포넌트

interface Props {
  children: ReactElement | (ReactElement | null)[];
  id: InputKeys;
  title?: string;
  errorData?: ErrorTypeData;
}

const FormerItem = ({ children, id, title, errorData }: Props) => {
  const isMultipleChildren = useMemo(() => Array.isArray(children), [children]);
  const isError = useMemo(() => errorData?.valid === false, [errorData]);

  return (
    <ItemWrapper>
      <TitleLabel htmlFor={id}>{title}</TitleLabel>
      {isMultipleChildren
        ? Children.map(children, child => !child ? null : cloneElement(child, { isError }))
        : cloneElement(children as ReactElement, { isError })
      }
      <ErrorMessage isShow={isError} message={errorData?.message} />
    </ItemWrapper>
  )
}

대략, 위의 사진은 내가 구현하고자 한 방향성, 아래 코드는 이를 위한 공통설정을 적용하는 컴포넌트 부분이다.

 

위의 errors 키값들을 받아서, 이를 각 컴포넌트에서 상태값(errorData)으로 갱신하고, 이를 다시 FormerItem에 내려주는 형태이다.

에러 핸들링만 해도 고민해야 할 요소들이 많다고 느껴졌다.

 

또 FormerItem을 개발하면서도, children에 props를 넘겨주기 위한 cloneElement 적용, children 복수요소 대응 등 다소 복잡하게 구현해야하는 부분이 불가피했다.

 

무엇보다, 폼과 폼 아이템들 간의 값 공유가 필수적인 만큼 여기서 허들을 많이 느꼈던 것 같다.

비밀번호 확인 입력에서 비밀번호 값과 비교하기 위한 경우나, 폼 제출 시 각 입력값들을 확인하기 위해서는 Context API로 값을 공통 관리하거나 <input> 엘리먼트 value에 직접 접근하는 등 컨트롤을 정하기가 쉽지 않았다.

 

이러한 이슈들이 거듭되면서, React Form 라이브러리들에 대한 유혹이 발생했고 간단히 리서치를 해보게 되었다!

 

 

 

🧐 Form 라이브러리 리서치

React에서 자주 활용되는 2가지 폼 라이브러리로, formikreact-hook-form 가 소개된다. 둘의 주요 특징은 아래와 같아.

구분 formik react-hook-form
기본원리 Controlled Component (state, Context 기반) Uncontrolled Component (ref 기반)
렌더링 성능 낮음 성능 높음
Bundle Size 44.34KB 12.12KB
Dependency 7개 0개 (dependency-free)
Typescript O O
부분 Watch X O
템플릿 코드 상대적으로 많음 상대적으로 적음
주요 장점 UI 프레임워크와 호환이 좋음 Hooks API 기반으로 되어있으 함수 컴포넌트에 적합
공식문서 설명이 잘 되 있음, 커뮤니티와 업데이트가 활발
주요 단점 업데이트가 활발하지 못함 (최근 업데이트가 1년 전) 클래스 컴포넌트에서 사용불가(Hooks)

위처럼, 두 라이브러리의 특성이 조금 다르기도 해서, 상황별 필요성에 맞게 선택해서 사용하면 된다.

개인적으로, react-hook-form이 코드가 덜 직관적일수도 있지만, 많은 기능과 높은 성능 면에서 장점이 있고 그래서 npm trends가 뒤집힌 거라고 추정되었다.

 

그러면, 각각의 라이브러리들에 대해 더 자세한 내용을 각 포스팅에서 다뤄보겠다!


💙 Formik

Formik은 기존 사용되던 redux-form 라이브러리에 비해, 좀 더 심플하고 경량화된 폼 라이브러리를 제공하고자 한다.

특히 값 상태관리, 유효성 검사, Submit 제어 등에서 장점을 가지며, Controlled Component와 Context API 기반으로 제작되었다고 이해하면 된다.

 

- 설치

npm install formik --save
yarn add formik

 

 

 

💙 Formik 주요 문법

Formik을 사용하는 기본문법과, 유효성 검사 및 에러 핸들링을 구현하기 위한 추가 기능들을 알아보겠다.

 

1. <Formik> - 기본문법

import React from 'react';
 import { Formik } from 'formik';
 
 const Basic = () => (
   <div>
     <h1>Anywhere in your app!</h1>
     <Formik
       initialValues={{ email: '', password: '' }}
       validate={values => {
         const errors = {};
         if (!values.email) {
           errors.email = 'Required';
         } else if (
           !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
         ) {
           errors.email = 'Invalid email address';
         }
         return errors;
       }}
       onSubmit={(values, { setSubmitting }) => {
         setTimeout(() => {
           alert(JSON.stringify(values, null, 2));
           setSubmitting(false);
         }, 400);
       }}
     >
       {({
         values,
         errors,
         touched,
         handleChange,
         handleBlur,
         handleSubmit,
         isSubmitting,
         /* and other goodies */
       }) => (
         <form onSubmit={handleSubmit}>
           <input
             type="email"
             name="email"
             onChange={handleChange}
             onBlur={handleBlur}
             value={values.email}
           />
           {errors.email && touched.email && errors.email}
           <input
             type="password"
             name="password"
             onChange={handleChange}
             onBlur={handleBlur}
             value={values.password}
           />
           {errors.password && touched.password && errors.password}
           <button type="submit" disabled={isSubmitting}>
             Submit
           </button>
         </form>
       )}
     </Formik>
   </div>
 );
 
 export default Basic;

위 코드는, <Formik> API를 활용한 기본적인 보일러 플레이트다.

 

 

- Formik Props

 

먼저, <Formik> 컴포넌트가 가장 외곽이 된다. 이는, initialValues, onSubmit 2가지 Props를 기본적으로 요구된다.

  • initialValues : 폼 내에서 쓸 데이터의 기본값. 객체를 받으며, 각 key는 내부 input의 name과 대응된다.
  • onSubmit : 폼에서 Submit 이벤트가 발생했을 때 실행할 핸들러 함수를 등록한다.
    인자는 values(입력값 객체), actions(폼의 에러상태, 제출중인 상태(submitting), 폼 리셋 등의 추가조작) 2가지를 받는다.
  • 이외에도, validate(유효성 검사 함수), validatoinSchema(유효성 검사 객체) 등 인자를 받는다. 이는 뒤에서 추가 기술하겠다.

 

 

- Render Props

 

<Formik> 내에서 렌더링될 컴포넌트에 전달되는 Props들이다. 필수로 사용하면 좋은 기능들 위주로 정리해보았다.

  • values : <input> 등의 각 name과 이에 해당하는 value들의 값을 저장하는 객체. 각 name을 key로 값을 조회 가능하며, 위의initialValutes로 초기화.
  • errors : validate 함수로 반환한 에러 정보를 저장하는 객체. 각 name을 key로 에러 메세지 조회 가능 (메세지가 있으면 에러)
  • handleSubmit : 위의 onSubmit에 등록한 핸들러 함수. <form>의 onSubmit 이벤트 핸들러로 등록
  • handleChange : 내부 엘리먼트의 값 변경을 연동하는 핸들러 함수. <input> 등의 onChange 이벤트 핸들러로 등록
  • handleBlur : 내부 엘리먼트의 focusOut 동작과 연동하는 핸들러 함수(에러 핸들링). <input> 등의 onBlur 이벤트 핸들러로 등록
  • touched : 사용자가 내부 엘리먼트와 인터렉션이 있었는지 여부를 저장하는 객체. 각 name을 key로 확인
  • isSubmitting : 위의 onSubmit 핸들러로 설정한 setSubmitting의 true/false 여부. 비동기 통신중임을 표현하기 위해 활용
  • 이외에도, dirty(initialValues와 깊은복사 일치 여부), isValid(유효성 여부), 및 value, error, 각 필드에 대한 제어 메서드 등이 존재

 

 

- Formik의 component Props (vs children)

 

<Formik> 은 템플릿 코드처럼 children 방식 외에, component 라는 Props로 자식 컴포넌트를 설정할 수 있다.

이 때, 이 자식 컴포넌트는 Render Props를 받는다.

 <Formik component={ContactForm} />;
 
 const ContactForm = ({
   handleSubmit,
   handleChange,
   handleBlur,
   values,
   errors,
 }) => (
   <form onSubmit={handleSubmit}>
     <input
       type="text"
       onChange={handleChange}
       onBlur={handleBlur}
       value={values.name}
       name="name"
     />
     {errors.name && <div>{errors.name}</div>}
     <button type="submit">Submit</button>
   </form>
 );

 

 

 

2. <Field>

위 템플릿을 보면, 폼 내부 엘리먼트에 많은 설정이 필요함을 알 수 있다. name부터 value, handleChange, handleBlur 등 이벤트.

이러한 설정들을 Formik에서 제공하는 <Field> 컴포넌트로 간편하게 할 수 있다.

// 위 Field 코드는 아래 input 설정과 같다

<Field name="id" />

<input name="id" value={values.name} onChange={handleChange} onBlur={handleBlur} />

이처럼, name만 등록하면 주요 설정들을 해주는 것이다.

<Field>는 기본적으로 input 이며, as 혹은 component Props로 다른 엘리먼트로 변경 가능하다. (select, textarea 등)

 

 

- Render Props

 

<Field> 컴포넌트도 children 형태로 사용 가능하며, 자식 컴포넌트는 아래와 같은 Props를 받는다.

  • field : 필드의 name, value, onChange, onBlur 등 정보를 담은 객체
  • form : 상위 form의 props (values, errors, touched, isValid 등)
  • meta : Metadata 정보 (initialValue, value, error, touched)
 // Children can be JSX elements
 <Field name="color" as="select">
   <option value="red">Red</option>
   <option value="green">Green</option>
   <option value="blue">Blue</option>
 </Field>
 
 // Or a callback function
 <Field name="firstName">
 {({ field, form, meta }) => (
   <div>
     <input type="text" {...field} placeholder="First Name"/>
     {meta.touched &&
       meta.error && <div className="error">{meta.error}</div>}
   </div>
 )}
 </Field>

 

* 소스코드 참고

 import React from 'react';
 import { Field, Form, Formik, FormikProps } from 'formik';
 
 const MyInput = ({ field, form, ...props }) => {
   return <input {...field} {...props} />;
 };
 
 const Example = () => (
   <div>
     <h1>My Form</h1>
     <Formik
       initialValues={{ email: '', color: 'red', firstName: '', lastName: '' }}
       onSubmit={(values, actions) => {
         setTimeout(() => {
           alert(JSON.stringify(values, null, 2));
           actions.setSubmitting(false);
         }, 1000);
       }}
     >
       {(props: FormikProps<any>) => (
         <Form>
           <Field type="email" name="email" placeholder="Email" />
           <Field as="select" name="color">
             <option value="red">Red</option>
             <option value="green">Green</option>
             <option value="blue">Blue</option>
           </Field>
 
           <Field name="lastName">
             {({
               field, // { name, value, onChange, onBlur }
               form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
               meta,
             }) => (
               <div>
                 <input type="text" placeholder="Email" {...field} />
                 {meta.touched && meta.error && (
                   <div className="error">{meta.error}</div>
                 )}
               </div>
             )}
           </Field>
           <Field name="lastName" placeholder="Doe" component={MyInput} />
           <button type="submit">Submit</button>
         </Form>
       )}
     </Formik>
   </div>
 );

 

 

 

3. <Form>

<Formik> 내부에서 폼을 감쌀 <form> 엘리먼트에 onSubmit, onReset 등 props들을 전달한 형태이다.

마찬가지로 <Form>은 기본적으로 form 이다.

// 위 Form 코드는 아래 form 설정과 같다

 <Form />

 <form onReset={formikProps.handleReset} onSubmit={formikProps.handleSubmit} {...props} />

 

 

 

4. <ErrorMessage>

폼 내부 엘리먼트들에 대한 에러 메세지를 표현하는 경우는 보편적이다. 에러가 존재할 경우, 조건부 렌더링으로 이를 표현해줘야 한다.

아래 코드처럼, errors 로부터 노출값 및 조건부 렌더링 설정이 name 하나로 해결되는 것을 참고할 수 있다.

     <Formik
       initialValues={{
         name: '',
         email: '',
       }}
       validationSchema={SignupSchema}
       onSubmit={values => {
         // same shape as initial values
         console.log(values);
       }}
     >
       {({ errors, touched }) => (
         <Form>
           <Field name="name"  />
         //  {errors.name && touched.name ? (
         //   <div>{errors.name}</div>
         // ) : null}
             <ErrorMessage name="name" />
           <Field name="email" type="email" />
         //  {errors.email && touched.email ? (
         //   <div>{errors.email}</div>
         // ) : null}
             <ErrorMessage name="email" />
           <button type="submit">Submit</button>
         </Form>
       )}
     </Formik>

 

 

 

5. useFormik()

<Formik> 과 동일한 문법을 hooks 형태로 구현한 문법이다. 내부 옵션 객체가 initialValues, onSubmit 등의 Props를 받는다.

또한, useFormik 이 반환한 인스턴스(formik)에서 Render Props 값이었던 values, handleSubmit 등을 사용하면 된다.

import React from 'react';
 import { useFormik } from 'formik';
 
 const SignupForm = () => {
   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>
   );
 };

* <Formik> 의 component가 좀 더 컴포넌트(JSX) 스러운 문법이면, useFormik은 Hooks 스러운 문법인 것이다.

 

 

 

6. useField()

마찬가지로, <Field> 의 자식 컴포넌트에서 사용됬던 Render Props(field, meta, helpers) 등을 활용할 수 있는 hooks 이다.

 

이외에도, 몇 가지 기능을 제공하는 API들을 공식문서에서 확인할 수 있다.

 

 

 

 

💙 Formik + Yup 을 통한 에러 핸들링

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;
}

return (
  <Formik validate={validate}>
    <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>
);
};

위 예시는, Formik에서 유효성 검사를 적용하는 기본적인 예시다.

validate Props로 함수를 전달하고, 이 함수는 values를 인자로 받아 각 key(name)에 대한 에러 메세지들을 객체로 반환한다.

 

- validationSchema + Yup

 

Formik에선 validationSchema 라는 Props를 지원하며, 이는 Yup Schema를 값으로 받는다.

 

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>
);
};

 

* Yup

Yup 라이브러리의 상세 문법들은 아래 npm 문서를 참고하자. 여기선 컨셉만 이해하고, 주요 문법들은 별도 포스팅으로 소개하겠다!

 

yup

Dead simple Object schema validation. Latest version: 1.0.2, last published: 19 days ago. Start using yup in your project by running `npm i yup`. There are 4345 other projects in the npm registry using yup.

www.npmjs.com


확실히, 라이브러리를 사용했을 때 신경써야했던 이벤트 핸들링, 에러 핸들링 등을 신경쓰지 않아도 된다는 점이 새로웠다.

특히, Yup을 활용한 유효성 검증 관리라던지, Field, ErrorMessage 등으로 간단한 커스텀들이 가능하단 점이 좋게 느껴졌다.

 

반면, Controlled + Context 기반이기 때문에 모든 Input 이벤트에 대해 폼 전체가 리렌더링 된다는 성능상의 이슈가 있다.

이러한 치명적인 단점과 더불어, 높은 라이브러리 의존성과 낮은 업데이트 빈도 등의 이유로 현재는 react-hook-form 이 조금 더 각광을 받는 것이지 않을까

 

다음 포스팅은, 이 react-hook-form 라이브러리에 대해 기술해보도록 하겠다!

 

 

📎 출처

- [Formik] 공식문서 : https://formik.org/docs/overview  

 

- [Why Formik] reason-to-code 블로그 : https://www.reason-to-code.com/blog/why-do-we-have-to-use-formik/ 

- [Formik vs React-hook-form] songc 님의 블로그 : https://blog.songc.io/react/react-form-control/  

- [Formik vs React-hook-form] refine blog : https://refine.dev/blog/react-hook-form-vs-formik/

 

- [Formik 사용법] krpeppermint100 님의 블로그 : https://krpeppermint100.medium.com/ts-formik-%EC%82%AC%EC%9A%A9%EB%B2%95-4f526888c81a
- [Formik 사용법] duckgugon 님의 블로그 : https://duckgugong.tistory.com/218

 

- [Formik + Yup] roh160308 님의 블로그 : https://velog.io/@roh160308/%EB%A6%AC%EC%95%A1%ED%8A%B8React-Formik-Yup

반응형