React-Query 알아보기

해준박·2023년 11월 6일
0
post-custom-banner

React를 위한 강력하고 성능 좋은 데이터 동기화
"전역 상태"를 건드리지 않고도 React 및 React Native 애플리케이션에서 데이터를 가져오고, 캐시하고, 업데이트할 수 있습니다.

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

한마디로 서버에서 받아오는 데이터를 유연하게 처리 할 수 있게 해주는 라이브러리다.

서버에서 데이터를 받아와 fetch 후 데이터를 캐싱하고 유지해 반복적인 서버요청을 줄일 수 있으며, 동일한 데이터 요청에 대해서 빠른 응답을 제공할 수 있다.

cache

React Query는 설정한 cacheTime만큼(기본값은 5분) 데이터를 메모리에 저장해 놓습니다. 좀 더 자세히 설명하자면 React Query에서의 cache는 QueryCache 객체의 queries 배열과 queriesInMap 객체에 Query 객체가 존재하는 것을 말합니다. 중요한 것은 cache가 존재한다고 해서 data refetching을 하지 않는 것이 아닙니다. cache가 존재하더라도 해당 데이터(Query 객체)가 stale 상태라면 refetching을 수행하게 됩니다.

캐싱의 역할은 데이터를 임시로 저장해 더 빠르게 액세스 할 수 있도록 하는 기술이다

서버에서 데이터를 캐싱후 일정시간(cacheTime)동안 메모리에 저장한다. 서버의 데이터를 갱신 할 때 캐싱된 데이터와 비교 후 데이터가 변경되었음을 감지할 경우 새로운 데이터를 가져와 캐시에 저장한다. 이를 통해 클라이언트는 항상 최신의 데이터를 볼 수 있게함

stale

React Query에는 cache와는 별도로 stale이라는 개념이 있습니다. 설정한 staleTime만큼(기본값은 0초) 데이터가 fresh 상태로 존재하였다가 그 후에는 stale 상태로 바뀝니다. fresh와 stale 단어의 대비에서 알 수 있듯이 fresh는 데이터를 그대로 사용해도 좋을 만큼 신선한 상태라는 뜻이며, stale은 데이터를 새롭게 fetch해 오는 것이 필요할 만큼 신선하지 못한 상태라는 뜻입니다.

예를들어 staleTime을 10,000으로 설정하였다면, data fetching이 성공한 후 10초(= 10,000ms) 동안 fresh 상태로 존재하다가 10초 이후에는 stale 상태가 됩니다. stale 상태가 된 후 특정 조건이 충족되면 refetching이 일어나게됩니다. 주의할 점은 stale 상태가 되었다고 해서 refetching이 곧바로 일어나는 것이 아니라, stale 상태가 되고 특정조건을 만족해야 refetching이 일어난다는 것입니다

클라이언트에 첫 캐싱된 데이터를 보여줄 때, 일반적으로 stale 상태이다. 하지만 stale 뜻 그대로 "탁한" 뭐 좋지않은 의미를 뜻해서 바로 refetching이 일어나지 않고 특정조건이 있을 경우 refetching이 일어난다.

useQuery를 이용후 특정 조건을 설정할수있는 코드

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

++
fresh 상태일 때는 Refetch 트리거(위의 3가지 경우)가 발생해도 Refetch가 일어나지 않는다!
기본값이 0이므로 따로 설정해주지 않는다면 Refetch 트리거가 발생했을 때 무조건 Refetch가 발생한다!

  • 브라우저에 포커스가 들어온 경우(refetchOnWindowFocus)
  • 새로운 컴포넌트 마운트가 발생한 경우(refetchOnMount)
  • 네트워크 재연결이 발생한 경우(refetchOnReconnect)

stale, fresh, cache의 필요성

블로그의 그림으로 아주 잘 설명 되어있었다. 정해진 staleTime으로 fresh의 지속 상태를 정할 수 있으며, 데이터의 상태를 알 수 있다

stale과 cache로 관리할 경우

cache만으로 관리할 경우

