React Query는 React 애플리케이션에서 서버 상태를 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 만들어주는 라이브러리이다.
서버 상태를 관리하기 위한 최고의 라이브러리 React Query에 대해 알아보자.
비동기 처리를 쉽게 관리할 수 있는 라이브러리이면서
Client State
에 적합한 라이브러리이다.
Client State
👉 세션 간 지속되지 않는 데이터, 동기적, 클라이언트가 소유하는 데이터를 말한다.
ex) 컴포넌트의 state, 동기적으로 저장되는 redux store의 데이터
Server State
👉 세션 간 지속되는 데이터, 비동기적, 여러 클라이언트에 의해 수정될 수 있는 공유되는 데이터를 말한다.
ex) 비동기 요청으로 받아올 수 있는 백엔드 DB에 저장되어 있는 데이터
React 자체가 데이터를 패칭해오거나 업데이트 하는 옵션을 제공하지 않기에 개발자 각각의 방식으로 http 통신을 구현하였다.
Redux 같은 전역 상태관리 라이브러리는 클라이언트 상태값에 대해서는 잘 동작하지만, 서버 상태에 대해서는 유용하지 않다.
React Query는 서버에서 주기적으로 fetch하기에 이 데이터를 전역에서 사용하도록 만들어주고, Optimistic Update (데이터 변경을 요청 후 실제로 요청이 성공하기 전 미리 UI만 변경한 뒤, 서버의
응답에 따라 다시 롤백하거나 업데이트 된 상태로 UI를 놔두는 것) 기능도 제공한다.
서버에서 받아오려는 데이터가 캐시에서 이미 최신 상태라면, 서버에 도달하기 전에 브라우저가 이를 가로채서 캐시 메모리에 있는 데이터를 사용하는 것이다.
React Query의 hook인 useQuery로 API 데이터의 만료 시간, 리프레싱 주기, 데이터를 캐시에서 유지할 기간, 브라우저 포커스 시 데이터 리프레시 여부, 성공/에러 콜백 등의 기능을 제어할 수 있다.
비동기 통신을 하기 위해서 axios과 같은 통신 관련 라이브러리는 미리 추가되어있어야 한다.
npm i react-query // npm 사용
or
yarn add react-query // yarn 사용
캐시를 관리하기 위한 Query Client
인스턴스를 생성하고, QueryClientProvider
를 통해 컴포넌트가 Query Client
에 접근할 수 있도록 최상단에 추가한다.
import React from "react";
import ReactDOM from "react-dom";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
{/* 컴포넌트들 */}
</QueryClientProvider>
);
}
export default App;
fresh
새롭게 추가된 query / 만료되지 않은 query
👉 component가 mount (최초 실행) / unmount (제거) 시에도 데이터를 요청하지 않는다.
fetching
요청 중인 query
stale
요청이 만료된 query
👉 component가 mount (최초 실행) / unmount (제거) 시에 캐싱된 데이터를 요청한다.
inactive
비활성화된 query
👉 특정 시간이 지나면 캐시가 가비지 컬렉터에 의해 제거된다.
GET 요청과 같은 CREATE 작업을 할 때 사용되는 훅이며. 데이터를 가져온다.
const requestData = useQuery( 쿼리 키, 쿼리 함수, 옵션);
쿼리 키
문자열 또는 배열
👉 캐싱 처리에 중요하다.
[ 쿼리 이름, 프로미스 함수의 인자 이름 ]
쿼리 함수
Promise를 반환하는 함수
👉 axios, fetch
옵션
useQuery 기능을 제어한다.
👉 cacheTime, staleTime, refetchOnMount, refetchOnWindowFocus, refetchInterval, refetchIntervalInBackground, enabled, onSuccess, onError, select
반환 값
isLoading, isError, isSuccess 등 Promise의 함수 상태를 status로 알 수 있다.
기본적으로 배열이 들어가고, 순서에 따라 다른 키로 인식한다.
키의 종류
문자열
자동으로 길이가 1인 배열로 변환한다.
ex) "first" 👉 [ "first"]
배열
문자열과 숫자가 함께 있으면 숫자로 구분한다.
Promise 함수의 인자 이름
배열의 마지막 요소이며 객체로 전달한다.
만약, 호출하는 API가 같더라도 쿼리 키가 다르다면 캐싱은 별도로 진행된다.
const firstQuery = useQuery(["todos", "first"], fetchTodos);
const secondQuery = useQuery(["todos", "second"], fetchTodos);
const thirdQuery = useQuery(["todos", "third"], fetchTodos);
["todos", "first"] , ["todos", "second"], ["todos", "third"] 각각에 대해 캐싱을 진행한다.
cacheTime
// cacheTime을 3초로 설정
const { data, isLoading } = useQuery('todos', fetchTodos, {
cacheTime : 3000,
});
staleTime
// staleTime을 3초로 설정
const { data, isLoading } = useQuery('todos', fetchTodos, {
staleTime : 3000,
});
refetchOnMount
// refetchOnMount을 3초로 설정
const { data, isLoading } = useQuery('todos', fetchTodos, {
refetchOnMount : true,
});
refetchOnWindowFocus
const { data, isLoading } = useQuery('todos', fetchTodos, {
refetchOnWindowFocus : true,
});
refetchInterval
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('todos', fetchTodos, {
refetchInterval: 2000,
});
refetchIntervalInBackground
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('todos', fetchTodos, {
refetchInterval: 2000,
refetchIntervalInBackground: true,
});
enabled
const { data, isLoading, refetch } = useQuery('todos', fetchTodos, {
enabled : false,
});
return (
<button onClick={ refetch }>Fetch Button</button>
)
onSuccess
const { data, isLoading } = useQuery('todos', fetchTodos, {
onSuccess : (data) => {
console.log('데이터 요청 성공', data);
}
});
onError
const { data, isLoading } = useQuery('todos', fetchTodos, {
onError : (error) => {
console.log('데이터 요청 실패', error);
}
});
select
const { data, isLoading } = useQuery('todos', fetchTodos);
console.log(data.data);
/*[
{ id : 1, content : 'React Query'},
{ id : 2, content : 'SWR'},
{ id : 3, content : '어렵다😭'},
]*/
👇 원하는 데이터로 가공시키면
const { data, isLoading } = useQuery('todos', fetchTodos, {
select : (data) => {
return data.data.map(todo => todo.content);
}
});
console.log(data); // [ 'React Query', 'SWR', '어렵다😭' ]
data
서버 요청에 대한 데이터
isLoading
캐시가 없는 상태에서의 데이터 요청 중인 상태 👉 true / false
isFetching
캐시의 유무 상관없이 데이터 요청 중인 상태 👉 true / false
isError
서버 요청 실패에 대한 상태 👉 true / false
error
서버 요청 실패 👉 object
const fetchTodos = () => {
return axios.get('http://localhost:3000/todos');
};
// 방법1. 구조분해 X
const responseData = useQuery('todos', fetchTodos);
// 방법2. 구조분해 O
const { data, isLoading, isFetching, isError, error } = useQuery('todos', fetchTodos);
if (isLoading) {
return <h2>Loading...</h2>
}
if (isError) {
return <h2>{error.message}</h2>
}
console.log(responseData);
데이터 패칭을 여러 개 실행하고 싶다면 useQuery를 병렬로 사용한다.
function App(){
const userQuery = useQuery('users', fetchUsers);
const todoQuery = useQuery('todos', fetchTodos);
const projecstQuery = useQuery('projects', fetchProjects);
}
👇 쿼리 처리의 동시성을 극대화 시키기 위해 병렬 처리를 시도해보자.
function App({users}){
const userQueries = useQueries(
user.map(user => {
return {
queryKey : [ "user", user.id ],
queryFunction : () => fetchUserById(user.id),
}
})
)
}
렌더링이 계속해서 일어나는 동안 쿼리가 수행된다면, 이 로직이 hook에 위배될 수 있기에 useQueries
를 사용하여 병렬 처리를 구현해야 한다.
동기적으로 수행되어야 하는 작업을 위해서는 enabled 속성을 활용한다.
👇 useQuery에서 enabled 속성 값이 true일 때 실행되며, 코드를 살펴보자.
const fetchUserById = (id) => {
return axios.get(`http://localhost:3000/users/${id}`);
};
const fetchTodosByTodoId = (todoId) => {
return axios.get(`http://localhost:3000/todos/${todoId}`);
};
const DependentQueries = ({id}) => {
const { data : user } = useQuery(['user', id], () => fetchUserById(id));
const todoId = user?.data.todoId;
useQuery(['todos', todoId], () => fetchTodosByTodoId(todoId), {
enabled : !!todoId,
});
return <div>DependentQueries</div>
};
export defalut DependentQueries;
id로 유저의 정보를 가져오는 함수, todoId로 할 일의 정보를 가져오는 함수를 만들고 동기적인 작업을 위한 DependentQueries에 이를 사용한다.
유저의 정보를 가져오게 된다면, todoId의 값이 존재하고, 그렇지 않다면 todoId에 undefined가 저장될 것 이다.
따라서 이를 이중 부정을 통해서 userId를 true, false로 판별하여 useQuery의 실행 유무를 판단한다.
useQuery 요청이 실패하는 경우, 지정한 최대 연속 요청 한계까지 재요청을 보낸다.
( 기본적으로 3번은 실행된다)
retry
재요청 횟수이다.retryDelay
다음 재요청 까지의 딜레이 시간이다.const userQuery = useQuery(['users',1], fetchUsers,
{ retry : 10, retryDelay : 400});
POST, PUT, DELETE와 같은 변경 및 수정작업을 할 때 사용되는 훅이며. 데이터를 변경하고 삭제한다.
const requestData = useMutation(API 호출 함수, 콜백);
API 호출 함수
콜백
👇 useMutation
을 통해 mutation 객체
를 생성하고, useQuery와 같은 반환값을 받으며, mutate메서드를 통해 API 요청 함수를 호출하여 요청한다.
import { useMutation } from 'react-query';
const AddTodo = () => {
const addTodoList = (todo) => {
return axios.post('http://localhost:3000/todos', todo);
};
const { mutate : addTodo, isLoading, isError, error } = useMutation(addTodoList);
const handleAddTodoClick = () => {
const todo = { 날짜, 할일 };
addTodo(todo);
};
if (isLoading) {
return <h2>Loading...</h2>
}
if (isError) {
return <h2>{ error.message }</h2>
}
};
❗️ useQuery vs useMutation ❗️
- useQuery는
Server State와 Client State의 정확성
을 유지하기 위해 사용한다.- useMutation으로 변경을 가하기 때문에
같은 서버 데이터를 공유
하는 다른 클라이언트에도영향
을 미친다.- useMutation에서는
custom으로 retry 옵션
을 따로 줄 수 있다.
👉 한 번 요청에 실패하면 그대로 실패이다.
위의 코드에서 한 가지 문제점은 Add Todo를 수동으로 클릭하여 Fetch를 통해 화면에 보여진다는 게 불편하다.
👇 이를 해결하기 위해서 이 전에 캐싱된 쿼리가 있다면 이를 직접 useQueryClient
로 무효화를 시키고, 데이터를 새로 패칭하도록 해야 한다.
import { useMutation, useQueryClient } from 'react-query';
const AddTodo = () => {
// 쿼리 무효화를 위한 셋팅
const queryClient = useQueryClient();
const addTodoList = (todo) => {
return axios.post('http://localhost:3000/todos', todo);
};
const { mutate : addTodo, isLoading, isError, error } = useMutation(addTodoList,
{
onSuccess : () => {
// 캐시가 있는 모든 쿼리 무효화
queryClient.invalidateQueries();
// queryKey가 'to-dos'로 시작하는 모든 쿼리 무효화
queryClient.invalidateQueries('to-dos');
}
});
const handleAddTodoClick = () => {
const todo = { 날짜, 할일 };
addTodo(todo);
};
if (isLoading) {
return <h2>Loading...</h2>
}
if (isError) {
return <h2>{ error.message }</h2>
}
};
👇 더불어 mutate가 실행되기 전에 성공 여부, 끝과 같은 라이프사이클에 따라 콜백함수를 작성할 수도 있다.
useMutation(addTodoList, {
onMutate : (variables) => {
// mutate 함수가 실행되기 전에 실행
// addTodo에 들어가는 인자
console.log(variables)
// context 로 참조 가능
return { date : "2022-04-23", content : "dance" };
},
onSuccess : (data, variables, context) => {
// 성공
console.log(`${context.date} - ${context.content}`);
},
onError : (error, variables, context) => {
// 에러 발생
},
onSettled : (data, error, variables, context) => {
// 성공 또는 실패 상관없이 실행
},
})
📚 학습할 때, 참고한 자료 📚