react-query
는 리액트 애플리케이션에서 서버 상태 가져오기
, 캐싱
, 동기화 및 업데이트
를 보다 쉽게 다룰 수 있도록 도와주는 라이브러리이다.
클라이언트 상태와 서버 상태를 명확히 구분하기 위해서 만들어졌다.
기존 상태 관리 라이브러리인 redux
, mobX
는 클라이언트 상태 작업에는 적합하지만, 비동기 또는 서버 상태 작업에는 좋지 않다고 언급한다.
react-query
와redux
모두 훌륭한 상태 관리 도구이지만, 사용되는 목적이 다르다.
react-query
는 서버 상태 및 캐싱 관리에 중점을 두어 주로 데이터 페칭 및 캐싱에 사용되지만,redux
는 클라이언트 측 애플리케이션 상태 관리에 중점을 두어 복잡한 응용 프로그램 상태를 처리할 수 있는 보다 일반적인 상태 관리 라이브러리이다.- 서버 데이터 측면에서
redux
의 단점을 보완하기 위해 여러 기업에서 redux -> react query로 전환하기도 했다. (ex. kakao)- 다만
react-query
로 서버 데이터를 관리할 때는, 이외 클라이언트 데이터를 관리하기 위한 별도의 상태 관리 라이브러리를 사용하는 것이 좋다. (주로 recoil 사용)
클라이언트 상태와 서버 상태는 완전히 다른 개념이다. 클라이언트 상태는 각각의 input값을, 서버 상태는 데이터베이스에 저장된 데이터를 예시로 들 수 있다.
Redux
, Recoil
, mobX
같은 전역 상태 라이브러리는 클라이언트 데이터를 효과적으로 관리할 수 있다.
다만, 비동기 함수를 처리하는데 추가적인 로직이 필요하거나 서드 파티 라이브러리를 사용해야 하는 것이 많다. 클라이언트 데이터를 관리하는데 로직이 집중되어 있기 때문에 서버 데이터까지 효율적으로 관리하기 어렵기 때문이다.
Redux toolkit
은 기존 redux의 복잡한 로직을 단순화하고, 비동기적인 처리를 훨씬 쉽게 가능하게 한다.react-query
와redux toolkit
각각의 장단점이 있다!
비교글: RTK Query VS React-Query 👊🏻
캐싱이란, 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 재접근 속도를 높이는 것을 말한다.
react-query는 캐싱을 통해 동일한 데이터에 대한 반복적인 비동기 데이터 호출을 방지하고, 불필요한 API 콜을 줄여 서버 부하를 줄인다.
react-query
에서 제공하는 기본적인 옵션은 다음과 같다.
//기본적인 사용 예시
const {
data,
// ...
} = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
gcTime: 5 * 60 * 1000,
staleTime: 1 * 60 * 1000,
...
});
queryKey
: 고유한 배열로서, 쿼리 캐싱을 관리한다.queryFn
: 실제 호출하고자 하는 비동기 함수가 들어간다.refetchOnWindowFocus, //default: true
refetchOnMount, //default: true
refetchOnReconnect, //default: true
staleTime, //default: 0
gcTime, //default: 5분 (60 * 5 * 1000)
refetchOnWindowFocus
: 윈도우에 포커스된 경우refetchOnMount
: 새로운 컴포넌트 마운트가 발생한 경우refetchOnReconnect
: 네트워크에 재연결된 경우staleTime
gcTime
const { data: superHeroes } = useQuery({
queryKey: ["super-heroes"],
queryFn: getAllSuperHero,
});
const { data: superHeroes } = useQuery({
queryKey: ["friends"],
queryFn: getFriends,
});
일반적인 상황에서 쿼리 함수들은 병렬
로 요청되어 처리되어, 쿼리 처리의 동시성
을 극대화한다.
const queryResults = useQueries({
queries: [
{
queryKey: ["super-hero", 1],
queryFn: () => getSuperHero(1),
staleTime: Infinity, // 다음과 같이 option 추가 가능!
},
{
queryKey: ["super-hero", 2],
queryFn: () => getSuperHero(2),
staleTime: 0,
},
...
],
});
쿼리 여러 개를 수행해야 하고, 렌더링을 거듭되는 사이에 계속 쿼리가 수행되어야 한다면 useQueries
를 사용한다.
쿼리가 입력된 순서대로 반환되어 최종적으로 모든 쿼리 결과가 포함된 배열을 반환하게 된다.
비동기 요청은 데이터양이 클수록 받아오는 속도가 느리고, 시간이 오래 걸린다. 사용자 경험을 위해 데이터를 미리 받아와서 캐싱해 놓으면 새로운 데이터를 받기 전에 사용자가 캐싱 된 데이터를 볼 수 있어 UX에 좋은 영향을 줄 수 있다.
예를 들어 페이지네이션을 구현했다고 가정하면, 페이지1에서 페이지2로 이동했을 때 페이지3의 데이터를 미리 받아놓을 수 있다.
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]);
데이터가 이미 캐싱되어 있으면 데이터를 가져오지 않는다.
R(read)에서는 useQuery
를, CUD(create, update, delete)는 useMutation
을 사용한다.
const mutation = useMutation({
mutationFn: createTodo,
onMutate() {
/* ... */
},
onSuccess(data) {
console.log(data);
},
onError(err) {
console.log(err);
},
onSettled() {
/* ... */
},
});
const onCreateTodo = (e) => {
e.preventDefault();
mutate({ title });
};
onMutate
는 mutation 함수가 실행되기 전에 실행된다.onSuccess
와 onError
는 함수가 성공, 실패했을 때 실행된다.onSettled
는 finally 처럼 요청의 성공여부와 상관없이 마지막에 실행된다.