본문 바로가기

React

[React] React Query 시작하기

React Query 공식 사이트)

 

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

Instead of writing reducers, caching logic, timers, retry logic, complex async/await scripting (I could keep going...), you literally write a tiny fraction of the code you normally would. You will be surprised at how little code you're writing or how much

tanstack.com

 

아래 블로그에 대한 내용을 아래에 정리)

 

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

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

velog.io

 

 

React- query 를 사용하는 이유

1. 데이터 캐싱

데이터를 주고 받은 결과를 캐시로 저장한다. 동일한 요청시 새로운 요청을 보내지 않고, 저장되어 있는 데이터를 사용한다.(애플리케이션 성능 향상, 네트워크 사용량 감소)

캐싱: 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 재접근 속도를 높이는 것

2. 로딩 및 오류 상태 관리

데이터 로딩시, 오류 발생시 사용자에게 알려준다.

3. 프리패칭

프리패칭: 다음 페이지 데이터를 미리 가져와서, 다음 페이지로 넘어갈 때 데이터를 미리 가져왔기 때문에 매끄럽게 처리가 됨.

ex.게시판

4. 가비지 컬렉션을 이용하여 자동으로 메모리를 관리

가비지 컬렉션 : 메모리 관리 방법 중 하나로, 프로그래머가 동적으로 할당한 메모리 영역 중 더 이상 쓰이지 않는 영역을 자동으로 찾아내어 해제하는 기능

 

[ 사용하기 ]

설치

npm install react-query

 

세팅

1. <QueryClientProvider> </QueryClientProvider>로 감싼다.

  • App.tsx 또는 Next 경우 _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; //추가(react-query말고 @tanstack/react-query로 불러와야함)

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

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

export default App;

 

2. React-query 개발자 도구도 추가해보기

리액트쿼리 개발자 도구를 사용하는 이유

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

 

버전 4에서 개발자 도구 설치할 때 명령어 (버전3은 설치할 필요X)

npm i @tanstack/react-query-devtools
or
yarn add @tanstack/react-query-devtools

공식사이트 링크

 

  • App.tsx 또는 Next 경우 _app.tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; //추가

return (
        <>
            <QueryClientProvider client={queryClient}>
                (...)
                <ReactQueryDevtools /> {/*추가*/}
            </QueryClientProvider>
        </>
    );

개발자 도구까지 세팅이 완료되면 브라우저 하단에 아이콘이 생기는데 클릭하면 아래와 같은 화면이 나온다. 그리고 devtools는 기본적으로 개발환경에서만 제공되므로 프로덕션 빌드 중에 제외하는 것에 대해 걱정할 필요 없다.

 

사용

react-query 사용 인터페이스

const { data } = useQuery("쿼리명", 쿼리함수 = 데이터를 가져오는 함수, 옵션);
  • 첫 번째 인수 : 쿼리명을 선언 -> 고유한 키값이라고 생각 (필수)
  • 두 번째 인수 : 데이터 함수를 선언 (필수)
  • 세 번째 인수 : (옵션) 이 구간에는 staleTime, cacheTime를 사용 가능

 

staleTime 란?

const { data, isError, isLoading } = useQuery("posts", fetchPosts, {staleTime: 3000})

if(isLoading) return <h3>로딩중...</h3>
  • 세번째 인수에 위와 같이 옵션을 주면 2초동안 fresh였다가 stale로 변한다.
  • 데이터가 만료되지 않으면 리페칭은 실행되지 않는다. 데이터가 만료된 경우에만 실행된다.
  • staleTime의 기본값은 왜 0ms이냐면, 데이터는 항상 만료 상태이므로 서버에서 다시 가져와야 한다고 가정한다는 뜻이다.
  • staleTime 윈도우가 다시 포커스될 때 같은 특정 트리거에서 쿼리 데이터를 다시 가져올지 결정한다.
    👉 트리거란? 데이터베이스가 미리 정해놓은 조건을 만족하거나 어떤 동작이 수행되면 자동적으로 수행되는 동작이다.

