본문 바로가기
Study/react.js

[React] React-query 정리

by 박히밍 2023. 2. 3.
반응형

[React] React-query 정리

 

 

 

 

[React] React-query 정리

리액트를 공부하긴 했으나, API 관련 기술을 딥하게 다루거나, 서버 데이터 패치 라이브러리를 이용하여 

데이터를 매핑한 경험은 거의 없거든요.

React portal 부분과 React-query 부분은 아무리 봐도 이해가 되질 않아서

이번 주 내내 React-query에 대해 스터디하고 정리하였습니다.

원래는 노션에 정리하였습니다만, 블로그에도 같이 올려보려 합니다. (노션 진짜 최고다.. 내다버린 티스토리..ㅎ)

 

 

 

 


 

참고자료들

Codevolution의 강의를 따라서 실습해보고 공식문서(한글번역판 포함)보면서 이해하면

흠! 이런 거였꾼! 상태로는 돌입할 수 있는 것 같다. 🤔 (물론 이것은 돌머리인 내 기준쓰)

Codevolution이 제공하는 스타터용 소스코드도 깃헙에 친절히 올려져있슴! ㅎㅅㅎ 그랜절 하겠슴다.

 

 

 

React-query 공식링크

https://react-query-v3.tanstack.com/

 

TanStack Query | React Query, Solid Query, Svelte Query, Vue Query

Powerful asynchronous state management, server-state utilities and data fetching for TS/JS, React, Solid, Svelte and Vue

tanstack.com

 

 

React-query 공식문서 한글판

https://velog.io/@familyman80/React-Query-%ED%95%9C%EA%B8%80-%EB%A9%94%EB%89%B4%EC%96%BC#mutations

 

React Query 한글 메뉴얼

공식 홈페이지 https://react-query.tanstack.com/guides/window-focus-refetching 의 내용을 주로 사용할 만한 것들을 발췌해서 작성. React Query React를 위한 강력하고 성능 좋은 데이터 동기화 "전역 상태

velog.io

 
 
 
 
Codevolution의 React-query Tutorial 👍👍👍👍👍
 

https://youtu.be/VtWkSCZX0Ec

 

 

 

 


 

 

 

 

Stale

React Query에서는 캐싱 된 데이터를 기본적으로 stale 상태로 여긴다.

 

 

 

refetch가 되는 조건으론

▶ 새로운 Query Instance가 마운트 될 때 ( 페이지를 이동했다가 왔을 경우 )

▶ 브라우저 화면을 이탈했다가 다시 Focus 할 때

▶ 네트워크가 다시 연결될 때

▶ 설정한 refetch interval에 의한 경우

 

 

 

 


 

 

 

Query Options

 

staleTime

데이터가 fresh 상태에서 stale 상태로 변경되는데 걸리는 시간을 나타내며, default 값은 0이다.

fresh 상태일 때는 페이지를 이동했다가 돌아왔을 경우에도 fetch가 일어나지 않는다.

즉, 데이터가 한번 fetch 되고 staleTIme이 지나지 않았다면 unmount 후 mount가 발생해도 다시

fetch가 발생하지 않는다.

default 값이 0이기 때문에 받아오는 즉시 stale하다고 판단해서 캐싱 데이터와는 무관하게 계속 fetching을 수행한다.

 

 

cacheTime

데이터가 inactive 상태일 때 캐싱된 상태로 남아있는 시간을 말한다. default 값은 5min.

쿼리 인스턴스가 unmount되면 데이터는 inactive 상태로 변경되며, 캐시는 cacheTime 만큼 유지된다.

이걸 cacheTime 만큼 유지시키는 이유는 쿼리 인스턴스가 다시 마운트되면 데이터를 fatch하는 동안

cacheTime이 지나지 않은 캐시 데이터를 보여준다.

 

 

refetchOnMount

네트워크 툴을 참고해서 볼 것

refetchOnMount 는 데이터가 stale 상태일 경우 마운트 시 마다 refetch를 실행하는 옵션이다. default 값은 true

always로 설정하면 마운트 시 마다 매번 refetch를 실행한다.

false로 설정 시 데이터가 stale 상태여도 refetch를 실행하지 않는다.

 

 

