not-a-gardener 개발기 3) 배열을 주면 폼을 드려요

메밀·2023년 5월 8일
0

not-a-gardener

목록 보기
3/13

1. Form 생성 자동화?

form을 만드는 건 너무 귀찮고 재미없다.
그놈이 그놈인 html에 name만 고치고, onChange에 제출용 state를 채울 함수만 바꾸고...

게다가 내가 만드는 프로젝트의 경우 validation도 굉장히 간단하다.
(빈칸이냐, 음수냐 정도?)

자동화가 가능해보여 Form 생성 컴포넌트를 만들어 진행해보기로 결정했다.



2. Form과 Submit Validation 자동화

배열을 주면 Form을 드려요!

1) 대략적인 흐름

2) ItemForm.jsx

Form 자동화의 시작이 되는 ItemForm 컴포넌트다.

import {CCard, CCardBody, CCol, CForm} from '@coreui/react'
import FormInputHandler from './FormInputHandler'

const ItemForm = ({title, itemObjectArray, submitBtn, inputObject, onChange}) => {
  return (
    <div className="row justify-content-md-center align-items-center height-95">
      <CCol md="auto" className="minWidth-70">
        <CCard sm={6} className="mb-4">
          <CCardBody>
            <h4 className="mt-3 mb-3">{title}</h4>
            <CForm validated={true}>
              {/* input, select 등을 구해서 채움 */}
              <FormInputHandler
                itemObjectArray={itemObjectArray}
                onChange={onChange}
                inputObject={inputObject}/>
              {/* 등록 제출 버튼 */}
              {submitBtn}
            </CForm>
          </CCardBody>
        </CCard>
      </CCol>
    </div>
  );
}

export default ItemForm;

ItemForm 컴포넌트의 인자는 다음과 같다.

인자설명
title페이지에 띄울 제목 (ex. 식물 추가, 장소 수정 등)
itemObjectArrayForm을 만들 정보를 담은 배열
submitBtn'제출하기' 버튼. validation을 만족하면 disabled가 해제된다.
onChangeinput 값 변경을 처리할 함수

FormInputHandler와 itemObjectArray를 통해 폼을 자동으로 생성하는 과정을 알아보자.

3) FormInputHandler와 itemObjectArray

FormInputHandler.jsx

import FormInputSelect from './input/FormInputSelect'
import FormInput from './input/FormInput'
import FormInputDate from './input/FormInputDate';

const FormInputHandler = ({itemObjectArray, onChange, inputObject}) => {
  // text, number
  const invalidMsg = (inputItem) => {
    return inputItem.inputType === "text" ? invalidMsgForText(inputItem) : invalidMsgForNumber(inputItem);
  }
  const invalidMsgForText = (inputItem) => {
    return inputItem.required && inputObject.name == "" ? `${inputItem.label}은 비워둘 수 없어요` : "";
  }

  const invalidMsgForNumber = (inputItem) => {
    return !Number.isInteger(inputObject[inputItem.name]) ? "0 이상의 정수를 입력해주세요" : "";
  }

  return (
    itemObjectArray.map((inputItem) => {
        const commonProps = {key: inputItem.name, inputItem, onChange};
        const type = inputItem.inputType;

        return type === "select" ? <FormInputSelect {...commonProps}/>
          : type === "date" ? <FormInputDate {...commonProps}/>
            : <FormInput {...commonProps} feedbackInvalid={invalidMsg(inputItem)}/>
      }
    ))
}

export default FormInputHandler

FormInputHandler의 구조는 간단하다. 인자로 전달받은 itemObjectArray를 순회하며 inputType에 맞는 인풋 컴포넌트를 반환한다.

반환하는 인풋 컴포넌트로는 FormInput(텍스트 인풋), FormInputDate(날짜), FormInputSelect(셀렉트)가 있다.

모두 각 타입에 맞는 인풋을 반환하고, 유효하지 않은 값일 시 아래에 작은 피드백 메시지를 띄우는 컴포넌트이므로 코드는 생략한다.

const plantFormArr = [
    {
      inputType: "text",
      label: "식물 이름",
      name: "name",
      required: true
    },
    {
      inputType: "text",
      label: "식물 종",
      name: "species",
      required: false
    },
    {
      inputType: "select",
      label: "장소",
      name: "placeId",
      optionArray: placeList,
      noPlace: noPlace
    },
    {
      inputType: "select",
      label: "식재 환경",
      name: "medium",
      optionArray: mediumArray
    },
    {
      inputType: "number",
      label: "최근 물주기",
      name: "recentWateringPeriod",
      required: false
    },
    {
      inputType: "date",
      label: "반려 일자",
      name: "birthday",
      required: false
    }
  ]

예컨대 식물 등록/수정에 해당하는 itemObjectArray는 위와 같다.
FormInputHandler 컴포넌트는 위와 같은 배열을 순회하여,