차이점은 ⓒ입니다. 현재처럼 stale과 cache로 관리하게 된다면, ⓒ에서 stale 상태의 캐싱된 데이터를 이용한 UI를 먼저 보여준 후, refetching을 완료하면 새로운 데이터를 이용한 UI로 교체해줄 수 있습니다. refetching 이전과 이후의 데이터가 많이 다르지 않다면, 사용자는 페이지 이동 후 더욱 빠르게 서비스를 이용할 수 있게됩니다.

반면 cache만으로 관리하게 된다면 이와는 달라집니다. 위 그래프에서 페이지 이동 직전에 캐시가 만료되었다고 가정해보겠습니다. 캐시가 만료되어 삭제되었을테니, ⓒ에서 refetching이 완료되기 전까지 데이터가 없으므로 Loading 페이지를 보여줘야합니다. 그리고 refetching이 완료되면 그제야 데이터를 이용한 UI를 보여줄 수 있습니다. 이렇게 되면 사용자는 페이지 이동 후 더욱 느리게 서비스를 이용하게 됩니다.

이러한 이유로 인해 React Query는 stale과 cache를 따로 분리하였고 UX를 높일 수 있습니다. 그러면 이제 본격적으로 stale과 cache가 React Query 내부에서 동작하는지 설명하겠습니다.

요약하자면, UIUX부분과 사용자 경험의 향상을 위해 stale기능이 필요하다. 사용자에게 새로운 데이터를 로딩중이라고 알린후, 새롭게 캐싱된 데이터를 보여주고, 이 캐싱된 데이터도 곧 stale 상태이며 언제든지 특정 조건에 따라 refetching 될 수 있다.

사실 stale을 응용해서 내 프로젝트에 사용할까 싶다. 딱히 없긴 하겠지만 staleTime을 이용해 불필요한 refetching을 막을수는 있을듯..

주요기능

useQuery

  • 첫 번째 파라미터로 unique key를 포함한 배열이 들어간다. 이후 동일한 쿼리를 불러올 때 유용하게 사용된다.

  • 첫 번째 파라미터에 들어가는 배열의 첫 요소는 unique key로 사용되고, 두 번째 요소부터는 query 함수 내부의 파라미터로 값들이 전달된다.

  • 두 번째 파라미터로 실제 호출하고자 하는 비동기 함수가 들어간다. 이때 함수는 Promise를 반환하는 형태여야 한다.

  • 최종 반환 값은 API의 성공, 실패 여부, 반환값을 포함한 객체이다.

// api
import axios from "axios";
import { sortProducts } from "../controller/sort";
import { IProduct } from "./productType";

const getProducts = async () => {
  // const res = await axios.get(`https://ikw-market.shop/api/product`);
  const res = await axios.get(`${process.env.REACT_APP_EXPRESS_URL}/api/product`);

  const data: IProduct[] = sortProducts(res.data.products.reverse());

  return data;
};

export { getProducts };
// productList.tsx
const { isLoading, data } = useQuery(["Products"], getProducts);

진행중인 프로젝트에 테스트 했다 정석적인 방법이 있는것 같았는데,
promise를 반환하는 getProducts만 있으면 유용하게 사용할 수 있을것 같아서 위 코드처럼 작성했다.
필요에 따라 사용해야 할 여러가지 옵션들이 있으니 잘 보고 사용하면 될듯

data: undefined;         // 현재 쿼리 결과 데이터
error: null;             // 현재 쿼리 오류 정보
isError: false;          // 현재 쿼리가 오류 상태인지 여부
isLoading: false;        // 현재 쿼리가 로딩 중인지 여부
isLoadingError: false;   // 현재 쿼리 로딩 중에 오류가 발생한지 여부
isRefetchError: false;   // 현재 쿼리 다시 가져오기 중에 오류가 발생한지 여부
isSuccess: false;        // 현재 쿼리가 성공한 상태인지 여부

useMutation

useQuery를 PUT, UPDATE, DELETE 와 같이 값을 변경할 때 사용하는 API다. 반환값은 useQuery와 동일하다.

어떤 상황일 때 쓰고, 왜 쓰는지 몰라서 찾아보니 좋은 예시가 있었다.

