본문 바로가기
Dev/react

formik 라이브러리 쓰면 쉽겠지?ㅋㅋ(겠냐고) / 복잡한 폼 다루기 / 추가되는 폼 / fieldArray 중첩

by 혜옹쓰 2023. 1. 20.

오늘은 이번 주 동안 나를 힘들게했던....form 에 대해서 간단히 정리해보려 한다,,

일단 내가 구현해야했던 구조의 form 은 그림과 같이 추가되는 항목이 있는 한 세트의 폼이 또 추가되는 형식...?

전에 구현했던 페이지를 그대로 가져와서 디자인만 수정하면 된다고 생각했는데,

이전과 비즈니스 로직이 수정되면서 form 기능을 다시 구현해야 했다,,,

기존에는 파란색으로 표시한 것과 같이 타이틀, 타입, 콘텐츠가 하나의 폼이라 폼 작성 -> 추가 -> api 요청 -> 다른 폼 작성 ... 과 같은 과정이었다면,

바뀐 로직은 폼 작성 -> 추가 -> 작성 -> 추가 ... -> 한꺼번에 api 요청 이렇게 되면서 form 기능을 처음부터 구현해야했다...ㅠㅠ

그럼 문제가 뭐였는지, 이 기능을 어떻게 구현했는지 살펴보자! (참고로 현재 프로젝트에서는 formik 과 yup 이라는 라이브러리를 이용하여 폼을 구현하고 있다! )


문제

  • 원래 구현하던 방식은 전체를 폼으로 감싸고, 추가되는 하나의 항목에 fieldarray 를 적용하는 방식
  • 하지만 이제 전체 폼도 여러 번 추가되어야 하기때문에, 더 큰 범위로 묶은 후 전체 폼을 하나의 항목처럼 fieldarray 로 묶어 추가되도록 해주어야 함
  • 이 경우 yup 을 이용한 validation 도 다시 정의해주어야 함
  • +) error 메세지 구현 시 사용된 touch 가 제대로 작동하지 않아 submit 로직을 새로 구현해야 함

여기서 말한 formArray 는, 배열처럼 같은 형식의 데이터가 추가되거나 삭제될 수 있도록 formik 에서 제공하는 기능이다!

사용된 개념

  • formik
    • values, setfieldvalue, errors, validateForm, isValid ...
  • fieldarray
    • remove, push
  • yup

 해결

  • 하나의 폼을 배열로 묶은 객체를 새로 만들어 initial value 로 설정
    • values 에 questions 라는 키값 안에 배열로 폼들의 값이 옴
// BEFORE
const initQuestion = {
    // id: '',
    question: '',
    questionType: 'SINGLE_CHOICE' as 'SINGLE_CHOICE' | 'MULTIPLE_CHOICE' | 'SHORT_FORM',
    required: true,
    answerList: ['', ''],
    // hasEtc: false,
};
// AFTER
const dummyQuestion: dummyQuestionType = {
    questions: [initQuestion],
};

// BEFORE - values
{
	question: '',
    questionType: 'SINGLE_CHOICE',
    required: true,
    answerList: ['', '']
}
// AFTER - values
{
	questions: [
    	{
        	question: ''.
            questionType: 'SNIGLE_CHOICE',
            required: true,
            answerList: ['','']
        },
        {
        	// ...
        },
    ]
}

 

  • 하나의 폼을 보여주는 코드를 분리해 컴포넌트화 시키고, 해당 컴포넌트를 fieldarray 를 이용해 반복시키는 동시에 각 컴포넌트 안에서도 fieldarray 를 사용해줌
    • 이때, 반복되는 field 들은 이름을 설정할 때 index 를 활용해주어야 값이 구분되어 설정된다
    • name 이 구분되면 특정 위치의 데이터에 바른 값이 들어가고, push, remove 와 같은 올바르게 기능도 작동함
  • push, remove 와 같은 내장 기능을 통해 항목 추가와 삭제를 쉽게 구현할 수 있다
// 전체 폼
<Formik
    initialValues={Number(typeid) ? _dummyResult : dummyQuestion} // 수정이면 불러온 데이터를 initial value 로, 생성이면 initValue 를 넣음
    onSubmit={onSubmit}
    validationSchema={validationSchema}
		// ...
