React

[React-query] 리액트 쿼리를 사용하는 이유 및 useQuery / useMutation 사용법

킹우현 2023. 6. 2. 14:00

1) React-query란 ?

react-query는 리액트 애플리케이션에서 서버 상태 가져오기캐싱동기화 및 업데이트를 보다 쉽게 다룰 수 있도록 도와주는 라이브러리이다. 클라이언트 상태와 서버 상태를 명확히 구분하기 위해 만들어졌다. 

 

react-query에서는 기존 상태 관리 라이브러리인 redux, mobX가 클라이언트 상태 작업에 적합하지만, 비동기 또는 서버 상태 작업에는 그다지 좋지 않다고 언급한다. 

 

클라이언트 상태(Client State)와 서버 상태(Server State)는 완전히 다른 개념이며, 클라이언트 상태는 각각의 input 값으로 예를 들 수 있고, 서버 상태는 데이터베이스에 저장되어 있는 데이터로 예를 들 수 있다.

 

2) React-query를 사용하는 이유

  1. 가장 기본적인 Data Fetching
  2. 캐싱 처리
  3. 로딩 및 오류 상태 관리 (데이터가 로딩되었을 때와 오류 발생시 사용자에게 알려준다)
  4. 리액트 쿼리 개발자 도구(React query dev tools)를 통해 쿼리에 무슨 일이 일어났는지 추적이 가능하다.
  5. 게시판 같은 것을 만들 때 Pagination 기능을 사용하여 프리패칭(Prefetching)을 수행할 수 있다. (*프리패칭(Prefetching) : 다음 페이지 데이터를 미리 가져와서, 다음 페이지로 넘어갈 때 미리 데이터를 가져왔기 때문에 매끄럽게 처리 된다.)
  6. 같은 데이터에 대한 여러번의 요청이 있을 시 마지막 요청건만 처리해준다.
  7. 가비지 컬렉션을 이용하여 자동으로 메모리를 관리해준다.

 

3) React-query 설치 및 세팅

npm install react-query
npm install react-query@^3 // 버전 3
import { QueryClient, QueryClientProvider } from "react-query";  //📍추가

const queryClient = new QueryClient();//📍추가

function App() {
  return (
    <QueryClientProvider client={queryClient}> //📍추가
      <div className="App">
        (...)
      </div>
    </QueryClientProvider>
  );
}

export default App;

 

react-query를 사용하기 위해서는 <QueryClientProvider> </QueryClientProvider>로 최상단 컴포넌트를 감싸주고 queryClient 인스턴스를 client props로 넣어 애플리케이션에 연결시켜야 한다. 

 

위 예시에서 App.js에 QueryClientProvider로 컴포넌트를 감싸고, client props에 queryClient를 연결함으로써, 이 context는 앱에서 비동기 요청을 알아서 처리하는 background 계층이 된다.

 

4) React-query 개발자 도구(Devtools)

npm i @tanstack/react-query-devtools

yarn 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>
  );
}

react-query는 전용 devtools를 제공한다. devtools를 사용하면 React Query의 모든 내부 동작을 시각화하는 데 도움이 되며 문제가 발생하면 디버깅 시간을 절약할 수 있다. 

 

devtools는 기본값으로 process.env.NODE_ENV === 'development' 인 경우에만 실행된다, 즉 일반적으로 개발환경에서만 작동하므로 설정되어있으므로, 프로젝트 배포 시에 Devtools 삽입코드를 제거해줄 필요가 없다.

 

4-1) react-query devtools를 사용하는 이유

  1. 쿼리 키로 쿼리를 표시해준다. 
  2. 개발 중인 모든 쿼리의 상태를 표시해준다. 이것에 대한 상태란 활성, 비활성, 만료(stale) 등 모든 쿼리의 상태를 알려준다. 
  3. 마지막으로 업데이트 된 타임 스탬프도 알려준다. 
  4. 데이터 탐색기도 있다. 
  5. 쿼리를 볼 수 있는 쿼리 탐색기도 있다.

 