Mutation은 변경 내용을 사용자에게 보여주거나 진행된 변경 내용을 등록하여 사용자가 볼 수 있게 하는 방법들이 있다

하나는 Optimistic UI이다
Optimistic UI는 서버 호출에서 모든 내용이 잘 진행될 것으로 간주하는 것이다
그리고 사실이 아닌 것으로 판명될 경우 롤백을 진행한다
예를들어 페이스북의 좋아요 기능을 보자
사용자가 좋아요를 누르면 서버에 좋아요 데이터가 업데이트 되고 완료된다면 버튼 색이 변할 것이다
하지만 Optimistic UI를 이용하면 좋아요를 누르면 서버 전송이 잘 될 것이라고 간주하여 즉시 버튼 색을 변화시킨다. 만약 좋아요 기능에 에러가 났다면 다시 롤백이 될 것이다

또 다른 방법은 Mutation 호출을 하면 서버에서 받는 데이터를 취하고 업데이트된 해당 데이터로 React Query 캐시를 업데이트 하는 것이다

마지막으로 관련 쿼리를 무효화(invalidation)할 수 있다
무효화하는 경우 서버에서 리페치를 개시하여 클라이언트에 있는 데이터를 서버의 데이터와 최신상태로 유지하게 된다

import { useMutation } from 'react-query';

// 비동기 함수 (예: 데이터 수정 또는 업데이트)
const updateUserProfile = async (newUserData) => {
  try {
    const response = await fetch('/api/updateUserProfile', {
      method: 'POST',
      body: JSON.stringify(newUserData),
      headers: {
        'Content-Type': 'application/json',
      },
    });

    if (!response.ok) {
      throw new Error('Failed to update user profile');
    }

    return response.json();
  } catch (error) {
    throw new Error(error.message);
  }
};

// useMutation 훅을 사용하여 데이터 수정 함수를 래핑
const { mutate, isLoading, isError, isSuccess } = useMutation(updateUserProfile);

// 컴포넌트 내에서 이벤트 핸들러 또는 함수에서 데이터 수정 요청 보내기
const handleUpdateProfile = (newUserData) => {
  mutate(newUserData);
};

위 처럼 비동기 수정함수를 래핑해서 사용해도 되고,

아래 코드 처럼 mutate메소드를 이용해서 함수로 전달하는 방법이 있다

const deleteMutation = useMutation((postId: number) => deletePost(postId));

.
.
.

<button onClick={() => deleteMutation.mutate(post.id)}>Delete</button>

제공 되는 옵션은 isFetching을 제외한 많은옵션이 있다. 상태에 따라 UI적으로 변경할 때 유용하다.

react-query 자체가 효율적인 데이터 관리도 있겠지만,

Mutaion을 활용함으로써 데이터의 RUD의 작동이 서버로부터 응답과 요청이 제대로 되었는지의 상태를 사용자에게 알려 줄 수 있다.

// useMutation
const {
  data,
  error,
  isError,
  isIdle,
  isLoading,
  isPaused,
  isSuccess,
  mutate,
  mutateAsync,
  reset,
  status,
} = useMutation(mutationFn, {
  cacheTime,
  mutationKey,
  networkMode,
  onError,
  onMutate,
  onSettled,
  onSuccess,
  retry,
  retryDelay,
  useErrorBoundary,
  meta
})

// mutate함수
mutate(variables, {
  onError,
  onSettled,
  onSuccess,
})

일단 리액트쿼리를 활용하면

  • api를 요청해서 얻는 데이터를 따로 관리 할 수 있다
  • 자동으로 fetching이 된다
  • fetch후 데이터가 업데이트 되면 캐싱을 갱신하여 자동으로 컴포넌트를 리렌더링해준다
  • 코드 유지 보수가 쉬워진다
  • 사용자 경험 개선

이 정도? 아직 프로젝트에 적용해보면서 큰 장점을 느껴지지 않았다. 유지보수가 젤 좋은거 같음...
내가 활용을 잘 못해서 그런거겠지만..

참고사이트
[React-Query] React-Query 개념잡기
[React Query] useQuery 동작원리

profile
기록하기
post-custom-banner

0개의 댓글