-
들어가며
-
Tanstack Query(React Query) 개요
-
기본 설정
-
useQuery
-
queryKey
-
queryFn
-
enabled ?
-
deprecated 된 옵션들
-
useQuery의 return data
-
prefetchQuery
-
useInfiniteQuery
-
useMutation
-
useMutation 이랑 mutate 의 차이는?
-
쿼리 무효화
-
쿼리 무효화를 하는 다른 방법
-
useQueryErrorResetBoundary
-
ErrorBoundary
-
useQueryErrorResetBoundary
-
Suspense
-
TypeScript 의 제네릭이 적용되는 경우
-
useQuery
-
useMutation
-
마치며

들어가며
마지막 사이드 프로젝트 때 Tanstack Query를 사용했었고, 마음에 들어서 외주 프로젝트와 이후 사이드 프로젝트에도 Tanstack Query를 도입했었습니다.
Tanstack Query를 쓰면서 '이런 기능이 있나? 있으면 좋을 것 같은데' 라고 생각했던 것들이 많았는데, 실제로 찾아보니 이미 있는 경우가 대부분이었고, 미리 한번 전체적으로 공식 문서를 정독했다면 두 번 찾아보지 않아도 되었겠다-라는 생각을 하게 되었어요.
그런 의미로, 이번 포스팅에서는 Tanstack Query에서 자주 쓰일 법한 개념들을 한번 전체적으로 정리하는 시간을 가져보도록 하겠습니다.
Tanstack Query(React Query) 개요
Tanstack Query는 서버 상태 데이터를 가져오고, 캐싱, 동기화 및 업데이트를 쉽게 다룰 수 있도록 도와주는 라이브러리입니다.
예전에는 클라이언트 상태와 서버 상태를 따로 구분짓지 않고, 하나로 취급했다면, 이 두 범위를 좀 더 명확히 나누고자 나온 라이브러리라고 할 수 있을 것 같아요.
이전 포스팅에서 클라이언트 상태와 서버 상태에 대해 다룬 적이 있는데, 궁금하신 분들은 아래 포스팅을 참고하셔도 좋을 것 같아요.
https://dawonny.tistory.com/491
클라이언트 상태(Client State)와 서버 상태(Server State)
tanstack query(react) 의 개요를 찾아보다가, 이 tanstack-query 가 리액트 애플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 다룰 수 있도록 도와주는 라이브러리이며 클라이언트
dawonny.tistory.com
주요 기능으로는 서버에서 받아온 응답을 캐싱하고, 중복 요청을 단일 요청으로 통합하고, 페이지네이션 및 lazy loading과 같은 성능을 최적화해 준다고 해요.
원래 같았으면 직접 구현해야 할 로직들을 제공해주니 편리하게 사용할 수 있어요.
기본 설정
Tanstack Query를 처음에 적용할 때 추가해야할 것들이 몇가지 있는데요.
일단 App.js에 있는 최상단 요소를 QueryClientProvider로 감싸주어야 합니다.
그리고 QueryClient의 인스턴스를 client라는 props로 넣어서 애플리케이션과 연결을 해주어야 해요.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({ /* options */});
function App() {
return (
<QueryClientProvider client={queryClient}>
<div>블라블라</div>
</QueryClientProvider>;
);
}
위와 같이 감싸고 나면 이 context는 앱에서 비동기 요청을 알아서 처리하는 background 계층이 됩니다.
쉽게 말하자면, 우리 앱의 모든 컴포넌트에서 QueryClient 인스턴스에 접근할 수 있게 되고, 이 인스턴스가 서버 데이터 요청, 캐싱, 상태 관리 등을 처리해 주는 중앙 관리소 역할을 하게 돼요.
useQuery
query는 서버에서 데이터를 fetch하는 경우 Promise 기반의 메서드(GET, POST)들과 같이 사용될 수 있어요.
useQuery는 하나의 객체 인자만 받으며, queryKey랑 queryFn은 필수예요.
const result = useQuery({
queryKey, // required, 의미: 쿼리를 식별하는 고유한 키
queryFn, // required, 의미: 데이터를 가져오는 비동기 함수
refetchOnMount, // mount 마다 refetch 를 실행
refetchInterval, // 일정 시간마다 자동으로 refetch 를 실행
refetchIntervalInBackground, // refetchInterval 와 함께 사용하는 옵션
enabled, // 쿼리가 자동으로 실행되지 않도록 할 때 설정
select, // 반환된 데이터의 일부를 변환하거나 선택
placeholderData, // query 가 pending 상태일 때 보여줄 가짜 데이터
// ...options ex) gcTime, staleTime, ...
});
result.data;
result.isLoading;
result.refetch;
// ...
queryKey
queryKey는 배열의 형태이며, 이 값들은 쿼리 캐싱의 고유한 기준이 되어요.
이후에 이 queryKey는 쿼리를 다시 가져오고, 캐싱하고, 공유하는데 내부적으로 사용됩니다.
// 기본 사용법
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ID로 특정 항목 조회
useQuery({ queryKey: ['todo', 5], queryFn: () => fetchTodoById(5) })
// 필터링된 쿼리
useQuery({
queryKey: ['todos', { status, page }],
queryFn: () => fetchTodos(status, page)
})
queryFn
queryFn에는 Promise를 반환하는 함수가 들어가는데요.
이 함수는 실제로 서버에서 데이터를 가져오는 비동기 함수예요.
Axios, Fetch API 등을 사용하여 HTTP 요청을 보내는 함수를 여기에 넣을 수 있어요.
queryKey의 값들은 이 함수 내에서 매개변수로 사용될 수 있어요.
useQuery({
queryKey: ['users', userId],
queryFn: async () => {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
}
})
enabled ?
boolean 값으로 설정할 수 있는 enabled는, ‘쿼리가 자동으로 실행되지 않도록 할 때 설정’할 수 있는 값이라고 하는데요.
보통 쿼리를 다시 가져오는 방법으로 invalidateQueries 나 refetchQueries 가 있는데, enabled 값을 false 로 하면 이 방법들이 다 무효화 된다고 하더라구요.
그런데 쿼리가 자동으로 실행되는 상황이 뭐가 있길래 그렇지? 라는 생각이 들었어요.
보통 컴포넌트가 마운트될 때, 창이 다시 포커스될 때, 네트워크 재연결 시, 리페칭 간격이 설정된 경우, queryKey가 변경되는 경우와 같은 상황에서 쿼리가 자동으로 실행돼요.
그리고 ‘종속 쿼리’가 존재하는 상황에서 유용하게 쓰일 수 있어요.
예를 들어서 2개의 쿼리가 있는데, 특정 쿼리가 또 다른 쿼리보다 항상 사전에 완료되어야한다고 가정한다면, 사전에 완료되어야 할 쿼리의 결과 데이터가 있을 때에만 이 후 쿼리의 enabled를 활성화 시켜줌으로써 구현할 수 있겠죠.
// 사전에 완료되어야 할 쿼리
const { data: user } = useQuery({
queryKey: ["user", email],
queryFn: () => getUserByEmail(email),
});
const channelId = user?.data.channelId;
// user 쿼리에 종속 쿼리
const { data: courses } = useQuery({
queryKey: ["courses", channelId],
queryFn: () => getCoursesByChannelId(channelId),
enabled: !!channelId,
});
deprecated 된 옵션들
v4 까지 onSuccess, onError, onSettled Callback 이 useQuery의 옵션에 존재했었는데, Deprecated 가 되었다고 합니다.
대신 useMutation 에서는 아직 사용이 가능하다고 하네요.
useQuery의 return data
useQuery 가 return 하는 데이터들은 굉장히 많은데, 주요한 것만 짚어보겠습니다.
const {
data, // useQuery 가 반환한 Promise 에서 resolved 된 데이터
error, // 오류 발생한 경우, query 에 대한 오류 객체
status, // pending/error/success 와 같은 3가지의 값으로 존재
isLoading, // 캐싱된 데이터가 없을 때 true/false 중 하나
isFetching, // 캐싱된 데이터 있더라도 true/false 중 하나
isError, // 요청 중 에러 발생한 경우 true
refetch, // query 를 수동으로 다시 가져오는 함수
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
prefetchQuery
만약에 1페이지 → 2페이지로 이동하는 상황에서, 3페이지를 미리 로드해오면 어떨까요?
사용자가 3페이지를 방문했을 때 새로 로드를 시작할 필요가 없기 때문에 좀 더 좋은 사용자 경험이 생길 것입니다.
이런 경우를 위해 queryClient에 prefetchQuery 의 기능도 존재합니다.
const prefetchNextPosts = async (nextPage: number) => {
const queryClient = useQueryClient();
// 해당 쿼리의 결과는 일반 쿼리들처럼 캐싱 된다.
await queryClient.prefetchQuery({
queryKey: ["posts", nextPage],
queryFn: () => fetchPosts(nextPage),
// ...options
});
};
// 단순 예
useEffect(() => {
const nextPage = currentPage + 1;
if (nextPage < maxPage) {
prefetchNextPosts(nextPage);
}
}, [currentPage]);
useInfiniteQuery
무한 스크롤 또는 데이터를 추가로 로드하는 경우에 무한 쿼리를 사용할 수도 있습니다.
const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["colors"],
queryFn: fetchColors,
initialPageParam: 1,
// (lastPage, allPages) => (가장 최근 가져온 페이지 목록, 현재까지 가져온 모든 페이지 데이터)
getNextPageParam: (lastPage, allPages) => {
return allPages.length < 4 && allPages.length + 1;
},
// ...
});
useQuery 와 비슷하게 queryKey, queryFn 을 받는 것은 비슷한데요.
반환 값으로 isFetchingNextPage, isFetchingPreviousPage, fetchNextPage, fetchPreviousPage, hasNextPage 등이 추가적으로 존재해요.
initialPageParam 은 첫 페이지를 가져올 때 사용할 기본 페이지를 의미하며 필수값이고,
getNextPageParam 을 사용해서 페이지를 증가시킬 수 있어요.
useMutation
기본적으로 GET 은 useQuery 를 사용한다면, POST/PATCH/PUT/DELETE 의 경우에는 useMutation 을 사용해요.
const mutation = useMutation({
mutationFn: createTodo,
onMutate() {
/* ... */
},
onSuccess(data) {
console.log(data);
},
onError(err) {
console.log(err);
},
onSettled() {
/* ... */
},
});
const onCreateTodo = (e) => {
e.preventDefault();
mutation.mutate({ title });
};
mutation 객체는 useMuation 의 반환 값인데요.
이 객체의 mutate 메서드를 이용해서 요청함수를 호출할 수 있어요.
위 예시 코드를 설명하자면, useMutation 함수로 mutation 객체를 생성한 후, 실제 서버 요청이 필요한 시점에 mutation.mutate 메서드를 호출해서 데이터를 전송해요. mutationFn으로 지정한 createTodo 함수는 실제 API 호출을 담당하고, onSuccess, onError 등의 콜백은 API 호출 결과에 따라 실행되는 로직을 정의해요.
useMutation 이랑 mutate 의 차이는?
useMutation은 mutation을 정의하는 훅이고, mutate는 실제로 그 mutation을 실행하는 메서드예요.
useMutation을 호출하면 mutation 객체가 반환되고, 이 객체에는 mutate와 mutateAsync 메서드가 포함되어 있어요.
- mutate: 비동기 함수를 실행하고 결과를 콜백(onSuccess, onError 등)으로 처리해요.
- mutateAsync: Promise를 반환하기 때문에 await 키워드와 함께 사용할 수 있어요.
// mutate 사용 (콜백 기반)
mutation.mutate(newTodo, {
onSuccess: (data) => {
console.log('성공:', data);
},
onError: (error) => {
console.error('오류:', error);
}
});
// mutateAsync 사용 (Promise 기반)
try {
const data = await mutation.mutateAsync(newTodo);
console.log('성공:', data);
} catch (error) {
console.error('오류:', error);
}
쿼리 무효화
화면을 최신 상태로 유지하려면 기존의 쿼리를 쓰는 것이 아니라, 무효화하고 새로 상태를 받아와야할 거예요(refetch).
이럴 때 쓸 수 있는 개념이 쿼리 무효화입니다.
invalidateQuries 를 사용하는 방식이 가장 간단한데요.
아래와 같이 사용할 수 있습니다.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation(addSuperHero, {
onSuccess(data) {
queryClient.invalidateQueries({ queryKey: ["super-heroes"] }); // 이 key에 해당하는 쿼리가 무효화!
console.log(data);
},
onError(err) {
console.log(err);
},
});
};
위 예시에서는 useMutation 이 성공했을 때 queryClient.invalidateQueries 를 사용해서 ["super-heroes"] 라는 key에 해당하는 쿼리를 무효화 시키고 있어요.
쿼리 무효화를 하는 다른 방법
queryClient.invalidateQueries 외에 queryClient.setQueryData 를 사용하는 방법도 존재해요.
똑같이 쿼리의 캐시 데이터를 즉시 업데이트 한다는 점에서 비슷한 역할을 하는데, 차이점이 있다면 invalidateQueries는 데이터를 실제로 다시 가져오는 반면, setQueryData는 네트워크 요청 없이 캐시 데이터만 직접 업데이트한다는 점이에요. 낙관적 업데이트(Optimistic Updates)를 구현할 때 자주 사용돼요.
사용하는 예시는 다음과 같습니다.
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addSuperHero,
onSuccess(data) {
queryClient.setQueryData(["super-heroes"], (oldData: any) => {
return {
...oldData,
data: [...oldData.data, data.data],
};
});
},
onError(err) {
console.log(err);
},
});
};
코드를 보면 2번째 인자로 ‘업데이트 함수'를 전달하고있어요.
이 업데이트 함수는 기존 캐시 데이터(oldData)를 받아서 새 데이터를 반환하는 함수예요.
예시에서는 기존 데이터를 복사한 후 새로운 데이터를 추가하는 방식으로 구현되어 있습니다.
이런 방식으로 서버에서 데이터를 다시 가져오지 않고도 캐시를 최신 상태로 유지할 수 있어요.
UI 업데이트가 즉시 반영되므로 사용자 경험을 향상시킬 수 있습니다.
invalidateQueries 와 setQueryData의 차이 정리
invalidateQueries
쿼리를 '오래된(stale)' 상태로 표시하고, 다음 렌더링 시 서버에서 새로운 데이터를 가져오도록 합니다.
즉, 실제로 새 네트워크 요청이 발생합니다.
setQueryData
네트워크 요청 없이 직접 캐시 데이터를 수정합니다. 서버와 통신하지 않고 UI를 즉시 업데이트할 수 있습니다.
useQueryErrorResetBoundary
useQueryErrorResetBoundary 에 대해 얘기하기전에, 먼저 ErrorBoundary 를 간단히 정리해볼게요.
ErrorBoundary
react 에서 ErrorBoundary 는 하위 구성 요소 트리의 위치에서 JavaScript 에러를 감지하는 컴포넌트입니다.
초기 렌더링, 이벤트 핸들러 내부 오류, 비동기 코드 등에서는 오류를 감지하지 않아요.
ErrorBoundary 는 트리에서 하위 구성 요소의 오류만 감지합니다.
문제가 생긴 컴포넌트 대신에 Fallback UI를 표시하기 위해 선언적으로 작성할 수 있어요.
여기서 선언형이란, 결과물에만 집중하고 복잡한 과정은 추상화하는 것을 말해요.
// 리액트
<QueryErrorBoundary
fallback={
<DefaultFallback onResetAction={() => location.replace("/start")} />
}
>
{/* ... */}
</QueryErrorBoundary>
예를 들어 위 코드를 봤을 때, 우리는 에러가 발생했을 때 DefaultFallback 을 보여주겠구나-하고 이해할 수 있어요.
QueryErrorBoundary 안에서 어떤 로직이 동작하게 되는지 신경쓰지 않아요.
이렇게 내부의 복잡성을 추상화 하는 것을 선언형이라고 합니다.
선언형의 반대 개념은 명령형이에요.
명령형 프로그래밍은 결과물 보다는 과정이 중요하기 때문에, 코드를 한 줄 씩 읽어나가면서 다음에 어떤일이 발생할지 추측해야해요.
useQueryErrorResetBoundary
useQueryErrorResetBoundary는 위에서 언급한 ErrorBoundary와 함께 쓰여요.
ErrorBoundary 와 useQueryErrorResetBoundary 를 결합해서, 선언적으로 에러가 발생했을 때 Fallback UI를 보여줄 수 있어요.
기본적으로 공식 문서에서도 react-error-boundary 라는 라이브러리를 사용하는데요.
사용 방법을 먼저 코드로 보겠습니다.
import { useQueryErrorResetBoundary } from "@tanstack/react-query"; // (*)
import { ErrorBoundary } from "react-error-boundary"; // (*)
interface Props {
children: React.ReactNode;
}
const QueryErrorBoundary = ({ children }: Props) => {
const { reset } = useQueryErrorResetBoundary(); // (*)
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
Error!!
<button onClick={() => resetErrorBoundary()}>Try again</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
};
export default QueryErrorBoundary;
일단 useQueryErrorResetBoundary 훅에서 reset 함수를 가져와줍니다.
그리고 ErrorBoundary에 onReset 에 reset 함수를 전달해주는데요.
만약 에러가 발생하면 ErrorBoundary의 fallbackRender 로 넘긴 내용이 렌더링되고, 에러가 없다면 children 이 렌더링됩니다.
참고로 fallbackRender 로 전달되는 콜백 함수의 인자를 넣어줄 때 구조 분해 할당을 통해서 resetErrorBoundary 를 가져올 수 있는데요.
이 resetErrorBoundary 는 모든 쿼리 에러를 reset(초기화)하는 기능을 할 수 있습니다.
따라서 위 예시에서는 에러가 생겼을 때에만 보이는 버튼을 클릭했을 때 쿼리 에러를 초기화하도록 하고 있네요.
추가로, 필수로 해야해줘야하는 작업이 하나있습니다.
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import QueryErrorBoundary from "./components/ErrorBoundary"; // (*)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true, // (*) 여기서는 글로벌로 세팅했지만, 개별 쿼리로 세팅 가능
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<QueryErrorBoundary>{/* 하위 컴포넌트들 */}</QueryErrorBoundary>
</QueryClientProvider>
);
}
App.js 에 QueryErrorBoundary를 추가해줘야하는데요.
이 때 queryClient 옵션에 throwOnError: true 를 추가 해주어야합니다.
ErrorBoundary 컴포넌트가 오류를 감지할 수 있도록 해주는 작업이에요.
Suspense
위에서 ErrorBoundary 에 대해 설명드렸는데요.
Suspense 와 ErrorBoundary 를 결합해서 서버 통신 상태가 loading 중일 때에 Fallback UI를 보여줄 수 있게 선언적으로 작성할 수도 있어요.
Suspense의 개념은 컴포넌트가 무언가를 로딩 중일 때 대체 UI를 보여줄 수 있게 해주는 React의 기능이에요.
데이터 불러오기, 코드 분할 등으로 인한 지연 시간 동안 로딩 UI를 쉽게 표시할 수 있어요.
이 Suspense를 사용하는 이유는 데이터 로딩 상태를 더 선언적으로 처리할 수 있고, 로딩 상태 관리 로직을 컴포넌트에서 분리해 코드를 더 깔끔하게 유지할 수 있기 때문이에요.
또한 여러 비동기 작업이 동시에 진행될 때 모든 작업이 완료될 때까지 일관된 로딩 UI를 보여줄 수 있어요.
Suspense 를 사용하는 예시를 아래에서 같이 보시겠습니다.
import { Suspense } from "react";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// suspense: true, - 💡 v5부터 Deprecated
// useQuery/useInfiniteQuery와 같은 일반 훅 대신 useSuspenseQuery/useSuspenseInfiniteQuery와 같은 suspense 훅 사용
throwOnError: true,
},
},
});
function App() {
return (
<QueryErrorBoundary>
<Suspense fallback={<Loader />}>{/* 하위 컴포넌트들 */}</Suspense>
</QueryErrorBoundary>;
);
}
App 컴포넌트를 보시면 하위 컴포넌트들을 보여주기 전에(로딩 중일 때) Suspense 컴포넌트를 사용해서 Loader 라는 컴포넌트를 보여주겠다-라는 의도를 확인할 수 있어요.
tanstack query 에서는 Suspense 를 타입 세이프하게 사용하기 위해서 useSuspenseQuery, useSuspenseInfiniteQuery, useSuspenseQueries 라는 훅들을 사용할 수 있는 @suspensive/react-query 라는 라이브러리를 소개하는데요.
‘타입 세이프’ 해지는 이유는, 이 3가지 훅을 사용했을 때에는 타입 레벨에서 data 가 undefined 되지 않기 때문이에요.
위처럼 Suspense 컴포넌트를 사용하는 것과 이 훅들을 사용하는 것에는 다음과 같은 차이가 있어요.
- Suspense 컴포넌트만 사용하는 경우: React의 기본 기능을 활용하지만, undefined 체크를 계속 해야 하는 불편함이 있어요.
- 전용 Suspense 훅을 사용하는 경우: 타입스크립트에서 data가 항상 존재한다고 보장받을 수 있어 타입 안전성이 높아져요.
TypeScript 의 제네릭이 적용되는 경우
tanstack query 를 사용할 때에는 typescript 의 제네릭 문법이 많이 사용됩니다.
실제로 쓰일 때에는 API 가 반환하는 데이터의 유형을 모르는 경우가 더 많기 때문이에요.
위에서 살펴보았던 훅들에는 제네릭이 설정되어있어요.
useQuery
// useQuery의 타입
export function useQuery<
TQueryFnData = unknown, // 실행 결과의 타입
TError = DefaultError, // 에러 형식
TData = TQueryFnData, // data 에 담기는 실질적인 데이터의 타입
TQueryKey extends QueryKey = QueryKey // queryKey의 타입(명시적으로 지정해줄 때)
>
import { AxiosError } from "axios";
// useQuery 타입 적용 예시
const { data } = useQuery<
AxiosResponse<Hero[]>,
AxiosError,
string[],
["super-heroes", number]
>({
queryKey: ["super-heroes", id],
queryFn: getSuperHero,
select: (data) => {
const superHeroNames = data.data.map((hero) => hero.name);
return superHeroNames;
},
});
/**
주요 타입
* data: string[] | undefined
* error: AxiosError<any, any>
* select: (data: AxiosResponse<Hero[]>): string[]
*/
useMutation
export function useMutation<
TData = unknown, // 실행 결과의 타입
TError = DefaultError, // 에러 형식
TVariables = void, // mutate 함수의 인자
TContext = unknown // onMutate의 return 값
>
onMutate은 muataionFn 실행 전에 실행되며, 다음과 같은 상황에 사용됩니다.
- Optimistic UI(낙관적 UI) 처럼 API 요청 전에 UI를 먼저 변경하는 경우
- 실패 시 복구할 수 있도록 이전 상태를 저장하는 경우
// useMutation 타입 적용 예시
const { mutate } = useMutation<Todo, AxiosError, number, number>({
mutationFn: postTodo,
onSuccess: (res, id, nextId) => {},
onError: (err, id, nextId) => {},
onMutate: (id) => id + 1,
onSettled: (res, err, id, nextId) => {},
});
const onClick = () => {
mutate(5);
};
/**
주요 타입
* data: Todo
* error: AxiosError<any, any>
* onSuccess: (res: Todo, id: number, nextId: number)
* onError: (err: AxiosError, id: number, nextId: number)
* onMutate: (id: number)
* onSettled: (res: Todo, err: AxiosError, id: number, nextId: number),
*/
마치며
Tanstack Query 의 여러 기능 중에서도 주로 많이 쓰일 법한 기능들을 위주로 살펴보았는데요.
복잡한 서버 상태 관리를 효율적으로 할 수 있다는 점에서 Tanstack Query는 프론트엔드 개발에 정말 유용한 라이브러리인 것 같습니다.
기존에 직접 구현해야 했던 많은 로직들을 선언적인 방식으로 간결하게 작성할 수 있게 해주고, 캐싱과 데이터 동기화 문제를 우아하게 해결해 주니까요.
실제 프로젝트에 적용해보면서 경험해보니, Tanstack Query는 단순히 데이터 불러오기 라이브러리를 넘어 프론트엔드 아키텍처를 개선하는 도구라고 느껴집니다.
여러분도 서버 상태 관리에 어려움을 겪고 계시다면 한번 도입해 보시는 것을 추천해 드려요!
코드는 react-query-tutorial 레포지터리를 참고하여 작성했습니다.
ref:
https://github.com/ssi02014/react-query-tutorial
https://github.com/ssi02014/react-query-tutorial/blob/main/document/errorBoundary.md
https://github.com/ssi02014/react-query-tutorial?tab=readme-ov-file#usequery-주요-리턴-데이터

