Formik에 대하여 알아보자 (feat. Yup)

SilverBeen·2022년 7월 26일
12

로그인/회원가입, 신청 양식을 제출 할 때 form 과 useState, onChange 등을 사용하여 코드를 작성합니다.

일반적인 폼 작성 방법 (React)

const Login = () => {
	const [loginInput, setLoginInput] = useState<LoginType>({
	    email: "",
	    password: "",
	});

	const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
	    const { value, name } = e.target;
	
	    setLoginInput({
	      ...loginInput,
	      [name]: value,
	    });
	};
	
	
	return (
			<>
				<input
				  type="text"
				  placeholder="이메일을 입력해주세요"
				  name="email"
				  value={email}
				  onChange={onChange}
				/>
				<input
				  type="password"
				  placeholder="비밀번호를 입력해주세요"
				  name="password"
				  value={password}
				  onChange={onChange}
				/>
			</>
		)
}

이메일과 비밀번호를 state에 저장하려해도 지저분하게 폼을 관리해야합니다. 여기에 이제 input을 더 추가하고 API 연동을 구현하게 되면 코드가 상당히 지저분해지고 가독성이 떨어지겠죠?

귀찮은 작업들을 깔끔하게 도와주는 것이 React Hook From, Redux From, Formik 등 여러가지 라이브러리가 있지만 Formik을 알아보겠습니다. (타입스크립트를 잘 지원해준다고 해요!)

알게된 계기

저는 회사에서 프로젝트 코드를 보다 처음 알게되었는데 너무 궁금해서 구글링 하기 시작했습니다. input 관리를 편하게 하고 유효성 검사, 오류 처리, submit 처리를 쉽게 해준다는 라이브러리를 알고 지금까지 왜 난 여러 input을 힘들게 관리했나!! 라는 생각이 순간 들었습니다.

유효성 검사?

보통 유효성 검사를 하려면 useEffect를 사용하여 input의 값이 바뀔때마다 validation 검사를 하고 error 처리를 해야하죠. 그렇다 해서 코드가 짧은 것도 아니고 조건이 많아지게 되면 굉장히..! 복잡해집니다. 그리고 리소스를 낭비할 수도 있죠. 이걸 쉽게 해줄 수 있다니 혁명이라고 생각이 들었죠.. (후반 설명)

편한 이유

  1. state를 연관 있는 애들끼리 묶을 수 있다.
  2. 유효성 검사, 오류 처리, submit을 한번에 처리할 수 있다.
  3. 다양한 API Reference를 제공한다. ex) usefield(), <field />, <fieldArray /> , useFormik,
    1. 자세한건 docs 확인

사용해보면서 가장 좋았던 것은 가독성이 정말 편리하고 state 관리가 편하다는게 가장 좋았습니다. 서비스가 커질 수록 컴포넌트에서 사용해야 하는 state이 정말 많은데, 이를 관련있는 것으로 묶는 것이니 좋았습니다. 그리고 무엇보다 유효성검사….!!! 오류 처리 등등등… useEffect() 사용해서 막 글자 수 비교하는게 정말 귀찮아요… (매우매우)

시작하기

yarn add formik @material-ui/core

기본 형태

<h3>Formik 기본</h3>
<Formik
  initialValues={{ username: "", password: "" }} // 필수로 들어가야 하는 요소
  onSubmit={(data, { setSubmitting }) => { // 필수로 들어가야 하는 요소
    setSubmitting(true);
    // 비동기 통신을 설정해주면, isSubmitting을 통해 현재 비동기 통신중인지 확인할 수 있다.

    console.log(data);
    setSubmitting(false);
  }}
>
  {({ values, handleChange, handleBlur, handleSubmit, isSubmitting }) => (
    <form onSubmit={handleSubmit}>
      <div>
        <TextField // mui
          name="username"
          value={values.username}
          onChange={handleChange}
        />
      </div>
      <div>
        <TextField
          name="password"
          value={values.password}
          onChange={handleChange}
        />
      </div>
      {/* isSubmitting 을 사용하여 true일 때는 양식을 두 번 보내지 않도록 버튼을 disabeld 시킨다. */}
      <Button type="submit" disabled={isSubmitting}> {/* mui */}
        Submit
      </Button>
    </form>
  )}
</Formik>

기본의 Formik은 위와 같은 형태를 띄고 있습니다.
그리고 빠르고 깔끔한 UI를 위해 @material-ui 를 사용하여 빠르게 UI를 구성했습니다.

formik 은 initialValuesonSubmit이 필수로 들어가야합니다.

  • initialValues : 초기 상태
  • onSubmit : 폼 제출시 실행될 함수들을 담는 공간
<Formik
	initialValues={}
	onSubmit={() => {}}
	...
>
	{(파라미터)  (jsx)} 
</Formik>

화살표 함수에 들어갈 파라미터는 values, handleChange, handleBlur, handleSubmit 등의 값들이 들어갑니다.

