게으른개발너D

React Query 개념 본문

개발/ReactJS

React Query 개념

lazyhysong 2023. 8. 30. 22:53

 

개발을 하다가 API를 불러오는데 복잡한 로직이 필요없어 React Query를 쓰지 않고 Async/ Await를 통해 api를 불러오고 있었다.

하지만 가장 눈에 거슬리는 것은 리스트 item들을 API로 불러온 후 상세보기 화면으로 진입, 그 후에 뒤로가기를 하면 다시 API를 호출하여 loading 아이콘이 빙글빙글 돌고 있는 것이다. 

날짜별로 localStorage에 저장한 후 다시 불러와 지난 날짜 data들은 삭제할까 싶다가 그냥 React Query를 사용하기로 했다. 역시 caching 기능을 무시할 수 없다.ㅠㅠ

 

 

react query는 fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리이다.

즉, React 환경에서 비동기 query 과정을 도와주는 라이브러리이다.

 

✨ React Query 사용 이유

카카오페이 프론트엔드 개발자들이 React-Query를 선택한 이유

  1. React Query는 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리이다.
  2. 복잡하고 장황한 코드가 필요한 다른 데이터 불러오기 방식과 달리 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있다.
  3. React Query에서 제공하는 캐싱, Window Focus Refetching 등 다양한 기능을 활용하여 API 요청과 관련된 번작한 작업 없이 '핵심 로직'에 집중할 수 있다.

React Query 장점

  1. 캐싱
  2. get을 한 데이터를 update를 하면 자동으로 get을 다시 수행한다.
  3. 데이터가 오래 되었다고 판단되면 다시 get (invalidateQueries)
  4. 동일 데이터 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절 가능)
  5. 무한 스크롤 (Infinite Queries)
  6. 비동기 과정을 선언적으로 관리할 수 있다.
  7. react hook과 구조가 비슷하다.

 

✨ React Query 설치

npm i react-query

먼저 react 프로젝트에 react-query를 설치한다.

 

index.js에 react-query를 세팅한다.

// src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from 'react-query';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

const client = new QueryClient();

