Hu.
About

React Query 시작하기

react2024/4/27

React query 대표이미지

React Query

카카오페이 프론트엔드 개발자와 많은 프론트엔드 개발자들이 채택하면서 아주 Hot해진 라이브러리이다. 간단히 설명해보자면, React환경에서 fectchingcaching, 서버 데이터와의 동기화를 지원해주는 라이브러리이다. 이번 프로젝트에 새롭게 도입하면서 사용했던 방법과 기능, Tip에 대해서 정리해두려고 한다.

그렇다면 왜 React Query를 사용할까?

React Query를 사용하는 이유?

  1. 간편한 데이터 관리

    서버 데이터와의 동기화를 간편하게 해주고 , caching, fetching처리를 간편하게 할 수 있다.

  2. 실시간 업데이트 및 동기화

    실시간 데이터 업데이트로 자동 동기화를 지원하여 서버와 클라이언트 데이터의 일관성을 유지할 수 있다. (ex. 브라우저의 포커스가 들어온 경우, 컴포넌트 마운트의 경우, 네트워크 재연결이 발생한경우)

  3. 데이터 캐싱

    데이터를 캐싱하여 불필요한 API요청을 줄이고 App 성능을 향상시킬 수 있다. caching은 많은 개발자들이 다루기 어려워하는 부분중 하나이다. 그런데 React Query는 이부분을 자동화하여 해결한다.

  4. 서버 상태관리

    fetching의 상태관리 (Loading, Error, Success)를 간편하게 처리할 수 있다.

설치

공식 React Query

$ 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상태를 한눈에 볼 수 있게 해준다.

공식 React Query Devtools

$ 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디렉토리 하위의 layoutQueryClientProvider를 감싸서 사용하면 되겠지? 하면 에러가 나게된다.

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> ) }

 

danger image

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 Layoutapp디렉토리의 최상위 수준에서 정의되며 모든 경로에 적용되는데 서버에서 반환된 초기 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()부분 이건 무엇을 하는데 사용하게 되는걸까?

bulb image

queryClient는 쿼리와 뮤테이션을 실행하고 캐시된 데이터를 관리하기위한 핵심 객체이다. useQuery, useMutation, useQueryClient와 같은 React Query 훅을 사용하여 데이터를 가져오고 변경할 때 queryClient를 사용한다.

 

따라서, queryClientQueryClientProvider에 전달하여 App의 모든 부분에서 동일한 인스턴스를 사용할 수 있도록 한다.

QueryClient 생성하기

공식문서대로 const queryClient = new QueryClient()사용하지만 사실 이렇게 사용하면 문제가 되는 부분이 있다. 서버와 클라이언트간 환경을 고려하지 않은 것이다. 서버환경과 클라이언트간의 캐시를 따로 관리해주어야 한다.

 

  1. 데이터 충돌

    서버는 사용자의 요청을 동시에 처리하게 된다. 만약 모든 요청이 동일한 QueryClient 인스턴스를 공유하게 된다면 한 사용자의 요청이 다른 사용자의 요청에 영향을 미칠 수 있다. 예를 들어 한 사용자가 데이터를 업데이트 하면, 그 변경 사항이 다른 사용자의 요청에 반영될 수 있다. 이는 데이터 일관성을 해칠 수 있다.

  2. 캐시 오염

    서버에서 캐시는 요청 간에 공유되지 않는 것이 이상적이다. 만약 캐시가 공유된다면, 한 사용자의 데이터가 다른 사용자에게 노출될 수 있다. 이는 보안 문제를 야기할 수 있으며, 특히 민감한 데이터를 다룰 때 심각한 문제가 될 수 있다.

  3. 상태 관리의 복잡성 증가

    서버는 각 요청이 독립적으로 처리되도록 설계되어야 한다. 동일한 QueryClient를 공유하면, 상태 관리가 복잡해지고, 요청 간의 상태를 분리하기 어려워진다.

  4. 성능 저하

    브라우저에서 매번 새로운 QueryClient 인스턴스를 생성하면, 불필요한 메모리 사용과 리소스가 발생한다. 이는 특히 많은 쿼리를 실행할 때 성능에 부정적인 영향을 미칠 수 있다. 브라우저는 단일 사용자 세션을 처리하기 때문에, 동일한 인스턴스를 재사용하는 것이 더 효율적이다.

  5. 캐시 비효율성

    동일한 데이터를 여러 번 요청할 때, 캐시를 재사용하지 못하면 네트워크 요청이 증가된다. 이는 굳이 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 })
  1. queryKey

    useQueryqueryKey는 배열로 지정해 줘야한다. 무엇으로 채울지는 개발자의 마음이겠지만, 문자열의 형태일 수도 있고, 객체로 구성된 복잡한 형태도 들어갈 수 있다. useQueryqueryKey를 기반으로 쿼리 캐싱을 관리하는 것이 중요한데 만약 쿼리가 특정 변수에 의해 의존하게 된다면 배열에 넣어주어야 한다.

  2. queryFn

    useQueryqueryFnPromise를 반환하는 함수를 넣어주어야 한다.

    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 })
  3. options

    공식문서를 보면 아주 많은 options이 제공되는 것을 볼 수 있다.

  4. 반환되는 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 : 쿼리를 수동으로 다시 가져오는 함수.

 

bulb image

status와 fetchStatus를 나눠서 다루는 이유는 무엇일까? fetchStatus는 HTTP 네트워크 연결 상태와 좀 더 관련된 상태 데이터이다.

  • 예를 들어 statussuccess 상태라면 주로 fetchStatusidle 상태이지만, 백그라운드에서 re-fetch가 발생할 때 fetching상태일 수 있다.

  • status가 보통 loading상태일 때 fetchStatus는 주로 fetching을 갖지만 네트워크 연결이 되어 있지 않은 경우 paused상태를 가질 수 있다.

정리하자면 statusdata가 있는지 없는지에 대한 상태를 의미하고, fetchStatusqueryFn요청이 진행중인지 아닌지에 대한 상태를 의미한다.

예시

자 그럼 위의 사용 방법을 토대로 데이터를 받아보자.

'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을 적었지만, 다음 글에서는 MuateInfinite Query, Query의 병렬처리에 대해서 글을 써보려고 한다.

profile image

권형안

Currently Managed
Currently not Managed
Git
Email
RocketPunch
Velog