[React] React Query란

김채운·2024년 2월 16일
0

React

목록 보기
23/26

➡️ React Query란?

React query의 공식 사이트에 들어가보면 '강력한 비동기 상태 관리 라이브러리'라고 소개되어 있다. 공식 사이트의 소개문구대로 React query는 React 애플리케이션에서 데이터를 가져오고 관리하는 데 사용되는 라이브러리로, 주로 HTTP API호출을 처리하고 결과를 캐싱하면, 데이터 상태를 관리해 UI의 성능을 향상시키고 데이터의 로딩 및 업데이트를 관리하는 데 도움이 된다. 즉, ‘비동기 로직을 쉽게 다룰수 있게 해준다’ 라고 이해하면 된다.

➡️ React Query사용의 이유?

query를 사용하기 전에는 API 비동기 통신을 수행하기 위해서 주로 Redux를 사용해서 비동기 데이터를 관리했다. Redux를 사용할 경우 전역적으로 비동기 데이터가 관리되기 때문에 캐싱과 같은 최적화 작업을 쉽게 수행할 수 있고 복잡한 사용자 시나리오에 대한 응대도 용이해지기 때문이다. 하지만 Redux를 사용하기 위해서 기본 원칙이 존재한다. 이 기본 원칙을 지키면서 코드를 진행하게 되면 장황한 Boilerplate코드가 요구가 되고 이를 해결하기 위해 Redux-toolkit이 나왔지만 코드양이 많이 줄었음에도 여전히 불필요하게 느껴지는 반복되는 boilerplate코드가 필요하다. 그래서 하나의 API요청을 처리하기 위해 여러 개의 Action과 Reducer가 필요하다. 이러한 구조하에 처리해야 하는 API의 개수가 많아질수록 코드의 분량이 늘어날 뿐만 아니라 비동기 Action을 처리하기 위한 복잡성이 높아질 우려가 있다. 이런 불편함은 React query를 사용함으로서 많은 부분이 해소될 수 있었다.

✨ 주요기능

1. 간편한 데이터 관리

  • 데이터 가져오기, 캐싱, 동기화 및 업데이트 처리를 간편하게 할 수 있게 해준다.

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

  • 실시간 데이터 업데이트와 자동 동기화를 지원하여 서버와 클라이언트 데이터의 일관성을 유지한다.

3. 데이터 캐싱

  • 데이터를 캐싱하여 불필요한 API 요청을 줄이고 애플리케이션의 성능을 향상 시킨다.

4. 서버 상태 관리

  • loading이나 error, data 등의 상태를 간편하게 처리할 수 있다.

5. 간편한 설정

  • React query는 간단한 설정으로 사용이 가능하다.

6. 동일한 데이터에 대한 중복 요청을 제거

  • 동일한 네트워크 요청이라면, 얼마동안 내가 우리 애플리케이션의 메모리상에 캐시를 해둘 건지 캐시 시스템을 제공 해주고 글로벌 상태도 제공을 해준다.

7. 네트워크 재요청 기능

  • 네트워크 요청에 실패했다면 조금 있다가 다시 시도해보는 ‘재시도’기능도 들어있다.

8. 백그라운드에서 “오래된” 데이터를 업데이트 해준다.

  • 따로 설정이 필요하지 않고 자동으로 update 해준다.

9. devtool 제공.

  • React Query를 디버깅하고 모니터링하는 데 사용되는 도구로 Devtools를 사용하여 현재 캐시된 쿼리 및 뮤테이션 상태를 확인할 수 있고, 쿼리가 발생하거나 캐시가 업데이트될 때 실시간으로 Devtools에서 확인할 수 있다.

➡️ 사용방법

설치하기

npm i @tanstack/react-query @tanstack/react-query-devtools

적용하기

import React from 'react';
import './App.css';
import MainProducts from './components/MainProducts';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MainProducts />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

