본문 바로가기
React

React-Query 사용하기

by 왜안돼요 2024. 6. 10.

 

이전에 강의를 들으면서 강사님이 리액트 쿼리를 한번은 써보는게 좋다는 얘기를 하신적이 있었는데 한번 써봐야지 생각 해뒀다가

이번 프로젝트를 통해 제대로 써보면서 공부하는 계기가 되었다.

❓React-Query가 뭐야?

 

공식문서에는 다음과 같이 React-Query를 설명하고 있다.

TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes 
fetching, caching, synchronizing and updating server state
 in your web applications a breeze.

TanStack Query (FKA React Query)는 흔히 웹 애플리케이션을 위한 데이터 가져오기 라이브러리로 설명되지만, 보다 기술적인 측면에서 보면 웹 애플리케이션에서 서버 상태를 쉽게 가져오기, 캐싱, 동기화 및 업데이트할 수 있게 해줍니다.

 

v5가 되면서 TanStack Query라는 명칭으로 바꾼듯하다.

 

정리하자면

  • 어플리케이션의 비동기 데이터를 쉽게 관리하고 캐시 및 동기화할 수 있는 react용 라이브러리
  • 사용자 인터랙션에 따른 데이터의 fetching,caching,updating과 컴포넌트 트리의 변경사항에 대한 과정을
    단순화 
  • 실시간 업데이트, 페이지네이션, 오류 처리 등과 같은 유용한 기능을 제공한다.
  • axios나 fetch API와 같은 데이터 fetching 라이브러리와 원활하게 작동하도록 설계됨

리액트 쿼리 장단점

장점

  1. 외부데이터 관리
    리액트 쿼리는 외부 API로부터 데이터를 가져오고, 캐싱하고 업데이트 할 수 있도록 해준다.
  2. 오류 처리와 네트워크 상태 관리
    리액트 쿼리는 오류 처리와 네트워크 상태를 자동으로 관리한다.
  3. 페이지네이션
    페이지네이션을 지원하여 데이터를 쪼개서 가져올 수 있도록 해준다.
  4. 실시간 업데이트 
    실시간 업데이트를 지원하여 데이터가 실시간으로 업데이트될 수 있도록 해준다
  5. 쉬운 관리
    리액트에서 사용하기 쉽고, 코드를 깔끔하게 유지할 수 있도록 해준다.
  6. 캐싱 정책
    리액트 쿼리는 캐싱정책 지원해서 데이터를 언제 캐싱할지, 언제 업데이트할지를 설정할 수 있다.
  7. 일관된 API
    동일 데이터를 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절가능)

단점

  1. 러닝 커브가 높아 컨셉을 이해하고 초기 설정하는데 시간이 걸릴 수 있다.
  2. 비교적 새로운 라이브러리이기 때문에 버그나 누락된 기능이 있을 수 있다.
  3. 소규모 프로젝트에서 사용 시 프로젝트 규모에 비해 복잡성이 추가 될 수 있다.
  4. 모든 케이스에서 적합한것은 아니므로 리액트 쿼리를 도입하는 정당한 가치를 평가해야한다.

캐싱

캐싱이란 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 재접근 속도를 높이는 것을 말한다.
데이터를 요청할 때마다 API에 요청하지 않고, 이전에 저장해둔 데이터를 사용할 수 있어서 응답 속도가 빠르다.

데이터에 대한 반복적인 비동기 데이터 호출을 방지, 불필요한 API 콜을 줄여 서버에 대한 부하를 줄이는 결과를 가져다 준다.

 

💡React-Query에서는 최신 데이터를 fresh한 데이터, 기존의 데이터를 stale한 데이터라고 한다.

언제 갱신해??

  1. 화면을 보고 있을 때
  2. 페이지의 전환이 일어났을 때
  3. 페이지 전환 없이 이벤트가 발생해 데이터를 요청할 때

이를 위해 React-Query에서는 기본적인 아래 옵션들을 제공해준다

refetchOnWindowFocus, //default: true
refetchOnMount, //default: true
refetchOnReconnect, //default: true
staleTime, //default: 0
cacheTime, //default: 5분 (60 * 5 * 1000)

 

이 옵션들을 통해 우리는 리액트 쿼리가 어떤 시점에 데이터를 Refetching하는지 알 수 있다.

