개발새발 로그

너 TanStackQuery(v5)+TypeScript 잘 쓰고 있니?- 제네릭, isSuccess 사용, 본문

React

너 TanStackQuery(v5)+TypeScript 잘 쓰고 있니?- 제네릭, isSuccess 사용,

이즈흐 2024. 3. 26. 20:17

 

나는 보통 reactQuery를 사용할 때 타입을 아래와 같이 할 때가 있었다.

// 예시
const query = useQuery<Todo>({
  queryKey: ['todos', id],
  queryFn: fetchTodo,
})

 

useQuery뒤에 제네릭을 붙여서 사용하는 것이다.

 

근데 이 부분에 대해서 팀원 중 한 분이 좋지 않은 사용이라고 지적해주었다.

팀원분께서 설명 해주었는데
정확히 이해한 것이 아닌 것 같아서 공식문서와 TkDodo님의 블로그를 보고 조금 깊이 공부했다.

그랬더니 내가 몰랐던 부분들이 많이 있었고, 기존에 사용하던 코드 패턴을 바꿔야겠다고 생각이 들었다.

 

이 글을 보고 다시 한번 reactQuery와 TanStackQuery를 정말 잘 사용하고 있는지 확인해보자.

 

1. 라이브러리를 사용하는 입장에서 라이브러리의 제네릭에 신경 쓸 필요가 없어야하는  게 이상적이다.

제네릭은 타입스크립트에서 매우 중요합니다.
약간 복잡한 것, 특히, 재사용 가능한 라이브러리를 구현하려는 즉시, 여러분은 제네릭을 찾게 될 겁니다.
하지만 라이브러리를 사용하는 입장에서는, 라이브러리의 제네릭에 신경 쓸 필요가 없어야 하는 게 이상적입니다. 제네릭은 구현할 때나 필요한 디테일입니다. 따라서 꺾쇠괄호로 함수에 직접 제네릭을 넣어주는 건, 두 가지 이유 중 하나로 나쁘다 할 수 있습니다.

라이브러리를 사용하는 입장에서는 제네릭을 사용하지 않고 타입이 알아서 지정되어야 한다고 한다.

 

과거에는 공식문서에서 제네릭을 명시적으로 지정하라고 알려줬다고 한다.

function useGroups() {
  return useQuery<Group[], Error>({
    queryKey: ['groups'],
    queryFn: fetchGroups,
  })
}

근데 이제는 이 패턴을 권장하지 않는다고 한다.

 

왜 그런 것일까?

 

useQuery의 4가지 제네릭, 꺽쇠괄호를 사용하면 복잡해보여!

일단 useQuery에 제네릭, 꺽쇠괄호를 사용하면 복잡해보일 수 있다고 한다.

const query = useQuery<Todo>({
  queryKey: ['todos', id],
  queryFn: fetchTodo,
})

뭐 그렇게 복잡해 보이는가? 할 수 있다.

근데 문제는 useQuery가 제네릭 4개를 갖고있다는 것이다.

export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
>

위에서 말했던 과거 패턴에서 React Query는 많은 기능이 추가되었고, useQuery에 많은 제네릭이 추가되었다.

그래서 제네릭을 사용해도 정상적으로 작동하지만 고급사용을 할 때, 즉 나머지 제네릭이 필요할 때는 제대로 작동하지 않는다.

 

아래코드를 보자

function useGroupCount() {
  return useQuery<Group[], Error>({
    queryKey: ['groups'],
    queryFn: fetchGroups,
    select: (groups) => groups.length,
    // 🚨 '(groups: Group[]) => number' 유형은 '(data: Group[]) => Group[]' 유형에 할당할 수 없습니다.
    // 'number' 유형은 'Group[]' 유형에 할당할 수 없습니다.
  })
}

세 번째 제네릭을 명시하지 않았기 땜누에 Grouo[]는 작동하지만 select 함수에서 number를 반환하게 되면 에러가 생긴다.

그러면 단순한 해결 법은 3번째 제네릭을 추가하는 것이다.

function useGroupCount() {
  // ✅ fixed it
  return useQuery<Group[], Error, number>({
    queryKey: ['groups'],
    queryFn: fetchGroups,
    select: (groups) => groups.length,
  })
}

그럼 아까 말했듯이 점점 복잡하게 보인다.

그리고 라이브러리를 사용하면서 우리가 직접 제네릭을 지정하는 것은 이상적이지 않다.

 

그럼 어떻게 해야할까?

 

타입스크립트가 알아서 추론하게 하자!

타입스크립트의 타입 추론기능은 코드를 더 간결하게 하고, 읽기 쉽게 한다.

그러면 queryFn에 등록된 함수에 적절한 반환 타입을 주면 된다.