refetchOnWindowFocus

refetchOnWindowFocus는 데이터가 stale 상태일 경우 윈도우 포커싱 될 때 마다 refetch를 실행하는 옵션이다. default 값은 true

• 예를 들어, 크롬에서 다른 탭을 눌렀다가 다시 원래 보던 중인 탭을 눌렀을 때도 이 경우에 해당한다. 심지어 F12로 개발자 도구 창을 켜서 네트워크 탭이든, 콘솔 탭이든 개발자 도구 창에서 놀다가 페이지 내부를 다시 클릭했을 때도 이 경우에 해당한다.

always 로 설정하면 항상 윈도우 포커싱 될 때 마다 refetch를 실행한다는 의미이다.

 

 

refetchInterval

일정 시간마다 자동으로 refetch를 시켜준다. ms 단위로 설정해줄 수 있다. defalut 값은 false

중요한 점은 브라우저에 focus 되어있어야만, 일정 시간마다 자동으로 refetch가 실행된다는 것이다.

하지만 focus 안되어있을 때도 작동하게 하는 방법이 있다.

 

 

refetchIntervalInBackground (Polling: 일정한 간격으로 데이터를 가져오는 프로세스)

브라우저에 focus 되어있지 않아도 refetch가 되게 해준다.

true 값을 설정해주면 된다.

 

 

enabled

쿼리가 자동으로 실행되지 않게 설정하는 옵션이다. false로 설정 시 자동 실행 되지 않는다.

 

 

select

쿼리 데이터 중 원하는 데이터만 추출하여 사용

const { isLoading, data, isError, error, isFetching, refetch } = useQuery(
    "super-heroes",
    fetchSuperHeroes,
    { select: (data) => {
        const SuperHeroNames = data.data.map(hero => hero.name)
        return SuperHeroNames
      }
    }
  );

 

 


 

 

useQuery

첫번째 인자는: 쿼리키

두번째 인자는: 쿼리함수(데이터 패치 함수)

세번째 인자는: 객체(옵션)

  • isLoading쿼리에 데이터가 없고 fetching 하는 상태.
  • isError쿼리에 에러가 발생한 상태.
  • isSuccess쿼리가 성공적으로 실행되었고 데이터를 사용 가능한 상태.
  • isIdle쿼리를 사용할 수 없는 상태. (disabled)
  • error쿼리가 isError 상태인 경우 에러 정보 확인을 위해 사용하는 프로퍼티.
  • data쿼리가 isSucess 상태인 경우 데이터 사용을 위해 사용하는 프로퍼티.
  • isFetching쿼리의 fetching/refetching 여부에 대한 boolean 값.
  • refetch onClick 이벤트로 수동으로 데이터를 fetching 할 때 사용 함
//refetch 예제 :: 훅이 별도로 필요없이 간단하게 데이터를 fetching 한다.
import { useQuery } from "react-query";
import axios from "axios";

const fetchSuperHeroes = () => {
  return axios.get("http://localhost:4000/superheroes");
};
export const RQSuperHeroesPage = () => {
  const { isLoading, data, isError, error, isFetching, refetch } = useQuery(
    "super-heroes",
    fetchSuperHeroes,
    { enabled: false }
  );
  console.log({ isLoading, isFetching });
  if (isLoading || isFetching) {
    return <h2>Loading..</h2>;
  }
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  return (
    <>
      <h2>RQ Super Heroes Page</h2>
      <button onClick={refetch}>Fetch button</button>
      {data?.data.map((hero) => {
        return <div key={hero.id}>{hero.name}</div>;
      })}
    </>
  );
};

 

 

 

Query Function

//둘은 어떤차이가 있는 것인가 ...'-'...

const fetchSuperHeroes = () => {
  return axios.get("<http://localhost:4000/superheroes>");
};

export const useSuperHeroesData = () => {
  return useQuery("super-heroes", fetchSuperHeroes);
};

const fetchSuperHero = (heroId) => {
  return axios.get(`http://localhost:4000/superheroes/${heroId}`);
};
export const useSuperHeroData = (heroId) => {
  return useQuery(["super-hero", heroId], () => fetchSuperHero(heroId));
};

 

 

 

 

React-query custom hooks

