[JS] 부분적용함수와 currying 함수 실 적용기

thru·2023년 12월 10일
1

이건 솔직히 각이다

서론

예전에 bind가 흥미로워서 관련된 함수들인 call, apply, bind에 대해 공부하면서 포스팅을 했었다. 당시 포스팅을 하면서도 부분적용함수와 currying 함수를 직접 사용하는 일이 올까라는 생각을 했었는데 이번 프로젝트때 사용할 기회가 있어 적용해보았다.


부분적용함수

부분적용함수는 bind나 클로저를 이용해 인수의 일부만 전달한 함수를 말한다. 예시는 위의 링크에서 볼 수 있다.

문제 상황 - fetch 추상화

Next.js 환경에서 fetch API를 사용하면서 에러 처리 로직이 포함된 safeFetch라는 함수를 만들었다. pathname 등의 인수를 사용할 때마다 직접 입력하기엔 휴먼 에러가 발생할 수 있으므로 아래와 같은 형식으로 다시 추상화해서 사용했다.

export const safeGetMissionFetch = (missionId: string, token: string) => {
  return safeFetch<MissionResponse>(`/missions/${missionId}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${token}` },
    next: { revalidate: 0 }
  });
};

문제는 글 생성 기능과 같이 POST 요청에서 발생했다. GET 요청은 필요한 정보가 정해져있으므로 위처럼 미리 선언해서 사용할 수 있었다. POST 요청은 body 데이터처럼 추가적인 정보가 필요한데 이는 보통 submit handler 내부에서만 접근이 가능했다.

safeFetch를 핸들러 내부에서 그대로 사용하면 작동에 문제는 없지만 앞서 언급한 휴먼 에러의 우려가 있었다.

적용 방법 - 부분적용

type MutationalFetchParams = string | RequestInit | (() => void);

export function useMutationalFetch<T>(
  pathname?: string,
  options?: RequestInit,
  onSuccess?: () => void,
  onError?: () => void
) {
  const safeFetchArguments: MutationalFetchParams[] = [];

  if (pathname) {
    safeFetchArguments.push(pathname);
    if (options) {
      safeFetchArguments.push(options);
      if (onSuccess) {
        safeFetchArguments.push(onSuccess);
        if (onError) safeFetchArguments.push(onError);
      }
    }
  }

  return {
    mutationalFetch: (safeFetch<T>).bind(null, ...safeFetchArguments)
  };
}

이름은 Tanstack QueryuseMutation과 역할이 비슷해서 mutationalFetch라고 지었다.

safeFetchbind하여 함수 객체로 반환해 선언 시점 외에도 사용할 수 있도록 했다. 또한 선택적으로 전달받은 인자는 적용되도록 bind의 두 번째 인수에 배열을 스프레드 연산자로 전달하여 바인딩시켰다. 단, bind는 인수를 앞에서 부터 순차적으로만 바인딩할 수 있으므로 배열에 인자를 채우는 것도 순차적으로 처리했다.

export const useCreateMissionFetch = () => {
  return useMutationalFetch<MissionResponse>(`/createMission`) as {
    mutationalFetch: (
      fetchOptions: RequestInit,
      onSuccess?: (response?: Response) => void,
      onError?: () => void
    ) => Promise<CustomResponse<MissionResponse>>;
  };
};

이제 위 코드처럼 pathname만 먼저 적용하면서 추상화할 수 있다.

추가로 사용할 때 어떤 인수를 넣을 차례인지 개발자가 알기 쉽도록 타입 단언을 사용해서 인텔리센스가 유추할 수 있게 설정했다.

인텔리센스로 유추되는 모습


문제 상황2 - isLoading 전달

추가로 POST 요청이 진행중인지 판단할 수 있는 state가 필요했다. 이름도 이 기능을 위해 use를 앞에 붙여 커스텀 훅으로 작성하고 isLoading이라는 state를 추가했다.

문제는 isLoading를 변경시키는 위치가 safeFetch 함수 내부여야 하므로 setter 함수를 전달해줘야 하는데, 인수를 더 추가하는 것은 기존 사용이나 현재 부분적용함수 사용에 지장이 생기리라 예상되었다.

적용 방법 - this

bind로 전달할 수 있는 게 인수만 아니라 this도 있다는 사실에 집중해서 this를 통해 보내기로 했다.

export function useMutationalFetch<T>(
  pathname?: string,
  options?: RequestInit,
  onSuccess?: (response?: Response) => void,
  onError?: (err?: Error) => void
) {
  const [isLoading, setIsLoadingState] = useState(false);

  const customThis = {
    setIsLoading: (value: boolean) => {
      setIsLoadingState(value);
    }
  };
  
  /*
   * 기존 로직 생략
   */

  return {
    mutationalFetch: (safeFetch<T>).bind(customThis, ...safeFetchArguments),
    isLoading
  };
}

위처럼 setter 함수를 가지고 있는 customThis 객체를 만들고 bind의 첫번째 인수로 전달했다.

export async function safeFetch<T>(
  this: any,
  pathname?: string,
  options?: RequestInit,
  onSuccess?: () => void,
  onError?: () => void
): Promise<CustomResponse<T>> {
  const setIsLoading = this?.setIsLoading;

  try {
    setIsLoading?.(true);
   
   /*
    * fetch 사용 및 에러 처리 로직
    */ 
  
    onSuccess?.(response);
    setIsLoading?.(false);

    return customResponse;
  } catch (err) {

safeFetch 내부에서는 this에 setter가 있으면 fetch 전후에 isLoading을 변경시키도록 구현했다.

이러한 방식을 통해 기존 safeFetch함수에 주는 영향은 최소화한 채로 useMutationalFetch 커스텀 훅을 만들어 기능을 확장할 수 있었다.

this를 이런 용도로 사용해도 될까라는 고민은 남아있다.


Currying 함수

currying 함수는 부분적용함수와 비슷하지만 순차적으로 모든 인수를 다 받아야 실행할 수 있다.

문제 상황 - 이벤트 핸들러

JSX 태그에 적용하는 이벤트 핸들러는 기본적으로 이벤트 객체를 인자로 가진다. 문제는 반복문 같은 곳에 위치한 요소에 이벤트 핸들러를 적용할 때 반복 item의 정보같은 추가적인 인수가 필요한 경우가 있다. 이 때 핸들러에 인수를 직접적으로 추가해버리면 인수 전달이 번거로워진다.

// 두 번째 인자로 넣어버리면..
const handleDelete = (e: MouseEvent, id: string) => {
  e.stopPropagation();
  const newFile = selectedImages?.filter((image) => image.id !== id);
  setSelectedImages(newFile ?? []);
};

// ~~

selectedImages.map((image) => (
  <div onClick={(e: MouseEvent) => handleDelete(e, image.id)} />
)
// 가독성이 구리다

적용 방법

이를 currying 함수를 이용하면 깔끔하게 작성할 수 있다.

const handleDelete = (id: string) => (e: MouseEvent) => {
  e.stopPropagation();
  const newFile = selectedImages?.filter((image) => image.id !== id);
  setSelectedImages(newFile ?? []);
};

// ~~

selectedImages.map((image) => (
  <div onClick={handleDelete(image.id)} />
)

여담

사실 망치 든 사람에겐 모든 게 못으로 보인다고 부분적용함수나 currying 이외에 더 효율적인 방법이 있을 거라고 생각한다. 다만 구현 방법을 고민하면서 아 이거 내가 포스팅했던 내용 적용하면 되겠는데? 라는 생각이 들었던 게 재밌어서 남겨보았다.

profile
프론트 공부 중

0개의 댓글