타입스크립트를 사용하는건 좋은 생각이다. 타입 안전을 싫어하는 사람이 있을까?
버그를 미리 알 수 있고, 복잡한 앱의 구조를 타입으로 정의하여 그 부분들은 우리 머리 속에 담아둘 필요가 없어진다.
"타입을 선언"한 것과 "타입 안전"한 것은 큰 차이가 있다.
타입스크립트의 진정한 힘을 이용하려면 한 가지가 더 필요하다.
신뢰하기
우리는 타입 정의를 신뢰할 수 있어야한다.
그렇지 않으면 타입은 단순 제안에 불과하고, 정확하다고 믿을 수 없다.
- 가장 엄격한 타입스크립트 설정을 활성화하자.
- typescript-eslint를 추가하여 `any`타입과 `ts-ignore`를 금지하자.
- 타입 단언을 지양하자.
제네릭
타입스크립트에서 제네릭은 필수적이다.
조금이라도 복잡한 걸 구현하려면 제네릭을 사용해야할 것이다. (라이브러리)
하지만 라이브러리 사용자로서 이상적인 것은 제네릭에 대해서 신경쓰지 않는 것이다.
제네릭은 그저 구현 세부사항이다.
제네릭을 수동으로 작성하는 것은 두 가지 이유 중 하나로 인해 좋지 않다.
1. 사실 불필요하다.
2. 스스로를 속이고 있는 것이다.
꺾쇠괄호에 대해(<>)
꺾쇠괄호는 우리 코드를 더 복잡해 보이게 만든다.
예를 들어, 우리는 useQuery를 이렇게 사용하곤 한다
type Todo = { id: number; name: string; done: boolean }
const fetchTodo = async (id: number) => {
const response = await axios.get(`/todos/${id}`)
return response.data
}
const query = useQuery<Todo>({ // 제네릭으로 Todo 타입을 넣어주었다
queryKey: ['todos', id],
queryFn: fetchTodo,
})
query.data
// ^?(property) data: Todo | undefined
useQuery가 네 개의 제네릭을 가지고 있다는 것이 문제가 된다.
수동으로 하나만 제공하면 나머지 세 개는 기본값으로 돌아간다. 이게 왜 나쁜지는 #6: React Query and TypeScript에 나와있다.
`axios.get`은 any를 반환한다.(`fetch`도 마찬가지고, `ky`는 기본적으로 unknown을 반환한다)
`/todos/id`에서 무엇을 반환할지 모르고, data 속성도 any가 되길 원하지 않기 때문에 제네릭을 이용해 타입을 덮어써야한다.
하지만 더 나은 방법은 `fetchTodo` 함수 자체에 타입을 지정하는 것이다.
type Todo = { id: number; name: string; done: boolean }
// ✅ typing the return value of fetchTodo
const fetchTodo = async (id: number): Promise<Todo> => {
const response = await axios.get(`/todos/${id}`)
return response.data
}
// 또는 axios.get에 제네릭을 바로 제공할 수 있다.
const fetchTodo = async (id: number) => {
const response = await axios.get<Todo>(`/todos/${id}`)
return response.data
}
// ✅ no generics on useQuery
const query = useQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
})
// 🙌 types are still properly inferred
query.data
// ^?(property) data: Todo | undefined
이제 React Query는 queryFn의 결과의 data가 무엇일지 제대로 추론할 수 있다.
제네릭의 중요 규칙
Effective TypeScript에서 언급되는 golden rule of Generics 이 있다.
For a Generic to be useful, it must appear at least twice.
(제네릭은 유용하다, 다만 적어도 두 번 이상 쓰일때만)
즉 "return-only" 제네릭은 타입 어설션과 다를게 없다는 뜻이다.
다음은 `axios.get`의 예시이다.
function get<T = any>(url: string): Promise<{ data: T, status: number}>
제네릭 T가 리턴타입 Promise에만 사용되고 있다.
그럼 사실 이렇게 쓸 수도 있다.
const fetchTodo = async (id: number) => {
const response = await axios.get(`/todos/${id}`)
return response.data as Todo // type assertion
}
적어도 타입 어셜션(`as Todo`)는 숨겨져 있지 않고 드러나있다.
다시, 신뢰하기
그리고 이제 다시 "신뢰"에 대해 생각해보자.
우리(프론트엔드)는 백엔드로부터 받는 정보가 실제로 그 타입인지 어떻게 믿을 수 있을까?
이 현상을 "신뢰 경계"라고 부르곤한다.
리턴 타입은 백엔드와 합의한 부분이기 때문에 믿고 작업할 수 밖에 없다.
만약 타입과 다른 데이터가 들어와서 문제가 생긴다면, 그건 우리(프론트엔드)의 문제가 아니다. - 문제는 백엔드 팀에게 있다.
물론 사용자는 신경쓰지 않는다.
그들은 단지 "cannot read property name of undefined"와 같은 에러 메시지를 볼 뿐이다.
프론트엔드 개발자로서 그 에러를 통해 백엔드로 부터 오는 데이터 타입이 잘못되었다는 사실을 알아채기는 꽤 오랜 시간이 걸린다.
그럼 백엔드에서 오는 데이터 타입에 대한 신뢰를 얻기위해 어떤 일을 할 수 있을까?
zod
zod는 런타임에 유효성 검사를 하는 스키마를 정의할 수 있는 검증 라이브러리이다.
또한 선언한 스키마로부터 타입을 추론(추출)할 수 있다.
타입을 선언한 다음, 데이터가 그 타입이라고 단언하는 대신
스키마를 작성하고 데이터가 그 스키마를 만족하는지 검증한다.
queryFn 내부에서 검증
import { z } from 'zod'
// 스키마 정의
const todoSchema = z.object({
id: z.number(),
name: z.string(),
done: z.boolean(),
})
const fetchTodo = async (id: number) => {
const response = await axios.get(`/todos/${id}`)
// 스키마를 이용해 데이터 파싱
return todoSchema.parse(response.data)
}
const query = useQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
})
이전 코드와 비교해서 두 가지가 바뀌었다.
- Todo 타입 선언 -> todoSchema 스키마 선언
- 타입 단언 -> 스키마를 이용한 데이터 파싱
파싱(todoSchema.parse)에 문제가 생기면 zod에서 에러를 던진다.
그럼 우리는 그 에러를 처리해야하는 코드를 작성해야한다.
Tradeoffs
스키마 파싱은 데이터가 스키마와 일치하는지 런타임에 분석하기 때문에, 오버헤드가 발생한다.
따라서 이 방식을 모든 곳에 적용하는건 적절하지 않을 수 있다.
결론
useQuery의 제네릭을 쓰지 말고
queryFn에 넣을 fetch함수에 리턴 타입을 정의하자. (런타임에 타입 검증을 하고싶으면 zod를 쓰자)
더하여 queryOptions API를 함께 사용하면 queryClient.getQueryData와 같은 메서드의 리턴 타입 추론을 잘 할 수 있다.
이 글은 https://tkdodo.eu/blog/type-safe-react-query 를 번역 & 정리한 글입니다.
'React Query' 카테고리의 다른 글
9. Mutation 이후 전체 Query를 invalidation 하기 (0) | 2024.08.30 |
---|---|
7. Query Options API (0) | 2024.07.31 |
6. React Query가 필요한 이유 (0) | 2024.07.21 |
5. React Query의 Status (0) | 2024.02.21 |
4. 효율적으로 React Query Key 선언하기 (5) | 2024.02.14 |