Refetch 트리거
브라우저에 포커스가 들어온 경우 refetchOnWindowFocus
새로운 컴포넌트 마운트가 발생한 경우 refetchOnMount
네트워크 재연결이 발생한 경우 refetchOnReconnect

 

staleTime, cacheTime 

staleTime

  1. staleTime은 데이터가 fresh -> stale 상태로 변경되는 데 걸리는 시간
  2. fresh 상태일때는 Refetch 트리거가 발생해도 Refetch가 일어나지 않는다.
  3. 기본값이 0이므로 따로 설정해주지 않으면 Refetch 트리거가 발생했을 때 무조건 Refetch가 발생함

cacheTime

  1. cacheTime은 데이터가 비활성화? inactive한 상태일 때 캐싱된 상태로있는 시간
  2. 특정 컴포넌트가 unmount( 페이지 전환등으로 화면에서 사라질 때) 되면 사용된 데이터는 비활성화 상태로 바뀌고, 이때 데이터는 cacheTime만큼 유지됨
  3. cacheTime이후 데이터는 가비지 콜렉터로 수집되어 메모리에서 해제
    만약 cacheTime이 지나지 않았는데 해당 데이터를 사용하는 컴포넌트가 다시 mount되면, 새로운 데이터를 fetch해오는 동안 캐싱된 데이터를 보여줌 -> 즉 캐싱된 데이터를 계속 보여주는게 아니라 fetch하는 동안 임시로 보여준다.

client 데이터와 server 데이터 간의 분리

Client Data : 모달 관련 데이터, 페이지 관련 데이터 등
Server Data : 사용자 정보, 비즈니스 로직 관련 정보 등

비동기 API 호출을 통해 불러오는 데이터들을 Server 데이터

 

클라이언트 데이터의 경우에는 전역상태 관리 라이브러리들을 통해 잘 관리하지만, 문제는 이러한 라이브러리들이 server 데이터 까지도 관리를 해야하는 상황이 발생한다는것.

 

상태 관리 라이브러리들이 client 데이터와 server 데이터를 완벽히 분리하여 관리에 용이하도록 충분한 기능이 지원된다고 보기 어렵고 client 데이터를 관리하는데 로직이 집중되어 있기때문에 server 데이터까지 효율적으로 관리하기에는 한계가 있다고 볼 수 있음.

 

TanStack Query는 이러한 문제에 대한 해결책을 제시해줌

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const { isPending, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/TanStack/query').then((res) =>
        res.json(),
      ),
  })

  if (isPending) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>✨ {data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  )
}

 

위와 같은 로직으로 server 데이터를 가지고오는데, 이때 data를 통해 성공 시 데이터, isPending을 통해 로딩 여부, error를 통해 에러 발생 여부를 반환할 수 있다. 

isPending과 error를 이용해 각 상황 별 분기를 쉽게 진행할 수 있음.

 

이는 server 데이터를 불러오는 과정에서 구현해야할 추가적인 설정들을 할 필요가 없다는 뜻

-> client 데이터는 상태관리 라이브러리가 하고 server데이터는 react-query가 관리하는 구조라 생각하면 됨

 

React-Query가 데이터를 다루는 방법

리액트쿼리는 리액트의 ContextAPI를 기반으로 동작한다. 

전체 scope가 되며 하위 컴포넌트들의 server data를 관리하는 queryClient가 존재하는데, 해당 QueryClient는 우리가 Query를 사용할 때 명시하는 key를 기반으로 데이터를 저장함

 

QueryClient는 단순히 서버에서 불러온 데이터를 저장하는 곳이라기보단, 데이터를 저장하는 용도로 사용되는 Context Store와 동일한 역할을 한다. 

 

tanstack/@react-query 공식문서에 getQueryData, setQueryData, fetchQuery등등 QueryClient라는 저장소를 관리할 수 있는 다양한 메서드가 존재함. 각 메서드의 특징과 활용 용도를 정확하게 파악하고 있어야한다.

 

 

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를 세팅한다.

 

// src/index.js
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 queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      {/* devtools */}
      <ReactQueryDevtools initialIsOpen={true} /> // 선택사항
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

 

useQuery