위와 같은 input(과 피드백 메시지)들을 반환하게 되는 것이다.



4) ValidationBtn.jsx

충족되지 않은 Form은 제출이 안 돼요!


식물 Form의 예시를 이어가보자.
식물 등록/수정의 경우 입력받는 값은 다음과 같은데,

  • 식물 이름
  • 식물 종
  • 장소
  • 식재 환경
  • 최근 물주기
  • 반려 일자

볼드체로 처리한 값은 꼭 필요한 값이다.

이 중 장소와 식재환경은 Select로 기본값이 지정되어 있으므로
사용자의 입력을 검증해야 할 값은 식물이름 뿐이다.

import {Button} from "antd";
import React, {useState} from "react";
import InputFeedbackSpan from "../etc/InputFeedbackSpan";

/**
 * submit시 form validation 값에 따라 회색버튼 / 오렌지색 버튼
 *
 * @param isValid boolean 유효성 검사
 * @param onClickValid data가 유효할 시(submit 가능 시) 실행할 함수
 * @param onClickInvalidMsg 유효하지 않은 데이터로 전송하려할 때 띄울 메시지
 * @param title 버튼 이름
 * @param className
 * @param size 사이즈
 * @returns {JSX.Element} 버튼
 */
const ValidationSubmitButton = ({isValid, onClickValid, onClickInvalidMsg, title, className, size}) => {
  const [invalidMsg, setInvalidMsg] = useState("");

  return isValid ? (
    <Button
      type="button"
      size={size}
      className={`bg-orange text-white ${className}`}
      onClick={onClickValid}>
      {title}
    </Button>
  ) : (
    <>
      <div className={className}>
        <div>
          <InputFeedbackSpan feedbackMsg={invalidMsg}/>
        </div>
        <Button
          size={size}
          className={`bg-light text-dark ${className}`}
          onClick={() => setInvalidMsg(onClickInvalidMsg)}>
          {title}
        </Button>
      </div>
    </>
  )
}

export default ValidationSubmitButton;

이에 required 값이 충족되지 않은 Form의 경우,
submit 버튼을 disabled 처리하고 피드백 메시지를 띄우고,
충족된 입력값에만 제출을 허용하는 버튼을 만들었다.


잘돌아간당

3. 기능별 메인 페이지 로직

다음은 장소 페이지를 통해 각종 메인페이지의 최초 로직을 살펴보자.

import React, {useEffect, useState} from "react";
import NoItem from "src/components/empty/NoItem";
import PlaceList from "./PlaceList";
import getData from "src/api/backend-api/common/getData";
import Loading from "../../components/data/Loading";
import AddPlace from "./AddPlace";

/**
 * 장소 메인 페이지
 * @returns {JSX.Element}
 * @constructor
 */
const Place = () => {
  const [isLoading, setLoading] = useState(true);
  const [hasPlace, setHasPlace] = useState(false);
  const [placeList, setPlaceList] = useState([]);
  const [originPlaceList, setOriginPlaceList] = useState([]);

  const onMount = async () => {
    const data = await getData("/place");
    console.log("data", data);

    setLoading(false);
    setHasPlace(data.length > 0);
    setPlaceList(data);
    setOriginPlaceList(data);
  }

  useEffect(() => {
    onMount();
  }, [])

  const addPlace = (place) => {
    placeList.unshift(place);

    setPlaceList(placeList => placeList);
    setOriginPlaceList(placeList => placeList);
  }

  if (isLoading) {
    return <Loading/>
  } else if (!hasPlace) {
    return <NoItem
      title="등록된 장소가 없어요"
      buttonSize="lg"
      buttonTitle={"장소 추가하기"}
      addForm={<AddPlace addPlace={addPlace} afterAdd={() => setHasPlace(true)}/>}
    />
  } else {
    return <PlaceList
      placeList={placeList}
      setPlaceList={setPlaceList}
      originPlaceList={originPlaceList}
      addPlace={addPlace}
    />
  }
}

export default Place;

해당 페이지가 호출되면, useEffect 내의 fetch 함수를 통해 서버에서 데이터를 받아온다.

이 때 일어날 수 있는 일은 다음과 같다.

  1. 로딩중: 서버에서 데이터를 받아오는 중
  2. 등록된 데이터가 없음: 위의 경우 장소를 하나도 등록하지 않은 경우
  3. db에 데이터가 존재하고 그 데이터를 받아왔으면 리스트를 띄움

이에 초기값이 true인 loading 스테이트를 통해 로딩창을 띄우고,
받아온 데이터의 크기(length == 0) 조건을 통해 '등록된 데이터 없음' 페이지와 '리스트' 페이지로 분기하는 로직을 만들었다.

0개의 댓글