개발/기타

Generic Property를 통한 콜백함수 인자의 타입 매핑하기

JonghwanWon 2022. 6. 12. 00:47

진행 중인 프로젝트 내 비슷한 로직을 처리하는 Modal UI가 10개가량 존재하는 것을 발견했다.

자연스럽게 컴포넌트로 추출되었을 법 한데 무엇 때문인지 각각 존재하고 있었다.

 

// 초기 컴포넌트의 형태
type Payload = {
  foo: string;
  bar: string;
  baz: string;
};

type WithRequestComponentProps = {};

const WithRequestComponent = ({}: WithRequestComponentProps) => {
  const { register, handleSubmit } = useForm<Payload>();

  const onSubmit: SubmitHandler<Payload> = (payload) => {
    // 서버로의 요청
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('foo')} />
      <input {...register('bar')} />
      <input {...register('baz')} />
    </form>
  );
};

해당 모달이 하는 일은 아래와 같았다.

 

  1. 사용자의 입력을 받아 데이터를 저장한다.
  2. 저장된 데이터를 통해 서버로 요청을 보낸다.

단순하고 하는일이 명확한데도 불구하고 해당 UI는 왜 컴포넌트가 되지 못했을까?

우선 코드들을 비교해보며 왜 컴포넌트 화가 되지 못했는지 아래와 같이 좁혀 추측해 볼 수 있었다.

 

  1. 모달 내부에 조합된 데이터를 가지고 서버와 통신하는 부분이 존재해 따로 만들었거나
  2. 비슷한 UI면서 요청을 보낼 payload의 특정 property의 이름이 달라 따로 만들었거나
  3. 단순하게 복사 붙여넣기를 해 조금씩 코드를 수정했거나

혹은 내가 추측하지 못한 다른 어떠한 이유로 컴포넌트화 하지 않았는지도 모른다.


우선, 해당 UI들을 하나의 컴포넌트를 활용할 수 있도록 계획을 세웠다.

 

  1. 내부에서 조합 된 데이터를 받아 서버로 요청하는 함수를 props로 변경해 외부에서 컨트롤하도록 만든다.
  2. payload 중 특정 property의 이름이 다를 수 이에 대응할 수 있도록 Generic을 활용한다.
const DEFAULT_FIELD_NAME = 'baz' as const;

type BasePayload = {
  foo: string;
  bar: string;
};

type MySubmitHandler<FieldName extends string | undefined = typeof DEFAULT_FIELD_NAME> = SubmitHandler<
  BasePayload & CallbackPayload<FieldName>
>;

type CallbackPayload<FieldName extends string | undefined = typeof DEFAULT_FIELD_NAME> = FieldName extends string
  ? { [key in FieldName]: string }
  : { [key in FieldName as typeof DEFAULT_FIELD_NAME]: string };

type GenericComponentProps<FieldName extends string | undefined = typeof DEFAULT_FIELD_NAME> = {
  fieldName?: FieldName;
  onSubmit: MySubmitHandler<FieldName>;
};

const GenericComponent = <
  FieldName extends string | undefined = typeof DEFAULT_FIELD_NAME,
  T extends BasePayload & CallbackPayload<FieldName> = BasePayload & CallbackPayload<FieldName>,
>({
  fieldName = DEFAULT_FIELD_NAME,
  onSubmit,
}: GenericComponentProps<FieldName>) => {
  const { register, handleSubmit } = useForm<T>();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('foo' as Path<T>)} />
      <input {...register('bar' as Path<T>)} />
      <input {...register(fieldName as Path<T>)} />
    </form>
  );
};

// 컴포넌트를 사용하는 부분에서 callback 함수를 넘겨준다.
const OtherComponent = () => {
  return (
    <GenericComponent
      onSubmit={(payload) => {
        // 
      }}
    />
  );
};

내부에서 처리되던 로직을 props로 변경하고, 내부 특정 프로퍼티 이름을 Generic으로 특정해 기본값을 지정하고 외부에서 전달한 이름으로 사용이 가능하도록 옵셔널 하게 넣어주었다.

 

실제로 의도한 대로 타입힌팅이 되는지 살펴보자.

 

fieldName을 부여하지 않은 경우

fieldName을 부여하지 않았을 때에는 컴포넌트 내에서 기본으로 설정된 baz를 힌팅하게 된다.

그렇다면 fieldName을 부여하면 ?

 

fieldName을 부여한 경우

fieldName을 부여하게 되면 특정 프로퍼티의 이름이 변경된 형태의 타입이 맵핑되게 된다.

이와 같은 작업을 통해 기존 10개가량 존재하던 컴포넌트는 하나의 컴포넌트로 통합할 수 있었다.


 

참고

 

Documentation - Mapped Types

이미 존재하는 타입을 재사용해서 타입을 생성하기

www.typescriptlang.org