프론트엔드 개발에 빠지지 않는 API 통신, 여러분들은 어떤 방식으로 처리하고 계신가요?!
SWR, Recoil 등등.. 많은 데이터 패칭 라이브러리들이 존재하는데요!
그 중 오늘은 React Query에 대해 다뤄보고자 합니다!
react-query는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용한다.
React Query는 React Application에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리이다.
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} "@tanstack/react-query";
import { getTodos, postTodo } from '../my-api'
// ✅ React Query는 내부적으로 queryClient를 사용하여 각종 상태를 저장하고, 부가 기능을 제공합니다.
const queryClient = new QueryClient()
function App() {
return (
// App에 Client를 제공합니다.
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// client에 접근합니다.
const queryClient = useQueryClient()
// ✅ Queries
// "/todos" API에 Get 요청을 보내 서버의 데이터를 가져옵니다.
const query = useQuery(['todos'], getTodos)
// ✅ Mutations
// ✅ "/todos" API에 Post 요청을 보내 서버에 데이터를 저장합니다.
const mutation = useMutation(
(todo) => axios.post('/todo', { todo }),
{
// Post 요청이 성공하면 위 useQuery의 데이터를 초기화합니다
// 데이터가 초기화되면 useQuery는 서버의 데이터를 다시 불러옵니다.
onSuccess: () => {
// Invalidate(데이터가 오래 되었다고 판단되면 다시 get) and refetch
queryClient.invalidateQueries('todos')
},
})
return (
<div>
<ul>
{query.data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
react query는 hook 기반의 로직들로 되어있어 해당 훅을 사용하는 컴포넌트에서 상태 값의 변경을 간편하게 파악하여 리렌더링을 유발하게 해준다.
또한 리엑트 쿼리는 데이터의 캐시 처리를 간편하게 할 수 있는 인터페이스를 제공한다.
즉, 클라이언트와 서버의 상태 값을 일치시켜줘야 하는 요구사항에서 부가적으로 생길 수 있는 로직들을 리엑트 쿼리의 api와 인터페이스로 간단하게 해결할 수 있도록 도와주는 것이다!
React Query 공식 문서에서는 React Query의 3가지 핵심 개념을 Queries, Mutations, Query Invalidation으로 소개하고 있다.
useQuery를 사용하고 싶다면 useQueries를 사용하는 것을 추천한다.enabled 옵션을 사용하면 useQuery를 동기적으로 사용 가능하다.import { useQuery } from "@tanstack/react-query";
function App() {
const info = useQuery(['todos'], fetchTodoList) // ✅ 'todos' -> Query Key, ✅ fetchTodoList -> Query Function
}
const fetchTodoList = useCallback(() => {
return axios.get("http://localhost:4000/todolist");
}
파라미터
**queryKey(필수)**, 두 번째 인자가 **queryFn(필수)**, 세 번째 인자가 **options**📌 React Query v4 변경사항!
// v3
useQuery("todos", fetchTodos); // (-)
// v4
**useQuery(["todos"], fetchTodos); // (+)**
useQueries는 인자로 queries 프로퍼티를 가진 객체를 넘겨줄 수 있다.queries의 값은 쿼리 배열이다.// v3
useQueries([
{ queryKey1, queryFn1, options1 },
{ queryKey2, queryFn2, options2 },
]);
// v4
**useQueries({
queries: [
{ queryKey1, queryFn1, options1 },
{ queryKey2, queryFn2, options2 },
],
});**
그럼
useQuery가 반환하는 건 뭘까?
const {
data,
status,
**fetchStatus,** // v4에서 추가
isLoading,
isError,
error,
isFetching,
...
} = useQuery(
status는 data가 있는지 없는지에 대한 상태
fetchStatus는 쿼리 즉, queryFn 요청이 진행중인지 아닌지에 대한 상태
data: 쿼리 함수가 리턴한 Promise에서 resolved된 데이터 (Response)
error: 에러가 발생했을 때 반환되는 객체
isError: 에러가 발생한 경우 true
isFetching: 캐싱 된 데이터가 있더라도 쿼리가 실행되면 로딩 여부에 따라 true/false로 반환
isLoading: 캐싱 된 데이터가 없을 때, 즉 처음 실행된 쿼리 일 때 로딩 여부에 따라 true/false로 반환
status: 쿼리 요청 함수의 상태를 표현하는 status는 4가지의 값 (문자열 형태)
fetchStatus
refetch : 해당 query refetch 함수 제공
remove : 해당 query remove 함수 제공
useQuery 주요 옵션
const { isLoading, isFetching, data, isError, error } = useQuery(
["todos"],
getTodos,
{
cacheTime: 3000, // 기본값 : 0초
staleTime: 50000, // 기본값 : 5분
networkMode: 'offlineFirst', // v4에서 추가
}
);
online - 오프라인 상태에서 network connection이 있기 전까지 fetch를 하지 않음always - 오프라인 상태에서도 온라인처럼 fetch를 시도offlineFirst - queryFn 최초 호출 후 retry를 멈춥니다.(number | Infinity) - 데이터가 fresh에서 stale 상태로 변경되는 데 걸리는 시간(number | Infinity) - 데이터가 inactive 상태일 때 캐싱 된 상태로 남아있는 시간boolean - 쿼리를 자동으로 실행시킬지 말지 여부boolean | "always” - 데이터가 stale 상태일 경우, mount마다 refetch를 실행하는 옵션stale 상태일 경우 윈도우 포커싱 될 때마다 refetch를 실행하는 옵션function App() {
const mutation = useMutation(newTodo => {
return axios.post('/todos', newTodo)
})
return (
<div>
{mutation.isLoading ? (
'todo 추가중...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: '웹 심화 스터디' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
파라미터
mutate
mutate 메서드를 이용해서 요청 함수를 호출할 수 있다.onSuccess, onError 메서드를 통해 성공 했을 시, 실패 했을 시 response 데이터를 핸들링할 수 있다.onMutate는 mutation 함수가 실행되기 전에 실행되고, mutation 함수가 받을 동일한 변수가 전달된다.onSettled는 try...catch...finally 구문의 finally처럼 요청이 성공하든 에러가 발생되든 상관없이 마지막에 실행된다.useMutate 주요 옵션
useMutation({
mutationFn: addTodo,
onMutate: (variables) => {
return { id: 1 }
},
onError: (error, variables, context) => {
},
onSuccess: (data, variables, context) => {
},
onSettled: (data, error, variables, context) => {
},
})
query Key가 변하지 않으므로 강제로 쿼리를 무효화하고 최신화를 진행해야 하는데, 이런 경우에 **invalidateQueries()** 메소드를 이용할 수 있다.refetch를 할 때 사용한다!import { useQuery, useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: ['todos'] })
// 아래 query들이 invalidated 된다.
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
queryKey: ['todos', { page: 1 }],
queryFn: fetchTodoList,
})
QueryClient
import { QueryClient, useQueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient()
const queryClient = useQueryClient();
useQueryClient Hook을 사용한다.mutations후 다시 get
invalidateQueries에 넣는다.setQueryData를 사용한다.const mutation = useMutation(postTodo, {
onSuccess: () => {
// postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행한다.
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
});
const mutation = useMutation(editTodo, {
onSuccess: data => {
// data가 fetchTodoById로 들어간다.
queryClient.setQueryData(["todo", { id: 5 }], data);
}
});
yarn create vite [프로젝트명] --template react-ts
cd [프로젝트명]
@tanstack/react-query v4부터 react-query에서 @tanstack/react-query로 패키지가 변경되었다.
따라서 설치와 import 할 때 주의해야 한다. 또한, Devtools는 별도의 패키지 설치가 필요하다.
yarn add @tanstack/react-query
yarn add @tanstack/react-query-devtools
// v3
import { useQuery } from "react-query"; // (-)
import { ReactQueryDevtools } from "react-query/devtools"; // (-)
// v4
**import { useQuery } from "@tanstack/react-query"; // (+)
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // (+)**
📌 Devtool이 뭔가요?!
전용 devtools를 제공한다.시각화하는 데 도움이 되며 문제가 발생하면 디버깅 시간을 절약할 수 있다.process.env.NODE_ENV === 'development' 인 경우에만 실행된다, 즉 일반적으로 개발환경에서만 작동하므로 설정되어있으므로, 프로젝트 배포 시에 Devtools 삽입코드를 제거해줄 필요가 없다.queryClient를 생성한다. 쿼리 클라이언트는 쿼리와 서버의 데이터 캐시를 관리하는 클라이언트이다.const queryClient = new QueryClient();
QueryClientProvider를 최상단에서 감싸주고 QueryClient 인스턴스를 client props로 넣어 애플리케이션에 연결시켜야 한다.// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
**import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';**
// ✅ QueryClient 생성
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
{/* ✅ QueryClientProvider */}
<**QueryClientProvider** **client={queryClient}**>
{/* ✅ devtools */}
**<ReactQueryDevtools initialIsOpen={true} />**
<App />
</**QueryClientProvider**>
</React.StrictMode>,
document.getElementById("root")
);
ReactQueryDevtools는 쿼리키로 쿼리를 표시해주고 활성(active), 비활성(inactive), 만료(stale) 등 모든 쿼리의 상태를 알려준다. 리액트 서버를 실행하고 브라우저를 확인해보면 브라우저 왼쪽아래에 리액트쿼리 로고모양 버튼이 생긴다. 이를 클릭하면 Devtools를 사용할 수 있다.
와 초심자에게 어려울 수 있는 리액트 쿼리에 대해 너무 쉽고 자세하게 설명해 주셔서 읽는 내내 이해가 쏙쏙 되었어요!!! 좋은 글 감사합니다 :)