GraphQL과 React-Query
이전에 Apollo Client 포스트를 통해 page router 환경에서 GraphQL을 사용하는 방법에 대해 알아보았었다. 하지만 며칠 간의 실험과 고민 끝에 나는 Apollo Client 대신 React-Query를 사용하기로 결정했다. TKDoDo가 올린 당신은 리액트 쿼리가 필요하지 않을 지도 모른다를 번역하면서도 느꼈지만, React-Query는 data fetching을 포함한 모든 종류의 비동기 작업을 관리하는 데 가장 뛰어나다고 생각된다.
React-Query에는 Apollo Client에는 존재하지 않는 몇 가지 강력한 기능이 포함되어있다. 간단하게 구현할 수 있는 infiniteQuery와 일괄적으로 선언할 수 있는 retry 및 stateTime, 그리고 구체적으로 선언할 수 있는 revalidate 로직 등이 그렇다. 또한 쿼리의 성공과 실패 여부를 떠나 반드시 실행해야 하는 로직을 onSattled로 분리할 수 있다는 점도 내 마음을 끌었다.
물론 Apollo Client가 가진 장점도 분명 존재한다. codegen과 함께 사용할 경우 각각의 쿼리 및 뮤테이션에 대해 커스터마이징 된 훅을 제공하고, 이를 통해 훨씬 적은 코드를 사용하면서도 타입 안전하게 데이터를 주고받을 수 있다. GraphQL은 무조건 200 응답이지만, Apollo Client를 사용하면 별다른 처리 없이 내부적으로 data와 error를 잘 분리해준다는 점도 매력적이긴 하다. 그렇지만 graphql-request를 사용하면 앞서 언급한 Apollo Client의 장점 대부분을 React-Query 환경에서 누릴 수 있다.
어댑터 패턴과 프론트엔드 주도 개발이라는 포스트에서 나는 http method에 따라 class QueryFn과 class MutationFn을 나누어서 작성하였다. 하지만 GraphQL은 post 만을 사용하기에 GraphQL.ts에서 일괄적으로 처리하여 다양한 클래스에 상속시킬 수 있다. AXIOS나 fetch와 달리 client.request 메소드를 사용하면 Apollo Client의 useQuery같이 data와 error를 자동으로 구분해준다.
import { GraphQLClient } from "graphql-request";
const client = new GraphQLClient(process.env.NEXT_PUBLIC_BASE_URL + "/graphql", {
credentials: "include",
});
export class GraphQL {
graphql<T, V extends object>(query: RequestDocument, option?: { variables: V }) {
return client.request<T>(query, option?.variables);
}
infiniteGraphql<T, V extends object>(query: RequestDocument, pageParam: V) {
return client.request<T>(query, pageParam);
}
}
graphql-request도 Apollo Client와 마찬가지로 gql 템플릿 리터럴 태그 함수를 제공한다. 이를 사용하면 마찬가지로 codegen이 백엔드와 통신하며 자동으로 필요한 타입을 생성해준다. 이런 스키마들을 어디에 어떤 식으로 배치해야 할지 아직 정하지는 못했지만 ─ 객체나 클래스 안에서 gql을 사용하면 codegen이 반응하지 않는 듯하다. ─ 일단은 api 폴더에 도메인별로 구분해서 넣어두었다.
import { gql } from "graphql-request";
const GET_ALL_BOOK_DATA = gql`
query getAllBookData($isbn13: String!) {
book: getBook(isbn13: $isbn13) {
...AllBook
subInfo {
...AllBookSubInfo
}
}
}
${ALL_BOOK}
${ALL_SUB_INFO}
`;
const GET_BOOK_LIST = gql`
query getBookList($BookSearchInput: BookSearchInput!) {
bookList: getBookList(bookSearchInput: $BookSearchInput) {
hasNext
items {
isbn13
title
author
description
cover
categoryId
categoryName
pubDate
publisher
priceStandard
customerReviewRank
}
}
}
`;
이전에 하나의 도메인을 각각 class BookQuery와 class BookMutation으로 구분했던 까닭은, 각각의 클래스가 class QueryFn과 class MutationFn라는 서로 다른 클래스를 상속 받았기 때문이다. 하지만 이 기본 클래스가 class GraphQL로 통합된 만큼 각 도메인 역시 Query와 Mutation으로 구분하지 않고, BookRequest라는 하나의 클래스로 합쳐서 관리하기로 했다.
import { GraphQL } from "../GraphQL";
export class BookRequest extends GraphQL {
constructor() {
super();
}
queryKey = ["book"];
getBook(isbn13: string) {
return {
queryKey: [...this.queryKey, isbn13, "getBook"],
queryFn: () =>
this.graphql<GetBookQuery, GetBookQueryVariables>(GET_BOOK_BY_ISBN, {
variables: { isbn13 },
}),
enabled: !!isbn13,
};
}
getBookList(keyword: string) {
return {
queryKey: [...this.queryKey, keyword],
queryFn: ({ pageParam }: { pageParam: GetBookListQueryVariables }) =>
this.infiniteGraphql<GetBookListQuery, GetBookListQueryVariables>(GET_BOOK_LIST, pageParam),
initialPageParam: { BookSearchInput: { keyword, take: 12, skip: 1 } },
getNextPageParam: (lastPage: GetBookListQuery, allPages: any, lastPageParam: GetBookListQueryVariables) => {
return {
BookSearchInput: { keyword, take: 12, skip: lastPage.bookList?.hasNext ? lastPageParam.BookSearchInput.skip + 1 : NaN }
};
},
};
}
}
다만 이렇게 하나의 클래스에 다 때려박다보니 일부 도메인이 너무 많은 일을 처리하게 되는 것 같다. 이런 클래스를 몇 개의 세부 클래스로 분리하고, 다시 메인 클래스에 컴포지션하는 방향을 검토하고 있다.
// ReportRequest.ts
// 각각의 세부 클래스에서 로직 선언
class ReportSearchRequest { ... }
class ReportMainRequest { ... }
class ReportLikeRequest {
constructor(private graphql: GraphQL["graphql"], private queryKey: Array<string>) {}
toggleLike(reportId: string) {
return () => this.graphql<ToggleLikeMutation, ToggleLikeMutationVariable>(TOGGLE_LIKE_MUTATION, {
variables: { reportId }
})
}
}
// 메인 클래스로 컴포지션
class ReportRequest extends GraphQL {
constructor() {
super()
}
queryKey = ["report"]
main = new ReportMainRequest(this.graphql, [...this.queryKey])
search = new ReportSearchRequest(this.graphql, [...this.queryKey, "search"])
like = new ReportLikeRequest(this.graphql, [...this.queryKey, "like"])
}
// 사용 예시
const reportRequest = new ReportRequest()
const { data } = useMutation({
mutationFn: reportRequest.like.toggleLike("1"),
onSuccess: () => { ... }
})
블로그의 정보
Ayden's journal
Beard Weard Ayden