치춘짱베리굿나이스

react-query 1. useQuery 본문

ClientSide/라이브러리

react-query 1. useQuery

치춘 2022. 5. 20. 22:00

react-query

설치

$> npm i react-query
$> yarn add react-query

npm 링크

react-query

 

react-query

Hooks for managing, caching and syncing asynchronous and remote data in React. Latest version: 3.39.0, last published: 13 days ago. Start using react-query in your project by running `npm i react-query`. There are 783 other projects in the npm registry usi

www.npmjs.com

yarn 링크

Contributors

 

https://yarnpkg.com/package/react-query

Fast, reliable, and secure dependency management.

yarnpkg.com

용례

비동기 로직을 다루는 데에 도움을 주는 라이브러리

대개 서버에서 가져오는 상태값들을 효율적으로 관리하거나, 서버의 상태값을 변경하는 데 큰 도움을 준다

리액트 쿼리로 편-안하게 개발할 수 있는 요소들은 다음과 같다

  • 데이터 캐싱하기
  • get해서 가져온 데이터 업데이트할 때 get 재수행
    • 예시: 게시판에 게시글 업로드했을 때, 게시글 목록 다시 불러오기 등
  • 오래된 데이터 get 재수행하여 새로운 데이터로 만들어주기
  • 무한스크롤
  • isLoading, isErrorSuspenseErrorBoundary로 다뤄주던 요소들
  • 중복 호출 허용 시간 조절 (같은 데이터 짧은 시간 내에 여러번 호출하지 않도록 방지)

대충 직접 짜려고 하면 아주 머리터지는 요소들이 많이 보인다... 이것들을 쉽게 해결해 준다

대신 설정해야할 값이 다양하고 캐시의 라이프사이클, 비동기에 대해 사전지식을 어느정도 갖추어야 설정값들을 자유자재로 다룰 수 있어 입문 난이도가 어려운 편이다

워낙에 다뤄야 할 내용이 많고 방대하니 이번에는 useQuery 위주로 정리해보도록 하겠다

1. 사전 세팅

import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevTools } from 'react-query/devtools';

...
// 최상단 요소
<React.StrictMode>
    <QueryClientProvider client={queryClient}>
        <ReactQueryDevTools /> {/* 개발 중에 리액트 쿼리 관련 요소들 쉽게 체크할 수 있도록 도와주는 툴 */}
        <Router>
    </QueryClientProvider>
</React.StrictMode>

라이브러리를 설치했다면, 리액트 프로젝트의 최상단에 (또는 리액트 쿼리를 적용할 컴포넌트에) QueryClientProvider로 감싸주자

만약 리액트 쿼리 개발에 도움을 주는 툴을 쓰고 싶다면 ReactQueryDevTools 컴포넌트도 추가한다 (단, 배포 단계에서 지워주지 않으면 앱 최상단에 리액트 쿼리 아이콘이 둥둥 떠다니는 ㅡ,,ㅡ; 흉물스러운 앱이 되어버리며, 사용자들이 API 데이터에 접근할 수 있게 되므로 위험하다)

1-1. React-Query Devtools

왼쪽 구석 (기본값) 에 있는 이 꽃이 데브툴이다

컴포넌트의 position 속성으로 위치를 변경할 수 있다

꽃 아이콘을 클릭하면 지금까지 요청된 쿼리들의 상태와 데이터, 설정값, 해당 쿼리를 요청하는 observer의 수, 마지막 업데이트 시점 등을 볼 수 있다

props로 받아오는 clientreact-query 라이브러리에서 불러온 QueryClient를 전달해 주자

1-2. 쿼리의 상태값 (Data fetching State)

const { data } = useQuery(
  ['todos'], // Query가 의존성을 갖는 값들
  fetchTodos,
    {
        // 옵션들
        // staleTime, cacheTime이 지정되어 있지 않다고 가정하자
        // staleTime의 기본값은 0, cacheTime의 기본값은 5분이다
    },
);

