리액트 쿼리(1)

yellowbutter·2024년 5월 27일
0

TIL

목록 보기
24/24

📌아래 내용은 react-query-tutorial을 읽고 개인 공부를 위해 정리해둔 것입니다.

React Query

  • react-query는 리액트 애플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 보다 쉽게 다룰 수 있도록 도와주는 라이브러리이다.
    클라이언트 상태와 서버 상태를 명확히 구분하기 위해 만들어졌다.
  • react-query에서는 기존 상태 관리 라이브러리인 redux, mobX가 클라이언트 상태 작업에 적합하지만,
    비동기 또는 서버 상태 작업에는 그다지 좋지 않다고 언급한다.
  • 클라이언트 상태(Client State)와 서버 상태(Server State)는 완전히 다른 개념이며, 클라이언트 상태는 각각의 input 값으로 예를 들 수 있고, 서버 상태는 데이터베이스에 저장되어 있는 데이터로 예를 들 수 있다.
  • react query는 현재 V5 까지 있으며, React 이 외에도 Vue 등에서도 사용이 가능해지면서 TanStack Query 로 이름을 바꿨다고 한다

TanStack Query

it makes fetching, caching, synchronizing and updating server state in your web applications a breeze.
웹 애플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 보다 쉽게 다룰 수 있도록 도와줌.
TanStack Query 공식문서

  • 서버로부터 데이터를 받을 때 서버의 상태를 관리하기 위해 만들어진 라이브러리

1. 기본 설정

import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
      // ...
    },
  },
});

QueryClient에서 모든 query 또는 mutation에 기본 옵션을 추가할 수 있으며, 종류가 상당하므로 공식 문서를 참고해 보자.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient({ /* options */});

function App() {
  return (
   <QueryClientProvider client={queryClient}>
      <div>블라블라</div>
   </QueryClientProvider>;
  );
}
  • react-query를 사용하기 위해서는
    QueryClientProvider를 최상단에서 감싸주고
    QueryClient 인스턴스를 client props로 넣어 애플리케이션에 연결
    해야 한다.
  • 위 예시에서
    App.js에 QueryClientProvider로 컴포넌트를 감싸고, client props에다 queryClient를 연결함으로써,
    이 context는 앱에서 비동기 요청을 알아서 처리하는 background 계층이 된다.

2. devtools

  • react-query는 전용 devtools를 제공하며 별도의 패키지 설치가 필요하다.
  • devtools를 사용하면 React Query의 모든 내부 동작을 시각화하는 데 도움이 되며 문제가 발생하면 디버깅 시간을 절약할 수 있다.
  • devtools는 기본값으로 process.env.NODE_ENV === "development" 인 경우에만 실행된다, 즉 일반적으로 개발 환경에서만 작동하도록 설정되어 있으므로, 프로젝트 배포 시에 Devtools 삽입 코드를 제거해 줄 필요가 없다.
  • Next 13+의 App Dir에선 dev dependency로 설치해야 동작한다.
$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools
# or
$ bun add @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );

options

  • initialIsOpen (Boolean)
    true이면 개발 도구가 기본적으로 열려 있도록 설정할 수 있다.
  • buttonPosition?: ("top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative")
    기본값: bottom-right
    devtools 패널을 여닫기 위한 로고 위치
    relative일 때 버튼은 devtools를 렌더링하는 위치에 배치된다.
  • 일반적으로 initialIsOpen, buttonPosition을 자주 사용하며 그 외에 position, client와 같은 옵션들도 존재한다.

3. 기능

캐싱
동일한 데이터에 대한 중복 요청을 단일 요청으로 통합
백그라운드에서 오래된 데이터 업데이트
데이터가 얼마나 오래되었는지 알 수 있다.
데이터 업데이트를 가능한 빠르게 반영
페이지네이션 및 데이터 지연 로드와 같은 성능 최적화
서버 상태의 메모리 및 가비지 수집 관리
구조 공유를 사용하여 쿼리 결과를 메모화

4. 용어

1. staleTime과 gcTime