적용됐는지를 확인하기 위해서는 개발자도구에서 2초 뒤에 데이터가 패칭되는 것을 확인해볼 수 있다.

 

cacheTime ?

  • 캐시는 나중에 다시 필요할 수도 있는 데이터용이다.
  • cacheTime은 데이터가 비활성화된 이후 남아 있는 시간을 말한다.
    특정 쿼리에 대한 활성 useQuery가 없는 경우, 해당 데이터는 콜드 스토리지로 이동한다.
    👉 콜드 스토리지란? 남겨진 데이터를 말함
  • 구성된 cacheTime이 지나면 캐시의 데이터가 만료되며 유효 시간의 기본값은 5분이다.
  • 캐시가 만료되면 가비지 컬렉션이 실행되고 클라이언트는 데이터를 사용할 수 없다.
    데이터가 캐시에 있는 동안에는 페칭할 때 사용될 수 있다.
  • 서버의 최신 데이터로 새로 고침이 가능하다.

 

1. fetching : 요청 상태인 쿼리. 대기중

2. fresh : 새롭게 추가된 쿼리 인스턴스이며, 만료되지 않은 쿼리. 컴포넌트의 mount, update 시에 데이터를 재요청하지 않음(항상 캐시된 데이터를 가져옴)

3. stale : 데이터 패칭이 완료되어 만료된 쿼리. stale 상태의 같은 쿼리를 useQuery로 재호출하여 컴포넌트 마운트를 한다면 캐싱된 데이터가 반환됨.

4. inactive : 비활성 쿼리로써 사용하지 않음. 5분 뒤에 가비지 콜렉터가 캐시에서 제거함.

5. delete : 가비지 콜렉터에 의하여 캐시에서 제거된 쿼리.


 

const { data, isError, isLoading, error } = useQuery("posts", fetchPosts, {staleTime: 2000})

if(isLoading) return <h3>로딩중...</h3>
if(isError) return <h3>잘못된 데이터입니다. {error.toString()}</h3>

useQuery에서 사용하고 싶은 기능들을 구조 분해 할당하여 사용하면 된다.

  • data : 데이터를 가져옴
  • isError : 데이터를 가져올 때 오류가 있는지 여부를 알려준다.
  • isLoading : 데이터를 가져오는 중
  • isFetching : 비동기 쿼리가 해결되지 않았음을 의미
  • error : 에러 메시지를 보여준다.

이것 외에도 여러 기능들이 있으니 공식문서 확인하면 된다.

🔑 쿼리키

어떠한 트리거가 있어야만 데이터를 다시 가져오게 된다.
그 어떠한 트리거란 ? 👇

  • 컴포넌트를 다시 마운트하거나 윈도우를 다시 포커스할 때
  • useQuery에서 반환되어 수동으로 리페칭을 실행할 때
  • 지정된 간격으로 리페칭을 자동 실행할 때
  • 변이를 생성한 뒤 쿼리를 무효화 할 때
  • 클라이언트의 데이터가 서버의 데이터와 불일치할 때 리페칭 할 경우
  • 쿼리는 게시물 아이디를 포함하기 때문에 쿼리별로 캐시를 남길 수 있다.
  • 각 쿼리에 해당하는 캐시를 가지게 된다.
  • 각 게시물에 대한 쿼리에 라벨을 설정해주면 된다. 바로 쿼리 키에 문자열 대신 배열을 전달하면 가능하다.
 const { data, isLoading, isError, error } = useQuery(
    ["comments", post.id], () => fetchComments(post.id)
 );

이처럼 의존성 배열로 취급해서 사용하면 된다. comments를 쿼리키로 지정한 것이다.
그리고 post.id가 업데이트 되면 리액트쿼리가 새 쿼리를 만들고 staleTime과 cacheTime을 갖게 된다.


1. Prefetching

  • 데이터를 미리 가져와 캐시에 넣는다.
  • 기본값으로 만료 (stale) 상태를 나타낸다.
  • useQueryClient 를 사용한다.