... // TodoList 컴포넌트
const TodoList = () => {
    const { data } = useQuery(['todos'], fetchTodos, ...);
    ...
}

... // SettingPage 컴포넌트
const SettingPage = () => {
    const { data } = useQuery(['todos'], fetchTodos, ...);
    ...
}

위의 아주 간단한 useQuery 훅으로 라이프사이클을 알아보자

  1. TodoList 컴포넌트가 마운트되면서, 컴포넌트 내부에서 지정한 쿼리 인스턴스가 마운트된다
    • 첫 마운트 시에는 ‘todos’ 키를 갖는 (또는 배열 안의 모든 값들에 의존성을 갖는) 쿼리가 존재하지 않으므로, fetchTodos 함수를 사용해서 데이터를 가져온다
    • useQuery 훅에서는 받아온 데이터를 ‘todos’ (또는 배열 안의 값들) 을 고유한 식별자로 갖는 캐시로 저장한다
    • 이 캐시는 처음에는 fresh하지만, staleTime 만큼의 시간이 흐르면 stale 상태로 변경된다
  2. 같은 쿼리를 사용하는 다른 컴포넌트 SettingPage가 마운트되면서, 새로운 인스턴스가 마운트된다
    • 위에서 이미 ‘todo’ 키를 갖는 쿼리가 생성되었으므로, useQuery는 데이터를 서버에서 가져오지 않고 고유 식별자를 이용하여 캐시에서 쿼리를 찾아 바로 데이터를 반환한다
  3. 화면에 새로운 인스턴스가 렌더링되면서 useQuery는 백그라운드에서 refetch를 진행한다
    • 한 쿼리를 두 컴포넌트가 사용하기 때문에, fetch는 단 한번만 이뤄지며 데이터는 양쪽에서 모두 업데이트된다
  4. useQuery를 쓰는 모든 컴포넌트가 언마운트되면, 해당 쿼리를 사용하는 컴포넌트가 없으므로 캐시는 inactive 상태로 변경된다
    • inactive 상태에서 cacheTime 만큼의 시간이 지나면 (기본값은 5분이다) 캐시된 쿼리와 데이터가 가비지 콜렉터에 의해 삭제된다

가능한 상태값의 종류는 다음과 같다

  • fetching: 현재 요청 중인 쿼리이며, 요청에 성공했을 경우 fresh로 바뀐다
  • fresh: 만료되지 않은 신선한 쿼리, 설정한 staleTime이 지나면 해당 쿼리는 stale 상태로 변경된다
    • fresh한 쿼리는 컴포넌트의 마운트와 업데이트가 발생해도 데이터를 재요청하지 않는다
    • 예를 들어 input 태그 내의 값이 변경되면 onChange 함수에 의해 컴포넌트가 업데이트되지만, 해당 값으로 불러오는 쿼리가 fresh한 쿼리일 경우 재요청 없이 값을 바로 가져온다
  • stale: 안 신선한 쿼리, fresh 상태의 쿼리에서 일정 시간이 지날 경우 stale로 변경된다
    • 컴포넌트가 마운트되거나 업데이트되었을 때
    • 브라우저 윈도우가 다시 포커스되었을 때 (refetchOnWindowFocus 설정값에 의존한다)
    • 네트워크가 다시 연결되었을 때 (refectchOnReconnect 설정값에 의존한다)
    • refetchInterval 옵션에 의해 설정된 refetch 주기가 지났을 때
    • 위의 조건을 만족하면 쿼리가 재요청되면서 fresh로 돌아온다
  • inactive: 사용되지 않는 쿼리, 캐시에만 남아있다
    • 설정한 cacheTime이 지나면 가비지 컬렉터가 해당 쿼리를 제거한다
    • 현재 화면에 보이는 컴포넌트 중 하나라도 쿼리를 사용하고 있을 경우 해당 쿼리는 stale 상태가 유지되며, 모든 컴포넌트가 쿼리를 사용하지 않을 때 inactive로 변경된다
    • cacheTime 값이 infinity라면 inactive한 쿼리는 영원히 삭제되지 않는다
    • stale은 리액트 쿼리가 데이터를 최신으로 유지할 지 말 지 결정하도록 돕는 상태값이고, inactive는 해당 쿼리 데이터가 실제로 사용되고 있는지 여부를 판단하는 상태값이라고 생각하면 된다

