공부하게된 계기

저번 게시물에 이은 react-hook-form 2탄...

react-hook-form 을 사용해서 form 을 컨트롤 하고 있었다.

내가 하려고 한 방법은 이러하다.

  1. react-hook-form 을 사용해 만들 input의 프로퍼티를 객체형태로 만든다 .
  2. 만들어진 객체를 배열로 감싼다.
  3. 만들어진 배열을 map을 돌려서 렌더링한다.

근데 문제는 무엇이냐..? 사실 유지보수나 만드는데 있어서는 크게 어려움이 없었지만 타입에러가 너무 심하게 났다는거..? 아무래도 react-hook-form 의 라이브러리 메소드를 사용해야 하다보니 거기에 대한 타입지정이 너무 힘들었다. 뭐 물론 끝까지 해볼 수는 있었지만 조언을 구한 동료가 그런거 맵돌리지마라! 라고 하기에..
왜 ? 그래야 하지? 라고 질문을 했다. 그는 나에게 링크 하나를 주었다.

뭐가 잘못되었는지 찾아보자

잘못된 추상화 바로잡기

https://ghdev.tistory.com/m/12

위 링크를 보고 내 코드를 한 번 비교해보자.

내 코드

// 만들고자했던 input 배열
cosnt InputValues = [
  {
    label: "비밀번호",
    column: "password",
    validationOptions: {
      required: "비밀번호는 필수 항목입니다.",
      validate: {
        minLength: (value: string) =>
          value.length >= 12 || "비밀번호는 12글자 이상이어야 합니다.",
        hasSpecialChar: (value: string) =>
          /[!@#$%^&*(),.?":{}|<>]/.test(value) ||
          "비밀번호에는 특수문자가 하나 이상 포함되어야 합니다.",
      },
    },
    isRequired: true,
  },
]

//사용하고자 했던 방식
const SignUpInput = ({
  id,
  label,
  placeholder,
  type,
  register,
  errors,
}: InputProps) => {
  return (
    <>
      {INPUT_CONSTANT.map(
        ({ label, column, validationOptions, isRequired }) => {
          return (
            <div className="mb-6">
              <div className="flex items-center">
                {isRequired && <p className="text-red-400 mr-1">*</p>}
                <label
                  htmlFor={column}
                  className="block text-gray-700 text-sm font-bold mb-2"
                >
                  {label}
                </label>
              </div>
              <input
                type="text"
                id={column}
                placeholder={label}
                className="w-full border p-2 rounded-lg"
                {...register(column, validationOptions)}
              />
              {errors.businessNumber && (
                <span className="text-red-500 text-sm">
                  {errors.businessNumber.message}
                </span>
              )}
            </div>
          );
        }
      )}
    </>
  );
};

export default SignUpInput;

이런 방식으로 사용하려고 했다.

여기서 보니 에러 메세지를 정확하게 전달할 수 없었다. 저기서는 businessNumber라고 나와있지만

저 값을 온전히 전달하려면 저렇게 코드를 쳐서는 안될 것이다.

그러면 블로그를 봐보자. 왜 이 방법이 잘못되었는지.

저 블로그에서는 객체가 거슬린다 라고 말한다. 예를 들어 링크를 추가했을 때 객체를 복사해 문자열 값을 변경해야하는데에 있어서 쉽게 항목을 추가하고 삭제할 수 있지만, 코드의 작동을 이해하기 위해서 위에서 아래로 읽는 것이 아니라 아래에서 위로 읽어야 하기 때문이고, 모든 ui을 동일하게 유지할 수 있지만 각 버튼이 다른 방향으로 발전할 수 있는 여지가 없기 때문에 나쁜 추상화라고 한다.

생각해보니 그렇다. 내 코드에서는 ui는 같지만 아래에 에러메세지나 필요한 비즈니스 로직이 조금씩 다르기 때문에 확장성이 매우 떨어진다는 점에서 나쁜 추상화가 되는 것이다.

그렇다면 여기서 어떻게 나눠야할까?

블로그에서는 메뉴와 버튼을 나눴다. 그렇다면 나는 인풋과 에러메세지를 나누면 될 것 같긴한데

에러메세지를 나누면 props로 받아서 그렇게 가독성있는 코드는 안될 것 같다.

그러면 input에서 props로 받아 추상화를 한 번 진행해보자.

///SignUp.tsx
import React from "react";
import {
  FieldError,
  FieldValues,
  Path,
  RegisterOptions as RHFRegisterOptions,
  UseFormRegister,
} from "react-hook-form";

const SignUp = () => {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors, isValid },
  } = useForm<SignUpFormData>({
    mode: "onChange",
    criteriaMode: "all",
  });