QueryClientProvider

  • QueryClient를 사용할 수 있도록 Application전체적으로 QueryClientProvider를 씌워준다. 이 QueryClientProvider는 React Query의 기능을 사용할 수 있도록 React App에 Query Client를 제공한다. 'client' prop으로 queryClient 객체를 전달한다. 이 queryClient는 쿼리와 캐시를 관리하는 핵심 객체이다. 이를 통해 전역 쿼리 클라이언트를 설정해 애플리케이션의 모든 컴포넌트에서 동일한 쿼리 클라이언트 인스턴스에 접근할 수 있게 된다.

ReactQueryDevtools

  • ReactQueryDevtools는 React Query Devtools를 애플리케이션에 추가하는 역할을 한다. 이 도구는 개발 중에 React Query의 상태를 모니터링하고 디버깅하는 데 도움을 준다. 여기서 'initialIsOpen={false}'는 Devtools가 초기에 열려 있는지 여부를 지정하는 prop이다. 값을 'false'로 설정하면 초기에 Devtools는 닫혀있게 된다.

기본 사용법

import { useQuery } from '@tanstack/react-query';

const { data, isLoading, isError, ... } = useQuery(queryKey, queryFn, options)

useQuery를 호출하면, 객체를 return 해주는데, 이 객체 안에는 많은 key들이 있다. 그래서 네트워크 통신을 통해 받아온 data뿐만 아니라, data가 언제 update되었는지, 에러인지 아닌지등 다양한 데이터를 확인해 볼 수 있다.
useQuery를 호출할 때는, queryKey와 어디서 data를 가져와야 하는지 queryFn(쿼리함수)를 전달해주고, 세 번째 함수 인자로는 option을 전달해주는데 이 많은 내용들을 option으로 설정해둘 수 있다. 아래에서 다양한 option들과 key들을 확인해 볼 수 있다.

const {
  data,
  dataUpdatedAt,
  error,
  errorUpdateCount,
  errorUpdatedAt,
  failureCount,
  failureReason,
  fetchStatus,
  isError,
  isFetched,
  isFetchedAfterMount,
  isFetching,
  isInitialLoading,
  isLoading,
  isLoadingError,
  isPaused,
  isPlaceholderData,
  isPreviousData,
  isRefetchError,
  isRefetching,
  isStale,
  isSuccess,
  refetch,
  remove,
  status,
} = useQuery({
  queryKey,
  queryFn,
  cacheTime,
  enabled,
  networkMode,
  initialData,
  initialDataUpdatedAt,
  keepPreviousData,
  meta,
  notifyOnChangeProps,
  onError,
  onSettled,
  onSuccess,
  placeholderData,
  queryKeyHashFn,
  refetchInterval,
  refetchIntervalInBackground,
  refetchOnMount,
  refetchOnReconnect,
  refetchOnWindowFocus,
  retry,
  retryOnMount,
  retryDelay,
  select,
  staleTime,
  structuralSharing,
  suspense,
  useErrorBoundary,
})

실제 프로젝트에 적용

import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';

export default function Products() {
  const [checked, setChecked] = useState(false);
  const {
    isLoading,
    error,
    data: products,
  } = useQuery({
    queryKey: ['products', checked],
    queryFn: async () => {
      console.log('fetching...')
      const response = await fetch(`data/${checked ? 'sale_' : ''}products.json`)
      return response.json()
    },
  });
  const handleChange = () => setChecked((prev) => !prev);

  if (isLoading) return <p>Loading...</p>;

  if (error) return <p>{error}</p>;

  return (
    <>
      <label>
        <input type='checkbox' checked={checked} onChange={handleChange} />
        Show Only 🔥 Sale
      </label>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <article>
              <h3>{product.name}</h3>
              <p>{product.price}</p>
            </article>
          </li>
        ))}
      </ul>
    </>
  );
}

이렇게 작성하면 useQuery가 호출 될 때 두 번째 인자로 등록해놓은 함수가 호출 되면서 반환된 data를 useQuery가 내부적으로 가지고 있게 된다. 그럼 객체구조할당분해를 통해서 변수에 할당하도록 만들어준다.
이렇게 데이터 fetching 로직을 따로 분리한다면 데이터를 사용하는 컴포넌트에서 작성되는 코드는 useQuery를 호출해주면 되니까 간편하다. 또한 useEffect를 사용하지 않음으로서 코드의 흐름을 파악하기 쉬워졌다.
그리고 React Query는 캐싱 기능이 있기 때문에 다른 컴포넌트에서 useQuery의 같은 로직을 사용해서 data를 fetching 해오려고 한다면 같은 key를 사용하게 됐을 때 캐싱된 data를 가져오게 되므로 최적화에 도움을 준다. 하지만 여기서 알아둬야 할 점이 있다.

