React Query 시작하기
React Query
카카오페이 프론트엔드 개발자와 많은 프론트엔드 개발자들이 채택하면서 아주 Hot해진 라이브러리이다. 간단히 설명해보자면, React환경에서 fectching
과 caching
, 서버 데이터
와의 동기화를 지원해주는 라이브러리이다. 이번 프로젝트에 새롭게 도입하면서 사용했던 방법과 기능, Tip에 대해서 정리해두려고 한다.
그렇다면 왜 React Query를 사용할까?
React Query를 사용하는 이유?
-
간편한 데이터 관리
서버 데이터
와의 동기화를 간편하게 해주고 ,caching
,fetching
처리를 간편하게 할 수 있다. -
실시간 업데이트 및 동기화
실시간 데이터 업데이트로 자동 동기화를 지원하여 서버와 클라이언트 데이터의 일관성을 유지할 수 있다. (ex. 브라우저의 포커스가 들어온 경우, 컴포넌트 마운트의 경우, 네트워크 재연결이 발생한경우)
-
데이터 캐싱
데이터를 캐싱하여 불필요한 API요청을 줄이고 App 성능을 향상시킬 수 있다.
caching
은 많은 개발자들이 다루기 어려워하는 부분중 하나이다. 그런데React Query
는 이부분을 자동화하여 해결한다. -
서버 상태관리
fetching
의 상태관리 (Loading, Error, Success)를 간편하게 처리할 수 있다.
설치
$ npm i @tanstack/react-query # or $ pnpm add @tanstack/react-query # or $ yarn add @tanstack/react-query # or $ bun add @tanstack/react-query
공식 문서대로 위의 커멘드로 간편하게 React Query를 받을 수 있다. 하지만 좀 더 추천하는 Tool이 있는데 그게 바로 react-query-devtools
이다. react-query-devtools
는 Query상태를 한눈에 볼 수 있게 해준다.
$ npm i @tanstack/react-query-devtools@4 # or $ pnpm add @tanstack/react-query-devtools@4 # or $ yarn add @tanstack/react-query-devtools@4
시작하기
필자는 NextJS를 v13이상을 사용하고 있기 때문에 기존의 React에서 사용하던 방법과는 다른 방법을 사용해야한다. 아래처럼 app
디렉토리 하위의 layout
에 QueryClientProvider
를 감싸서 사용하면 되겠지? 하면 에러가 나게된다.
import './globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', } export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { const queryClient = new QueryClient() return ( <html lang="en"> <QueryClientProvider client={queryClient}> <body className={inter.className}>{children}</body> </QueryClientProvider> </html> ) }
Error**:** Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.
해석하자면, 클라이언트 컴포넌트로 전달되는 데이터 형식이 허용되지 않는다는 것이다. Root Layout
은 app
디렉토리의 최상위 수준에서 정의되며 모든 경로에 적용되는데 서버에서 반환된 초기 HTML을 수정할 수 있게된다. Root Layout
은 서버 컴포넌트이며, 클라이언트 컴포넌트로 설정할 수 없다. 하여 위와 같은 오류가 나타나는 것이다.
그렇다면 클라이언트 컴포넌트로 바꿔보자.
'use client' import React from 'react' import { QueryClientProvider } from '@tanstack/react-query' import { getQueryClient } from '@/utils/ReactQueryProvider/ReactQueryProvider' const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => { const queryClient = getQueryClient() return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> } export default ReactQueryProvider
utils
라는 폴더를 만들고 ReactQueryProvider
컴포넌트를 만들어 아래의 Root Layout
에 넣어 감싸주면 되겠다. 이후 React Query Devtools
를 넣기 위해 app
디렉토리 하위에 Layout
디렉토리를 만들고 Layout.tsx
를 만들어 Root Layout
에 감싸주면 되겠다.
'use client' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import React from 'react' const Layout = ({ children }: { children: React.ReactNode }) => { return ( <> <div> {children} <ReactQueryDevtools position="bottom" /> </div> </> ) } export default Layout import ReactQueryProvider from '@/utils/ReactQueryProvider/ReactQueryProvider' import './globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' import Layout from '@/components/Layout/layout' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', } export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <ReactQueryProvider> <Layout> <body className={inter.className}>{children}</body> </Layout> </ReactQueryProvider> </html> ) }
자, 여기서 궁금한게 있다. 바로 const queryClient = new QueryClient()
부분 이건 무엇을 하는데 사용하게 되는걸까?
queryClient
는 쿼리와 뮤테이션을 실행하고 캐시된 데이터를 관리하기위한 핵심 객체이다.useQuery
,useMutation
,useQueryClient
와 같은 React Query 훅을 사용하여 데이터를 가져오고 변경할 때queryClient
를 사용한다.
따라서,
queryClient
를QueryClientProvider
에 전달하여 App의 모든 부분에서 동일한 인스턴스를 사용할 수 있도록 한다.
QueryClient 생성하기
공식문서대로 const queryClient = new QueryClient()
사용하지만 사실 이렇게 사용하면 문제가 되는 부분이 있다. 서버와 클라이언트간 환경을 고려하지 않은 것이다. 서버환경과 클라이언트간의 캐시를 따로 관리해주어야 한다.
-
데이터 충돌
서버는 사용자의 요청을 동시에 처리하게 된다. 만약 모든 요청이 동일한
QueryClient
인스턴스를 공유하게 된다면 한 사용자의 요청이 다른 사용자의 요청에 영향을 미칠 수 있다. 예를 들어 한 사용자가 데이터를 업데이트 하면, 그 변경 사항이 다른 사용자의 요청에 반영될 수 있다. 이는 데이터 일관성을 해칠 수 있다. -
캐시 오염
서버에서 캐시는 요청 간에 공유되지 않는 것이 이상적이다. 만약 캐시가 공유된다면, 한 사용자의 데이터가 다른 사용자에게 노출될 수 있다. 이는 보안 문제를 야기할 수 있으며, 특히 민감한 데이터를 다룰 때 심각한 문제가 될 수 있다.
-
상태 관리의 복잡성 증가
서버는 각 요청이 독립적으로 처리되도록 설계되어야 한다. 동일한
QueryClient
를 공유하면, 상태 관리가 복잡해지고, 요청 간의 상태를 분리하기 어려워진다. -
성능 저하
브라우저에서 매번 새로운
QueryClient
인스턴스를 생성하면, 불필요한 메모리 사용과 리소스가 발생한다. 이는 특히 많은 쿼리를 실행할 때 성능에 부정적인 영향을 미칠 수 있다. 브라우저는 단일 사용자 세션을 처리하기 때문에, 동일한 인스턴스를 재사용하는 것이 더 효율적이다. -
캐시 비효율성
동일한 데이터를 여러 번 요청할 때, 캐시를 재사용하지 못하면 네트워크 요청이 증가된다. 이는 굳이
React-query
의 가장 큰 장점을 살리지 못할 뿐더러 리소스 비용의 증가, 사용자 경험이 저하될 수 있으며, 특히 네트워크가 느리거나 데이터 사용량이 제한된 환경에서 문제가 될 수 있다. 또한 불필요한 네트워크 트래픽을 유발하고, 서버 부하를 증가시키게 된다.
이를 해결하기위해 어떻게 해야할까? 간단하다. 서버에서 사용하는 캐시의 경우엔 매번 QueryClient
를 생성해야하고, 클라이언트에선 생성한 QueryClient
를 재사용해서 사용하면 되겠다. 내가 사용하고 있는 v5기준으로 보자면, 공식문서에 나와있는 코드를 사용하면 되겠다.
import { QueryClient, isServer, QueryCache } from '@tanstack/react-query' export const defaultStaleTime = 60 * 1000 function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: defaultStaleTime, }, }, queryCache: new QueryCache({}), }) } let browserQueryClient: QueryClient | undefined export function getQueryClient() { if (isServer) { return makeQueryClient() } if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient }
사용하기
useQuery 기본 사용 방법
useQuery
는 v5부터 하나의 객체를 파라미터로 받는다. 그중 반드시 들어가야할 인자는 queryKey
, queryFn
이 필수값으로 들어가게 된다.
const result = useQuery({ queryKey: ['queryKey'], // required queryFn: queryFunction, // required // ...options })
-
queryKey
useQuery
의queryKey
는 배열로 지정해 줘야한다. 무엇으로 채울지는 개발자의 마음이겠지만, 문자열의 형태일 수도 있고, 객체로 구성된 복잡한 형태도 들어갈 수 있다.useQuery
는queryKey
를 기반으로쿼리 캐싱
을 관리하는 것이 중요한데 만약 쿼리가 특정 변수에 의해 의존하게 된다면 배열에 넣어주어야 한다. -
queryFn
useQuery
의queryFn
은Promise
를 반환하는 함수를 넣어주어야 한다.const queryFunction = async () => { const data = await fetch('https://jsonplaceholder.typicode.com/posts').then((response) => response.json()) return data } const result = useQuery({ queryKey: ['queryKey'], queryFn: queryFunction })
-
options
공식문서를 보면 아주 많은 options이 제공되는 것을 볼 수 있다.
-
반환되는
result
의 값들은 아래와 같다.const { data, error, status, fetchStatus, isLoading, refetch, // ... } = useQuery(queryKey, queryFn })
-
data : 쿼리 함수가 리턴한
Promise
에서resolve
된 데이터 -
error : 쿼리 함수에 오류가 발생한 경우 쿼리에 대한 오류 객체
-
status :
data
쿼리 결과값에 대한 상태를 표현한다. 문자열로 3가지의 값이 존재한다.- Pending : 쿼리 데이터가 없는 상태이며, 쿼리 시도가 아직 완료되지 않은 상태.
- error : 에러가 발생했을 때 상태.
- success : 쿼리 함수가 오류 없이 요청에 성공하고 데이터를 표시할 준비가된 상태.
-
fetchStatus :
queryFn
에 대한 정보를 나타낸다.- fetching : 쿼리가 현재 실행중인 상태.
- paused : 쿼리를 요청했지만, 잠시 중단된 상태.
- Idle : 쿼리가 현재 아무 작업도 수행하지 않은 상태.
-
-
isLoading :
캐싱 된 데이터
가 없을 때 즉, 처음 실행된 쿼리일 때 로딩 여부에 따라서true
/false
로 반환된다. 이는 캐싱된 데이터가 있다면 로딩 여부에 상관없이false
를 반환한다. -
refetch : 쿼리를 수동으로 다시 가져오는 함수.
-
status와 fetchStatus를 나눠서 다루는 이유는 무엇일까?
fetchStatus
는 HTTP 네트워크 연결 상태와 좀 더 관련된 상태 데이터이다.
예를 들어
status
가success
상태라면 주로fetchStatus
는idle
상태이지만, 백그라운드에서 re-fetch가 발생할 때fetching
상태일 수 있다.
status
가 보통loading
상태일 때fetchStatus
는 주로fetching
을 갖지만 네트워크 연결이 되어 있지 않은 경우paused
상태를 가질 수 있다.정리하자면
status
는data
가 있는지 없는지에 대한 상태를 의미하고,fetchStatus
는queryFn
요청이 진행중인지 아닌지에 대한 상태를 의미한다.
예시
자 그럼 위의 사용 방법을 토대로 데이터를 받아보자.
'use client' import { useQuery } from '@tanstack/react-query' import React from 'react' interface JsonPlaceHolder { id: number body: string title: string userId: number } const Home = () => { const getListData = async (): Promise<JsonPlaceHolder[]> => { const data = await fetch('https://jsonplaceholder.typicode.com/posts').then((response) => response.json()) return data } const { data, status } = useQuery({ queryKey: ['list'], queryFn: getListData, }) return ( <div className="w-full h-full"> {status === 'pending' && <span>Loading...</span>} {status === 'success' && ( <ul> {data.map((item, index) => ( <li key={`item-list-${item.id}-${index}`}> <span>{item.title}</span> </li> ))} </ul> )} </div> ) } export default Home
위와 같이 status
로 상태를 분류하여 사용할 수 있게되었다. 기존에 데이터를 받아올 때 useState
를 사용하여 loading처리를 해주었던것 보다 좀 더 간결한 처리가 가능하게 되었다.
이전에 State
관리를 위해 사용했던 Redux
와 같은 라이브러리는 장황한 Boilerplate code가 필요했다. 컴포넌트가 마운트 되었을때, 필요한 Dispatch
, 상태를 업데이트하기위한 useEffect
, Auction
등 개발자들이 입력해야할 코드 또한 확연하게 줄어들게 된다.
또한 Redux
는 비동기 데이터 관리를 위한 라이브러리가 아닌 state
를 관리하기 위한 라이브러리이다. 그렇기 때문에 Redux
를 이용해서 비동기 데이터를 관리하기 위해 미들웨어부터 State
구조까지 여러 부분을 설계해야했다. 하지만 React Query
는 비동기 데이터를 관리하기위한 라이브러리로, 쉽고 간결하게 API호출 로직을 작성할 수 있도록 해준다.
이번 글에서는 단순한 사용방법과 Tip을 적었지만, 다음 글에서는 Muate
와 Infinite Query
, Query
의 병렬처리에 대해서 글을 써보려고 한다.