React Query는 서버에서 데이터를 가져오고 관리하는 과정을 도와주는 라이브러리이다.
React Query를 사용하면 데이터 패칭, 캐싱, 동기화, 업데이트 등 복잡한 로직을 쉽게 처리할 수 있다.
참조
React Query는 원래 React 애플리케이션 전용으로 설계된 라이브러리였으나, 이후 TanStack Query라는 이름 하에 다양한 프레임워크를 지원하는 범용적인 데이터 패칭 라이브러리로 발전했다.
TanStack Query는 React뿐만 아니라 Vue, Svelte 등 여러 프레임워크에서도 사용할 수 있게 되었고, React용 버전은 여전히 React Query라는 독립적인 이름을 유지하고 있다.
React Query는 컴포넌트가 마운트될 때마다 자동으로 데이터를 패칭한다.
기본적으로, useQuery 훅을 사용하여 데이터를 가져오고, 쿼리 키를 기반으로 데이터를 캐싱하여 다시 패칭할 필요 없이 빠르게 데이터를 제공할 수 있다.
예를 들어, 아래 코드는 컴포넌트가 마운트될 때마다 fetchTodos 함수로 데이터를 자동으로 패칭한다.
import { useQuery } from 'react-query';
const fetchTodos = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
return response.json();
};
const TodoList = () => {
const { data, isLoading, isError } = useQuery('todos', fetchTodos);
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>Error fetching todos!</p>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
};
React-Query가 데이터를 Refetching하는 시점은 아래와 같다.
- 브라우저에 포커스가 들어온 경우(refetchOnWindowFocus)
- 새로운 컴포넌트 마운트가 발생한 경우(refetchOnMount)
- 네트워크 재연결이 발생한 경우(refetchOnReconnect)
React Query는 데이터를 자동으로 캐싱하여 성능을 최적화한다.
동일한 쿼리 키로 요청이 오면, 서버에서 데이터를 다시 가져오는 대신 캐시된 데이터를 즉시 반환하여 불필요한 네트워크 요청을 줄일 수 있다. 예를 들어, 아래와 같이 동일한 쿼리 키(todos)로 요청하면 두 번째 요청은 캐시된 데이터로 응답을 반환한다.
const { data } = useQuery('todos', fetchTodos); // 첫 번째 호출
// 두 번째 호출 시 캐시된 데이터 반환
React Query는 쿼리의 데이터를 최신 상태로 유지하기 위해 주기적으로 데이터를 갱신하거나, 특정 이벤트가 발생할 때 데이터를 동기화할 수 있다. 예를 들어, staleTime과 refetchInterval을 사용하여 데이터 갱신 주기를 설정할 수 있다.
const { data } = useQuery('todos', fetchTodos, {
staleTime: 5000, // 데이터가 5초 동안 갱신되지 않고 유지됨
refetchInterval: 10000, // 10초마다 데이터 리패치
});
React Query는 데이터가 오래되었을 때 백그라운드에서 자동으로 데이터를 갱신할 수 있다. 이 방식은 사용자가 데이터를 최신 상태로 유지하도록 도와준다. refetchIntervalInBackground를 사용하여 백그라운드에서 데이터를 갱신할 수 있다.
const { data } = useQuery('todos', fetchTodos, {
refetchIntervalInBackground: true, // 백그라운드에서 자동 리패치
});
React Query는 클라이언트와 서버 간의 상태를 효율적으로 관리하는 데 유리하다.
서버 상태는 자주 변할 수 있으므로, React Query는 이를 자동으로 관리하고, 리액트 컴포넌트에서 최신 상태를 쉽게 반영할 수 있도록 도와준다. 예를 들어, 데이터를 변경한 후 이를 즉시 UI에 반영하기 위해 mutate 함수와 useMutation 훅을 사용할 수 있다.
import { useMutation, useQueryClient } from 'react-query';
const addTodo = async (newTodo) => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
return response.json();
};
const AddTodo = () => {
const queryClient = useQueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos'); // 데이터 변경 후 'todos' 쿼리 재패치
},
});
const handleAdd = () => {
mutation.mutate({ title: 'New Todo', completed: false });
};
return <button onClick={handleAdd}>Add Todo</button>;
};
React Query는 요청이 실패할 경우, 자동으로 에러를 처리하는 기능을 제공한다. onError와 같은 콜백 함수를 통해 에러가 발생했을 때의 처리를 커스터마이즈할 수 있다. 예를 들어, useQuery 훅에서 데이터를 가져올 때 에러 발생 시 이를 처리하는 방법은 다음과 같다.
const { data, error } = useQuery("todos", fetchTodos, {
onError: (error) => {
console.error("Error fetching todos:", error);
}
});
React Query는 데이터 패칭 및 상태 관리를 최적화할 수 있는 여러 기능을 제공한다.
예를 들어, 쿼리의 중복 요청을 방지하거나, 데이터 요청 간격을 최적화하는 등의 성능 최적화 기능이 포함되어 있다. debounce 또는 throttle을 사용하여 네트워크 요청 횟수를 줄일 수 있다.
const { data } = useQuery('todos', fetchTodos, {
staleTime: 60000, // 데이터가 1분 동안 갱신되지 않고 유지됨
});
React Query는 Next.js와 같은 서버 사이드 렌더링(SSR) 프레임워크와 잘 호환된다.
React Query의 Hydration API를 사용하면 서버에서 패칭한 데이터를 클라이언트에서 재사용할 수 있다. 이를 통해 서버에서 미리 데이터를 패칭하고, 클라이언트에서 데이터를 재사용하는 방식으로 렌더링 성능을 최적화할 수 있다.
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
const App = ({ dehydratedState }) => {
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={dehydratedState}>
<TodoList />
</Hydrate>
</QueryClientProvider>
);
};
React Query에서 제공하는 대표적인 hook으로는 useQuery, useMutation, useQuries, useQueryClient 등이 있다.
(이외에도 useQueryClient, useInfiniteQuery, useIsFetching, useIsMutating, useQueryErrorResetBoundary 등 다양한 훅들을 제공하고 있다.)
이 중 가장 자주 사용하는 useQuery와 useMutation에 대해 설명하려 한다.
useQuery 훅은 React Query에서 데이터를 패칭하고 관리하는 가장 핵심적인 기능이다.
서버에서 데이터를 가져오는 작업을 단순화하고 데이터의 로딩, 에러 처리, 캐시 관리 등 다양한 기능을 제공한다.
useQuery 훅을 사용하면 데이터를 간편하게 패칭할 수 있다. 기본적인 사용법은 다음과 같다.
const { data, error, isLoading } = useQuery("todos", fetchTodos);
useQuery 훅은 여러 가지 옵션을 제공하여 데이터 패칭 방식을 세밀하게 제어할 수 있다.
주요 옵션은 다음과 같다.
const fetchTodos = async () => {
const response = await fetch("/api/todos");
return response.json();
};
const { data, isLoading, error, refetch } = useQuery("todos", fetchTodos, {
enabled: true, // 쿼리 실행 여부 제어
staleTime: 10000, // 10초 동안 데이터 갱신하지 않음
cacheTime: 300000, // 5분 동안 캐시 유지
retry: 3, // 실패 시 3번까지 재시도
select: (data) => data.filter(todo => todo.completed), // 완료된 항목만 필터링
onSuccess: (data) => {
console.log("데이터 패칭 성공:", data);
},
onError: (error) => {
console.error("데이터 패칭 실패:", error);
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={refetch}>리패칭</button>
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
React Query는 쿼리의 상태를 자동으로 관리한다. useQuery 훅에서 반환되는 상태 변수들을 통해 데이터의 로딩, 에러 상태 등을 쉽게 처리할 수 있다.
주요 상태 변수는 다음과 같다.
const { data, isLoading, isError, isSuccess, isFetching } = useQuery("todos", fetchTodos);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error occurred</div>;
return (
<div>
{isSuccess && (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)}
</div>
);
참조 ) useQuery에서 제공하는 전체 옵션과 상태는 아래와 같다. (상세한 설명은 블로그 하단 공식 문서 참조)
const { data, dataUpdatedAt, error, errorUpdatedAt, failureCount, failureReason, fetchStatus, isError, isFetched, isFetchedAfterMount, isFetching, isInitialLoading, isLoading, isLoadingError, isPaused, isPending, isPlaceholderData, isRefetchError, isRefetching, isStale, isSuccess, promise, refetch, status, } = useQuery( { queryKey, queryFn, gcTime, enabled, networkMode, initialData, initialDataUpdatedAt, meta, notifyOnChangeProps, placeholderData, queryKeyHashFn, refetchInterval, refetchIntervalInBackground, refetchOnMount, refetchOnReconnect, refetchOnWindowFocus, retry, retryOnMount, retryDelay, select, staleTime, structuralSharing, subscribed, throwOnError, }, queryClient, )
useMutation은 데이터를 수정하거나 업데이트할 때 사용한다. (PUT, POST, DELETE)
useMutation 훅을 사용하면 데이터를 수정하거나 업데이트하는 작업을 간편하게 처리할 수 있다.
기본적인 사용법은 다음과 같다.
const { mutate, isError, isLoading, error } = useMutation(addTodo, {
onSuccess: () => {
// 성공 시 추가 작업 수행
},
});
useMutation 훅은 다양한 옵션을 제공하여 뮤테이션의 동작 방식을 제어할 수 있다.
const addTodo = async (newTodo) => {
const response = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(newTodo),
});
return response.json();
};
const {
data,
error,
isError,
isIdle,
isPending,
isPaused,
isSuccess,
failureCount,
failureReason,
mutate,
mutateAsync,
reset,
status,
submittedAt,
variables,
} = useMutation(addTodo, {
onSuccess: (data) => {
console.log("Todo added successfully:", data);
},
onError: (error) => {
console.error("Error adding todo:", error);
},
retry: 3, // 실패 시 최대 3번 재시도
retryDelay: 1000, // 재시도 간 1초 대기
});
mutate({ title: "New Todo" });
React Query는 뮤테이션의 상태를 자동으로 관리한다. useMutation 훅에서 반환되는 상태 변수들을 통해 데이터 수정 작업의 진행 상황을 쉽게 처리할 수 있다.
const { data, isLoading, isError, isSuccess, isPending } = useMutation(addTodo);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error occurred</div>;
return (
<div>
{isSuccess && <div>Todo added successfully: {data.title}</div>}
</div>
);
참조 ) useMutation에서 제공하는 전체 옵션과 상태는 아래와 같다. (상세한 설명은 공식 문서 참조)
const { data, error, isError, isIdle, isPending, isPaused, isSuccess, failureCount, failureReason, mutate, mutateAsync, reset, status, submittedAt, variables, } = useMutation( { mutationFn, gcTime, meta, mutationKey, networkMode, onError, onMutate, onSettled, onSuccess, retry, retryDelay, scope, throwOnError, }, queryClient, ) mutate(variables, { onError, onSettled, onSuccess, })
React Query는 페이지네이션을 구현할 때 유용한 여러 기능을 제공한다.
예를 들어, getNextPage와 getPreviousPage를 사용하면 다음 페이지와 이전 페이지의 데이터를 손쉽게 불러올 수 있다. 이를 통해, 데이터를 한 번에 전부 로딩하지 않고 필요한 만큼만 로드하여 성능을 최적화할 수 있다.
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery('items', fetchItems, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
});
React Query의 useInfiniteQuery 훅을 사용하면 무한 스크롤을 쉽게 구현할 수 있다.
const {
data,
fetchNextPage,
isLoading,
isFetchingNextPage,
hasNextPage,
} = useInfiniteQuery('posts', fetchPosts, {
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{data.pages.map((page) => (
<div key={page.id}>
{page.items.map((item) => (
<p key={item.id}>{item.title}</p>
))}
</div>
))}
{isFetchingNextPage && <div>Loading more...</div>}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
옵티미스틱 업데이트는 클라이언트가 서버 응답을 기다리지 않고 즉시 UI를 업데이트하는 방식이다. React Query는 이 기능을 지원하여 사용자가 데이터를 수정할 때 서버 응답을 기다리지 않고 UI를 즉시 변경할 수 있도록 한다. 이 방식은 사용자 경험을 향상시키고, 반응성이 좋은 UI를 제공하는 데 유용하다.
const { mutate } = useMutation(addTodo, {
onMutate: (variables) => {
const previousTodos = queryClient.getQueryData('todos');
queryClient.setQueryData('todos', (oldData) => [
...oldData,
{ id: new Date().toISOString(), ...variables },
]);
return { previousTodos };
},
onError: (err, variables, context) => {
queryClient.setQueryData('todos', context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries('todos');
},
});
mutate({ title: 'New Todo' });
참고 자료