Copyinferred-types: copy code to clipboard
function fetchGroups(): Promise<Group[]> {
  return axios.get('groups').then((response) => response.data)
}

// ✅ 여기서 데이터는 `Group[] | undefined`이 됩니다.
function useGroups() {
  return useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
}

// ✅ 여기서 데이터는 `Group[] | undefined`이 됩니다
function useGroupCount() {
  return useQuery({
    queryKey: ['groups'],
    queryFn: fetchGroups,
    select: (groups) => groups.length,
  })
}

 

이 접근 방식의 장점은 다음과 같다.

  • 더 이상 수동으로 제네릭을 지정할 필요가 없습니다.
  • 는 세 번째(선택) 및 네 번째(쿼리키) 제네릭이 필요한 경우에 작동합니다.
  • 제네릭이 더 추가되면 계속 작동합니다.
  • 코드가 덜 혼란스럽고 자바스크립트처럼 보입니다.

 

2.   Error 타입은 어떻게 해?

아까 위 코드를 보면 useQuery에 제네릭을 안하고 정상적으로 작동하게 했다.

근데 Error는 어떻게 되는 것일까?

기본적으로 Error는 제네릭이 없으면 unknown으로 유추된다.

이는 자바스크립트에서 무엇이든 throw할 수 있으면 반드시 Error 유형일 필요가 없다.

throw 5
throw undefined
throw Symbol('foo')

다시 돌아와서 reactQuery는 Promise를 반환하는 함수를 담당하지 않기 때문에 어떤 유형의 에러가 발생할 지 알 수 없다.

따라서 unknown인 것이 맞다.

