이건 솔직히 각이다
예전에 bind
가 흥미로워서 관련된 함수들인 call, apply, bind에 대해 공부하면서 포스팅을 했었다. 당시 포스팅을 하면서도 부분적용함수와 currying 함수를 직접 사용하는 일이 올까라는 생각을 했었는데 이번 프로젝트때 사용할 기회가 있어 적용해보았다.
부분적용함수는 bind
나 클로저를 이용해 인수의 일부만 전달한 함수를 말한다. 예시는 위의 링크에서 볼 수 있다.
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 Query
의 useMutation
과 역할이 비슷해서 mutationalFetch라고 지었다.
safeFetch
를 bind
하여 함수 객체로 반환해 선언 시점 외에도 사용할 수 있도록 했다. 또한 선택적으로 전달받은 인자는 적용되도록 bind
의 두 번째 인수에 배열을 스프레드 연산자로 전달하여 바인딩시켰다. 단, bind
는 인수를 앞에서 부터 순차적으로만 바인딩할 수 있으므로 배열에 인자를 채우는 것도 순차적으로 처리했다.
export const useCreateMissionFetch = () => {
return useMutationalFetch<MissionResponse>(`/createMission`) as {
mutationalFetch: (
fetchOptions: RequestInit,
onSuccess?: (response?: Response) => void,
onError?: () => void
) => Promise<CustomResponse<MissionResponse>>;
};
};
이제 위 코드처럼 pathname만 먼저 적용하면서 추상화할 수 있다.
추가로 사용할 때 어떤 인수를 넣을 차례인지 개발자가 알기 쉽도록 타입 단언을 사용해서 인텔리센스가 유추할 수 있게 설정했다.
인텔리센스로 유추되는 모습
추가로 POST 요청이 진행중인지 판단할 수 있는 state가 필요했다. 이름도 이 기능을 위해 use
를 앞에 붙여 커스텀 훅으로 작성하고 isLoading
이라는 state를 추가했다.
문제는 isLoading
를 변경시키는 위치가 safeFetch
함수 내부여야 하므로 setter 함수를 전달해줘야 하는데, 인수를 더 추가하는 것은 기존 사용이나 현재 부분적용함수 사용에 지장이 생기리라 예상되었다.
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 함수는 부분적용함수와 비슷하지만 순차적으로 모든 인수를 다 받아야 실행할 수 있다.
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 이외에 더 효율적인 방법이 있을 거라고 생각한다. 다만 구현 방법을 고민하면서 아 이거 내가 포스팅했던 내용 적용하면 되겠는데? 라는 생각이 들었던 게 재밌어서 남겨보았다.