//SignUpInput.tsx

interface Props<T extends FieldValues> extends React.ComponentProps<"input"> {
  label: string;
  id: Path<T>;
  type: string;
  placeholder: string;
  register: UseFormRegister<T>;
  registerOptions: RHFRegisterOptions<T, Path<T>>;
  errors?: FieldError;
  isRequired: boolean;
}

const SignUpInput = <T extends FieldValues>({
  label,
  id,
  register,
  errors,
  placeholder,
  isRequired,
  registerOptions,
  type,
  ...props
}: Props<T>) => {
  return (
    <div>
      <div className="mb-6">
        <div className="flex items-center">
          {isRequired && <p className="text-red-400 mr-1">*</p>}
          <label
            htmlFor={id}
            className="block text-gray-700 text-sm font-bold mb-2"
          >
            {label}
          </label>
        </div>
        <input
          type={type}
          id={id}
          placeholder={placeholder}
          className="w-full border p-2 rounded-lg"
          {...register(id, { ...registerOptions })}
          {...props}
        />
        {errors && (
          <span className="text-red-500 text-sm">{errors.message}</span>
        )}
      </div>
    </div>
  );
};

export default SignUpInput;

짜-잔!

훨씬 깔끔하고 예뻐지지 않았나?

  <SignUpInput
            label="사업자 등록번호"
            id="businessNumber"
            type="text"
            register={register}
            placeholder={"사업자 등록번호"}
            isRequired={true}
            errors={errors.businessNumber}
            registerOptions={{
              required: "사업자 등록번호는 필수 항목입니다.",
              validate: {
                noHyphen: (value: string | FileList | null) =>
                  (typeof value === "string" && !value.includes("-")) ||
                  "사업자 등록번호에 하이픈(-)을 포함할 수 없습니다.",
                isTenDigits: (value: string | FileList | null) =>
                  (typeof value === "string" && value.length === 10) ||
                  "사업자 등록번호는 10자리여야 합니다.",
              },
            }}
          />

물론 이런 형태의 input을 여러개 반복해야 하는 것은 사실이나

원래 Input과 레이아웃을 합쳤을 때는 코드가 약 300줄 정도라고 했을 때

214 라인까지 줄였다!! 근데 이런 방법으로 추상화 하는 것을 팩토리 메서드 패턴이라고 한다!

그러면 팩토리 메서드 패턴을 알아보도록 하자!

팩토리 메서드 패턴

개념은 이러하다.

부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지만, 자식 클래스들이 생성될 객체의 유형을 변경할 수 있도록 하는 패턴이다.

뭔가 말이 조금 어려운 느낌인데?

예시

물류 관리 앱을 개발하고 있다고 할 때, 첫 번째 버전에서는 트럭 운송만 처리할 수 있어, 대부분의 코드가 Truck 클래스에 있다. 얼마 후에 유명해져서 Truck 클래스에 기능을 추가해야된다고 가정해보자!

좋은 소식이지만, 대부분의 클래스는 Truck 클래스에 결합되어있어 앱이 Ship 클래스를 추가하려면 전체 코드 베이스를 반영해아한다. 또한 다른 유형의 교통수단을 추가하려면 전체 코드베이스를 변경해야할 것이다.

