[React-Query] React-Query 개념, 기본 문법, 사용법

현지렁이·2023년 4월 30일
1
post-custom-banner

들어가며

프론트엔드 개발에 빠지지 않는 API 통신, 여러분들은 어떤 방식으로 처리하고 계신가요?!

SWR, Recoil 등등.. 많은 데이터 패칭 라이브러리들이 존재하는데요!

그 중 오늘은 React Query에 대해 다뤄보고자 합니다!

| 📌 TanStack Query(React Query v4) 기준으로 작성되었습니다 !!

1️⃣ React Query

react-query는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용한다.

React Query는 React Application에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리이다.

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} "@tanstack/react-query";
import { getTodos, postTodo } from '../my-api'

// ✅ React Query는 내부적으로 queryClient를 사용하여 각종 상태를 저장하고, 부가 기능을 제공합니다.
const queryClient = new QueryClient()

function App() {
  return (
    // App에 Client를 제공합니다.
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

function Todos() {
  // client에 접근합니다.
  const queryClient = useQueryClient()

  // ✅ Queries
  // "/todos" API에 Get 요청을 보내 서버의 데이터를 가져옵니다.
  const query = useQuery(['todos'], getTodos)

  // ✅ Mutations
  // ✅  "/todos" API에 Post 요청을 보내 서버에 데이터를 저장합니다.
  const mutation = useMutation(
    (todo) => axios.post('/todo', { todo }),
    {
    // Post 요청이 성공하면 위 useQuery의 데이터를 초기화합니다
    // 데이터가 초기화되면 useQuery는 서버의 데이터를 다시 불러옵니다.
    onSuccess: () => {
      // Invalidate(데이터가 오래 되었다고 판단되면 다시 get) and refetch
      queryClient.invalidateQueries('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>
  )
}

render(<App />, document.getElementById('root'))

react query는 hook 기반의 로직들로 되어있어 해당 훅을 사용하는 컴포넌트에서 상태 값의 변경을 간편하게 파악하여 리렌더링을 유발하게 해준다.

또한 리엑트 쿼리는 데이터의 캐시 처리를 간편하게 할 수 있는 인터페이스를 제공한다.

  • 몇 초 이후에는 데이터가 유효하지 않은 것으로 간주하고 데이터를 다시 불러온다.
  • 데이터에 변경점이 있는 경우에만 리렌더링을 유발한다.
  • 유저가 탭을 이동했다가 다시 돌아왔을 때 데이터를 다시 불러온다.
  • 데이터를 다시 호출할때 응답이 오기 전까지는 이전 데이터를 계속 보여준다. 필요에 따라서는 로딩바와 같은 대안 UI를 보여주기 위해 loading state를 기본적으로 제공한다.

즉, 클라이언트와 서버의 상태 값을 일치시켜줘야 하는 요구사항에서 부가적으로 생길 수 있는 로직들을 리엑트 쿼리의 api와 인터페이스로 간단하게 해결할 수 있도록 도와주는 것이다!

2️⃣ React Query의 3가지 핵심 개념 (기본 문법, 옵션)

React Query 공식 문서에서는 React Query의 3가지 핵심 개념을 Queries, Mutations, Query Invalidation으로 소개하고 있다.

☝🏻 Queries

  • GET으로 받아올 대부분의 API에 사용할 아이!!
  • Data Fetcing용! (CRUD중 Reading만 사용)
  • 기본적으로 비동기로 동작하며 여러 useQuery를 사용하고 싶다면 useQueries를 사용하는 것을 추천한다.
  • enabled 옵션을 사용하면 useQuery를 동기적으로 사용 가능하다.
import { useQuery } from "@tanstack/react-query";

function App() {
	const info = useQuery(['todos'], fetchTodoList) // ✅ 'todos' -> Query Key, ✅ fetchTodoList -> Query Function
}

const fetchTodoList = useCallback(() => {
  return axios.get("http://localhost:4000/todolist");
}

파라미터

  • useQuery는 기본적으로 3개의 인자를 받는다. → 첫 번째 인자가 **queryKey(필수)**, 두 번째 인자가 **queryFn(필수)**, 세 번째 인자가 **options**
  • Query Key에 따라 데이터 캐싱을 관리한다. (=Key, Value 맵핑구조)

📌 React Query v4 변경사항!

쿼리 키는 배열로 통일

  • v3에서는 queryKey를 문자열 또는 배열로 지정할 수 있었지만, v4에서는 배열로 통일시킨다.
// v3
useQuery("todos", fetchTodos); // (-)

// v4
**useQuery(["todos"], fetchTodos); // (+)**

useQueries

  • Query를 여러 개 넘겨줄 때 사용
  • useQueries는 인자로 queries 프로퍼티를 가진 객체를 넘겨줄 수 있다.
  • queries의 값은 쿼리 배열이다.
// v3
useQueries([
  { queryKey1, queryFn1, options1 },
  { queryKey2, queryFn2, options2 },
]);

// v4
**useQueries({
  queries: [
    { queryKey1, queryFn1, options1 },
    { queryKey2, queryFn2, options2 },
  ],
});**
  • Query Function : Promise를 반환하는 함수! (=fetch, axios)
  • Options : 쿼리에 사용할 옵션

그럼 useQuery 가 반환하는 건 뭘까?

const { 
	data, 
	status,
  **fetchStatus,** // v4에서 추가
	isLoading, 
	isError, 
	error, 
	isFetching, 
	... 
} = useQuery(
  • status는 data가 있는지 없는지에 대한 상태

  • fetchStatus는 쿼리 즉, queryFn 요청이 진행중인지 아닌지에 대한 상태

  • data: 쿼리 함수가 리턴한 Promise에서 resolved된 데이터 (Response)

  • error: 에러가 발생했을 때 반환되는 객체

  • isError: 에러가 발생한 경우 true

  • isFetching: 캐싱 된 데이터가 있더라도 쿼리가 실행되면 로딩 여부에 따라 true/false로 반환

  • isLoading: 캐싱 된 데이터가 없을 때, 즉 처음 실행된 쿼리 일 때 로딩 여부에 따라 true/false로 반환

  • status: 쿼리 요청 함수의 상태를 표현하는 status는 4가지의 값 (문자열 형태)

    • loading, error, success (idle → v4에서 없어짐)
  • fetchStatus

    • fetching: 쿼리가 현재 실행 중
    • paused: 쿼리를 요청했지만, 잠시 중단된 상태
    • idle: 쿼리가 현재 아무 작업도 수행하지 않고 있음
  • refetch : 해당 query refetch 함수 제공

  • remove : 해당 query remove 함수 제공

useQuery 주요 옵션

const { isLoading, isFetching, data, isError, error } = useQuery(
  ["todos"],
  getTodos,
  {
    cacheTime: 3000, // 기본값 : 0초
    staleTime: 50000, // 기본값 : 5분
    networkMode: 'offlineFirst', // v4에서 추가
  }
);
  • networkMode : query와 mutation의 명시적인 오프라인 모드를 제공
    • online - 오프라인 상태에서 network connection이 있기 전까지 fetch를 하지 않음
    • always - 오프라인 상태에서도 온라인처럼 fetch를 시도
    • offlineFirst - queryFn 최초 호출 후 retry를 멈춥니다.
  • stale : 최신 상태가 아니라는 의미 / fresh : 최신 상태라는 의미
  • staleTime(number | Infinity) - 데이터가 fresh에서 stale 상태로 변경되는 데 걸리는 시간
  • cacheTime: (number | Infinity) - 데이터가 inactive 상태일 때 캐싱 된 상태로 남아있는 시간
  • onSuccess, onError, onSettled : query fetching 성공/실패/완료 시 실행할 Side Effect 정의
  • enabled : boolean - 쿼리를 자동으로 실행시킬지 말지 여부
  • retry : query 동작 실패 시, 자동으로 retry할지 결정하는 옵션
  • seletct : 성공 시 가져온 data를 가공해서 전달
  • keepPreviousData : 새롭게 fetching 시 이전 데이터 유지 여부
  • refetchOnMount : boolean | "always” - 데이터가 stale 상태일 경우, mount마다 refetch를 실행하는 옵션
  • refetchOnWindowFocus : 데이터가 stale 상태일 경우 윈도우 포커싱 될 때마다 refetch를 실행하는 옵션
  • refetchInterval : 주기적으로 refetch 할지 결정하는 옵션

☝🏻 Mutations

  • query와 달리 mutations는 데이터를 create, update, delete하거나 서버의 side-effects를 수행할 때 사용한다.
  • 서버의 data를 post, patch, put, delete와 같이 수정하고자 할 때 useMutation을 이용한다.
function App() {
  const mutation = useMutation(newTodo => { 
    return axios.post('/todos', newTodo) 
  })

  return (
    <div>
      {mutation.isLoading ? (
        '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: '웹 심화 스터디' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

파라미터

  • mutationKey : mutation에 사용할 unique key 값.
  • mutationFn : mutation에 사용할 promise 기반의 비동기 API 함수.
  • options : mutation에 사용할 옵션 값.
  • useQuery보다 간단하게, Promise를 반환하는 함수인 mutationFn만 있어도 된다! (단, mutationKey를 넣어주면 devtools에서 볼 수 있다.)

mutate

  • mutate 함수를 호출하여 mutation을 실행
  • useMutation의 반환 값인 mutation 객체의 mutate 메서드를 이용해서 요청 함수를 호출할 수 있다.
  • mutate는 onSuccessonError 메서드를 통해 성공 했을 시, 실패 했을 시 response 데이터를 핸들링할 수 있다.
  • onMutate는 mutation 함수가 실행되기 전에 실행되고, mutation 함수가 받을 동일한 변수가 전달된다.
  • onSettled는 try...catch...finally 구문의 finally처럼 요청이 성공하든 에러가 발생되든 상관없이 마지막에 실행된다.

useMutate 주요 옵션

useMutation({
  mutationFn: addTodo,
  onMutate: (variables) => {
    return { id: 1 }
  },
  onError: (error, variables, context) => {
  },
  onSuccess: (data, variables, context) => {
  },
  onSettled: (data, error, variables, context) => {
  },
})
  • onMutate : 본격적인 Mutation 동작 전에 먼저 동작하는 함수, Optimistic update를 적용할 때 유용하다.
  • 나머지는 useQuery 옵션과 비슷함!

☝🏻 Query Invalidation

  • invalidateQueries은 화면을 최신 상태로 유지하는 가장 간단한 방법이다.
  • data를 Post 하거나 Delete 해서 변경된 data를 실시간으로 최신화 해야 할 때,  query Key가 변하지 않으므로 강제로 쿼리를 무효화하고 최신화를 진행해야 하는데, 이런 경우에 **invalidateQueries()** 메소드를 이용할 수 있다.
  • 즉, query가 오래되었다는 것을 판단하고 다시 refetch를 할 때 사용한다!
import { useQuery, useQueryClient } from '@tanstack/react-query'

const queryClient = useQueryClient()

queryClient.invalidateQueries({ queryKey: ['todos'] })

// 아래 query들이 invalidated 된다.
const todoListQuery = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
  queryKey: ['todos', { page: 1 }],
  queryFn: fetchTodoList,
})

QueryClient

  • react-query에서 QueryClient의 인스턴스를 사용하여 캐시와 상호작용 할 수 있다.
  • 내부적으로 context를 사용한다.
import { QueryClient, useQueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient()

const queryClient = useQueryClient();
  • 일반적으로 QueryClient의 옵션들을 이용할 때는 현재 QueryClient의 인스턴스를 반환하는 useQueryClient Hook을 사용한다.

mutations후 다시 get

  • react-query 장점으로 update후에 get 함수를 간단히 재실행 할 수 있다.
  • mutation 함수가 성공할 때, unique key로 맵핑된 get 함수를 invalidateQueries에 넣는다.
  • 만약 mutation에서 return된 값을 이용해서 get 함수의 파라미터를 변경해야할 경우 setQueryData를 사용한다.
const mutation = useMutation(postTodo, {
  onSuccess: () => {
    // postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행한다.
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  }
});

const mutation = useMutation(editTodo, {
  onSuccess: data => {
    // data가 fetchTodoById로 들어간다.
    queryClient.setQueryData(["todo", { id: 5 }], data);
  }
});

3️⃣ react-query 사용 방법

1. React 프로젝트 만들기

yarn create vite [프로젝트명] --template react-ts

cd [프로젝트명]

2. react-query v4 설치

@tanstack/react-query v4부터 react-query에서 @tanstack/react-query로 패키지가 변경되었다.

따라서 설치와 import 할 때 주의해야 한다. 또한, Devtools는 별도의 패키지 설치가 필요하다.

yarn add @tanstack/react-query

yarn add @tanstack/react-query-devtools
// v3
import { useQuery } from "react-query"; // (-)
import { ReactQueryDevtools } from "react-query/devtools"; // (-)

// v4
**import { useQuery } from "@tanstack/react-query"; // (+)
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // (+)**

📌 Devtool이 뭔가요?!

  • react-query는 전용 devtools를 제공한다.
  • devtools를 사용하면 React Query의 모든 내부 동작을 시각화하는 데 도움이 되며 문제가 발생하면 디버깅 시간을 절약할 수 있다.
  • devtools는 기본값으로 process.env.NODE_ENV === 'development' 인 경우에만 실행된다, 즉 일반적으로 개발환경에서만 작동하므로 설정되어있으므로, 프로젝트 배포 시에 Devtools 삽입코드를 제거해줄 필요가 없다.

3. react-query 기본 세팅

  • queryClient를 생성한다. 쿼리 클라이언트는 쿼리와 서버의 데이터 캐시를 관리하는 클라이언트이다.
const queryClient = new QueryClient();
  • react-query를 사용하기 위해서는 QueryClientProvider를 최상단에서 감싸주고 QueryClient 인스턴스를 client props로 넣어 애플리케이션에 연결시켜야 한다.
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
**import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';**

// ✅ QueryClient 생성
const queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    {/* ✅ QueryClientProvider */}
    <**QueryClientProvider** **client={queryClient}**>
      {/* ✅ devtools */}
      **<ReactQueryDevtools initialIsOpen={true} />**
      <App />
    </**QueryClientProvider**>
  </React.StrictMode>,
  document.getElementById("root")
);
  • ReactQueryDevtools는 쿼리키로 쿼리를 표시해주고 활성(active), 비활성(inactive), 만료(stale) 등 모든 쿼리의 상태를 알려준다. 리액트 서버를 실행하고 브라우저를 확인해보면 브라우저 왼쪽아래에 리액트쿼리 로고모양 버튼이 생긴다. 이를 클릭하면 Devtools를 사용할 수 있다.

4️⃣ react-query는 어떤 상황에서 쓰면 좋을까?

👍🏻 이런 상황에서 사용하면 좋아요!

  • 클라이언트 데이터보다 서버 데이터에 대한 관리가 더 많을 때 Admin 페이지와 같은 관리형 페이지에서는 클라이언트의 전역 상태 데이터는 많이 필요하지 않을 수 있다. 이러한 페이지에서 Ducks 구조보다는 React Query를 적용하면 구조를 더 단순화 시킬 수 있다.
  • 수많은 전역상태가 API 통신과 엮여 있어 Store가 비대해졌을 때 → Client Store가 FE에서 필요한 전역상태만 남음
  • FE에서 데이터 Caching 전략이 필요할 때
  • devtool 제공으로 원활한 디버깅이 가능
  • API 통신 관련 코드를 보다 간단히 구현할 수 있고, API 처리에 대한 각종 인터페이스 및 옵션 사용 가능
  • 무한 스크롤 (Infinite Queries)

👎🏻 이런 상황에서는 고민해보는게 좋아요!

  • 서버 사이드 데이터가 거의 없는 경우 → recoil, redux 사용, react-query는 서버 데이터가 더 많아질 때 적용!
  • React 18에서 단순히 비동기 처리 상태때문에 도입하는 경우 → Suspense 사용 (더이상 API가 오고 있는지가 중요하지 않게 됨)
  • 항상 서버 데이터과 같은 데이터를 바라보는 것이 좋은 건 아니다! (1회성 데이터인 경우)
  • 데이터의 전체적인 흐름 파악이 어려울 수 있다.
  • Component가 상대적으로 비대해질 수 있다 (Component 설계/분리에 대한 고민 필요)
  • 프로젝스 설계에 난이도가 조금 더 높아질 수 있다. (Component 유착 최소화 및 사용처 파악 필요)
post-custom-banner

1개의 댓글

와 초심자에게 어려울 수 있는 리액트 쿼리에 대해 너무 쉽고 자세하게 설명해 주셔서 읽는 내내 이해가 쏙쏙 되었어요!!! 좋은 글 감사합니다 :)

답글 달기