이전 SWR 포스팅을 작성했는데요. SWR보다 react-query 다운로드 수가 2배가량 높습니다. 원티드 프리온보딩 챌린지를 하면서도 사용했고 점점 대중화되어가는 눈치입니다. 부랴부랴 포스팅 남겨봅니다.
다행히도 Next.js와 리액트에서 SWR을 사용해 본 경험이 있어 덕분에 리액트 쿼리를 이해하는데 오래 걸리지 않았습니다.
React-Query
리액트 쿼리는 리액트 내에서 서버의 상태(데이터), 캐싱, 동기화 및 상태 업데이트를 쉽게 도와주는 라이브러리로, 말이 어려운데 HTTP 통신과 서버에서 받은 데이터를 캐싱하고 설정에 기반해 클라이언트 데이터를 서버 데이터로 동기화해 주는 라이브러리입니다.
저 나름대로 리액트 쿼리를 정의해 봤는데요. 클라이언트에서 서버 데이터를 현명한 접근 방식으로 처리해주는 라이브러리! 어떤가요? 🤔
두리뭉실한데, 의미를 좁혀보자면 데이터 통신, 캐싱, 업데이트, 에러핸들링, 효율적인 비동기 과정을 처리해 줍니다.
Redux vs React-Query
리액트 쿼리에서 자주 나오는 말은 리덕스를 리액트 쿼리가 대체할 수 있다는 얘기입니다.
리액트를 공부했던 분들이라면 리덕스라는 높은 러닝커브의 라이브러리를 알고 있을 텐데요. 보통의 리액트 커리큘럼에는 꼭 들어가 있는 라이브러리지만 여기에 비동기 작업에 사용해야 하는 미들웨어(saga, thunk)를 또 학습해야 합니다. 리덕스 자체로도 코드나 파일 분리가 생겨 복잡도를 상승시키는데 추가로 미들웨어까지 붙여야 한다니, 개인적으로 답답할 따름입니다.
하지만 리덕스는 리덕스에 맞는 데이터를, 리액트 쿼리는 리액트 쿼리에 맞는 데이터를 다루는 데 사용하면 될 것 같습니다. 둘 다 다루는 데이터의 관심사가 달라서 Redux vs React-Query의 구도가 맞는지는 의문입니다.
Reference
React-Query 설치
> yarn add @tanstack/react-query
> npm i @tanstack/react-query
React-Query 사용하기
// src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
const queryOptions = {
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 0,
},
},
}
const queryClient = new QueryClient(queryOptions);
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<App />
</QueryClientProvider>
)
리액트 엔트리 포인트에서 리액트 쿼리를 사용할 수 있도록 Provider로 감싸주기
queryOptions
- staleTime: 캐시가 fresh 상태로 유지되는 시간 (default: 0)
- cacheTime: 메모리에 캐시가 남아있는 시간으로, 만료 시 가비지 컬렉션이 데이터를 처리 (default: 5*60*1000 => 5 min)
- enable: 자동으로 데이터를 불러옴 (default: true)
- retry: API호출 실패 시 자동으로 재요청, true일 경우 무한 재요청, Number로 입력할 경우 N번 재요청 (default: false)
- onSuccess/onError/onSettled/select: 성공/실패/무조건실행(data: 성공하면 데이터, error: 에러)/성공 시 데이터 가공
- keepPrevioueData: 새로 가져온 데이터를 화면에 나오기 전까지 기존 데이터를 화면에 유지할지 여부
useQuery / useMutation
useQuery: GET Method API를 호출할 때 사용하는 훅
useMutation: POST / PUT / DELETE Method는 useMutation 훅을 사용
example
import { useQuery } from '@tanstack/react-query'
export default function Todos() {
// Queries
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['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>
)
관심사 분리를 통해 React-Query 사용
Project
│
├─ src
│ ├─ api (🌏) => API 함수 모음
│ │ └─ todo.tsx
│ ├─ hooks (🕹️) => 커스텀 훅 모음
│ │ ├─ mutation (📫) useMutation 훅
│ │ └─ query (🕸️) useQuery 훅
...
제 경우 기존 개발 로직에서는 api 디렉토리에 정의한 함수를 컴포넌트 로직에서 불러와 별도의 함수를 선언해 try-catch문으로 정의해 주고 useEffect 훅으로 호출해 주는 방식으로 사용했는데요.
API -> Component( + API 로직 )
API(Rest) / Hooks(query, mutation) / Component 이렇게 관심사를 분리해서 코드를 작성할 경우 각 영역 별로 코드의 목적을 명확하게 알 수 있고 유지보수나 재사용성이 높아집니다.
API -> Hooks(query/mutation) -> Component
가운데 추가된 Hooks의 역할은 API 호출뿐만 아니라 캐싱처리 및 에러 핸들링이나 데이터 가공 등 Rest API를 호출하고 진행해야 하는 로직을 처리하고 그 결과는 컴포넌트가 전달받아 렌더링 해줍니다.
기존 개발 로직에서는 하나의 컴포넌트에 많은 로직이 포함 됐다면 지금은 관심사 및 기능이 명확히 분리되어 코드가 정리됩니다.
API
// src/api/todo.js
import axios from 'axios
export const getTodoList = () => {
const url = '/todos'
return axios.get(url)
}
Hooks
// src/hooks/query/useGetTodos.js
import { useQuery } from 'react-query'
import { getTodoList } from '../../api/todo'
const useGetTodos = () => {
return useQuery(['getTodoList'], () => getTodoList(), {
select: (data) => {
// data.data.reverse() // 데이터 가공
return data
},
staleTime: 30000,
cacheTime: Infinity,
})
}
export default useGetTodos
Component
import { useState } from 'react'
import useGetTodos from '../../hook/query/useGetTodos'
import { Link } from 'react-router-dom'
const Home = () => {
const [list, setList] = useState([])
const { data, isLoading } = useGetTodos()
useEffect(() => {
if (data) setList(data.data)
}, [data])
if (isLoading) return <>로딩중 ...</>
return (
<ul>
{list &&
list.map((todo) => (
<li key={todo.id}>
<Link to={`/todo/${todo.id}`}>
<span className="todo-title">{todo.title}</span>
<span className="todo-date">{dateFormatter(todo.createdAt)}</span>
</Link>
</li>
))}
</ul>
)
}
export default Home