React Query

박민형·2023년 2월 20일
2
post-thumbnail

📌 React Query를 알아본 이유

  • 최종 프로젝트 때 React Query를 사용함에 있어서 도움이 될 것 같아서
  • React Query를 이전에도 사용해봤지만 개념적이나 경험적으로 많이 부족하다고 생각이 들었음

📌 React Query

  • 공식문서에 따르면 데이터의 fetching, caching, synchronizing, updating server state가 가능한 라이브러리

📌 React Query를 사용하는 이유

server state

  • client state는 Redux, Recoil 등에서 관리
  • Client에서 제어하거나 소유되지 않은 원격의 공간에서 관리되고 유지됨
  • Fetching이나 Updating에 비동기 API가 필요함
  • 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 업데이트 될 수 있음
  • 앱에서 사용하는 데이터가 유효기간이 지난 상태가 될 가능성을 가짐

기능의 특성에 의한 필요성

  • 게시글 목록, 상품목록 등은 지속적인 업데이트를 통해 최신상태를 유지해야 하는 특성이 있고 그로인해 고려해야 할 부분들이 생긴다.
  • 캐싱
  • 서버 데이터 중복 호출 제거
  • 만료된 데이터를 백그라운드에서 제거
  • 데이터의 만료시점 인지
  • 만료된 데이터의 업데이트
  • 위에 작성한 것들을 client에서 관리하는 것이 적합하지 않다는 생각에서 React-Query의 필요성 느낌

우아한 형제들이 React Query를 선택한 이유

  • Client Store(Redux, Recoil) 등이 비동기 데이터를 관리하려고 사용되는데 너무 비대해지는 것 같아 그 본질에 있어 의문
  • 100개 이상의 데이터를 패칭하는 것과 받은 데이터를 조작과 가공까지 store에서 하는 문제점
  • store는 상태 관리를 하는거냐 API 데이터 관리를하는 거냐?
  • React Query가 server state를 관리함에 있어 유용하다고 해서 그냥 도입하는것이 아니라 기존에 사용되고 있는 기술에 있어 의문점을 품고 단점을 보완하기 위해 도입
  • Unique Key를 통해 다른곳에서도 해당 query의 결과를 꺼내올 수 있음

📌 React Query 설치 및 설정

 $ npm i react-query
 $ yarn add react-query
 
 <script src="https://unpkg.com/react-query/dist/react-query.production.min.js"></script>
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from "react-query/devtools";

const client = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <QueryClientProvider client={client}>
    <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
    <App />
  </QueryClientProvider>
);
  • 캐시를 사용하기 위해 QueryClient 인스턴스 사용 및 client에 접근이 가능하도록 QueryClientProvider 설정
  • ReactQueryDevtools는 말 그대로 React query 관련 작업 진행 상황을 눈으로 볼 수 있도록 하는 도구

📌 React Query 기본 개념

React Query LifeCycle

  • fetching: 현재 요청중인 쿼리
  • fresh: stale 상태가 아닌 쿼리, 컴포넌트가 마운트 및 업데이트되도 데이터를 다시 요청하지 않음
  • stale: fresh 상태가 아닌 상태로서 만료된 쿼리, 컴포넌트가 업데이트 되면 데이터를 다시 요청
  • inactive: 사용하지 않는 쿼리로써 일정 시간이 지나면 가비지 컬렉터를 통해 캐시에서 제거
  • delete: 캐시에서 제거된 쿼리

React Query 핵심 개념

  • Queries: Unique Key를 가지며 GET, POST 메서드와 관련
  • Mutations: C, U, D 등 서버 데이터의 수정과 관련된 기능
  • Query Invalidation: 캐싱된 쿼리 데이터가 유효한지 여부를 판단

알아두면 도움이 되는 React Query의 기본 설정

  • useQuery로 가져온 데이터는 기본적으로 stale한 상태(staleTime: 0)
  • stale한 상태의 쿼리가 데이터를 다시 요청하는 경우
    • 새로운 쿼리 인스턴스가 마운트되었을 때
    • 브라우저 윈도우가 다시 포커스되었을 때
    • 네트워크가 다시 연결되었을 때
    • refetchInterval 옵션이 있을 때
  • inactive한 상태의 쿼리는 300초 뒤에 메모리에서 해제(cacheTime: 5분)
  • 백그라운드에서 3회 이상 실패한 쿼리는 에러처리 된다. (retry 옵션으로 재시도 횟수, retryDelay 옵션으로 재시도 대기시간 설정)
  • 쿼리결과는 memoization 을 위해 structural sharing(구조상 데이터들을 공유, 원본 유지)을 사용. 데이터 레퍼런스는 불변.

