
프로젝트를 만들다 보면 정말 많은 상태와 마주하게 된다. 상태는 유저가 서비스에서 수행만 모든 동작의 결과가 데이터로 저장된 형태라고 볼 수 있다.
현재 프로젝트에서도 상태를 관리해주는 툴로 zustand를 사용하고 있다. 이 친구는 로그인 상태 / 다크 모드 상태 / 할일 토글 버튼 상태 등 프론트 단위에서 유저와 소통하는 상태가 메인을 이루고 있다.
이러한 상태 관리 툴의 예로는 Redux, Recoil등이 존재하는데 Tanstack Query는 서버 상태 관리라는 관점에서 위의 상태 관리와는 결이 다르다.
일단 이 친구가 하는 역할은 본질적으로 비동기 상태 관리이다.
우리가 서버와 통신을 할때 또는 리엑트에서 통신한 데이터를 관리해줄때 이 착한 친구가 상태 관리, refetching, 자동 업데이트, 에러 처리 등 만능이다. 기존에는 if문을 사용하여 error일때를 처리해주고 로딩중이거나 에러일때 상태를 따로 하나의 state로 저장해서 화면에 보여주는 것을 관리해줘야 했다.
그래서 적용해본 후기? 장점은?
이제 코드로 적용해보자. 할일을 GET / POST 하는 메인 페이지에 적용해보겠다.
우선 지금 코드는 어떤 상태인가?
interface TodoItem {
id: number;
task: string;
status: string;
user_id: number;
}
const getTodoList = async (userId: string) => {
try {
const response = await axios.get(`http://localhost:7777/todo/${userId}`);
console.log("response >>", response);
return response.data.result;
} catch (err) {
console.log("Err >>", err);
return [];
}
};
const deleteATodo = async (userId: string, todoId: number) => {
try {
const response = await axios.delete(
`http://localhost:7777/todo/${todoId}`,
{
data: { userId },
}
);
console.log("response >>", response);
return response.status === 200;
} catch (err) {
console.log("Err >>", err);
return false;
}
};
const MainPage = () => {
const location = useLocation();
const userData = location.state;
const [todos, setTodos] = useState<TodoItem[]>([]);
const { currentState } = useStateStore();
const fetchTodos = async () => {
const todoList = await getTodoList(userData.id);
setTodos(todoList);
};
const handleDelete = async (todoId: number) => {
const isDeleted = await deleteATodo(userData.id, todoId);
if (isDeleted) {
// 삭제에 성공하면 할일 목록을 다시 불러옴
fetchTodos();
}
};
const filteredTodos = todos.filter((todo) => {
if (currentState === "all") return true;
if (currentState === "completed") return todo.status === "completed";
if (currentState === "active") return todo.status === "active";
});
useEffect(() => {
fetchTodos();
}, []);
return (
// JSX
);
};
export default MainPage;
할일 목록 조회: 컴포넌트가 처음 렌더링될 때 useEffect 안에서 fetchTodos가 실행되어 서버에 GET 요청을 보내고, 할일 목록을 받아와 todos 상태에 저장.
할일 삭제: handleDelete 함수는 특정 할일을 삭제하고, 성공적으로 삭제되면 다시 fetchTodos를 호출해 최신 데이터를 가져옴.
수동으로 상태 업데이트 및 서버 데이터 요청을 관리해야 하며, 코드의 중복(예: 데이터 갱신을 위한 fetchTodos 재호출)이 발생할 수 있다.
캐싱이 따로 관리되지 않기 때문에 불필요한 서버 요청이 발생할 가능성이 높다.
코드가 길다. 더럽다. try...catch문 싫다..
⭐️ 이제 아름다운 Tanstack Query를 적용해보자~ ⭐️
우선 App.tsx 또는 최상위 컴포넌트에서 TanStack Query의 QueryClientProvider를 설정해준다.
// QueryClient 인스턴스 생성
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
);
}
이제 MainPage에서 useQuery와 useMutation을 사용하여 할 일 목록을 가져오고 삭제하는 로직을 구현한다.
그 전에 사용할 useQuery와 useMutation의 기본 구조와 역할에 대해서 학습한다.
useQuery : 데이터를 가져오는 로직을 관리해주는 훅으로 useQuery를 사용하면 API 호출을 통한 데이터 가져오기, 캐싱, 로딩 상태, 에러 처리 등을 간편하게 처리할 수 있다.
const { data, isLoading, error } = useQuery(queryKey, queryFn, options);
queryKey: 쿼리를 식별하는 키. 이 키를 기반으로 캐싱 및 리페치(재요청)를 관리
queryFn: 데이터를 가져오기 위한 비동기 함수. 이 함수가 실행되어 API 요청 등을 처리
options: 선택적인 옵션들로, 리페치 조건, 타임아웃, 초기 값 등을 설정
useMutation : 비동기적인 데이터 변경 작업(예: 데이터 추가, 수정, 삭제 등)을 처리하기 위해 사용. 이 훅을 사용하면 삭제 작업을 수행한 후 자동으로 쿼리 캐시를 업데이트하거나 관련된 데이터를 새로고침이 가능하다.
const mutation = useMutation("서버와 통신하는 함수", {
// 요청이 성공했을 때 실행되는 콜백
onSuccess: (data) => {},
// 성공 후 추가로 실행할 작업
// 요청이 실패했을 때 실행되는 콜백
onError: (error) => {},
// 요청이 진행 중일 때 실행되는 콜백
onMutate: (newTodo) => {},
// 요청이 끝났을 때 항상 실행되는 콜백 (성공, 실패 상관없이)
onSettled: () => {},
});
const handleCreateTodo = () => {
const newTodo = { task: 'Learn React Query', status: 'active' };
mutation.mutate(newTodo);
};
mutate
비동기 작업(서버 요청)을 실행하는 함수로 mutation.mutate(변수) 형태로 호출되며, 변수는 서버에 전달될 데이터
옵션 객체
onSuccess: 비동기 작업이 성공했을 때 실행되는 콜백 함수로 여기서 추가 작업 -> 데이터 갱신, UI 업데이트가 가능onError: 비동기 작업이 실패했을 때 실행되는 콜백onMutate: 비동기 작업이 실행되기 전에 호출onSettled: 성공 또는 실패와 상관없이 비동기 작업이 끝났을 때 항상 호출추가 상태 값
isLoading: 요청이 진행 중인지 여부를 나타냄
isError: 요청이 실패했는지 여부를 나타냄
isSuccess: 요청이 성공했는지 여부를 나타냄
이제 직접 코드에 적용해보면 다음과 같다.
interface TodoItem {
id: number;
task: string;
status: string;
user_id: number;
}
const getTodoList = async (userId: string) => {
const response = await axios.get(`http://localhost:7777/todo/${userId}`);
return response.data.result;
};
const deleteATodo = async (userId: string, todoId: number): Promise<void> => {
await axios.delete(`http://localhost:7777/todo/${todoId}`, {
data: { userId },
});
};
const MainPage = () => {
const location = useLocation();
const userData = location.state;
const { currentState } = useStateStore();
const queryClient = useQueryClient();
// 할일 목록 가져오기 (useQuery)
const {
data: todos = [],
isLoading,
isError,
refetch,
} = useQuery<TodoItem[]>({
queryKey: ["todos", userData.id],
queryFn: () => getTodoList(userData.id),
});
// 할일 삭제하기 (useMutation)
const deleteMutation = useMutation({
mutationFn: (todoId: number) => deleteATodo(userData.id, todoId),
onSuccess: () => {
// 성공하게 된다면 - 하단 추가 설명
queryClient.invalidateQueries({
queryKey: ["todos", userData.id],
});
},
});
// 삭제 호출 함수
const handleDelete = (todoId: number) => {
deleteMutation.mutate(todoId);
};
const filteredTodos = todos.filter((todo) => {
if (currentState === "all") return true;
if (currentState === "completed") return todo.status === "completed";
if (currentState === "active") return todo.status === "active";
});
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>에러 발생</div>;
return (
// JSX code
)
};
export default MainPage;
deleteMutation 함수에서 onSuccess 하게 된다면 queryClient.invalidateQueries를 통해 queryKey를 가진 쿼리를 무효화해주고, 이 쿼리의 데이터가 더 이상 최신이 아니라고 표시한다.
다음으로 자동 재요청을 통해 쿼리가 무효화되면, 해당 데이터를 사용하는 컴포넌트가 다시 렌더링되거나 포커스가 되었을 때, TanStack Query는 자동으로 해당 데이터를 서버로부터 다시 요청하여 UI는 항상 최신 상태의 데이터를 반영해준다.
기존 방식에서 Tanstack Query를 사용해서 서버와의 비동기적 통신을 관리해보았다. 기존 방식과 비교하여 느낀 장점은 다음과 같다.
기존 방식
- 데이터를 직접적으로 페칭하고 로딩 상태, 에러 상태, 성공 상태 등을 모두 수동으로 관리해야 하는 불편함이 추가적인 코드와 복잡한 로직으로 이어지는 경우가 다수
TanStack Query
- useQuery로 로딩 상태(isLoading), 에러 상태(isError), 성공 상태 등을 자동으로 관리해서 개발자는 데이터의 생명주기를 신경 쓸 필요 없이 훨씬 단순한 코드로 비동기 로직을 작성
queryKey: ["todos", userData.id]를 통해 해당 사용자에 대한 할일 목록을 캐싱해줘서 다른 곳에서 같은 데이터를 요청하더라도 이미 캐시된 데이터가 있으면 추가 요청 없이 캐시에서 데이터를 반환하여 네트워크 트래픽을 줄이고 UI 렌더링 속도를 향상시킨다.
invalidateQueries를 사용하여 할일을 삭제할 때 자동으로 데이터가 동기화되도록 처리 할일을 삭제하면 기존 데이터가 무효화되고, 쿼리가 다시 실행되어 최신 할일 목록을 가져온다.
isError상태를 통해 API 요청 중 에러가 발생했을 때 처리
- 데이터 페칭 및 상태 관리를 컴포넌트 내에서 독립적으로 처리하여 중앙 상태 관리를 이용할 필요 없이 각 컴포넌트가 필요한 데이터를 쿼리를 통해 직접 관리하고 공유 가능
이것이... 라이브러리의 힘인가 싶었다.
라이브러리를 학습하고 올바르게 사용하는 것은 좋은 개발자가 되는데 유익하다고 생각한다.
무지성 복붙이 아닌 필요한 상황에 적절하게 사용해주면 아주 플러스가 된다.
Main 페이지 이외의 페이지에도 적용해보면서 익숙해져봐야겠다.