Ayden's journal

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

활동하기