들어가며
마지막 사이드 프로젝트 때 Tanstack Query를 사용했었고, 마음에 들어서 외주 프로젝트와 이후 사이드 프로젝트에도 Tanstack Query를 도입했었습니다.
Tanstack Query를 쓰면서 '이런 기능이 있나? 있으면 좋을 것 같은데' 라고 생각했던 것들이 많았는데, 실제로 찾아보니 이미 있는 경우가 대부분이었고, 미리 한번 전체적으로 공식 문서를 정독했다면 두 번 찾아보지 않아도 되었겠다-라는 생각을 하게 되었어요.
그런 의미로, 이번 포스팅에서는 Tanstack Query에서 자주 쓰일 법한 개념들을 한번 전체적으로 정리하는 시간을 가져보도록 하겠습니다.
Tanstack Query(React Query) 개요
Tanstack Query는 서버 상태 데이터를 가져오고, 캐싱, 동기화 및 업데이트를 쉽게 다룰 수 있도록 도와주는 라이브러리입니다.
예전에는 클라이언트 상태와 서버 상태를 따로 구분짓지 않고, 하나로 취급했다면, 이 두 범위를 좀 더 명확히 나누고자 나온 라이브러리라고 할 수 있을 것 같아요.
이전 포스팅에서 클라이언트 상태와 서버 상태에 대해 다룬 적이 있는데, 궁금하신 분들은 아래 포스팅을 참고하셔도 좋을 것 같아요.
https://dawonny.tistory.com/491
클라이언트 상태(Client State)와 서버 상태(Server State)
tanstack query(react) 의 개요를 찾아보다가, 이 tanstack-query 가 리액트 애플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 다룰 수 있도록 도와주는 라이브러리이며 클라이언트
dawonny.tistory.com
주요 기능으로는 서버에서 받아온 응답을 캐싱하고, 중복 요청을 단일 요청으로 통합하고, 페이지네이션 및 lazy loading과 같은 성능을 최적화해 준다고 해요.
원래 같았으면 직접 구현해야 할 로직들을 제공해주니 편리하게 사용할 수 있어요.
기본 설정
Tanstack Query를 처음에 적용할 때 추가해야할 것들이 몇가지 있는데요.
일단 App.js에 있는 최상단 요소를 QueryClientProvider로 감싸주어야 합니다.
그리고 QueryClient의 인스턴스를 client라는 props로 넣어서 애플리케이션과 연결을 해주어야 해요.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient({ /* options */}); function App() { return ( <QueryClientProvider client={queryClient}> <div>블라블라</div> </QueryClientProvider>; ); }
위와 같이 감싸고 나면 이 context는 앱에서 비동기 요청을 알아서 처리하는 background 계층이 됩니다.
쉽게 말하자면, 우리 앱의 모든 컴포넌트에서 QueryClient 인스턴스에 접근할 수 있게 되고, 이 인스턴스가 서버 데이터 요청, 캐싱, 상태 관리 등을 처리해 주는 중앙 관리소 역할을 하게 돼요.
useQuery
query는 서버에서 데이터를 fetch하는 경우 Promise 기반의 메서드(GET, POST)들과 같이 사용될 수 있어요.
useQuery는 하나의 객체 인자만 받으며, queryKey랑 queryFn은 필수예요.
const result = useQuery({ queryKey, // required, 의미: 쿼리를 식별하는 고유한 키 queryFn, // required, 의미: 데이터를 가져오는 비동기 함수 refetchOnMount, // mount 마다 refetch 를 실행 refetchInterval, // 일정 시간마다 자동으로 refetch 를 실행 refetchIntervalInBackground, // refetchInterval 와 함께 사용하는 옵션 enabled, // 쿼리가 자동으로 실행되지 않도록 할 때 설정 select, // 반환된 데이터의 일부를 변환하거나 선택 placeholderData, // query 가 pending 상태일 때 보여줄 가짜 데이터 // ...options ex) gcTime, staleTime, ... }); result.data; result.isLoading; result.refetch; // ...
queryKey
queryKey는 배열의 형태이며, 이 값들은 쿼리 캐싱의 고유한 기준이 되어요.
이후에 이 queryKey는 쿼리를 다시 가져오고, 캐싱하고, 공유하는데 내부적으로 사용됩니다.
// 기본 사용법 useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ID로 특정 항목 조회 useQuery({ queryKey: ['todo', 5], queryFn: () => fetchTodoById(5) }) // 필터링된 쿼리 useQuery({ queryKey: ['todos', { status, page }], queryFn: () => fetchTodos(status, page) })
queryFn
queryFn에는 Promise를 반환하는 함수가 들어가는데요.
이 함수는 실제로 서버에서 데이터를 가져오는 비동기 함수예요.
Axios, Fetch API 등을 사용하여 HTTP 요청을 보내는 함수를 여기에 넣을 수 있어요.
queryKey의 값들은 이 함수 내에서 매개변수로 사용될 수 있어요.
useQuery({ queryKey: ['users', userId], queryFn: async () => { const response = await axios.get(`/api/users/${userId}`); return response.data; } })
enabled ?
boolean 값으로 설정할 수 있는 enabled는, ‘쿼리가 자동으로 실행되지 않도록 할 때 설정’할 수 있는 값이라고 하는데요.
보통 쿼리를 다시 가져오는 방법으로 invalidateQueries 나 refetchQueries 가 있는데, enabled 값을 false 로 하면 이 방법들이 다 무효화 된다고 하더라구요.
그런데 쿼리가 자동으로 실행되는 상황이 뭐가 있길래 그렇지? 라는 생각이 들었어요.
보통 컴포넌트가 마운트될 때, 창이 다시 포커스될 때, 네트워크 재연결 시, 리페칭 간격이 설정된 경우, queryKey가 변경되는 경우와 같은 상황에서 쿼리가 자동으로 실행돼요.
그리고 ‘종속 쿼리’가 존재하는 상황에서 유용하게 쓰일 수 있어요.
예를 들어서 2개의 쿼리가 있는데, 특정 쿼리가 또 다른 쿼리보다 항상 사전에 완료되어야한다고 가정한다면, 사전에 완료되어야 할 쿼리의 결과 데이터가 있을 때에만 이 후 쿼리의 enabled를 활성화 시켜줌으로써 구현할 수 있겠죠.
// 사전에 완료되어야 할 쿼리 const { data: user } = useQuery({ queryKey: ["user", email], queryFn: () => getUserByEmail(email), }); const channelId = user?.data.channelId; // user 쿼리에 종속 쿼리 const { data: courses } = useQuery({ queryKey: ["courses", channelId], queryFn: () => getCoursesByChannelId(channelId), enabled: !!channelId, });
deprecated 된 옵션들
v4 까지 onSuccess, onError, onSettled Callback 이 useQuery의 옵션에 존재했었는데, Deprecated 가 되었다고 합니다.
대신 useMutation 에서는 아직 사용이 가능하다고 하네요.
useQuery의 return data
useQuery 가 return 하는 데이터들은 굉장히 많은데, 주요한 것만 짚어보겠습니다.
const { data, // useQuery 가 반환한 Promise 에서 resolved 된 데이터 error, // 오류 발생한 경우, query 에 대한 오류 객체 status, // pending/error/success 와 같은 3가지의 값으로 존재 isLoading, // 캐싱된 데이터가 없을 때 true/false 중 하나 isFetching, // 캐싱된 데이터 있더라도 true/false 중 하나 isError, // 요청 중 에러 발생한 경우 true refetch, // query 를 수동으로 다시 가져오는 함수 // ... } = useQuery({ queryKey: ["super-heroes"], queryFn: getAllSuperHero, });
prefetchQuery
만약에 1페이지 → 2페이지로 이동하는 상황에서, 3페이지를 미리 로드해오면 어떨까요?
사용자가 3페이지를 방문했을 때 새로 로드를 시작할 필요가 없기 때문에 좀 더 좋은 사용자 경험이 생길 것입니다.
이런 경우를 위해 queryClient에 prefetchQuery 의 기능도 존재합니다.
const prefetchNextPosts = async (nextPage: number) => { const queryClient = useQueryClient(); // 해당 쿼리의 결과는 일반 쿼리들처럼 캐싱 된다. await queryClient.prefetchQuery({ queryKey: ["posts", nextPage], queryFn: () => fetchPosts(nextPage), // ...options }); }; // 단순 예 useEffect(() => { const nextPage = currentPage + 1; if (nextPage < maxPage) { prefetchNextPosts(nextPage); } }, [currentPage]);
useInfiniteQuery
무한 스크롤 또는 데이터를 추가로 로드하는 경우에 무한 쿼리를 사용할 수도 있습니다.
const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ["colors"], queryFn: fetchColors, initialPageParam: 1, // (lastPage, allPages) => (가장 최근 가져온 페이지 목록, 현재까지 가져온 모든 페이지 데이터) getNextPageParam: (lastPage, allPages) => { return allPages.length < 4 && allPages.length + 1; }, // ... });
useQuery 와 비슷하게 queryKey, queryFn 을 받는 것은 비슷한데요.
반환 값으로 isFetchingNextPage, isFetchingPreviousPage, fetchNextPage, fetchPreviousPage, hasNextPage 등이 추가적으로 존재해요.
initialPageParam 은 첫 페이지를 가져올 때 사용할 기본 페이지를 의미하며 필수값이고,
getNextPageParam 을 사용해서 페이지를 증가시킬 수 있어요.
useMutation
기본적으로 GET 은 useQuery 를 사용한다면, POST/PATCH/PUT/DELETE 의 경우에는 useMutation 을 사용해요.
const mutation = useMutation({ mutationFn: createTodo, onMutate() { /* ... */ }, onSuccess(data) { console.log(data); }, onError(err) { console.log(err); }, onSettled() { /* ... */ }, }); const onCreateTodo = (e) => { e.preventDefault(); mutation.mutate({ title }); };
mutation 객체는 useMuation 의 반환 값인데요.
이 객체의 mutate 메서드를 이용해서 요청함수를 호출할 수 있어요.
위 예시 코드를 설명하자면, useMutation 함수로 mutation 객체를 생성한 후, 실제 서버 요청이 필요한 시점에 mutation.mutate 메서드를 호출해서 데이터를 전송해요. mutationFn으로 지정한 createTodo 함수는 실제 API 호출을 담당하고, onSuccess, onError 등의 콜백은 API 호출 결과에 따라 실행되는 로직을 정의해요.
useMutation 이랑 mutate 의 차이는?
useMutation은 mutation을 정의하는 훅이고, mutate는 실제로 그 mutation을 실행하는 메서드예요.
useMutation을 호출하면 mutation 객체가 반환되고, 이 객체에는 mutate와 mutateAsync 메서드가 포함되어 있어요.
- mutate: 비동기 함수를 실행하고 결과를 콜백(onSuccess, onError 등)으로 처리해요.
- mutateAsync: Promise를 반환하기 때문에 await 키워드와 함께 사용할 수 있어요.
// mutate 사용 (콜백 기반) mutation.mutate(newTodo, { onSuccess: (data) => { console.log('성공:', data); }, onError: (error) => { console.error('오류:', error); } }); // mutateAsync 사용 (Promise 기반) try { const data = await mutation.mutateAsync(newTodo); console.log('성공:', data); } catch (error) { console.error('오류:', error); }
쿼리 무효화
화면을 최신 상태로 유지하려면 기존의 쿼리를 쓰는 것이 아니라, 무효화하고 새로 상태를 받아와야할 거예요(refetch).
이럴 때 쓸 수 있는 개념이 쿼리 무효화입니다.
invalidateQuries 를 사용하는 방식이 가장 간단한데요.
아래와 같이 사용할 수 있습니다.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; const useAddSuperHeroData = () => { const queryClient = useQueryClient(); return useMutation(addSuperHero, { onSuccess(data) { queryClient.invalidateQueries({ queryKey: ["super-heroes"] }); // 이 key에 해당하는 쿼리가 무효화! console.log(data); }, onError(err) { console.log(err); }, }); };
위 예시에서는 useMutation 이 성공했을 때 queryClient.invalidateQueries 를 사용해서 ["super-heroes"] 라는 key에 해당하는 쿼리를 무효화 시키고 있어요.
쿼리 무효화를 하는 다른 방법
queryClient.invalidateQueries 외에 queryClient.setQueryData 를 사용하는 방법도 존재해요.
똑같이 쿼리의 캐시 데이터를 즉시 업데이트 한다는 점에서 비슷한 역할을 하는데, 차이점이 있다면 invalidateQueries는 데이터를 실제로 다시 가져오는 반면, setQueryData는 네트워크 요청 없이 캐시 데이터만 직접 업데이트한다는 점이에요. 낙관적 업데이트(Optimistic Updates)를 구현할 때 자주 사용돼요.
사용하는 예시는 다음과 같습니다.
const useAddSuperHeroData = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: addSuperHero, onSuccess(data) { queryClient.setQueryData(["super-heroes"], (oldData: any) => { return { ...oldData, data: [...oldData.data, data.data], }; }); }, onError(err) { console.log(err); }, }); };
코드를 보면 2번째 인자로 ‘업데이트 함수'를 전달하고있어요.
이 업데이트 함수는 기존 캐시 데이터(oldData)를 받아서 새 데이터를 반환하는 함수예요.
예시에서는 기존 데이터를 복사한 후 새로운 데이터를 추가하는 방식으로 구현되어 있습니다.
이런 방식으로 서버에서 데이터를 다시 가져오지 않고도 캐시를 최신 상태로 유지할 수 있어요.
UI 업데이트가 즉시 반영되므로 사용자 경험을 향상시킬 수 있습니다.
invalidateQueries 와 setQueryData의 차이 정리
invalidateQueries
쿼리를 '오래된(stale)' 상태로 표시하고, 다음 렌더링 시 서버에서 새로운 데이터를 가져오도록 합니다.
즉, 실제로 새 네트워크 요청이 발생합니다.
setQueryData
네트워크 요청 없이 직접 캐시 데이터를 수정합니다. 서버와 통신하지 않고 UI를 즉시 업데이트할 수 있습니다.
useQueryErrorResetBoundary
useQueryErrorResetBoundary 에 대해 얘기하기전에, 먼저 ErrorBoundary 를 간단히 정리해볼게요.
ErrorBoundary
react 에서 ErrorBoundary 는 하위 구성 요소 트리의 위치에서 JavaScript 에러를 감지하는 컴포넌트입니다.
초기 렌더링, 이벤트 핸들러 내부 오류, 비동기 코드 등에서는 오류를 감지하지 않아요.
ErrorBoundary 는 트리에서 하위 구성 요소의 오류만 감지합니다.
문제가 생긴 컴포넌트 대신에 Fallback UI를 표시하기 위해 선언적으로 작성할 수 있어요.
여기서 선언형이란, 결과물에만 집중하고 복잡한 과정은 추상화하는 것을 말해요.
// 리액트 <QueryErrorBoundary fallback={ <DefaultFallback onResetAction={() => location.replace("/start")} /> } > {/* ... */} </QueryErrorBoundary>
예를 들어 위 코드를 봤을 때, 우리는 에러가 발생했을 때 DefaultFallback 을 보여주겠구나-하고 이해할 수 있어요.
QueryErrorBoundary 안에서 어떤 로직이 동작하게 되는지 신경쓰지 않아요.
이렇게 내부의 복잡성을 추상화 하는 것을 선언형이라고 합니다.
선언형의 반대 개념은 명령형이에요.
명령형 프로그래밍은 결과물 보다는 과정이 중요하기 때문에, 코드를 한 줄 씩 읽어나가면서 다음에 어떤일이 발생할지 추측해야해요.
useQueryErrorResetBoundary
useQueryErrorResetBoundary는 위에서 언급한 ErrorBoundary와 함께 쓰여요.
ErrorBoundary 와 useQueryErrorResetBoundary 를 결합해서, 선언적으로 에러가 발생했을 때 Fallback UI를 보여줄 수 있어요.
기본적으로 공식 문서에서도 react-error-boundary 라는 라이브러리를 사용하는데요.
사용 방법을 먼저 코드로 보겠습니다.
import { useQueryErrorResetBoundary } from "@tanstack/react-query"; // (*) import { ErrorBoundary } from "react-error-boundary"; // (*) interface Props { children: React.ReactNode; } const QueryErrorBoundary = ({ children }: Props) => { const { reset } = useQueryErrorResetBoundary(); // (*) return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> Error!! <button onClick={() => resetErrorBoundary()}>Try again</button> </div> )} > {children} </ErrorBoundary> ); }; export default QueryErrorBoundary;
일단 useQueryErrorResetBoundary 훅에서 reset 함수를 가져와줍니다.
그리고 ErrorBoundary에 onReset 에 reset 함수를 전달해주는데요.
만약 에러가 발생하면 ErrorBoundary의 fallbackRender 로 넘긴 내용이 렌더링되고, 에러가 없다면 children 이 렌더링됩니다.
참고로 fallbackRender 로 전달되는 콜백 함수의 인자를 넣어줄 때 구조 분해 할당을 통해서 resetErrorBoundary 를 가져올 수 있는데요.
이 resetErrorBoundary 는 모든 쿼리 에러를 reset(초기화)하는 기능을 할 수 있습니다.
따라서 위 예시에서는 에러가 생겼을 때에만 보이는 버튼을 클릭했을 때 쿼리 에러를 초기화하도록 하고 있네요.
추가로, 필수로 해야해줘야하는 작업이 하나있습니다.
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import QueryErrorBoundary from "./components/ErrorBoundary"; // (*) const queryClient = new QueryClient({ defaultOptions: { queries: { throwOnError: true, // (*) 여기서는 글로벌로 세팅했지만, 개별 쿼리로 세팅 가능 }, }, }); function App() { return ( <QueryClientProvider client={queryClient}> <QueryErrorBoundary>{/* 하위 컴포넌트들 */}</QueryErrorBoundary> </QueryClientProvider> ); }
App.js 에 QueryErrorBoundary를 추가해줘야하는데요.
이 때 queryClient 옵션에 throwOnError: true 를 추가 해주어야합니다.
ErrorBoundary 컴포넌트가 오류를 감지할 수 있도록 해주는 작업이에요.
Suspense
위에서 ErrorBoundary 에 대해 설명드렸는데요.
Suspense 와 ErrorBoundary 를 결합해서 서버 통신 상태가 loading 중일 때에 Fallback UI를 보여줄 수 있게 선언적으로 작성할 수도 있어요.
Suspense의 개념은 컴포넌트가 무언가를 로딩 중일 때 대체 UI를 보여줄 수 있게 해주는 React의 기능이에요.
데이터 불러오기, 코드 분할 등으로 인한 지연 시간 동안 로딩 UI를 쉽게 표시할 수 있어요.
이 Suspense를 사용하는 이유는 데이터 로딩 상태를 더 선언적으로 처리할 수 있고, 로딩 상태 관리 로직을 컴포넌트에서 분리해 코드를 더 깔끔하게 유지할 수 있기 때문이에요.
또한 여러 비동기 작업이 동시에 진행될 때 모든 작업이 완료될 때까지 일관된 로딩 UI를 보여줄 수 있어요.
Suspense 를 사용하는 예시를 아래에서 같이 보시겠습니다.
import { Suspense } from "react"; const queryClient = new QueryClient({ defaultOptions: { queries: { // suspense: true, - 💡 v5부터 Deprecated // useQuery/useInfiniteQuery와 같은 일반 훅 대신 useSuspenseQuery/useSuspenseInfiniteQuery와 같은 suspense 훅 사용 throwOnError: true, }, }, }); function App() { return ( <QueryErrorBoundary> <Suspense fallback={<Loader />}>{/* 하위 컴포넌트들 */}</Suspense> </QueryErrorBoundary>; ); }
App 컴포넌트를 보시면 하위 컴포넌트들을 보여주기 전에(로딩 중일 때) Suspense 컴포넌트를 사용해서 Loader 라는 컴포넌트를 보여주겠다-라는 의도를 확인할 수 있어요.
tanstack query 에서는 Suspense 를 타입 세이프하게 사용하기 위해서 useSuspenseQuery, useSuspenseInfiniteQuery, useSuspenseQueries 라는 훅들을 사용할 수 있는 @suspensive/react-query 라는 라이브러리를 소개하는데요.
‘타입 세이프’ 해지는 이유는, 이 3가지 훅을 사용했을 때에는 타입 레벨에서 data 가 undefined 되지 않기 때문이에요.
위처럼 Suspense 컴포넌트를 사용하는 것과 이 훅들을 사용하는 것에는 다음과 같은 차이가 있어요.
- Suspense 컴포넌트만 사용하는 경우: React의 기본 기능을 활용하지만, undefined 체크를 계속 해야 하는 불편함이 있어요.
- 전용 Suspense 훅을 사용하는 경우: 타입스크립트에서 data가 항상 존재한다고 보장받을 수 있어 타입 안전성이 높아져요.
TypeScript 의 제네릭이 적용되는 경우
tanstack query 를 사용할 때에는 typescript 의 제네릭 문법이 많이 사용됩니다.
실제로 쓰일 때에는 API 가 반환하는 데이터의 유형을 모르는 경우가 더 많기 때문이에요.
위에서 살펴보았던 훅들에는 제네릭이 설정되어있어요.
useQuery
// useQuery의 타입 export function useQuery< TQueryFnData = unknown, // 실행 결과의 타입 TError = DefaultError, // 에러 형식 TData = TQueryFnData, // data 에 담기는 실질적인 데이터의 타입 TQueryKey extends QueryKey = QueryKey // queryKey의 타입(명시적으로 지정해줄 때) >
import { AxiosError } from "axios"; // useQuery 타입 적용 예시 const { data } = useQuery< AxiosResponse<Hero[]>, AxiosError, string[], ["super-heroes", number] >({ queryKey: ["super-heroes", id], queryFn: getSuperHero, select: (data) => { const superHeroNames = data.data.map((hero) => hero.name); return superHeroNames; }, }); /** 주요 타입 * data: string[] | undefined * error: AxiosError<any, any> * select: (data: AxiosResponse<Hero[]>): string[] */
useMutation
export function useMutation< TData = unknown, // 실행 결과의 타입 TError = DefaultError, // 에러 형식 TVariables = void, // mutate 함수의 인자 TContext = unknown // onMutate의 return 값 >
onMutate은 muataionFn 실행 전에 실행되며, 다음과 같은 상황에 사용됩니다.
- Optimistic UI(낙관적 UI) 처럼 API 요청 전에 UI를 먼저 변경하는 경우
- 실패 시 복구할 수 있도록 이전 상태를 저장하는 경우
// useMutation 타입 적용 예시 const { mutate } = useMutation<Todo, AxiosError, number, number>({ mutationFn: postTodo, onSuccess: (res, id, nextId) => {}, onError: (err, id, nextId) => {}, onMutate: (id) => id + 1, onSettled: (res, err, id, nextId) => {}, }); const onClick = () => { mutate(5); }; /** 주요 타입 * data: Todo * error: AxiosError<any, any> * onSuccess: (res: Todo, id: number, nextId: number) * onError: (err: AxiosError, id: number, nextId: number) * onMutate: (id: number) * onSettled: (res: Todo, err: AxiosError, id: number, nextId: number), */
마치며
Tanstack Query 의 여러 기능 중에서도 주로 많이 쓰일 법한 기능들을 위주로 살펴보았는데요.
복잡한 서버 상태 관리를 효율적으로 할 수 있다는 점에서 Tanstack Query는 프론트엔드 개발에 정말 유용한 라이브러리인 것 같습니다.
기존에 직접 구현해야 했던 많은 로직들을 선언적인 방식으로 간결하게 작성할 수 있게 해주고, 캐싱과 데이터 동기화 문제를 우아하게 해결해 주니까요.
실제 프로젝트에 적용해보면서 경험해보니, Tanstack Query는 단순히 데이터 불러오기 라이브러리를 넘어 프론트엔드 아키텍처를 개선하는 도구라고 느껴집니다.
여러분도 서버 상태 관리에 어려움을 겪고 계시다면 한번 도입해 보시는 것을 추천해 드려요!
코드는 react-query-tutorial 레포지터리를 참고하여 작성했습니다.
ref:
https://github.com/ssi02014/react-query-tutorial
https://github.com/ssi02014/react-query-tutorial/blob/main/document/errorBoundary.md
https://github.com/ssi02014/react-query-tutorial?tab=readme-ov-file#usequery-주요-리턴-데이터