root.render(
  <React.StrictMode>
    <QueryClientProvider client={client}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

 

 

✨ React Query Hooks & 사용하기

1. useQuery

api 데이터를 get 하기 위한 hook이다.

 

useQuery("unique Key", api 호출 함수, {etc..});

  • 첫번째 파라미터:  unique Key, 두번째 파라미터: 비동기 함수(api호출 함수)
  • 첫번째 파라미터로 설정한 unique Key는 다른 컴포넌트에서도 해당 키를 사용하면 호출 가능하다. unique Key는 string과 배열을 받는다. 배열인 경우, 0번 값은 다른 컴포넌트에서 부를 string 값이며, 1번 값은 query 함수 내부에 파라미터로 해당 값이 전달된다.
  • 해당 hook의 return 값은 api의 성공과 실패여부, api return 값 등이다.
  • useQuery는 비동기로 작동한다. (한 컴포넌트에 여러개의 useQuery가 있다면 두개의 useQuery가 동시에 실행됩니다. ) 여러개의 비동기 query가 useQueries를 사용하면 된다.
  • enabled를 사용하면 useQuery를 동기적으로 사용 가능하다.
const { data: books, isLoading, isError, error } = useQuery<IRequestList>({
  queryKey: 'bookDetail',
  queryFn: () => fetchBookDetailByIsbn(isbn),
});

아래처럼 바꿀 수도 있다.

const { data: books, isLoading, isError, error } = useQuery<IRequestList>(
  'bookDetail', () => fetchBookDetailByIsbn(isbn), { 
    retry: 0, // 호출 실패시 재호출 횟수
    onSuccess: data => {
      // 호출 성공시
    },
    onError: e => {
      // 호출 실패시
    },
  });
  
  
 return (<>
   {isError ? 
     <span>api 호출에 실패! {error.message}</span> : 
   isLoading ?
     <Loader /> : 
     {books?.map(book => <div>{book.title}</div>)}
 </>);

isLoading, isSuccess 대신 status로 사용가능하다.

const { data: books, status, error } = useQuery<IRequestList>(
  'bookDetail', () => fetchBookDetailByIsbn(isbn), { 
    retry: 0, // 호출 실패시 재호출 횟수
    onSuccess: data => {
      // 호출 성공시
    },
    onError: e => {
      // 호출 실패시
    },
  });
  
  
 return (<>
   {status === 'error' ? 
     <span>api 호출에 실패! {error.message}</span> : 
   status === 'loading' ?
     <Loader /> : 
     {books?.map(book => <div>{book.title}</div>)}
 </>);

 

2. useQuery 동기적 실행

enabled 옵션을 사용하면 useQuery를 동기적으로 사용 가능하다.

3번째 파라미터 옵션값으로 들어가는데, 그 옵션의 enabled에 boolean인 어떠한 값을 넣고, 그 값이 true일 때 useQuery를 실행한다.

const { data: books, isFetching } = useQuery("bookList", fetchBookList);
const { data: nextBooks, isFetching } = useQuery(
  "nextBookList",
  fetchNextBookList,
  {
    enabled: !!books // true가 되면 fetchNextBookList를 실행한다
  }
);

 

3. useQueries

useQuery를 여러개 실행하는 경우를 보자.

const bookList = useQuery("bookList", fetchBookList);
const musicList = useQuery("musicList", fetchMusicList);
const authorList = useQuery("authorList", fetchAuthorList);

세 변수에 대한 로딩, 성공과 실패 처리를 각각 해줘야한다.

이걸 해결하기 위해 Promise.all 처럼 useQuery를 하나로 묶을 수 있는 hook이 useQueries이다.

Promise.all처럼 배열에 각 쿼리의 상태 값이 객체로 들어온다.

const result = useQueries([
  {
    queryKey: 'bookList',
    queryFn: fetchBookList
  }, {
    queryKey: 'musicList',
    queryFn: fetchMusicList
  }, {
    queryKey: 'authorList',
    queryFn: fetchAuthorList
  }
]);

useEffect(() => {
	console.log(result); 
    // [
    //   {bookList 정보, data: [], isSucces: true ...}, 
    //   {musicList 정보, data: [], isSucces: true ...}, 
    //   {authorList 정보, data: [], isSucces: true ...}
    // ]
  const loadingFinishAll = result.some(result => result.isLoading);
  console.log(loadingFinishAll); // false이면 최종 완료
}, [result]);

 

 

4. unique key 활용하기

useQuery의 첫번째 인자인 unique key를 배열로 넣으면 query 함수 내부에서 변수로 사용 가능하다.

const result = useQueries([
  {
    queryKey: ["bookList", "Korea"],
    queryFn: params => {
      console.log(params); 
      // {queryKey: ['bookList', 'Korea'], pageParam: undefined, meta: undefined}

      return fetchBookList;
    }
  },
  {
    queryKey: "musicList",
    queryFn: fetchMusicList
  }
]);

 

 

5. QueryCache

쿼리의 성공, 실패 전처리를 할 수 있다.

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      console.log(error, query);
      if (query.state.data !== undefined) {
        toast.error(`🤔: ${error.message}`);
      },
    },
    onSuccess: data => {
      console.log(data)
    }
  })
});

 

 

6. useMutation

post, update와 같이 값을 바꿀 때 사용하는 hook이다. return 값은 useQuery와 동일하다.

import { useState, useContext, useEffect } from "react";
import loginApi from "api";
import { useMutation } from "react-query";

function Home() {
  const [id, setId] = useState("");
  const [password, setPassword] = useState("");

  const loginMutation = useMutation(loginApi, {
    onMutate: variable => {
      console.log("onMutate", variable);
      // variable : {loginId: 'xxx', password; 'xxx'}
    },
    onError: (error, variable, context) => {
      // error
    },
    onSuccess: (data, variables, context) => {
      console.log("success", data, variables, context);
    },
    onSettled: () => {
      console.log("end");
    }
  });

  const handleSubmit = () => {
    loginMutation.mutate({ loginId: id, password });
  };

  return (
    <div>
      {loginMutation.isSuccess ? "success" : "pending"}
      {loginMutation.isError ? "error" : "pending"}
      <input type="text" value={id} onChange={e => setId(e.target.value)} />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      <button onClick={handleSubmit}>로그인</button>
    </div>
  );
};

export default Home;

 

 

7. update 후 get 실행

react-query의 장점으로 update 후에 get 함수를 간단히 재실행 할 수 있다.

mutation 함수가 성공할 때, unique key로 맵핑된 get 함수를 invalidateQueries에 넣으면 된다.

 

const mutation = useMutation(postComment, {
  onSuccess: () => {
    // postComment가 성공하면 comments로 맵핑된 useQuery api 함수를 실행한다.
    queryClient.invalidateQueries("comments");
  }
});

 

만약 mutation에서 return 된 값을 이요하여 get 함수의 파라미터를 변경해야할 경우 setQueryData를 사용한다.