import { useQuery, 🌟useQueryClient } from "react-query";

  const queryClient = useQueryClient();

  useEffect(() => {
    // 10페이지에 있다면 미리 데이터를 가져온 필요가 없다.
    if (currentPage < maxPostPage) {
      const nextPage = currentPage + 1;

      queryClient.prefetchQuery(["posts", nextPage], () =>
        fetchPosts(nextPage)
      );
    }
  }, [currentPage, queryClient]);

    const { data, isError, isLoading, error } = useQuery(
    // 배열에 담긴 첫 번째 요소를 쿼리키라고 한다.
    ["posts", currentPage],
    // 이 배열이 바뀌면 함수도 바뀌기 때문에 데이터가 바뀔 수밖에 없다.
    () => fetchPosts(currentPage), // -> 함수의 파라미터값을 currentPage로 함
    {
      staleTime: 2000,
      // 쿼리키가 바껴도 지난 데이터를 유지해서 이전 페이지로 돌아갔을 때 캐시에 해당 데이터가 있도록 해준다.
      keepPreviousData: true,
    }
  );

Dev tools를 확인해보면 "posts", 9가 "posts", 8보다 더 미리 패칭 된 것을 보아 미리 데이터를 fetch 된 것을 확인할 수 있다. 이것이 바로 프리패칭이라고 한다.


2. Mutations (변이)

  • 변이는 서버에 데이터를 업데이트 하도록 서버에 네트워크 호출을 실시한다.
  • 데이터를 추가, 삭제, 업데이트에 해당된다.
  • 서버를 실제로 업데이트 하지는 않는다 ! 변이를 생성하는 서버 호출을 전송하게 된다.
  • 값이 false일 경우 롤백 진행
  • 업데이트 된 해당 데이터로 리액트쿼리 캐시를 업데이트 하는 것이다.
  • 관련 쿼리를 무효화 할 수 있다. -> 서버에서 리페치를 개시하여 클라이언트에 있는 데이터를 서버의 데이터와 최신 상태로 유지한다.

적용 예시

 import { useQuery, 👉 useMutation, useQueryClient } from "react-query";

 const deleteMutation = useMutation((postId) => deletePost(postId));
 const updateMutation = useMutation((postId) => updatePost(postId));

 <button onClick={() => deleteMutation.mutate(post.id)}>삭제</button>
 <button onClick={() => updateMutation.mutate(post.id)}>업데이트</button>


또 다른 방법으로는 아래와 같은 방법으로 적용해볼 수 있다.

import { useMutation } from '@tanstack/react-query';

// 댓글 삭제
const { mutate } = useMutation((id: any) => deleteComment(id));

const handleCommentDelete = () => {
        confirm('댓글을 삭제하시겠습니까?');
       
        mutate(userId, {
            // 데이터 요청에 성공했을 경우
            onSuccess: () => {
                alert('삭제 완료되었습니다.');
            },
        });
    };

3. infinite scroll 무한스크롤

  • 사용자가 스크롤 할 때마다 새로운 데이터를 가져온다. (모든 데이터를 한 번에 가져오는 것보다 훨씬 효율적)
    ex) 인스타그램, 페이스북, 트위터같은 플랫폼을 예시로 들 수 있다.
  • useInfiniteQuery 라는 훅을 사용한다.
  • 업데이트된 상태가 쿼리 키를 업데이트하고 쿼리 키가 데이터를 업데이트 한다.

적용 예시

  • useInfiniteQuery 훅 사용 인터페이스
useInfiniteQuery(['쿼리명'], ({ pageParam = defaultUrl}) => 데이터함수(pageParam))

 

  • useInfiniteQuery훅의 인터페이스에서 data 객체는 두 개의 프로퍼티를 가지고 있다.
const { ⭐️ data, fetchNextPage, hasNextPage, isLoading, isError } = useInfiniteQuery(
        ['page'],
        ({ pageParam = 0 }) => getFeedPost({ page: pageParam, content: searchText, view: 5 }),
        {
            getNextPageParam: (lastPage, allPosts) => {
                return lastPage.page !== allPosts[0].totalPage ? lastPage.page + 1 : undefined;
            },
        },
    );