>
    {({ values, setFieldValue, validateForm, errors, isValid }) => (
        <>
            <Form>
                <FieldArray name="questions">
                    {({ remove, push, form }) => (
                        <>
                            <div>
                                {values.questions.map((f, index) => {
                                    return (
                                        <div key={index}>
                                            <FormCard
                                                // ...
                                            />
                                        </div>
                                    );
                                })}
                            </div>
                            {/* ... */}
                        </>
                    )}
                </FieldArray>
            </Form>
            {/* ... */}
        </>
    )}
</Formik>
// 컴포넌트
<div css={formStyle}>
      <div className="ds-flex fd-c gap-48 mb-48">
          <section>
              <Label type="body1" bold="medium" color={palette.gray.gray6} isRequired>
                  질문 제목
              </Label>
              <div
                  css={css`
                      div {
                          width: 552px;
                      }
                  `}
              >
                  <Input
                      name={`questions.${index}.question`}
                      value={question}
                      onChange={(e) => setFieldValue(`questions.${index}.question`, e)}
                      placeholder="질문을 입력해주세요,"
                      style={{ height: '37px' }}
                  />
                  {errorMsgVisible && (
                      <p className="err">{errors && errors[index] && errors[index]?.question}</p>
                  )}
              </div>
          </section>
          {/* ... */}
					<section>
						<FieldArray name={`questions.${index}.answerList`}>
                {({ remove, push }) => (
                    <div>
                        <section className="answer-list-header">
                            {/* ...개별 폼에서 추가되는 항목 */}
                        </section>
                    </div>
                )}
            </FieldArray>
					</section>
  </div>
</div>
  • validation 을 위한 yup 은 initial value 형태와 동일하도록 array(), object() 를 활용하여 정의해준다
const validationSchema = yup.object().shape({
    questions: yup.array().of(
        yup.object().shape({
            question: yup.string().required('질문 제목을 입력해주세요.'),
            // questionType: yup.string().required('질문 유형을 선택해주세요.'),
            answerList: yup.array().when('questionType', {
                is: (type: string) => type !== 'SHORT_FORM',
                then: yup
                    .array()
                    .min(2, '항목을 2개 이상 입력해주세요.')
                    .test({
                        name: 'is-blank',
                        message: '항목을 모두 입력해주세요.',
                        test: (value) => {
                            for (let i = 0; i < value?.length!; i++) {
                                if (!!value?.[i]) {
                                    continue;
                                } else {
                                    return false;
                                }
                            }
                            return true;
                        },
                    }),
            }),
        }),
    ),
});

이 때 참고한 자료는 아래에!

https://jforj.tistory.com/279

 

[React] react-hook-form과 yup으로 동적 배열 유효성 검증하기

안녕하세요. J4J입니다. 이번 포스팅은 react-hook-form과 yup을 이용하여 동적 배열 유효성 검증하는 방법에 대해 적어보는 시간을 가져보려고 합니다. 기본적인 방법 긴말 없이 바로 코드부터 보도

jforj.tistory.com

https://www.npmjs.com/package/yup#api

 

yup

Dead simple Object schema validation. Latest version: 0.32.11, last published: a year ago. Start using yup in your project by running `npm i yup`. There are 4112 other projects in the npm registry using yup.

www.npmjs.com

  • submit 시 최초 submit 이후부터 에러메세지가 출력되도록 구현.
    • 최초 submit 구분하는 상태 추가
  • 최초 상태에서 에러메세지가 출력되지 않아 빈 상태로 submit 이 가능하기 때문에 isValid 옵션을 이용하여 submit 버튼에 분기처리 해줌
<Button
    type="solid"
    size="large"
    // htmlType="submit"
    style={{ width: '240px' }}
    onClick={() => {
        validateForm();
        !firstSubmitToggle && setFirstSubmitToggle(true);

        if (isValid) return onSubmit(values);
    }}
>
    저장하기
</Button>

후딱 정리하려니까 되게 간단해보이네,,,

프로젝트 하면서 공부했던 부분이라 코드는 거의 다 생략해서 그런지 코드도 짧아졌다! 그래도 간단히 결과물을 조금 보여주면서

오늘 글도 마무리해본당! 안녕!