fresh 데이터가 staleTime이 지난 후 stale로 변경되는 모습이다 (Devtool)

위의 검색결과 컴포넌트에서 해당 데이터를 사용하고 있기 때문에, inactive로 변하진 않는다

2. 데이터 받아오는 api 함수 작성하기

import axios from "axios";

export const getDiseaseData = (searchText: string) =>
  axios.get("/getDissNameCodeList", {
    params: {
      serviceKey: process.env.REACT_APP_API_KEY,
      searchText,
    },
  });

아주 간단하게 axios를 사용해서 작성해보자

예시로는 axios.get 메서드만 사용해서 바로 반환하도록 해주었지만, then / catchtry / catch, finally 등을 이용하여 데이터를 가공해도 무방하다

Promise 형태만 띄고 있으면 된다

3. useQuery 이용하여 컴포넌트 내에서 데이터 받아오기

import { useQuery } from 'react-query';
import { getDiseaseData } from 'services';

export const SearchResultList = () => {
    const searchText = // redux, recoil, useState 등으로 상태값 선언
    const { data } = useQuery(
        ['diseaseData', searchText],
        () => getDiseaseData(searchText),
        {
            // 옵션
        },
    });

    return (
        { data.map((item) => { /* 받아온 결과값으로 렌더링하는 부분 */} }
    );
};

useQuery의 첫 번째 인자로 배열이 들어온다

  • 보통 배열의 0번째 인덱스 값은 고유 키값이 들어오며, 리액트 쿼리에서 이 값을 보고 어떤 쿼리인지 판단한다
  • 배열에 추가적으로 해당 쿼리가 의존성을 가지는 값을 넣어줄 수 있으며, 여기에 들어온 상태값 중 하나라도 바뀔 때마다 리액트 쿼리는 데이터를 새로 요청하거나, 캐시에서 값을 가져온다
  • 예시에서는 [’diseaseData’, searchText] 가 첫 번째 인자로 들어왔으므로, 쿼리는 고유한 식별자로 ‘diseaseData’searchText 상태값을 가진다
    • 예를 들어 searchText‘간’ 이라는 문자열일 경우, 이 때 요청된 쿼리는 ‘diseaseData’‘간’ 이라는 두 개의 식별자를 가지며, 이후에 searchText가 다시 한번 ‘간’ 이라는 값을 가질 때 재요청을 할 지, 같은 식별자를 가진 쿼리 캐시를 가져올지 판단한다
  • 의존성을 갖는 값이 바뀔 때마다 리액트 쿼리는 데이터를 재요청한다
    • 예를 들어, searchText가 ‘간' 에서 ‘뇌' 로 바뀔 경우, 식별자가 ‘diseaseData’, ‘뇌' 인 쿼리를 서버에 요청하거나, 캐시에서 찾는다

두 번째 인자는 데이터를 서버에 요청할 때 사용하는 함수이다

  • 굳이 axiosfetch를 사용할 필요는 없고, 비동기 Promise 형태이기만 하면 된다
  • then 이나 try / catch 체인을 이용하여 값을 반환하는 함수여도 상관없다
  • 이 함수에서 반환되는 값이 각 쿼리별 데이터로 캐싱된다
    • 캐시의 키 값 (식별자) 는 첫 번째 인자로 넣은 배열의 값들, 데이터 값은 두 번째 인자로 넣은 함수로 받아온 데이터가 들어간다고 생각하면 된다