예시를 보니

new 연산자를 사용해 transport 인터페이스를 두고 필요한 운송수단에 따라서 유연하게 정의하고,

팩토리 메서드를 사용하는 코드를 종종 클라이언트 코드 라고 하며 다양한 클래스들에서 실제로 반환되는 차이에 대해 알지 못하고, 클라이언트 코드는 모든 제품을 추상(Transport)로 간주한다고 한다.

결국에 배달을 받는 주체는 어떻게 배달되는지 굳이 알 필요가 없기 때문이다.

결론적으로 확장을 열려있으며, 수정은 닫혀있는 것.

클래스를 한 곳에서 관리하여 결합도를 줄이는 것에 의의가 있다!!!

[사진 출처] : https://refactoring.guru/ko/design-patterns/factory-method

나는 리액트 개발자기 때문에 리액트에 대한 예시도 찾아봤다.

내가 찾은 블로그에서는 리액트에서 팩토리 패턴은 이럴 때 사용된다! 라고 써있었다.

리액트에서 팩토리 패턴은 객체 생성을 추상화하여 동적으로 객체를 생성하는 방법

컴포넌트를 조건에 따라 동적으로 생성하고 반환하는데 유용함

객체 생성 로직이 복잡성이나 변경가능성이 높거나 다양한 종류의 객체를 생성해야할때, 객체 생성 과정을 캡슐화하여 중복 코드를 줄이고자 하는 상황에서 사용됨

[출처] https://hyeon-e.tistory.com/m/206

방법은 간단하게 이러하다!

  1. 여러 종류의 컴포넌트 생성하기 위한 팩토리함수를 만들고, 종류를 식별하는 인자를 만든다
const SignUpInput = <T extends FieldValues>({
  label,
  id,
  register,
  errors,
  placeholder,
  isRequired,
  registerOptions,
  type,
  ...props
}: Props<T>)
  1. 팩토리 함수를 사용하여 컴포넌트를 생성하고 필요한 속성을 전달하고 렌더링한다!
<SignUpInput
            label="사업자 등록번호"
            id="businessNumber"
            type="text"
            register={register}
            placeholder={"사업자 등록번호"}
            isRequired={true}
            errors={errors.businessNumber}
            registerOptions={{
              required: "사업자 등록번호는 필수 항목입니다.",
              validate: {
                noHyphen: (value: string | FileList | null) =>
                  (typeof value === "string" && !value.includes("-")) ||
                  "사업자 등록번호에 하이픈(-)을 포함할 수 없습니다.",
                isTenDigits: (value: string | FileList | null) =>
                  (typeof value === "string" && value.length === 10) ||
                  "사업자 등록번호는 10자리여야 합니다.",
              },
            }}
          />

여기서 얻을 수 있는 것은

  • 객체 생성로직의 캡슐화.
    • 객체 생성로직에 대한 관심사를 집중
  • 확장성 증가
    • 수정시에 객체를 생성하는 부분만 수정하면 되어 아주 쉽게 수정할 수 있다.
  • 중복 코드 제거!
    • 이게 가장 큰 이점인 것 같다!

오늘은 이렇게 해서 react-hook-form , 팩토리 메서드 패턴을 사용해 수정을 해보았다!

디자인 패턴이 어렵다고 해서 겁먹을 필요 없이 위와 같이 간단하게 패턴을 적용해볼 수 있다는 사실을

깨달았다!

좋은 코드를 위해서 더욱 노력해보자!

출처

[잘못된 추상화 바로잡기] : https://ghdev.tistory.com/m/12

[팩토리 메서드 패턴 개념 ] : https://refactoring.guru/ko/design-patterns/factory-method

[리액트 팩토리 예시] : https://hyeon-e.tistory.com/m/206

profile
새로운 걸 배우는 것을 좋아하는 프론트엔드 개발자입니다!

0개의 댓글

Powered by GraphCDN, the GraphQL CDN