목표
팀 프로젝트에서 여러곳에 사용하는 infinityQuery를 템플릿으로 작성하여, 변수만 입력하면 추가 코드작성 없이 사용할 수 있는 코드를 작성하려 했습니다.
문제
타입스크립트에서 리턴값을 추론하는데 실패하여 오류가 발생하거나, 실행은 되지만 리턴값의 타입이 InfiniteData가 아닌 상황이 발생했습니다.
해결 과정
1: 이전에 작성된 infiniteQuery는 리턴값이 InfiniteData<데이터 타입 | null> 로 리턴되었습니다. 그러나 다른 팀원분들도 사용할 수 있도록 option으로 지정된 상수값을 변수로 전환하자 문제가 발생했습니다.
2: No overload matches this call. 제가 입력한 변수값들을 받아줄 함수가 없다는 에러였습니다.
에러 코드를 살펴보니 UseInfiniteQueryOptions이라는 단어가 나와서 tanstackQuery의 공식 홈페이지에서 검색을 해봤지만 아무런 설명도 없었습니다.
3: 잠시 마음을 가라앉히고 자리에 다시 앉아 UseInfiniteQueryOptions이 어떻게 정의되어있는지 확인했습니다.
interface UseInfiniteQueryOptions<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown> extends OmitKeyof<InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, 'suspense'> {
/**
* Set this to `false` to unsubscribe this observer from updates to the query cache.
* Defaults to `true`.
*/
subscribed?: boolean;
}
types.d.ts 라는 파일에 이런 인터페이스 정의가 있었고, 이대로 따르기로 했습니다.
4: 그렇게 완성된 코드는 다음과 같았습니다.
options?: UseInfiniteQueryOptions<
T,
Error,
T,
[string],
number | null
>
그러나 이번에는 실행된 결과값의 타입이 엉뚱한 T 로 나오는 상황이 발생했습니다. 알아서 InfiniteData<데이터 타입 , number | null> 으로 변환될것이라 생각했던 저는 왜 이런지 원인을 몰랐습니다.
디폴트값이 TData = TQueryFnData 인데 InfiniteData<데이터 타입 | null> 이 리턴되던 함수였으니 당연히 변환해주는것 아닌가? 라는 생각에 작성했던 코드였는데 전혀 예상치 못한 상황이 발생하자 많이 당황스러웠습니다. …options 때문에 타입스크립트가 타입 추론에 실패한건가 라는 생각도 했었습니다.
5: 함수의 입력값들 타입을 고정하면 될까? 내부에서 자동으로 변환해주잖아. 라는 생각으로 return useInfiniteQuery를 return useInfiniteQuery<T, Error, T, [string], number | null>로도 바꿔보았습니다. 에러는 나진 않았지만 여전히 T 가 리턴됐습니다.
6: 결국 원인을 찾지 못하고 강제로 타입을 변환해서 임시로 조치한 후 useInfiniteQuery 로 계속 검색하다가 세번째 인자 T를 아예 InfiniteData로 지정한 코드들을 봤습니다. TData = TQueryFnData 코드를 보고 잘못된 선입견을 가진것이었습니다. 바로 임시 조치한 타입 변환을 해제하고 코드를 수정하니 정상 동작했습니다.
원인
아래는 useInfiniteQuery 가 정의되어있는 코드입니다
import { DefaultError, InfiniteData, QueryKey, QueryClient } from '@tanstack/query-core';
import { DefinedUseInfiniteQueryResult, UseInfiniteQueryResult, UseInfiniteQueryOptions } from './types.js';
import { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions } from './infiniteQueryOptions.js';
declare function useInfiniteQuery<TQueryFnData, TError = DefaultError, TData = InfiniteData<TQueryFnData>, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown>(options: DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, queryClient?: QueryClient): DefinedUseInfiniteQueryResult<TData, TError>;
declare function useInfiniteQuery<TQueryFnData, TError = DefaultError, TData = InfiniteData<TQueryFnData>, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown>(options: UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, queryClient?: QueryClient): UseInfiniteQueryResult<TData, TError>;
declare function useInfiniteQuery<TQueryFnData, TError = DefaultError, TData = InfiniteData<TQueryFnData>, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown>(options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, queryClient?: QueryClient): UseInfiniteQueryResult<TData, TError>;
export { useInfiniteQuery };
TData = InfiniteData<TQueryFnData>로 정의되어있었고, 이는 TData를 정의하지 않으면 디폴트로 InfiniteData<TQueryFnData>를 사용한다는 의미였습니다.
options 와 이전 코드들의 동작으로 잘못된 선입견이 만들어져 헤메게 되었습니다.
해결방법
options?: UseInfiniteQueryOptions<
T,
Error,
InfiniteData<T, number | null>,
[string],
number | null
>
return useInfiniteQuery<
T,
Error,
InfiniteData<T, number | null>,
[string],
number | null
>
정상적으로 InfiniteData<T, number | null> 를 리턴하도록 타입을 지정하여 문제를 해결했습니다.
후기
비슷한 문제가 생겼을때, 어떤 자료들을 먼저 찾아서 생각을 정리해야 하는지 알 수 있었습니다.
다음에 비슷한 문제가 생겼을때 이 경험이 큰 도움이 되리라고 생각합니다.