📌 useQuery

useQuery의 필요성

import axios from 'axios';
import { useEffect, useState } from 'react';

interface IData {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const ReactQuery = () => {
  const [data, setData] = useState<IData>();

  const fetchData = async () => {
    axios.get('https://jsonplaceholder.typicode.com/posts').then((res: any) => {
      setData(res.data);
    });
  };

  useEffect(() => {
    fetchData();
  }, []);

  return (
    <>
      <div>{data?.userId}</div>
      <div>{data?.id}</div>
      <div>{data?.title}</div>
      <div>{data?.body}</div>
    </>
  );
};

export default ReactQuery;
// useQuery 사용
import axios from 'axios';
import { useQuery } from 'react-query';

interface IData {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const ReactQuery = () => {
  const fetchData = () => {
    return axios.get('https://jsonplaceholder.typicode.com/posts');
  };

  const { isLoading: loading, data } = useQuery<IData>(['fetch-data'], fetchData);

  if (loading) return;

  return (
    <>
      <div>{data?.userId}</div>
      <div>{data?.id}</div>
      <div>{data?.title}</div>
      <div>{data?.body}</div>
    </>
  );
};

export default ReactQuery;

useQuery의 주요 개념

  • queryKey: 고유 키값으로 문자열의 배열로 할당, 데이터 캐싱에 참조되며, key가 같은 쿼리들은 하나의 요청으로 값을 공유한다.
  • queryFn : 데이터 패치함수로써 fetcher 즉 Promise 처리하는 함수를 할당. fetch나 axios를 통한 request 함수가 적용된다.
  • options : 필수가 아니며, 여러 옵션 지정 가능
  • 반환값
    • data: 요청 성공시 받는 데이터
    • error: 요청 실패시 받는 에러 정보
    • refetch: 수동으로 데이터를 refetch하는 함수(stale 및 cache 설정을 무시)
    • status: idle(초기상태) / loading(fetch 중) / error(fetch 실패) / success(fetch 성공)
    • status의 Boolean 값: isLoading(API 재호출 + 캐시저장), isFetching(API 재호출), isIdle, isSuccess, isError, isStale

📌 useQuery 옵션

stale Time 및 cache Time

  const { isLoading: loading, data } = 
        useQuery<IData>(['fetch-data'], 
                        fetchData, 
                        {
                          staleTime: 3000, 
                          cacheTime: 3000
                        });
  • StaleTIme
    • fresh 상태일 때 쿼리 인스턴스가 새롭게 mount 되어도 fetch 미발생. 또한, unmount 후 mount 되도 fetch 미발생
    • 데이터가 fresh한 상태이어야 캐싱 데이터를 활용할 수 있다.
  • cacheTime
    • 데이터가 inactive(쿼리 unmount 이후) 상태일 때 캐싱된 상태로 남아있는 시간. 기본값은 5분.
    • cacheTime이 지나면 가비지 콜렉터로 수집된다. 브라우저의 캐시 삭제와 관련이 있음.
    • cacheTime이 지나기 전에 쿼리 인스턴스가 mount 되면, 데이터를 fetch하는 동안 캐시 데이터를 보여준다.
  • staleTime을 설정하지 않으면 데이터는 캐싱되나, 항상 stale 상태를 가지기에 refetch가 계속해서 발생하게 된다. 또한, cacheTime이 staleTime보다 짧다면 fresh 상태동안 캐싱이 되지 않을 것이므로 두 값 모두를 적절히 설정

쿼리를 주기적으로 최신화: refetchInterval

  const { isLoading: loading, data } = 
        useQuery<IData>(['fetch-data'], 
                        fetchData, 
                        {
                          refetchInterval: 2000, // refetch 주기
      					  refetchIntervalInBackground: true, // 윈도우 focus 아니어도 refetch
                        });

쿼리를 필요할 때 최신화: refetch

  const { isLoading: loading, data, refetch } = 
        useQuery<IData>(['fetch-data'], 
                        fetchData, 
                        {
          				  // 데이터 자동패치 여부(default: true)
                          // flase로 설정하면 stale 상태에 자동 패칭 비 활성화
          			      enable: false;
                        });

<button onClick={refetch}>refresh</button>

성공 및 에러 처리: onSuccess, onError