setSubmitting 비동기 통신

setSubmitting 는 비동기 통신에 사용이 됩니다.

submitting 중인지 boolean 값으로 설정할 수 있어, 양식을 여러번 보내지 않도록 버튼을 disable 시켜 사전에 방지합니다.

onSubmit={(data, { setSubmitting }) => { // 필수로 들어가야 하는 요소
    setSubmitting(true);
    // 비동기 통신을 설정해주면, isSubmitting을 통해 현재 비동기 통신중인지 확인할 수 있다.

    console.log(data);
    setSubmitting(false);
}}

// isSubmitting가 false 이면 button은 클릭되지 않는다. 
<Button type="submit" disabled={isSubmitting}>Submit</Button>

그럼 모든 input에 onChange랑 넣어줘야 해요?

인풋이 적은 경우는 위에 처럼 사용해도 충분하지만 인풋도 많고 너무너무 귀찮다….. 하는 사람들을 위한 것이 따로 있습니다. props를 자동으로 넣어주는 인풋이 존재하죠(헉..)

바로바로

<Form/><Field/>컴포넌트 입니다.

import { Formik, Field, Form} from "formik”

위에 것들을 가져 온 후

<Field/>

<Form>
  <div>
    <Field name="username" as={TextField} />

		{/* error message */}
    {touched.username && errors.username ? (
      <ErrorText>{errors.username}</ErrorText>
    ) : null}
  </div>
  {/* Field의 name만으로도 handleSubmitm value, handleChange, handleBlur가 전부 자동 설정되고 as 를 통해 어떤 스타일의 input을 사용할 건지 정의 해주면 된다.  */}
  <div>
    <Field name="password" type="password" as={TextField} />
    {touched.password && errors.password ? (
      <ErrorText>{errors.password}</ErrorText>
    ) : null}
  </div>
</Form>

<Field/> 안에 name을 설정한 init key로 설정한 값을 넣어주면 자동으로 value, onChange 와 같은 것들이 자동 설정이 됩니다. as 로 어떤 스타일로 정할건지 컴포넌트를 넣어주면 끝입니다!

그리고 여기서 validation을 추가하고 싶으면 propstouched를 받아온뒤 errorText를 보여 줄 수 있습니다.

유효성 검사 (feat.Yup)

저는 validationYup을 사용해서 검증을 했는데 Yup에 대해선 다음에 자세하게 설명하도록 할게요.

간단하게 Form validation을 위한 라이브러리입니다.

<Formik 
	initialValues={{ username: "", password: "" }} // 필수로 들어가야 하는 요소
  onSubmit={(data, { setSubmitting, resetForm }) => { // 필수로 들어가야 하는 요소
    setSubmitting(true);
    // 비동기 통신을 설정해주면, isSubmitting을 통해 현재 비동기 통신중인지 확인할 수 있다.

    console.log(data);
    setSubmitting(false);
		resetForm(); // 제출시 폼 초기화
  }}
 // 이런식으로 Yup을 사용한다....
	validationSchema={Yup.object().shape({
	  username: Yup.string().required(),
	  password: Yup.string().required(),
	  skills: Yup.array().min(1, "최소 한개의 필드를 선택해주세요").required(), // 밑에 나옴
})} >
	{({isSubmitting, touched, errors}) => ()}
</Formik>

이렇게 하면 검증을 간편하게 할 수 있습니다.

다양한 input들들

이런 양식의 폼을 만들고 싶어서 checkBox, radio를 만들어 보았습니다.

기존 label을 사용할 수 있지만 깔끔하고 가독성있는 코드를 만들기 위해

<LabeledCheckbox /> 커스텀 checkbox 를 만들어 주었습니다.

import { Checkbox, FormControlLabel } from "@material-ui/core";
import { FieldAttributes, useField } from "formik";

type CheckboxProps = { label: string } & FieldAttributes<{}>;

const LabeledCheckbox = ({ label, ...rest }: CheckboxProps) => {
  const [field] = useField(rest); // field : name, type, id ...

  return <FormControlLabel control={<Checkbox />} label={label} {...field} />;
};

export default LabeledCheckbox;

이때 사용한것이 useField()를 사용하여 field를 가져와 에 뿌려주었습니다.

control={} : 어떤 컴포넌트를 사용할 건지 컴포넌트를 넣어주면 됩니다.

<LabeledCheckbox name="skills" value="Redux" label="Redux" type="checkbox" />
<LabeledCheckbox name="skills" value="Redux-saga" label="Redux-saga" type="checkbox" />

컴포넌트를 불러와 이렇게 사용할 수 있습니다.

그리고 제출 폼에 skills를 추가해야하기 때문에 initialValues에 skills 초기값을 넣어주면 됩니다.

initialValues={{ username: "", password: "", skills : [] }} 

radio 버튼도 위와 같이 만든 후 컴포넌트를 추가해줍니다.