➡️ refetch되는 이유

React Query를 사용하면 data가 캐싱된다고 했는데 현재 사이트를 벗어났다가 다시 원래있던 window로 돌아만 와도 fetching이 된다. 그리고 다른 이벤트를 실행시키면 실행시키는 만큼 또 네트워크 통신이 일어난다. 이게 어떻게 된 일인지 알아보기 위해 React Query DevTools를 사용해보자. 설치는 이미 위해서 react-query를 설치할 때 같이 해줬고 App.js에서 initialIsOpen을 true로 해주면

바로 이렇게 react-query 개발툴이 보인다. 현재 우리 APP에 cache된 게 어떤 게 있는지 확인해 볼 수 있다. 여기서 봐야될 부분은 'stale'이라 표시된 부분이다. 이 stale은 '오래된 데이터'라는 뜻인데, 공식 문서를 확인해 보면,
Query instance는 useQuery를 사용하던지 아니면 useInfiniteQuery를 사용할 수 있는데 이 두개를 사용하면 캐시된 데이터는 stale이라고 간주한다 한다. 신선하지 않은 데이터라고 간주해서 자동으로 refetch를 시키는 거다. 그래서 이렇게 refetch가 일어나는 조건이 있는데,
1. refetchOnWindowFocus

  • window에 focus된 경우.

2. refetchOnMount

  • 마운트 될 때.

3. refetchOnReconnect

  • 네트워크에 다시 연결이 됐을 때.

기본적으로 React Query는 위의 세가지 기능의 기본값은 모두 true 상태이다. 이외에도 queryKey와 함께 State값을 같이 넘겨준 경우 State값이 변경된다면 refetch가 일어나게 된다.
이런 기본적인 행동을 바꿀 수 있게 해주는 option이 있다.

✔️ staleTime (갱신 지연 시간)

  • fresh한 데이터가 stale한 데이터로 변화되는 시간을 말한다. 캐시된 데이터의 유통기한을 나타내는 옵션이다. 호출된 데이터는 리액트 쿼리 자체적으로 저장하는데 staleTime에 대한 기본 옵션은 0으로 설정되어 있어서 이 외에도 다른 옵션을 설정하지 않았다면 캐시된 즉시 stale한 상태로 변하게 되고, refetch가 일어나는 조건과 일치하게되면 데이터가 fetch된다.
    그래서 이 값을 조정해서 데이터를 일정 시간 동안 캐시로 사용하고, 그 이후에만 다시 요청하도록 만들 수 있다. 즉, 데이터의 유통기한을 정해준다고 할 수 있다.