기본적으로 get에는 useQuery가 사용된다.
put, update,delete에는 useMutation이 사용된다.

 

  • 첫번째 파라미터로 unique key가 들어가고, 두번째 파라미터로 비동기함수 (api 호출함수)가 들어간다.(두번째 파라미터는 Promise가 들어가야 한다.
  • 첫번째 파라미터로 설정한 unique key는 다른 컴포넌트에서도 해당 키를 사용하면 호출이 가능하다. unique key는 string과 배열을 받는다. 배열로 넘기면 0번 값은 string값으로 다른 컴포넌트에서 부를 값이 들어가고 두번째 값을 넣으면  query함수 내부에 파라미터로 해당 값이 전달된다.
  • return값은 api의 성공, 실패여부, 반환값을 포함한 객체이다.
  • useQuery는 비동기로 작동한다. 한 컴포넌트에 여러개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는것이 아니라 두개의 useQuery가 동시에 실행된다. 여러개의 비동기 query가 있다면 useQuery보다는 useQueries를 사용하는것이 좋다.
  • enabled를 사용하면 useQuery를 동기적으로 사용이 가능하다. 
function Example() {
  const { isPending, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/TanStack/query').then((res) =>
        res.json(),
      ),
  })

  if (isPending) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>✨ {data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  )
}
  • useQuery의 세번째 인자로 다양한 옵션 값들이 들어간다, 여기서 enabled에 값을 대입하면 해당 값이 true일때 useQuery를 동기적으로 실행한다

동기적으로 실행

// Get the user
const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: getUserByEmail,
})

const userId = user?.id

// Then get the user's projects
const {
  status,
  fetchStatus,
  data: projects,
} = useQuery({
  queryKey: ['projects', userId],
  queryFn: getProjectsByUser,
  // The query will not execute until the userId exists
  enabled: !!userId,
})

 

useQueries

여러개의 useQuery를 한 번에 실행하고자 하는 경우, 기존의 Promise.all()처럼 묶어서 실행할 수 있도록 도와준다.

const ids = [1, 2, 3]
const combinedQueries = useQueries({
  queries: ids.map((id) => ({
    queryKey: ['post', id],
    queryFn: () => fetchPost(id),
  })),
  combine: (results) => {
    return {
      data: results.map((result) => result.data),
      pending: results.some((result) => result.isPending),
    }
  },
})

 

결과의 데이터를 하나의 값으로 결합하려면 combine 옵션을 사용하면 된다.

combine?: (result: UseQueriesResults) => TCombinedResult
// combine을 사용하여 쿼리 결과를 단일 값으로 결합할 수 있음

 

useMutation

put,update,delete와 같이 값을 변경할 때 사용하는 API 반환값은 useQuery와 동일하다.

function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {mutation.isPending ? (
        'Adding 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: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

 

useMuation의 첫번째 파라미터에 비동기 함수가 들어가고, 두번째 인자로 상황별 분기 설정이 들어간다.

실제 사용 시에는 mutation.mutate 메서드를 사용하고 , 첫번째 인자로 api 호출시에 전달해주어야 하는 데이터를 넣어준다.

 

 

update후에 get 다시 실행

mutation 함수가 성공할 때, unique key로 맵핑된 get 함수를 invalidateQueries에 넣어주면 된다.

const queryClient = useQueryClient()

useMutation({
  mutationFn: updateTodo,
  // mutate가 호출되었을 때:
  onMutate: async (newTodo) => {
    // 진행 중인 refetch 취소
    // (낙관적 업데이트를 덮어쓰지 않도록)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // 이전 값을 스냅샷
    const previousTodos = queryClient.getQueryData(['todos'])

    // 낙관적으로 새로운 값으로 업데이트
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

    // 스냅샷된 값을 가진 context 객체 반환
    return { previousTodos }
  },
  // 변이가 실패하면,
  // onMutate에서 반환된 context를 사용하여 롤백
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // 오류 또는 성공 후 항상 refetch:
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})
  • 만약 mutation에서 return된 값을 이용해서 get 함수의 파라미터를 변경해야할 경우 setQueryData를 사용한다.

 

참고 블로그
TanStack

react-query 개념 및 정리

[React-Query] React-Query 개념잡기

 

최근댓글

최근글

skin by © 2024 ttuttak