  const { isLoading: loading, data } = 
        useQuery<IData>(['fetch-data'], 
                        fetchData, 
                        {
          				  onSuccess: sucessData // 성공 시 호출 되는 callback 함수
                          onError: errorData // 에러 발생 시 호출 되는 callback 함수
                        });

필요한 데이터만 가져오고 싶다: select

  const { isLoading: loading, data } = 
        useQuery<IData>(['fetch-data'], 
                        fetchData, 
                        {
          				  // select() - Data를 변형시키는 옵션 메서드
      					  select: (res) => {
                          return res.data.map((item) => item.title);
                        });

📌 useMutation

  • useQuery는 데이터를 조회하는 용도라면 useMutation은 서버에 데이터 변경 작업을 가능하게하는 Hook(Creat, Update, Delete)

mutationFn

  • axios 또는 fetch를 통해 서버에 API 요청을 하기위한 함수
// 1
const saveData = useMutation((data: any) => axios.post('https://jsonplaceholder.typicode.com/posts', data));

// 2
const saveData = useMutation({
    mutationFn: (person: any) => axios.post('https://jsonplaceholder.typicode.com/posts', person)
})

mutate

  • useMutaion의 전달인자로 넘겨준 콜백함수나 내용들이 실행될 수 있도록 도와주는 trigger 역할
import axios from 'axios';
import { useState } from 'react';
import { useMutation, useQuery } from 'react-query';

interface IData {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const ReactQuery = () => {
  const [data, setData] = useState(
        {
            "userId": 1,
            "id": 2,
            "title": "qui est esse",
            "body": "hello"
        },
    )
  const saveData = useMutation((data: IData) => 
                               axios.post('https://jsonplaceholder.typicode.com/posts', 
                                          data));

  const saveData = useMutation(saveData); // useMutate 정의

  const onSavePerson = () => {
    savePerson.mutate(data); // 데이터 저장
  }

    return (

  return (
    <>
      <div onClick={onSavePerson}>데이터 추가</div>
    </>
  );
};

export default ReactQuery;

onSuccess, onError, onSettled

const saveData = useMutation(saveData, {
  onSuccess: () => { // 요청이 성공(try)
    console.log('onSuccess');
  },
  onError: (error) => { // 요청시 에러 발생(cath)
    console.log('onError');
  },
  onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우(finally)
    console.log('onSettled');
  }
});
  • mutate에서도 사용 가능
  const onSavePerson = () => {
    savePerson.mutate(data, {
      onSuccess: () => { // 요청이 성공(try)
          console.log('onSuccess');
      },
      onError: (error) => { // 요청시 에러 발생(cath)
          console.log('onError');
      },
      onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우(finally)
          console.log('onSettled');
      }); // 데이터 저장
  }

📌 invalidateQueries

  • useQuery에서 설정한 queryKey의 유효성을 제거하기 위해 사용
  • queryKey의 유효성을 제거하는 이유는 새로운 데이터를 fetch 하기 위해
  • staleTime과 cacheTime에 의해서 최신의 데이터를 사용자가 보지 못하는 문제 발생
  • queryKey의 유효성을 제거해주면 캐싱되어있는 데이터를 화면에 보여주지 않고 서버에 새롭게 데이터 요청
// 쿼리와 쿼리 상태를 관리하는 메소드들을 포함한 객체 인스턴스
const queryClient = useQueryClient(); 

const saveData = useMutation(saveData, {
  onSuccess: () => { // 요청이 성공(try)
    console.log('onSuccess');
    // query key를 전달인자로 넘겨줌으로써 쿼리 무효화
    queryClient.invalidateQueries('fetch-data');
  },
  onError: (error) => { // 요청시 에러 발생(cath)
    console.log('onError');
  },
  onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우(finally)
    console.log('onSettled');
  }
});

setQueryData

  • 기존 queryKey에 해당되는 데이터를 업데이트
  onSuccess: (successData) => { // 요청이 성공(try)
    console.log('onSuccess');
    queryClient.setQueryData('fetch-data', (data) => {
      const newData = data; // fetch-data의 현재 데이터
      newData.data.push(successData); // 새로운 데이터 push

      return newData; // 변경된 데이터로 set
    })
  },

📌 부족한 점

  • queryClient 관련해서 더 알아볼 예정

📌 참고

우아한 형제들에서 React Query를 선택한 이유
https://techblog.woowahan.com/6339/
React-Query(기본 개념)
https://abangpa1ace.tistory.com/m/263
React-Query(useQuery)
https://abangpa1ace.tistory.com/m/264
React-Query(useMutation, invalidateQueries)
https://jforj.tistory.com/244

0개의 댓글