stale은 용어 뜻대로 썩은이라는 의미이다. 즉, 최신 상태가 아니라는 의미이다.
fresh는 뜻 그대로 신선한이라는 의미이다. 즉, 최신 상태라는 의미이다.

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  gcTime: 5 * 60 * 1000, // 5분
  staleTime: 1 * 60 * 1000, // 1분
});

📌 staleTime: (number | Infinity)

  • staleTime은 데이터가 fresh에서 stale 상태로 변경되는 데 걸리는 시간
  • 만약 staleTime이 3000이면 fresh 상태에서 3초 뒤에 stale로 변환
  • fresh 상태일 때는 쿼리 인스턴스가 새롭게 mount 되어도 네트워크 요청(fetch)이 일어나지 않는다.
  • 참고로, staleTime의 기본값은 0이기 때문에 일반적으로 fetch 후에 바로 stale이 된다.

📌 gcTime: (number | Infinity)

  • 데이터가 사용하지 않거나, inactive 상태일 때 캐싱 된 상태로 남아있는 시간(밀리초)이다.
  • 쿼리 인스턴스가 unmount 되면 데이터는 inactive 상태로 변경되며, 캐시는 gcTime만큼 유지된다.
  • gcTime이 지나면 가비지 콜렉터로 수집된다.
    gcTime이 지나기 전에 쿼리 인스턴스가 다시 mount 되면, 데이터를 fetch 하는 동안 캐시 데이터를 보여준다.
  • gcTime은 staleTime과 관계없이, 무조건 inactive 된 시점을 기준으로 캐시 데이터 삭제를 결정한다.
    gcTime의 기본값은 5분이다. SSR 환경에서는 Infinity이다.
    -여기서 주의할 점은 staleTime과 gcTime의 기본값은 각각 0분과 5분이다. 따라서 staleTime에 어떠한 설정도 하지 않으면 해당 쿼리를 사용하는 컴포넌트(Observer)가 mount 됐을 때 매번 다시 API를 요청할 것이다.
    staleTime을 gcTime보다 길게 설정했다고 가정하면, staleTime만큼의 캐싱을 기대했을 때 원하는 결과를 얻지 못할 것이다. 즉, 두 개의 옵션을 적절하게 설정해 줘야 한다.
    참고로, TkDodo의 reply에 따르면 TkDodo는 staleTime을 gcTime보다 작게 설정하는 것이 좋다.는 의견에 동의하지 않는다고 한다.
    예컨대, staleTime이 60분일지라도 유저가 자주 사용하지 않는 데이터라면 굳이 gcTime을 60분 이상으로 설정하여 메모리를 낭비할 필요가 없다.

2. useQuery 기본 문법

const result = useQuery({
  queryKey, // required
  queryFn, // required
  // ...options ex) gcTime, staleTime, select, ...
});

result.data;
result.isLoading;
result.refetch;
// ..

useQuery는 v5부터 인자로 단 하나의 객체만 받는다. 그중에 첫 번째 인자가 queryKey, queryFn가 필수 값이다.

// 실제 예제
// 💡 queryFn의 반환 타입을 지정해주면 useQuery의 타입 추론이 원활합니다.
const getAllSuperHero = async (): Promise<AxiosResponse<Hero[]>> => {
  return await axios.get("http://localhost:4000/superheroes");
};

const { data, isLoading } = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
});

2.1 queryKey

  • useQuery의 queryKey는 배열로 지정해 줘야 한다.
  • 이는 단일 문자열만 포함된 배열이 될 수도 있고, 여러 문자열과 중첩된 객체로 구성된 복잡한 형태일 수도 있다.
// An individual todo
useQuery({ queryKey: ["todo", 5], ... })

// An individual todo in a "preview" format
useQuery({ queryKey: ["todo", 5, { preview: true }], ...})
  • useQuery는 queryKey를 기반으로 쿼리 캐싱을 관리하는 것이 핵심이다.
  • 만약, 쿼리가 특정 변수에 의존한다면 배열에다 이어서 줘야 한다.
    ex: ["super-hero", heroId, ...]
  • 이는 사실 굉장히 중요하다. 예를 들어, queryClient.setQueryData 등과 같이 특정 쿼리에 접근이 필요 할 때 초기에 설정해둔 포맷을 지켜줘야 제대로 쿼리에 접근할 수 있다.
  • 아래 options 예제를 살펴보면 useSuperHeroData의 queryKey는 ["super-hero", heroId]이다. 그렇다면 queryClient.setQueryData를 이용할 때 똑같이 ["super-hero", heroId] 포맷을 가져야 한다. 그렇지 않으면 원하는 쿼리에 접근할 수 없다.