(1) pages: 데이터에 해당
(2) pageParams: 이것은 각 페이지의 쿼리 함수에 전달되는 매개변수이다.

  • 모든 쿼리는 페이지 배열에 고유한 요소를 가지고 있고 그 요소는 해당 쿼리에 대한 데이터에 해당한다.

ex) 피드 게시물 api 데이터(파라미터 값에 page랑 totalPage와 view가 있다.)

  • page : 페이지 수를 의미
  • totalPage : 총 페이지 수를 의미
  • view : 한 페이지에 몇개의 게시물을 보여줄 지

그래서 해당 데이터를 불러오기 위한 패칭 함수를 생성하고 page 파라미터를 리액트 쿼리에 pageParams로 적용하고, 리액트 쿼리에서 제공해주는 함수들로 제어하여 스크롤 위치가 페이지 하단에 맞닿았을 때마다 page가 +1이 되면서 다음 페이지의 데이터들이 스크롤 할 때마다 새롭게 불러와져 보여지는 것을 확인할 수 있다.

그리고 스크롤 위치가 페이지 하단에 맞닿았을 때마다 직접 코드를 구현해도 좋지만 'react-infinite-scroller' 라이브러리를 설치하여 정말 간편하게 구현가능하다. (이 라이브러리에 쓰로틀링도 내재되어 있다.)

쓰로틀링이란? 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것.
쓰로틀링은 스크롤을 올리거나 내릴 때 보통 사용.

// 설치 명령어
npm install react-infinite-scroller

 

해당 라이브러리 사용 설명서 참고
https://www.npmjs.com/package/react-infinite-scroller

 

 


현재 react-query에서 tanstack/react-query로 변경됨에 따라 아래와 같이 코드 쓰는 방식이 변경되었다.

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

function Home() {
  const { isLoading, error, data, isFetching } = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
    /* fetch의 경우:
       const response = await fetch(
        'https://api.github.com/repos/tannerlinsley/react-query'
      );
      const data = await response.json();
    */
    /*axios 사용*/
      const response = await axios.get(
        'https://api.github.com/repos/tannerlinsley/react-query'
      );
      const data: any = response;
      console.log(data);
      return data;
    },
  });

  if (isLoading) return <div>로딩중</div>;
  if (error) return <div>에러남</div>;

  return (
    <div>
      <div>{data?.data.name}</div>
      <div>{isFetching ? 'Updating...' : ''}</div>
    </div>
  );
}

export default Home;

 

아래 사이트로 참고하여 코드 작성)

 

[react-query]@tanstack/react-query를 사용해보자

😯@tanstack/react-query를 사용하는 이유 리액트쿼리는 강력한 데이터페칭도구입니다. 비동기 상태 관리를 도와주고 로딩, 에러처리, 리페칭, 캐싱 등 개발자가 생각하기 귀찮은 문제들을 대신 해결

xionwcfm.tistory.com

 

 

Quick Start | TanStack Query Docs

This code snippet very briefly illustrates the 3 core concepts of React Query: If you're looking for a fully functioning example, please have a look at our simple codesandbox example tsx import { useQuery, useMutation, useQueryClient, QueryClient, QueryCli

tanstack.com

 

 

참고사이트)

 

[React-Query] React-Query 개념잡기

React-Query

velog.io

 

 

React Query 강좌 1편. useQuery 사용법 기초

React Query에서 기초적인 useQuery 사용법

mycodings.fly.dev

 

 

[react-query] fresh 상태와 stale 상태의 차이

react-query 에서 데이터의 상태에 관하여 이전에 문서에 적혀있는 데이터가 리페칭 되는 경우(refetchOnWindowFocus)를 보자 Stale queries are refetched automatically in the background when: 쿼리 마운트의 새 인스턴스

seungddak.tistory.com