프론트엔드 개발에 빠지지 않는 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를 사용할 수 있다.
와 초심자에게 어려울 수 있는 리액트 쿼리에 대해 너무 쉽고 자세하게 설명해 주셔서 읽는 내내 이해가 쏙쏙 되었어요!!! 좋은 글 감사합니다 :)