// (1) queryKey는 데이터를 고유하게 식별에 더해 쿼리 함수에 아래와 같이 편리하게 전달할 수도 있다.
const getSuperHero = async ({
  queryKey,
}: {
  queryKey: ["super-hero", number];
}): Promise<AxiosResponse<Hero>> => {
  const heroId = queryKey[1]; // ex) queryKey: ["super-hero", "3"]

  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: getSuperHero, // (*)
  });
};

2.2. queryFn

useQuery의 queryFn는 Promise를 반환하는 함수를 넣어야 한다.

// (2) 상단의 queryKey 예제와 반대로 queryFn 자체적으로 인자를 받는 형태
const getSuperHero = async (heroId: string): Promise<AxiosResponse<Hero>> => {
  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: () => getSuperHero(heroId), // (*)
  });
};

2.3. useQuery 주요 리턴 데이터

const {
  data,
  error,
  status,
  fetchStatus,
  isLoading,
  isFetching,
  isError,
  refetch,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
});
  • ✔️data: 쿼리 함수가 리턴한 Promise에서 resolved된 데이터

  • ✔️error: 쿼리 함수에 오류가 발생한 경우, 쿼리에 대한 오류 객체

  • ✔️status: data, 쿼리 결과값에 대한 상태를 표현하는 status는 문자열 형태로 3가지의 값이 존재한다.
    📌 pending: 쿼리 데이터가 없고, 쿼리 시도가 아직 완료되지 않은 상태.
    { enabled: false } 상태로 쿼리가 호출되면 이 상태로 시작된다.
    📌error: 에러 발생했을 때 상태
    📌success: 쿼리 함수가 오류 없이 요청 성공하고 데이터를 표시할 준비가 된 상태.

  • ✔️fetchStatus: queryFn에 대한 정보를 나타냄
    📌fetching: 쿼리가 현재 실행 중인 상태
    📌paused: 쿼리를 요청했지만, 잠시 중단된 상태 (network mode와 연관)
    📌idle: 쿼리가 현재 아무 작업도 수행하지 않는 상태

  • ✔️isLoading: 캐싱 된 데이터가 없을 때 즉, 처음 실행된 쿼리일 때 로딩 여부에 따라 true/false로 반환된다.
    이는 캐싱 된 데이터가 있다면 로딩 여부에 상관없이 false를 반환한다.
    isFetching && isPending 와 동일하다.

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

  • ✔️isSuccess: 쿼리 요청이 성공하면 true

  • ✔️isError: 쿼리 요청 중에 에러가 발생한 경우 true

  • ✔️refetch: 쿼리를 수동으로 다시 가져오는 함수.
    그 외 반환 데이터들을 자세히 알고 싶으면 useQuery 공식 문서 참고

💡 status, fetchStatus 나눠서 다루는 걸까?

  • fetchStatus는 HTTP 네트워크 연결 상태와 좀 더 관련된 상태 데이터이다.
    예를 들어, status가 success 상태라면 주로 fetchStatus는 idle 상태지만, 백그라운드에서 re-fetch가 발생할 때 fetching 상태일 수 있다.
  • status가 보통 loading 상태일 때 fetchStatus는 주로 fetching를 갖지만, 네트워크 연결이 되어 있지 않은 경우 paused 상태를 가질 수 있다.
  • 정리하자면 아래와 같다.
    status는 data가 있는지 없는지에 대한 상태를 의미한다.
    fetchStatus는 쿼리 즉, queryFn 요청이 진행 중인지 아닌지에 대한 상태를 의미한다.

참고자료
react-query

profile
기록은 희미해지지 않는다 🐾🧑‍💻

0개의 댓글