세 번째 인자는 옵션으로, 객체가 들어온다

  • suspense 옵션은 Suspense (데이터 요청 시 Loading 컴포넌트 fallback 설정) 여부를 결정한다
  • useErrorBoundary 옵션은 에러 발생 시 별도의 fallback 컴포넌트 렌더링 여부를 결정한다
  • staleTimefresh에서 stale 상태로 바뀔 때까지 걸리는 시간을 결정한다 (기본: 0)
  • cacheTimeinactive 상태에서 캐시가 제거되기까지 걸리는 시간을 결정한다 (기본: 5분)
  • enabled 옵션은 false일 때 쿼리 요청을 중단하며, 조건식을 넣어 특정 조건에서만 쿼리가 요청되도록 조정할 수 있다
  • refetchOnWindowFocus, refetchOnReconnect 등은 특정 상황에서 refetch (데이터 재요청) 를 진행할 지 여부를 설정할 수 있다
  • onSuccess, onError은 데이터 받아오기 성공, 실패 시 후속 동작 함수를 정의할 수 있다

4. useQuery 반환값

useQuery로부터 다양한 반환값을 받아올 수 있으며, 이를 이용하여 로딩 중 여부, 재요청, 캐시의 상태, 에러 값 등을 받아올 수 있다

  • data: 데이터 요청에 대한 응답을 성공적으로 받았을 때 응답이 반환된다
  • error: 데이터 요청이 실패했을 때 에러 값이 반환된다
  • refetch: 이 함수를 호출하면 현재 지정된 쿼리가 재요청된다
  • isLoading, isError, isSuccess, … : 데이터 요청 상황을 반환한다

5. 커스텀 훅으로 만들기

const useFilteredQuery = () => {
    const searchText = // redux, recoil, useState 등으로 상태값 선언
    const { data, refetch, } = useQuery(
        ['diseaseData', searchText],
        () => axios.get('test',
            {
                params: {
                    apiKey: process.env.REACT_APP_API_KEY,
                    searchText,
                    pages: 1,
                },
            }).then((res) => {
                return data.filter((item) => {
                    // 필터링 조건
                });
            }),
        {
            staleTime: 60000, // 1분
            suspense: true,
            useErrorBoundary: true,
            enabled: searchText !== "",
            onSuccess: () => {
                console.log("fetched successfully");
            },
        }
    );

    return { data, refetch, searchText };
};

같은 쿼리 설정으로 다른 컴포넌트에서도 데이터를 요청하거나, useQuery 인자값들이 너무 길어질 경우 커스텀 훅으로 만들어 분리하여 사용할 수 있다

커스텀 훅 특성상 컴포넌트의 리렌더나 내부에서 의존성을 갖는 값이 있을 경우 재실행되어 값을 최신 상태로 유지하므로 어느 컴포넌트던 커스텀 훅 호출 코드 한 줄이면 리액트 쿼리로 데이터를 관리할 수 있다

번외: 쿼리 무효화하기

import { useQueryClient } from "react-query";

...
const queryClient = useQueryClient();

queryClient.invalidateQueries();

커뮤니티 게시판 등, 데이터를 가급적 최신 상태로 유지해야 하는 경우 임의로 쿼리를 무효화한 후 새 데이터를 요청하도록 만들 수 있다

useQueryClient 훅으로 쿼리 클라이언트를 받아오고, invalidateQueries 메서드로 특정 쿼리를 무효화하자

  • 인자로 아무 것도 안 넣을 경우, 현재 클라이언트 내의 모든 쿼리를 무효화한다
  • 인자로 배열을 넣을 경우, 해당 배열의 값들을 의존성으로 갖는 쿼리를 무효화한다
  • 인자로 키 하나만을 넣을 경우, 해당 키를 의존성으로 갖는 모든 쿼리를 무효화한다

무효화란, fresh 상태 (재요청을 하지 않는 상태) 를 무시하고 즉각 서버에 데이터 재요청을 보낸다는 것이다

원래대로라면 쿼리가 stale 상태여야 재요청을 시도하지만, 쿼리를 무효화하면 stale 여부와 관계없이 바로 데이터를 새로 가져오도록 할 수 있다