전체 코드

import * as Yup from "yup";
import styled from "styled-components";
import { Form, Formik, Field } from "formik";
import { TextField, Button, Checkbox } from "@material-ui/core";
import LabeledRadio from "./lebel/LabeledRadio";
import LabeledCheckbox from "./lebel/LabeledCheckbox";
import YUP_SCHEMA from "../../constants/yupSchema";

const FormikFieldExam = () => {
  return (
    <Container>
      <h3>입력하는 input이 많은 경우 자동으로 넣어주는 인풋을 사용한다. </h3>
      <p>ex) {"<Formik /> <Field /> <Form />"} 태그들</p>

      <Formik
        initialValues={{
          username: "",
          password: "",
          isNoob: false,
          skills: [],
        }}
		// YUP_SCHEMA는 전역으로 사용하기 위해 따로 뺌
        validationSchema={Yup.object().shape({
          username: YUP_SCHEMA.USER_NAME,
          password: YUP_SCHEMA.PASSWORD,
          skills: YUP_SCHEMA.CHECK_BOX,
        })}
        onSubmit={(data, { setSubmitting, resetForm }) => {
          setSubmitting(true);
		  // 비동기 통신을 위함
          console.log(data);

          setSubmitting(false);
          resetForm(); // 제출시 form reset
        }}
      >
        {({ isSubmitting, touched, errors }) => (
          <Form>
            <div>
			{/* Field의 name만으로도 handleSubmitm value, handleChange, handleBlur가 전부 자동 설정되고 as 를 통해 어떤 스타일의 input을 사용할 건지 정의 해주면 된다.  */}
              <Field name="username" as={TextField} />
			  {/* error message를 위함 */}
              {touched.username && errors.username ? (
                <ErrorText>{errors.username}</ErrorText>
              ) : null}
            </div>
           
            <div>
              <Field name="password" type="password" as={TextField} />
              {touched.password && errors.password ? (
                <ErrorText>{errors.password}</ErrorText>
              ) : null}
            </div>

            <h3> checkbox / radio example</h3>

            <Field name="isNoob" as={Checkbox} />

            <h3>상태관리</h3>
            <div>
              <LabeledCheckbox
                name="skills"
                value="Redux"
                label="Redux"
                type="checkbox"
              />

              <LabeledCheckbox
                name="skills"
                value="Redux-saga"
                label="Redux-saga"
                type="checkbox"
              />

              <LabeledCheckbox
                name="skills"
                value="react-query"
                label="react-query"
                type="checkbox"
              />

              <LabeledCheckbox
                name="skills"
                value="Recoil"
                label="Recoil"
                type="checkbox"
              />

              <LabeledCheckbox
                name="skills"
                value="zustand"
                label="zustand"
                type="checkbox"
              />

              {touched.skills && errors.skills ? (
                <ErrorText>{errors.skills}</ErrorText>
              ) : null}
            </div>
						
            <h3>사용언어</h3>
            <div>
              <LabeledRadio
                name="lang"
                value="Javascript"
                label="Javascript"
                type="radio"
              />
              <LabeledRadio
                name="lang"
                value="Typescript"
                label="Typescript"
                type="radio"
              />
              <LabeledRadio name="lang" value="All" label="All" type="radio" />
            </div>

            <Button type="submit" disabled={isSubmitting}>
              Submit
            </Button>
          </Form>
        )}
      </Formik>
    </Container>
  );
};

const Container = styled.div`
  width: 600px;
  margin: 100px auto;

  & label {
    cursor: pointer;
  }
`;

const ErrorText = styled.p`
  color: #ed7e7e;
  font-size: 14px;
  font-weight: bold;
`;
export default FormikFieldExam;

예외 사항

감당할 수 없을 만큼의 input들과 validation이면..?

useFormik()<FieldArray /> 를 사용하면 됩니다. (기회가 되면 시도해보는 걸로 ! )

간단하게 설명하자면

<Formik 
    // friends 라는 배열로 초기값을 설정해 준 후
	initialValues={{ friends: ['jared', 'ian', 'brent'] }} 
	onSubmit={() => {}}
	render={({values}) => ( // values의 값을 파라미터로 받고
		<Form>
			<FieldArray name="friends" render={arrayHelpers => (
			// 여기서 values.firends를 map 돌려주면 된다고 합니다다다!
			)} />
		</Form>
	)}
/>

https://formik.org/docs/api/fieldarray

참고하시면 이해 쏙 될거에요

결과

console.log 로 찍어보면

제출이 잘 된 것을 볼 수 있죠?

잘못됐거나 이해가 안되시는 부분이 있으면 편하게 댓글 달아주세요!!
감사합니다.

참고자료

https://formik.org/docs/overview

https://krpeppermint100.medium.com/ts-formik-사용법-4f526888c81a

2개의 댓글

comment-user-thumbnail
2022년 7월 29일

멋있어요

1개의 답글