5) 캐싱 라이프 사이클(Cache Life Cycle)

* Query Instances with and without cache data(캐시 데이터가 있거나 없는 쿼리 인스턴스)
* Background Refetching(백그라운드 리패칭)
* Inactive Queries(비활성 쿼리)
* Garbage Collection(가비지 컬렉션)

 

cacheTime의 기본값 '5분', staleTime 기본값 '0초'를 가정

  1. A라는 queryKey를 가진 A 쿼리 인스턴스가 mount됨
  2. 네트워크에서 데이터 fetch하고, 불러온 데이터는 A라는 queryKey로 캐싱함
  3. 이 데이터는 fresh상태에서 staleTime(기본값 0) 이후 stale 상태로 변경됨
  4. A 쿼리 인스턴스가 unmount됨
  5. 캐시는 cacheTime(기본값 5min) 만큼 유지되다가 가비지 콜렉터(GC)로 수집됨
  6. 만일, cacheTime이 지나기 전이고, A 쿼리 인스턴스 fresh한 상태라면 새롭게 mount되면 캐시 데이터를 보여준다.

 

6) React-query 사용법

6-1) useQuery

const { data } = useQuery("쿼리명", 쿼리함수 = 데이터를 가져오는 함수, 옵션);

const { data, isLoading, ... } =  useQuery(queryKey, queryFn, {
  // ...options ex) enabled, staleTime, ...
});
// 실제 예제
const getAllSuperHero = async () => {
  return await axios.get("http://localhost:4000/superheroes");
};

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

 

react-query에서 기본적으로 서버에서 데이터를 Get 할 때는 useQuery를 사용한다. useQuery는 기본적으로 3개의 인자를 받는다.

 

첫 번째 인자가 queryKey(필수), 두 번째 인자가 queryFn(필수), 세 번째 인자가 options(optional)이다.

 

1. queryKey

v3까지는 queryKey로 문자열 또는 배열 모두 지정할 수 있는데, v4부터는 무조건 "배열"로 지정해야 한다. useQuery는 첫 번째 인자인 queryKey를 기반으로 데이터 캐싱을 관리한다.

 

만약, 쿼리가 특정 변수에 의존한다면 배열에다 이어서 넣어주면 된다. ex: ["super-hero", heroId, ...]

 

2. queryFn

useQuery의 두 번째 인자인 queryFn는 Promise를 반환하는 함수를 넣어야한다.

 

3. options

// 예
const useSuperHeroData = (heroId: string) => {
  return useQuery(["super-hero", heroId], () => getSuperHero(heroId), {
    cacheTime: 5 * 60 * 1000, // 5분
    staleTime: 1 * 60 * 1000, // 1분
    retry: 1,
    // ...options
  });
};

첫번째와 두번째 인수는 필수지만 세번째는 옵션이다. 이 구간에는 staleTime, cacheTime를 사용할 수 있다.

 

4. 리턴 값

const { status, isLoading, isError, error, data, isFetching, ... } = useQuery(
  ["colors", pageNum],
  () => fetchColors(pageNum)
);
  1. status: 쿼리 요청 함수의 상태를 표현하는 status는 4가지의 값이 존재한다.(문자열 형태) 
    1. idle: 쿼리 데이터가 없고 비었을 때, { enabled: false } 상태로 쿼리가 호출되면 이 상태로 시작된다. 
    2. loading: 말 그대로 아직 캐시된 데이터가 없고 로딩중일 때 상태 
    3. error: 요청 에러 발생했을 때 상태 
    4. success: 요청 성공했을 때 상태
  2. data: 쿼리 함수가 리턴한 Promise에서 resolved된 데이터 
  3. isLoading: 캐싱 된 데이터가 없을 때 즉, 처음 실행된 쿼리 일 때 로딩 여부에 따라 true/false로 반환된다. 이는 캐싱 된 데이터가 있다면 로딩 여부에 상관없이 false를 반환한다. 
  4. isFetching: 캐싱 된 데이터가 있더라도 쿼리가 실행되면 로딩 여부에 따라 true/false로 반환된다. 이는 캐싱 된 데이터가 있더라도 쿼리 로딩 여부에 따라 true/false를 반환한다. 
  5. error: 쿼리 함수에 오류가 발생한 경우, 쿼리에 대한 오류 객체 
  6. isError: 에러가 발생한 경우 true