그래서 타입스크립트에서 여러 제네릭이 있는 함수를 호출할 때 일부 제네릭을 건너뛸 수 있게 되면 이 문제는 신경 쓸 필요가 없어지지만(https://github.com/microsoft/TypeScript/issues/10571)

지금은 오류를 처리해야하고, 제네릭을 전달하고 싶지 않은 상황이다.

그러면 instance of 검사로 해결할 수 있다.

const groups = useGroups()

if (groups.error) {
  // 🚨 이것은 작동하지 않습니다: 객체 유형이 'unknown'.입니다.
  return <div>An error occurred: {groups.error.message}</div>
}

// ✅ 인스턴스 오브 체크는 `Error` 유형으로 좁혀집니다.
if (groups.error instanceof Error) {
  return <div>An error occurred: {groups.error.message}</div>
}

 

위처럼 검사하면 인스 오류가 있는지 확인할 수 있고,

런타임에 실제로 어떤 error.message를 가지고 있는지도 확인이 가능하다.

 

이는 TypeScript가 4.4 릴리스에서 캐치 변수를 알 수 없는 대신 알 수 있는 새로운 컴파일러 플래그
useUnknownInCatchVariables를 도입할 계획과도 일치한다.

https://github.com/microsoft/TypeScript/issues/10571
-> 이슈에서 과거에 어떤 일이 있었는지 볼 수 있었다.

아래 글이 위 이슈가 도입된 이후다.

TypeScript 4.4 에서 error 의 타입을 any에서 unknown으로 변경할 수 있게 됨
any 타입일 때보다 더 정확하고 type-safe하게 에러 처리 가능
   - error에 다양한 Error가 들어올 수 있어 any 타입을 사용했지만,
   - unknown 타입이 typescript에 도입된 이후 unknown 타입으로 더 명확하게 에러 처리 가능
--useUnknownInCatchVariables을 사용하여 변경 가능
   - true 인 경우, any -> unknown

https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables

try {
  // ...
} catch (err) {
  // We have to verify err is an
  // error before using it as one.
  if (err instanceof Error) {
    console.log(err.message);
  }
}

 

 

 

현재 공식문서에서는 아래와 같이 권장하고 있다.

import axios from 'axios'

const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
 //  const error: Error | null

if (axios.isAxiosError(error)) {
  error
  //const error: AxiosError
}

글로벌로 오류를 등록할 수 도 있다.\

import '@tanstack/react-query'

declare module '@tanstack/react-query' {
  interface Register {
    defaultError: AxiosError
  }
}

const  { error }  = useQuery({ queryKey:  ['groups'], queryFn: fetchGroups })
// const 오류: AxiosError | null

이렇게 하면 추론은 계속 작동하지만 오류 필드는 지정된 유형이 된다.

 

3. react Query를 사용할 때는 구조분해할당을 거의 사용하지 않는다? - TypeScript 4.6 변경사항

우선 data나 error와 같은 변수명은 매우 보편적이기 때문에 보통 변수명을 바꿔서 사용한다.

그래서 그냥 객체를 유지한채 사용하면 어떤 데이터인지 어떤 오류가 어디서 발생했는지 알 수 있다.

 

또한 status field or one of the status booleans 중 하나를 사용할 때 타입스크립트가 유형을 좁히는데 도움이 되는데 
이는 구조분해할당을 사용하면 할 수 없는 작업이다.

const { data, isSuccess } = useGroups()
if (isSuccess) {
  // 🚨 여기서 데이터는 여전히 `Group[] | undefined`입니다.
}

const groupsQuery = useGroups()
if (groupsQuery.isSuccess) {
  // ✅ groupsQuery.data는 이제 `Group[]`이 됩니다.
}

이것은 React 쿼리와는 아무런 관련이 없으며, 단지 타입스크립트가 작동하는 방식일 뿐입니다. 이 동작에 대한 좋은 설명은 @danvdk에 있습니다.

그런데 이제는 구조분해할당을 한다고 해도 정상적으로 작동한다고 한다.

업데이트
TypeScript 4.6에는 파괴된 차별적 유니온에 대한 제어 흐름 분석이 추가되어 위의 예제가 작동합니다.
따라서 더 이상 문제가 되지 않습니다. 🙌

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-6.html

 

그래서 나도 정말 되는건지 실험해봤는데 아무리 isSuccess를 묶어도 undefined가 포함되었다.

그래서 useQuery를 사용한 커스텀 훅을 확인해봤다.

내가 사용한 useQuery 커스텀 훅이다.

const useGetCategory = () => {
  const { data, isLoading, isSuccess } = useQuery({
    queryKey: ["auction", "category"],
    queryFn: getSortedCategory
  });

  return { data, isLoading, isSuccess };
};

이렇게 구성하게 되면 에러가 생겼고, 아래와 같이 하면 에러가 생기지 않았다.

const useGetCategory = () => {
  return useQuery({
    queryKey: ["auction", "category"],
    queryFn: getSortedCategory
  });
};

저 둘의 코드는 거의 차이가 없는데 단지 바로 반환하지 않았다고 undefined가 뜬 것이다.

 

이를 통해서 내가 useQuery 커스텀 훅 만드는 방식을 바꿔야겠다는 생각이 들었다.

 

 

4. 만약 enabled를 사용하고 QueryFn 매개변수에 undefined 값이 들어가는 상황엔 어떻게?

아래와 같이 id라는 값을 기준으로 API요청을 하는 상황일 때 보통은 enabled로 처리할 것이다.

function fetchGroup(id: number): Promise<Group> {
  return axios.get(`group/${id}`).then((response) => response.data)
}

function useGroup(id: number | undefined) {
  return useQuery({
    queryKey: ['group', id],
    queryFn: () => fetchGroup(id), // 이곳에 오류가 뜬다.
    enabled: Boolean(id),
  })
  //  🚨'number | undefined' 유형의 인수를 'number' 유형의 매개변수에 할당할 수 없습니다.
  // 'number' 유형에 'undefined' 유형을 할당할 수 없습니다.ts(2345)
}

근데 id 값은 undefined니까 타입스크립트에서는 에러를 발생시킨다.

enabled 자체는  type narrowing을 수행하지 않는다.

그래서 이 경우에는 refetch메서드를 호출해서 enabled를 우회하는 방법을 사용할 수도 있다.

하지만 그러면 id가 실제로 undefined 일 수 있는 것이다.

 

타입 단언을 사용하지 않고 할 수 있는 가장 좋은 방법은 아래와 같이 id가 undefined 일 수 있다는 것을 받아들이고 queryFn에서 Promise를 거부하는 것이다.
약간의 중복이 있지만 명시적이고 안전하다.

function fetchGroup(id: number | undefined): Promise<Group> {
  // ✅ 'undefined'일 수 있으므로 런타임에 ID를 확인하십시오.
  return typeof id === 'undefined'
    ? Promise.reject(new Error('Invalid id'))
    : axios.get(`group/${id}`).then((response) => response.data)
}

function useGroup(id: number | undefined) {
  return useQuery({
    queryKey: ['group', id],
    queryFn: () => fetchGroup(id),
    enabled: Boolean(id),
  })
}

 

 

 

 

출처

https://tkdodo.eu/blog/react-query-and-type-script#the-four-generics

 

React Query and TypeScript

Combine two of the most powerful tools for React Apps to produce great user experience, developer experience and type safety.

tkdodo.eu

 

https://tanstack.com/query/v5/docs/framework/react/typescript#typing-query-options

 

TypeScript | TanStack Query Docs

 

tanstack.com

https://velog.io/@cnsrn1874/%EB%B2%88%EC%97%AD-Type-safe-React-Query

 

[번역] Type-safe React Query

type-safe한 React Query 작성하기

velog.io

 

728x90
반응형
LIST