자주 사용될 것 같은 쿼리문을 커스텀 훅 함수로 생성하여 사용할 수 있다.

//useSuperHeroesData.js

import { useQuery } from "react-query";
import axios from "axios";

const fetchSuperHeroes = () => {
  return axios.get("<http://localhost:4000/superheroes>");
};

export const useSuperHeroesData = (onSuccess, onError) => {
  return useQuery("super-heroes", fetchSuperHeroes, {
    onSuccess,
    onError,
    select: (data) => {
      const SuperHeroNames = data.data.map((hero) => hero.name);
      return SuperHeroNames;
    },
  });
};
//RQSuperHeroes.page.js

import { useSuperHeroesData } from "../hooks/useSuperHeroesData";

export const RQSuperHeroesPage = () => {
  const onSuccess = (data) => {
    console.log("data fetching", data);
  };
  const onError = (error) => {
    console.log("error", error);
  };
  const { isLoading, data, isError, error, isFetching, refetch } =
    useSuperHeroesData(onSuccess, onError);

  console.log({ isLoading, isFetching });
  if (isLoading || isFetching) {
    return <h2>Loading..</h2>;
  }
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  return (
    <>
      <h2>RQ Super Heroes Page</h2>
      <button onClick={refetch}>Fetch button</button>
      {/* {data?.data.map((hero) => {
        return <div key={hero.id}>{hero.name}</div>;
      })} */}
      {data.map((heroName) => {
        return <div key={heroName}>{heroName}</div>;
      })}
    </>
  );
};

 

 

 

 

Parallel Queries = Promise.all(병렬쿼리 / 비동기적)

"병렬" 쿼리는 병렬로 실행되거나 동시에 가져오기 동시성을 최대화하기 위해 실행되는 쿼리이다.

수동 병렬 쿼리

//수동 병렬 쿼리 방법
import { useQuery } from "react-query";
import axios from "axios";

const fetchSuperHeroes = () => {
  return axios.get("<http://localhost:4000/superheroes>");
};

const fetchFriends = () => {
  return axios.get("<http://localhost:4000/friends>");
};
export const ParallelQueriesPage = () => {
  const { data: superHeroes } = useQuery("super-heroes", fetchSuperHeroes);
  const { data: friends } = useQuery("friends", fetchFriends);

  return <div>ParallelQueriesPage</div>;
};

동적 병렬 쿼리

//App.js
<Route path="/rq-dynamic-parallel" element={
	<DynamicParallelPage heroIds={[1, 3]} />
} />

//DynamicParallel.page.js
import { useQueries } from "react-query";
import axios from "axios";