💡 staleTime과 cacheTime
stale은 용어 뜻대로 썩은 이라는 의미이다. 즉, 최신 상태가 아니라는 의미이다.
fresh는 뜻 그대로 신선한 이라는 의미이다. 즉, 최신 상태라는 의미이다.

1. staleTime: (number | Infinity) : staleTime은 데이터가 fresh에서 stale 상태로 변경되는 데 걸리는 시간, 만약 staleTime이 3000이면 fresh상태에서 3초 뒤에 stale로 변환

fresh 상태일 때는 쿼리 인스턴스가 새롭게 mount 되어도 네트워크 요청(fetch)이 일어나지 않는다. ⭐️

데이터가 한번 fetch 되고 나서 staleTime이 지나지 않았다면(fresh상태) unmount 후 다시 mount 되어도 fetch가 일어나지 않는다. staleTime의 기본값은 0이기 때문에 일반적으로 fetch 후에 바로 stale이 된다.

2. cacheTime: (number | Infinity) : 데이터가 inactive 상태일 때 캐싱 된 상태로 남아있는 시간

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

cacheTime이 지나면 가비지 콜렉터로 수집된다. cacheTime이 지나기 전에 쿼리 인스턴스가 다시 mount 되면, 데이터를 fetch하는 동안 캐시 데이터를 보여준다.

cacheTime은 staleTime과 관계없이, 무조건 inactive 된 시점을 기준으로 캐시 데이터 삭제를 결정한다.cacheTime의 기본값은 5분이다.

🚨 여기서 주의할 점은 staleTime과 cacheTime의 기본값은 각각 0분과 5분이다. 따라서 staleTime에 어떠한 설정도 하지 않으면 캐싱이 전혀 되지 않는다.왜냐하면, 항상 캐싱 되어 있는 데이터가 stale하다고 여기기 때문이다.

staleTime을 길게 설정하더라도 cacheTime이 짧다면 이 또한 캐싱이 원활하게 진행되지 않을 것이다. 결국에는 두 개의 옵션을 적절하게 설정해줘야 한다.

6-2) useMutation

const CreateTodo = () => {
  const mutation = useMutation(createTodo, {
    onMutate() {
      /* ... */
    },
    onSuccess(data) {
      console.log(data);
    },
    onError(err) {
      console.log(err);
    },
    onSettled() {
      /* ... */
    },
  });

  const onCreateTodo = (e) => {
    e.preventDefault();
    mutation.mutate({ title });
  };

  return <>...</>;
};

만약 서버의 data를 post, patch, put, delete와 같이 수정하고자 한다면 이때는 useMutation을 이용한다. 

👉🏻 R(read)는 useQuery, CUD(Create, Update, Delete)는 useMutation을 사용한다 !

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

출처 : https://velog.io/@leemember/React-query-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-useInfiniteQuery-%EB%A1%9C-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-react-infinite-scroller

 

📲 React-query 사용하는 이유, useInfiniteQuery 로 무한 스크롤 구현하기 (+ react-infinite-scroller)

현재 동료들과 함께 진행하고 있는 사이드 프로젝트에 React-query를 도입해봤습니다. React-query를 왜 사용했는지? 에 대해서 소개하고 사용 방법과 React-query를 이용하여 무한 스크롤을 구현해본 방

velog.io

https://github.com/ssi02014/react-query-tutorial

 

GitHub - ssi02014/react-query-tutorial: 😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리

😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리 - GitHub - ssi02014/react-query-tutorial: 😃 TanStack Query(aka. react query) 에서 자주 사용되는 개념 정리

github.com