일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 알고리즘
- 익스프레스
- 리액트
- 프로그래머스코테
- dp알고리즘
- 리액트댓글기능
- HTML5
- 다이나믹프로그래밍
- 프로그래머스
- JS프로그래머스
- 백준nodejs
- 백준알고리즘
- 몽고DB
- 백준구현문제
- 안드로이드 스튜디오
- 코딩테스트
- 백준구현
- 리액트커뮤니티
- 자바스크립트
- 포이마웹
- JS
- 코테
- 백준
- 프로그래머스JS
- js코테
- CSS
- 백준js
- HTML
- css기초
- 백준골드
- Today
- Total
개발새발 로그
너 TanStackQuery(v5)+TypeScript 잘 쓰고 있니?- 제네릭, isSuccess 사용, 본문
나는 보통 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
https://tanstack.com/query/v5/docs/framework/react/typescript#typing-query-options
https://velog.io/@cnsrn1874/%EB%B2%88%EC%97%AD-Type-safe-React-Query
'React' 카테고리의 다른 글
NextJS에서 쿠키에 접근할 때 이 문제 겪은 사람..? - SSR과 CSR에서 동일한 함수로 쿠키 접근하기 (0) | 2024.03.26 |
---|---|
useEffect에서 의존성 배열의 경고를 무시하지마..! (1) | 2024.03.26 |
브라우저 렌더링 과정을 보면서 성능 최적화를 해보자 (1) | 2024.01.27 |
[2024-01-25] 개인 프로젝트 회고 (0) | 2024.01.25 |
React - 공식문서[1-8] 컴포넌트 순수성 유지 (0) | 2024.01.25 |