const { data } = useQuery(['data', getServerData,{
  staletime: 1000 * 60 * 5; // 5분
})

자주 변경되지 않는 데이터의 경우 캐시된 데이터를 사용함으로써 불필요한 네트워크 요청을 줄이고, 애플리케이션의 성능을 향상시킬 수 있다.

✔️ cacheTime (캐시 유지 시간)

애플리케이션에서 더이상 useQuery나 useInfiniteQuery를 사용하지 않는다면, inactive상태로 표기가 되는데 ‘inactive’상태로 5분정도 지나면 아무도 참조하지 않으니까 자동으로 garbage collected가 된다고 한다. 그렇게 되면 메모리를 자동으로 청소해 주니까 cache에서도 사라지게 된다. 이걸 바꾸기 위한 option이 cacheTime이다.‘cacheTime’을 조금 더 긴 시간으로 설정해주면 된다.

const { data } = useQuery(['data', getServerData], {
  cachetime: 30 * 60 * 1000, // 30분
})
  • cachetime은 캐시된 데이터가 얼마나 오랫동안 메모리에 유지될지를 나타내는 옵션으로 데이터가 inactive상태일 때를 기준으로 캐싱된 상태로 남아있는 시간이다.
    이 값을 설정하면 기준 시점으로부터 데이터의 삭제가 결정되고, 캐시된 데이터가 일정 시간 동안 메모리에 유지된 후 cacheTime이 지나면 가비지 콜렉터로 수집된다. 이 option을 사용하면, 데이터가 일정 기간 동안 메모리에 남아 있게 함으로써 같은 데이터를 다시 요청할 때 캐시에서 가져오기 때문에 성능을 향상 시킬 수 있다.

✔️ Update하기

staleTime을 설정해서 캐시된 데이터를 사용하게 해줬는데, 여기서 또 다른 문제가 생길 수 있는 게 data의 update가 빈번하게 일어나게 되면 background에 데이터가 새롭게 추가 되어도 우리는 staleTime을 5분으로 설정해뒀기 때문에 캐시된 데이터를 5분동안 사용해야 한다. 이렇게 되면 background에서는 새롭게 데이터가 update되었는데 화면상에 우리는 캐시된 데이터를 사용하고 있기 때문에 오래된 데이터를 사용하게 되는 것이다. 그래서 이럴 때 데이터가 업데이트 되었다면, 연관있는 캐시된 데이터는 모두 invalidate하게 해주는 옵션이 있다.

invalidateQueries

기존에 캐싱되어 있는 데이터를 무효화하는 함수이다. 기존의 데이터를 무효화하게 되면 refetch가 일어나면서 업데이트 된다. 이때 조심해야 할 것은 리스트의 개수가 많다면 refetch하는 시간이 소요되기때문에 사용자는 업데이트 되는데 느리다고 생각할 수 있다.

import { useQueryClient } from '@tanstack/react-query';

export default function MainProducts() {

  const client = useQueryClient();

  return (
    <main className='container'>
      <button onClick={() => { client.invalidateQueries({ queryKey: ['products'] }) }}>정보가 업데이트 되었음!</button>
    </main>
  );
}

App.js에서 우리가 QueryClientProvider를 씌워줬기 때문에 모든 자식 컴포넌트들에서 useQueryClient를 이용해서 client를 가지고 올 수 있다.
그럼 client를 가지고 onClick에서 client에게 invalidateQueries라는 API를 사용할 수 있다. 그래서 cache된 key인 products의 데이터를 모두 invalidate해줘 라고 할 수 있다. 캐시된 정보를 보관하면서, 우리가 서버에 어떤 새로운 데이터를 추가 했을 때 우리가 수동적으로 이렇게invalidate 할 수 있다.

➡️ 결론

그동안에는 네트워크 통신을 위해서 useEffect를 사용해서 데이터 통신 코드를 작성해 주고 따로 상태관리 라이브러리를 사용하지 않으면 일일이 데이터 통신을 해야하는 컴포넌트마다 코드를 똑같이 작성해주는 번거로움이 있었다. 그래서 사용하게 된 게 Redux인데 Redux같은 경우는 초기 설정이 복잡한 것도 있고 편하자고 사용했지만 작성해줘야 하는 코드도 많았다. 그에 대한 방편으로 react-query를 사용하는 건 꽤 도움이 많이 된다고 느낀 게 일단 react-query를 사용하면 다른 상태관리 라이브러리에 비해 직관적이고 깔끔한 코드를 사용할 수 있다는 것이고, 내부적으로 가지고 있는 함수를 통해서 loading중인지 error가 발생했는지 data를 받아왔는지 손쉽게 알 수 있어 네트워크 통신을 간편하게 할 수 있게 해주고, 동일한 요청에 대해서는 우리의 설정에 따라 캐시도 해준다. 그렇기 때문에 관리도 편하고 다른 컴포넌트에서 사용하더라도 동일한 요청이면 캐시된 데이터를 불러오기 때문에 불필요한 네트워크 요청을 줄여주고 성능을 향상 시키는 데 도움을 준다. 사용법도 간단하기 때문에 앞으로 프로젝트를 하게 된다면 react query를 어디에서 활용할 수 있을지 고민해볼 것 같다.

참조

0개의 댓글