이번 글에서는 TanStack Query를 활용하여 Todo List를 구현한 후 리팩토링한 부분에 대해서 정리해보고자 합니다.
1. 상위로 타입을 끌어올리기(타입 추론의 이점)
직전 방식
기존에는 useQuery에서 직접 타입을 명시하여 데이터를 가져오는 방식이었습니다. 이때, useQuery 훅에 타입을 지정하여 반환 데이터를 타입스크립트가 알 수 있도록 했습니다.
const { data } = useQuery<Todo[]>({
queryKey: ['todos'],
queryFn: getTodos,
});
변경된 방식
상위에서 타입을 한 번만 지정해주고, useQuery나 useMutation에서는 별도로 타입을 명시하지 않는 방식으로 변경하였습니다. 이제, 데이터 통신 로직인 getTodos 함수에서 타입을 한 번 지정하면, useQuery 훅에서 타입을 자동으로 추론할 수 있습니다.
export const getTodos = async () => {
const res = await fetch(`${BASE_URL}/todos?_page=1&_per_page=25`);
if (!res.ok) {
throw new Error("Failed to fetch todos");
}
const data: Paginate<Todo> = await res.json();
return data.data;
};
2. mutation에서 Omit을 활용한 타입 처리
직전 방식
useMutation을 사용할 때, mutationFn에서 받아오는 데이터 타입을 명확하게 처리하지 않으면, 불필요한 속성이나 잘못된 값이 들어갈 수 있습니다. 이전에는 Todo 타입을 그대로 사용했지만, 특정 필드가 필요 없을 때는 Omit을 활용하여 불필요한 필드를 제외한 타입을 만들어주는 방식으로 개선하였습니다.
export const toggleTodo = async ({ id: string, completed: boolean }) => {
const res = await fetch(`${BASE_URL}/todos/${id}`, {
method: "PATCH",
body: JSON.stringify({
completed: !completed,
}),
});
if (!res.ok) {
throw new Error("Failed to update todos");
}
};
변경된 방식
Omit을 사용하여 mutationFn에 필요한 인자의 타입만 전달하는 방식으로 수정했습니다. text를 제외하여 완료 기능에 필요하지 않은 text를 제외할 수 있습니다.
export const toggleTodo = async ({ id, completed }: Omit<Todo, "text">) => {
const res = await fetch(`${BASE_URL}/todos/${id}`, {
method: "PATCH",
body: JSON.stringify({
completed: !completed,
}),
});
if (!res.ok) {
throw new Error("Failed to update todos");
}
};
3. 타입스크립트에서 전역 타입 관리: 필요한 타입만 공유하고 나머지는 로컬에서 처리
직전 방식
모든 Todo에 관련된 타입을 todoType.ts에서 작업하였습니다.
export type Todo = {
id: string;
text: string;
completed: boolean;
};
export type TodoItemProps = Todo;
export type Paginate<T> = {
data: T[];
first: number;
items: number;
last: number;
next: number | null;
pages: number;
prev: number | null;
};
변경된 방식
전역으로 관리해야 할 타입과 로컬에서 관리해야 할 타입으로 구분하였습니다.
전역으로 관리해야 할 타입
- Todo: id, text, completed 속성으로 이루어진 기본적인 Todo 객체를 정의합니다. 이는 여러 컴포넌트에서 공통으로 사용되므로 전역 타입으로 관리하는 것이 합리적입니다.
- Paginate<T>: 제너릭 타입으로, 페이징 처리된 데이터를 관리하는데 사용됩니다. data, first, last, next, prev 등의 필드를 포함하여 페이징에 필요한 정보를 포함하고 있습니다. 이 타입 역시 전역적으로 필요한 타입으로 관리하는 것이 유리합니다.
로컬에서 관리할 타입
TodoItemProps(type Todo를 사용해도 되지만 예시를 위해 생성하였습니다.)는 특정 파일이나 컴포넌트에서만 사용되므로, 각 파일 내에서 해당 타입을 정의하여 사용하는 것이 더 직관적이고 가독성을 높입니다.
import { Todo } from "../types/todoTypes";
type TodoItemProps = Todo;