options in Fetcher
요즘 나는 아래와 같이 다섯 단계에 걸쳐 컴포넌트에 데이터를 넣어주고 있다. 각각의 단계는 한 줄로 정의되는 최소한의 책임을 구현하며, 이번 포스트를 통해 추상 클래스 Fetcher 내에서 어떻게 options를 정의하고 있는지와 그것을 각 클래스 Repository에서 어떻게 사용하는지에 이야기해보고자 한다.
axios.instance of graphql.client // 백엔드 호출 방법
=> abstract class Fetcher // 백엔드 호출 로직 및 options 함수 정의
=> class Repository extends Fetcher // 도메인 별 queryFactory 및 repository 패턴 구현
=> useAdaptor // 백엔드 인터페이스와 프론트 인터페이스를 매개
=> react component
리액트 쿼리는 queryFactory 패턴을 구현함에 있어 타입 오류가 발생하는 것을 막고자 queryOptions 함수와 infiniteQueryOptions 함수를 제공하고 있다. 여기에 더해 나는 비슷하지만 useMutation에 대응하는 mutationOptions 함수를 따로 구현하여 사용하고 있다. 이런 options 함수는 전달받은 아규먼트를 그대로 리턴하면서도 쿼리와 뮤테이션에 필요한 옵션 객체의 타입에 문제가 없는지를 검사한다.
mutationOptions이야 리액트 쿼리 차원에서 제공되고 있지 않으니 어쩔 수 없이 만들어 써야 한다. 그런데 다른 두 options 함수는 이미 리액트 쿼리가 제공하는데도 불구하고 나는 이를 Fetcher 내부에 정의하고 있는 걸까? 이유는 크게 두 가지이다.
하나는 Fetcher를 extends 받는 클래스는 거의 반드시 options 함수를 사용하게 된다. 그런데 이걸 각각의 클래스 Repository 파일에서 import하는 것보다 Fetcher 하나로 처리하는 게 낫다고 판단했다. 또한, mutationOptions는 Fetcher에서 제공되는데 다른 두 options는 그렇지 않을 경우 호출 방식에 있어 mutationOptions만 this로 호출하게될테니 중구난방으로 보이지 않을까 염려한 데도 있었다. 아래는 세 options 함수를 Fetcher에서 모두 제공하는 방식이다.
import { GraphQLFetcher } from "@/apis/GraphQLFetcher";
export class BookRequest extends GraphQLFetcher {
constructor() {
super();
}
queryKey = ["book"];
getBook = (isbn13: string) =>
this.queryOptions({
queryKey: [...this.queryKey, isbn13, "getBook"],
queryFn: () =>
this.graphql<GetBookQuery, GetBookQueryVariables>(GET_BOOK_BY_ISBN, {
variables: { isbn13 },
}),
enabled: !!isbn13,
});
postBook = (
isbn13: CreateReportMutationVariables["isbn13"],
push: ReturnType<typeof useRouter>["push"],
) =>
this.mutationOptions({
mutationFn: (ReportInput: CreateReportMutationVariables["ReportInput"]) =>
this.graphql<CreateReportMutation, CreateReportMutationVariables>(CREATE_REPORT_MUTATION, {
variables: {
isbn13,
ReportInput,
},
}),
onSuccess: (data) => {
push(`books/${data.book.isbn13}`);
},
onError: (error) => {
console.log(error);
},
});
}
다른 이유로는 '타입 주입' 문제가 있다. 아래는 리액트 쿼리가 제공하는 queryOptions 함수의 타입 일부이다. 살펴보면 TError 부분이 Error로 되어있다. 백엔드가 어떤 형식의 에러를 던지게 될는지는 타입스크립트로 추론할 수 있는 게 아닌 만큼, 기본적으로 우리가 제공해주어야 하는 타입이다.
function queryOptions<
TQueryFnData = unknown,
TError = Error,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
문제는 TQueryFnData는 queryOptions 함수가 충분히 추론할 수 있음에도 불구하고 TError 타입을 제공하기 위해 TQueryFnData까지 명시적으로 제공해야 한다는 점이었다. 게다가 이것은 queryOptions를 사용하는 모든 곳에서 발생할 것이기 때문에 가능하다면 어디 한 곳에서 TError의 타입을 일괄적으로 필터링해주어야 했다. 그 한 곳이 지금으로서는 Fetcher 일 뿐이다.
현재 내가 사용하고 있는 추상 클래스 Fetcher의 형태는 아래와 같다. 이것을 각 도메인 별 클래스 Repository에 상속하고, 클래스 Repository는 엔드포인트 별 커스텀 어댑터 훅에서 호출된다.
export abstract class GraphQLFetcher {
client = new GraphQLClient(process.env.NEXT_PUBLIC_BASE_URL + "/graphql", {
credentials: "include",
});
graphql<T, V extends object>(query: RequestDocument, option?: { variables: V }) {
return this.client.request<T>(query, option?.variables);
}
infiniteGraphql<T, V extends object>(query: RequestDocument, pageParam: V) {
return this.client.request<T>(query, pageParam);
}
mutationOptions = <TData = unknown, TError = CustomError, TVariables = void, TContext = unknown>(
options: UseMutationOptions<TData, TError, TVariables, TContext>,
): UseMutationOptions<TData, TError, TVariables, TContext> => options;
queryOptions = <TQueryFnData = unknown, TError = CustomError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
options: Parameters<typeof queryOptions<TQueryFnData, TError, TData, TQueryKey>>[0],
) => queryOptions(options);
infiniteQueryOptions = <
TQueryFnData,
TError = CustomError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: Parameters<typeof infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>>[0],
) => infiniteQueryOptions(options);
}
블로그의 정보
Ayden's journal
Beard Weard Ayden