Data fetch 함수의 타입 지정

Seonup·2023년 11월 11일
0

본 시리즈는 정재남님의 풀스택 리액트 라이브코딩 - 간단한 쇼핑몰 만들기 강의 내용을 기반으로, 추가적인 학습을 통해 습득한 지식 또는 강의 코드를 다른 방법으로 구현한 경험을 작성하고 있습니다. 강의 코드(GitHub)를 확인하세요.

강의에서 제공된 data fetch 함수를 작성하고 사용하는 과정에서 타입 에러를 만났다. 강의에서는 에러가 발생하지 않았었는데, 동일한 코드를 작성했음에도 에러가 발생한 것을 보니 React Query 버전을 강의와 다르게 적용했기 때문인 것으로 보인다. 에러를 해결하는 과정을 살펴보자.

구현 코드

// data fetch 함수 선언부
export const graphqlFetcher = (query: RequestDocument, variables = {}) =>
  request(`${BASE_URL}/graphql`, query, variables, {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': BASE_URL,
  });

// 사용 예제
const { data } = useQuery<Products>(QueryKeys.PRODUCTS, () => graphqlFetcher(GET_PRODUCTS));

에러 메세지

스크린샷 2023-04-21 오후 3 26 04 스크린샷 2023-03-24 오후 4 31 55

No overload matches this call.
The last overload gave the following error.
Type Promise<unknown\> is not assignable to type ProductsType | Promise<ProductsType\>.
Type Promise<unknown\> is not assignable to type Promise<ProductsType\>.
Type unknown is not assignable to type 'ProductsType'.ts(2769)
types.d.ts(9, 89): The expected type comes from the return type of this signature.
useQuery.d.ts(23, 25): The last overload is declared here.

문제 원인 분석

현재 useQuery에 정의한 data의 타입은 queryFunction가 실행되었을 때 fetch API가 반환하는 Promise 객체의 status가 fulfiled 되어 기대한 데이터를 정상적으로 가져올 경우만을 고려하여 ProductsType으로 지정해주었다.
하지만, 타입스크립트는 fetch에 늘 성공하는 것이 아니라 대기(pending)나 실패(rejected)할 수 있으므로 Promise<unknown> 타입을 반환할 수 있다고 경고한다. 따라서 ProductsType 뿐만 아니라 Promise<unknown>일 경우를 고려하여 타입을 지정해줘야 한다.

문제 해결 방법

에러를 일으키는 queryFunction의 타입을 지정해주는 방법은 두가지이다.

1. useQuery 제너릭 타입 매개변수를 이용한다.

useQuery은 제너릭 타입 매개변수를 제공하기 때문에, 이를 이용하여 반환되는 데이터의 타입을 설정할 수 있다.

// https://github.com/TanStack/query/blob/main/packages/react-query/src/useQuery.ts
export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
>(
  options: Omit<
    UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
    "initialData"
  > & { initialData?: () => undefined }
): UseQueryResult<TData, TError>;
  • TQueryFnData: useQuery의 첫 번째 제너릭 타입 매개변수에 작성하는 값으로, useQuery 메서드의 두 번째 인수인 queryFunction이 반환하는 GraphQL 쿼리 실행 결과의 타입을 지정한다.
  • TError: useQuery의 두 번째 제너릭 타입 매개변수에 작성하는 값으로, queryFunction의 error 형식을 지정한다.
  • TData: useQuery의 세 번째 제너릭 타입 매개변수에 작성하는 값으로, queryFunction가 반환하는 값의 실질적인 data의 타입을 지정한다.
  • TQueryKey: useQuery의 세 번째 제너릭 타입 매개변수에 작성하는 값으로, useQuery 메서드의 첫 번째 인수인 queryKey의 타입을 지정한다.

위 제너릭 타입을 사용하여 useQuery를 다시 설정해보면, 아래와 같다.

구현 코드

const { data } = useQuery<Promise<unknown>, Error, ProductsType>(
  [QueryKeys.PRODUCTS],
  () => graphqlFetcher(GET_PRODUCTS)
);

이렇게 수정하면 useQuery의 첫번째 제너릭 타입 매개변수로 Promise<unknown>을 설정해 두었기 때문에 queryFunction이 Promise 객체를 반환하더라도 타입이 추론되고,
queryFunction이 반환한 값의 data가 정상적으로 들어올 때도 ProductsType으로 타입 추론이 가능하다.

2. queryFunction 선언부에 type을 지정해준다.

useQuery의 queryFunction이 호출하는 것은 data를 쉽게 fetch 할 수 있도록 작성된 custom Hook(이하 graphqlFetcher)이다.
이 방법은 graphqlFetcher를 호출하는 useQuery에 타입을 지정해주는 1번 방법과 달리 graphqlFetcher가 선언된 곳에 직접 타입을 지정해주어 해결할 수 있다.

구현 코드

// 선언부
const graphqlFetcher = async (
  query: RequestDocument,
  variables = {}
): Promise<any> => request(BASE_URL, query, variables);

// 호출부
const { data } = useQuery<ProductsType>([QueryKeys.PRODUCTS], () =>
  graphqlFetcher(GET_PRODUCTS)
);

이렇게 지정해주면 grapqlFetcher는 Promise를 반환한다는 것을 명확히 할 수 있고, useQuery는 queryFunction이 평가되어 반환하는 data의 값이 ProductsType이라는 추론이 가능하다.

결론

  • Promise<unknown>Promise<any>는 작성 당시 Promise 객체가 반환하는 값이 무엇인지 알 지 못한다는 것은 동일하다.
  • Promise<unknown>unknown, 즉 "알 수 없는 값"을 반환하기 때문에 해당 값을 사용하려면 타입 캐스팅이나 타입 가드를 통한 타입 체크가 필요하다. 이는 타입 검사를 강제할 수 있으며, 런타임시 타입이 결정되기 때문에 안전하다.
  • Promise<any>any, 즉 "어떤 값"을 반환하기 때문에 어떠한 값이 들어와도 된다. 이는 유연한 사용이 가능하다는 장점이 있지만, 타입 검사를 할 수 없어 예측 불가능한 결과를 초래할 수 있는 위험이 있다.

이러한 특징을 고려하여, 나는 두 가지 방법 중 1번 방법을 선택했다.

profile
박선우

0개의 댓글