const queryClient = useQueryClient();

const mutation = useMutation(editComment, {
  onSuccess: data => {
    // data가 fetchCommentsById로 들어간다
    queryClient.setQueryData(["comments", { id: 3 }], data);
  }
});

const { status, data, error } = useQuery(["todo", { id: 3 }], fetchCommentsById);

mutation.mutate({
  id: 3,
  name: "lazyshy"
});

 

 

8. react Suspense

react-query는 비동기를 좀 더 선언적으로 사용할 수 있다.

Suspense를 사용하며 loading을, Error boundary를 사용하여 에러 핸들링을 더욱 직관적으로 할 수 있다.

 

Suspense를 사용하기 위해 QueryClient에 옵션 하나를 추가한다. (Global하게 Suspense 사용하기)

// src/index.tsx
const client = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0,
      suspense: true
    }
  }
});

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={client}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

 

함수마다 suspense를 사용하는 방법은 아래와 같다.

const { data } = useQurey("test", testApi, { suspense: true });

이렇게 suspense 세팅을 완료 후 react에서 제공하는 Suspense를 사용한다.

const { data } = useQurey("test", testApi, { suspense: true });

return (
  // isLoading이 true이면 Suspense의 fallback 내부 컴포넌트가 보인다.
  // isError가 true이면 ErrorBoundary의 fallback 내부 컴포넌트가 보인다.
  <Suspense fallback={<div>loading</div>}>
    <ErrorBoundary fallback={<div>에러 발생</div>}>
      <div>{data}</div>
    </ErrorBoundary>
  </Supense>
);

 

 

 

✨ 캐싱 (Caching)

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

 

react-query는 캐싱을 통해 동일한 데이터의 반복적인 비동기 호출을 방지한다.

이는 불필요한 API 콜을 줄여 서버의 부하를 줄여준다.

 

react-query에서는 최신 데이터를 fresh, 기존 데이터를 stale이라 한다.

 

데이터를 갱신하는 상황은 다음과 같다.

1. 화면을 보고 있을 때

2. 페이지 전환이 일어났을 때

3. 페이지 전환 없이 이벤트가 발생해 데이터를 요청할 때

 

이를 위해 react-query에서는 다음과 같은 옵션을 제공한다.

이를 통해 react-query가 어떤 시점에 데이터를 refereshing 하는지 알 수 있다.

refetchOnWindowFocus, //default: true
refetchOnMount, //default: true
refetchOnReconnect, //default: true
staleTime, //default: 0
cacheTime, //default: 5분 (60 * 5 * 1000)

1. refetchOnWindowFocus: 브라우저에 포커스가 들어온 경우

2. refetchOnMount: 새로운 컴포넌트 마운트가 발생한 경우

3. refetchOnReconnect: 네트워크 재연결이 발생한 경우

4. staleTime: 데이터가 frech -> stale 상태로 변경되는데 걸리는 시간

0으로 설정되어있지 않는다면 refresh 트리거가 발생했을 때 refetch가 발생한다.

5. cacheTime: 더이터가 inactive한 상태일 때 캐싱된 상태로 남아있는 시간

특정 컴포넌트가 unmount(페이지 전환 등으로 화면에서 사라질 때) 되면 사용된 데이터는 inactive 상태로 바뀌고, 이때 데이커는 chcheTime만큼 유지된다.

cacheTime 전에 해당 데이터를 가져오는 컴포넌트가 mount되면, 새로운 데이터를 fetch해오는 동안 캐싱된 데이터를 보여준다.

cacheTime 이후엔 가비지 콜렉터로 수집된다.

 

 

✨ 무한 스크롤 구현

SWR과 React-Query 모두 무한 스크롤을 구현하는 데 필요한 기능들을 제공한다.

SWR로 무한 스크롤을 구현하려면 유저가 부가적인 코드를 작성해야하는 반면, React-Query에는 getPreviousPageParam, fetchPreviousPage, hasPreviousPage 와 같은 다양한 페이지 관련 기능이 존재해 이를 이용해 무한 스크롤을 쉽게 구현할 수 있다!

 

 

✨ Selectors

React-Query에서는 select 키워드를 사용해 raw data로부터 원하는 데이터를 추출하여 반환할 수 있다.

import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery('user', fetchUser, {
    select: user => user.username,
  })
  return <div>Username: {data}</div>
}

select를 통해 원하는 데이터에 접근한 후 추출하는 것이 가능하다.

 

 

 


 

출처 : 노경환님 블로그

Comments