const fetchSuperHero = (heroId) => {
  return axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

export const DynamicParallelPage = ({ heroIds }) => {
  const queryResults = useQueries(
    heroIds.map((id) => {
      return {
        queryKey: ["super-hero", id],
        queryFn: () => fetchSuperHero(id),
      };
    })
  );
  console.log({ queryResults });
  return <div>DynamicParallelPage</div>;
};

 

 

 

 

Dependent Queries(직렬쿼리 / 동기적)

Dependent Queries는 다른 쿼리에 의존적이며 순서가 보장되는 동기적 실행이라고 보면 된다.

앞의 쿼리가 실행되고, 앞선 쿼리 조건에 의해 이후 쿼리가 실행되도록 하는 종속적 쿼리를 만들 때 사용

//App.js
<Route path="/rq-dependent" element={
	<DependentQueriesPage email="vishwas@example.com" />
} />

//DependentQueries.page.js
import { useQuery } from "react-query";
import axios from "axios";

const fetchUserByEmail = (email) => {
  return axios.get(`http://localhost:4000/users/${email}`);
};

const fetchCoursesByChannelId = (channelId) => {
  return axios.get(`http://localhost:4000/channels/${channelId}`);
};

export const DependentQueriesPage = ({ email }) => {
  const { data: user } = useQuery(["user", email], () =>
    fetchUserByEmail(email)
  );
  const channelId = user?.data?.channelId;
  useQuery(["courses", channelId], () => fetchCoursesByChannelId(channelId), {
    enabled: !!channelId, //channelId가 undefinde / "" / NaN / 0 이면 false(0)을 반환한다.
  });
  return <div>DependentQueries</div>;
};

Dependent Queries

 

/rq-dependent 접속 시 user 쿼리 실행 전으로 channelId가 null 상태이기 때문에 courses 쿼리는 실행 되지 않아 inactive 상태.

그 이후 user 쿼리가 실행 되면서 const channelId = user?.data?.channelId;가 할당 되었고.

["user","vishwas@example.com"]가 표시된다.

할당된 channelId를 courses 쿼리 키로 배열에 할당하여서, channelId가 더이상 null이 아니기 때문에

["courses","codevolution"] 쿼리 키 배열이 나타난다.

 

 

 

Initial Query Data

데이터가 실제로 백그라운드에서 가져오는 동안 빈페이지나 로딩 화면이 뜨지 않고, 데이터가 즉시 호출 된 것 처럼

만들 수 있는데, 그것이 Initial Query Data이다.

//useSuperHeroData.js
import { useQuery, useQueryClient } from "react-query";
import axios from "axios";

const fetchSuperHero = ({ queryKey }) => {
  const heroId = queryKey[1];
  return axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

export const useSuperHeroData = (heroId) => {
  const queryClient = useQueryClient();
  return useQuery(["super-hero", heroId], fetchSuperHero, {
    **initialData**: () => {
      const hero = queryClient
        .getQueryData("super-heroes")
        ?.data?.find((hero) => hero.id === parseInt(heroId)); //find 함수는 hero.id === parseInt(heroId) 조건이 같은 요소의 값을 리턴한다.
      if (hero) {
        return { data: hero };
      } else {
        return undefined;
      }
    },
  });
};

 

 

 

Paginated Queries

//PaginatedQueries.page.js
import { useState } from "react";
import { useQuery } from "react-query";
import axios from "axios";

const fetchColors = (pageNumber) => {
  return axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageNumber}`);
};
**//json 데이터는 호출데이터 URL로 데이터를 제한할 수 있는데, ?_limit=2 은 불러오는 한페이지 당 데이터의 수,
// &_page=${pageNumber}은 페이징 넘버를 의미한다. 해석하자면 한페이지당 두개의 데이터를 가져온다는 뜻.**

export const PaginatedQueriesPage = () => {
  **const [pageNumber, setPageNumber] = useState(1);**
  const { isLoading, isError, error, data, isFetching } = **useQuery(
    ["colors", pageNumber],
    () => fetchColors(pageNumber)
  );**

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    return <h2>{error.message}</h2>;
  }

  return (
    <>
      <div>
        {data?.data.map((color) => {
          return (
            <div key={color.id}>
              <h2>
                {color.id}. {color.label}
              </h2>
            </div>
          );
        })}
      </div>
      <div>
        <button
          onClick={() => {
            setPageNumber((page) => page - 1);
          }}
          disabled={pageNumber === 1}
        >
          Prev page
        </button>
        <button
          onClick={() => {
            setPageNumber((page) => page + 1);
          }}
          disabled={pageNumber === 4}
        >
          Next page
        </button>
      </div>
      {isFetching && "Loading"}
    </>
  );
};

 

 

 

Mutations

Mutations은 데이터를 생성하거나, 업데이트하거나, 지우거나 혹은 서버 사이드 이펙트를 수행하는 목적으로 사용된다. Mutations는 useMutation 훅으로 사용한다. useMutation은 인자로 mutationFn과 options을 받는다. mutationFn은 생성/수정/삭제 API를 할당한다. options에는 성공/실패 등 다양한 콜백함수를 할당할 수 있다. useMutation을 반환하는 객체의 메서드인 mutate로 mutationFn을 호출한다. mutate인자는 mutationFn의 매개변수로 전달된다.

//RQSuperHeroes.page.js
import { useState } from "react";
import { Link } from "react-router-dom";
import {
  useSuperHeroesData,
  useAddSuperHeroData,
} from "../hooks/useSuperHeroesData";

export const RQSuperHeroesPage = () => {
  const [name, setName] = useState("");
  const [alterEgo, setAlterEgo] = useState("");

  const onSuccess = (data) => {
    console.log("data fetching", data);
  };
  const onError = (error) => {
    console.log("error", error);
  };
  const { isLoading, data, isError, error, isFetching, refetch } =
    useSuperHeroesData(onSuccess, onError);

  const { mutate: addHero } = useAddSuperHeroData();

  const handleAddHeroClick = () => {
    console.log({ name, alterEgo });
    const hero = { name, alterEgo };
    addHero(hero);
  };

  console.log({ isLoading, isFetching });
  if (isLoading || isFetching) {
    return <h2>Loading..</h2>;
  }
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  return (
    <>
      <h2>RQ Super Heroes Page</h2>
      <div>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <input
          type="text"
          value={alterEgo}
          onChange={(e) => setAlterEgo(e.target.value)}
        />
        <button onClick={handleAddHeroClick}>Add Hero</button>
      </div>

      <button onClick={refetch}>Fetch button</button>

      {data?.data.map((hero) => {
        return (
          <div key={hero.id}>
            <Link to={`/rq-super-heroes/${hero.id}`}>{hero.name}</Link>
          </div>
        );
      })}
      {/* {data.map((heroName) => {
        return <div key={heroName}>{heroName}</div>;
      })} */}
    </>
  );
};
//RQSuperHeroes.page.js
import { useState } from "react";
import { Link } from "react-router-dom";
import {
  useSuperHeroesData,
  useAddSuperHeroData,
} from "../hooks/useSuperHeroesData";

export const RQSuperHeroesPage = () => {
  **const [name, setName] = useState("");
  const [alterEgo, setAlterEgo] = useState("");**

  const onSuccess = (data) => {
    console.log("data fetching", data);
  };
  const onError = (error) => {
    console.log("error", error);
  };
  const { isLoading, data, isError, error, isFetching, refetch } =
    useSuperHeroesData(onSuccess, onError);

  **const { mutate: addHero } = useAddSuperHeroData();**

  **const handleAddHeroClick = () => {
    console.log({ name, alterEgo });
    const hero = { name, alterEgo };
    addHero(hero);
  };**

  console.log({ isLoading, isFetching });
  if (isLoading || isFetching) {
    return <h2>Loading..</h2>;
  }
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  return (
    <>
      <h2>RQ Super Heroes Page</h2>
      **<div>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <input
          type="text"
          value={alterEgo}
          onChange={(e) => setAlterEgo(e.target.value)}
        />
        <button onClick={handleAddHeroClick}>Add Hero</button>
      </div>**

      <button onClick={refetch}>Fetch button</button>

      {data?.data.map((hero) => {
        return (
          <div key={hero.id}>
            <Link to={`/rq-super-heroes/${hero.id}`}>{hero.name}</Link>
          </div>
        );
      })}
      {/* {data.map((heroName) => {
        return <div key={heroName}>{heroName}</div>;
      })} */}
    </>
  );
};
//useSuperHeroesData.js
import { useQuery, **useMutation** } from "react-query";
import axios from "axios";

const fetchSuperHeroes = () => {
  return axios.get("<http://localhost:4000/superheroes>");
};

**const addSuperHero = (hero) => {
  return axios.post("<http://localhost:4000/superheroes>", hero);
};**

export const useSuperHeroesData = (onSuccess, onError) => {
  return useQuery("super-heroes", fetchSuperHeroes, {
    onSuccess,
    onError,
    // select: (data) => {
    //   const SuperHeroNames = data.data.map((hero) => hero.name);
    //   return SuperHeroNames;
    // },
  });
};
**export const useAddSuperHeroData = () => {
  return useMutation(addSuperHero);
};**

input 값을 넣고 Add Hero 버튼 클릭 시 axios.post를 통해 db.json 파일에 데이터가 추가되고, 화면에는 Ferch 버튼 클릭 시 추가된 데이터가 렌더링 되며, 라우트 페이지도 생성이 됨

**mutate**는 useMutation을 이용해 작성된 내용들이 실행될 수 있도록 도와주는 Trigger 역할을 한다.

즉, useMutation을 정의 해둔 뒤 이벤트가 발생되었을 때 mutate를 사용해주면 되는 것.

 

 

 

Invalidate Queries

Invalidate Queries는 useQuery에서 사용되는 queryKey의 유효성을 제거해주는 목적으로 사용한다.

그리고 queryKey의 유효성을 제거해주는 이유는 서버로부터 다시 데이터를 조회해오기 위함이다.

예를 들자면,

 ❗ 이름, 전화번호, 나이를 적은 뒤 저장 버튼을 누르게 되면 리스트가 아래에 바로 표현되는 것을 생각할 수 있으나,
useQuery에는 staleTime과 cacheTime이라는 개념이 존재하기 때문에
정해진 시간이 도달하지 않으면 새로운 데이터가 적재되었더라도 useQuery는 변동 없이 동일한 데이터를 화면에 보여준다.
결국 사용자 입장에서는 데이터 생성이 제대로 되었는지에 대한 파악이 힘들기 때문에 혼란을 겪을 수 있기 때문에
이를 해결해줄 수 있는 것이 바로 Invalidate Queries 이다.
데이터를 저장할 때 Invalidate Queries를 이용해 useQuery가 가지고 있던 queryKey의 유효성을 제거해주면
캐싱 되어있는 데이터를 화면에 보여주지 않고 서버에 새롭게 데이터를 요청하게 된다.
결국 데이터가 새롭게 추가되었을 때 다시 서버에서 데이터를 가져오게 되면서 추가한 데이터까지
자동으로 화면에서 확인할 수 있게 되는 것.

 

사용 방법은 우선 hook을 이용해 등록했던 queryClient를 가져와야 한다.

그리고 원하는 장소에서 사용해주면 되는데, 일반적으로 요청이 성공했을 때 queryKey의 유효성을 제거할 것이기 때문에

useMutation이 성공했을 때, queryClient.invalidateQueries("super-heroes"); 가 실행 되도록 옵션을 작성해준다.

 

//useSuperHeroesData.js
import { useQuery, useMutation, **useQueryClient** } from "react-query";
import axios from "axios";

const fetchSuperHeroes = () => {
  return axios.get("<http://localhost:4000/superheroes>");
};

const addSuperHero = (hero) => {
  return axios.post("<http://localhost:4000/superheroes>", hero);
};

export const useSuperHeroesData = (onSuccess, onError) => {
  return useQuery("super-heroes", fetchSuperHeroes, {
    onSuccess,
    onError,
    // select: (data) => {
    //   const SuperHeroNames = data.data.map((hero) => hero.name);
    //   return SuperHeroNames;
    // },
  });
};
export const useAddSuperHeroData = () => {
  **const queryClient = useQueryClient();**
  return useMutation(addSuperHero**, {
    onSuccess: () => {
      queryClient.invalidateQueries("super-heroes");
    },
  });**
};

 

 

 

setQueryData (문법을 잘 모르겟슴)

Invalidate Queries를 사용하지 않고도 데이터를 업데이트해줄 수 있는 방법

setQueryData를 활용하면 되는데 setQueryData는 기존에 queryKey에 매핑되어 있는 데이터를 새롭게 정의해준다.

////useSuperHeroesData.js
import { useQuery, useMutation, useQueryClient } from "react-query";
import axios from "axios";

const fetchSuperHeroes = () => {
  return axios.get("<http://localhost:4000/superheroes>");
};

const addSuperHero = (hero) => {
  return axios.post("<http://localhost:4000/superheroes>", hero);
};

export const useSuperHeroesData = (onSuccess, onError) => {
  return useQuery("super-heroes", fetchSuperHeroes, {
    onSuccess,
    onError,
    // select: (data) => {
    //   const SuperHeroNames = data.data.map((hero) => hero.name);
    //   return SuperHeroNames;
    // },
  });
};
export const useAddSuperHeroData = () => {
  const queryClient = useQueryClient();
  return useMutation(addSuperHero, {
    onSuccess: (data) => {
      //queryClient.invalidateQueries("super-heroes");
      **queryClient.setQueryData("super-heroes", (oldQueryData) => {
        return {
          ...oldQueryData,
          data: [...oldQueryData.data, data.data],
        };
      });**
    },
  });
};

 

 

 


 

 

ETC.

react-query devtools

//react-query devtools
import { ReactQueryDevtools } from "react-query/devtools";

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
    </QueryClientProvider>
  );
}
반응형