inactive 쿼리와는 관계 없다

번외: Suspense, ErrorBoundary 사용하기

const { data } = useQuery(
    ['diseaseData', searchText],
    () => getDiseaseData(searchText),
    {
        suspense: true, // Suspense 사용
        useErrorBoundary: true, // Error Boundary 사용
    },
});    

위와 같이 설정하면 각각 suspense, errorBoundary 를 사용할 수 있다

const Loading = () => { // suspense fallback 컴포넌트
    return (
        <div classame={styles.loading}>
            Loading...
        </div>
    );
};
const Error = ({ error }: Props) => { // errorBoundary fallback 컴포넌트
    return (
        <div className={styles.error}>
            {error.message}
        </div>
    );
};

로딩 컴포넌트와 에러 컴포넌트를 만들어준다

각각 데이터 응답을 기다리는 중일 때와, 에러가 발생하였을 때 폴백으로 렌더링되는 컴포넌트이다

<Suspense fallback={<Loading />}>
    <ErrorBoundary fallbackRender={({ error }) => <Error error={error} />}>
        <Weather />
    </ErrorBoundary>
</Suspense>

위에서 지정한 useQuery로 데이터를 받아오는 컴포넌트를 ErrorBoundarySuspense 컴포넌트로 감싸주자

ErrorBoundaryreact-error-boundary 라이브러리를 사용하고, Suspense는 기본으로 탑재되어 있다

Weather 컴포넌트 내의 useQuery에서 에러 발생 시, 가장 가까운 ErrorBoundary로 에러 처리를 위임한다

또한 데이터를 받아오는 중일 때 Loading 컴포넌트가 렌더링된다

번외: 그외 정보

  • 같은 키 (식별자) 를 갖는 쿼리가 컴포넌트별로 여러 개 존재하더라도, 리액트 쿼리에 의한 요청은 1번만 수행된다 = 모든 쿼리는 키 값 (배열 내의 값) 에 의해 구분되므로, 서로 다른 데이터를 요청하고 싶다면 키 값을 다르게 주어야 한다
  • 같은 키에 대한 쿼리 요청을 넣었을 때, 응답이 이전 응답 데이터와 같다면, 메모리에 다시 할당하지 않고 기존의 참조를 반환한다
  • queryClient의 메서드를 이용하여 캐싱된 데이터를 가져오거나 조작하는 방법이 있다

참고자료

Caching Examples

 

Caching Examples

Subscribe to Bytes Your weekly dose of JavaScript news. Delivered every Monday to over 80,000 devs, for free.

react-query.tanstack.com

What Is The Difference Between Stale State and Inactive State in React Query

 

https://backbencher.dev/articles/react-query-what-is-inactive-query-state

What Is The Difference Between Stale State and Inactive State in React Query Published on 3 Aug, 2021 When working with Queries in React Query, we can see the state of Query is sometimes stale or inactive. Other than these 2, there are fresh and fetching s

backbencher.dev

React Query로 서버 상태 관리하기

 

blog.rhostem.com

프론트엔드 웹 개발 기술 블로그

blog.rhostem.com

react-query를 이해하는 몇가지 지식과 질문들

 

react-query를 이해하는 몇가지 지식과 질문들

전제 지식들 staleTime : 마운트 되어 있는 시점에서 데이터가 구식인지 판단함. cacheTime : 언마운트된 후 어느 시점까지 메모리에 데이터를 저장하여 캐싱할 것인지를 결정함. refetching의 조건 New ins

darrengwon.tistory.com

 

'ClientSide > 라이브러리' 카테고리의 다른 글

구글 애널리틱스  (0) 2022.05.29
redux + toolkit  (0) 2022.05.21
gh-pages로 리액트 프로젝트 배포 + 404 에러처리  (1) 2022.05.15
React-Beautiful-Dnd  (0) 2022.05.15
lodash  (0) 2022.05.14
Comments