[ React-Query ] 총 정리

angie·2023년 1월 26일
4

1. 알아보기

1) React-Query란?

  • 어플리케이션의 비동기 데이터를 쉽게 관리, 캐시 및 동기화할 수 있는 React용 라이브러리
  • 사용자 인터랙션에 따른 데이터의 fetching, caching, updating와 컴포넌트 트리의 변경사항에 대한 과정을 단순화
  • 실시간 업데이트, 페이지네이션, 오류 처리 등과 같은 유용한 기능을 제공
  • Axios나 Fetch API와 같은 데이터 fetching 라이브러리와 원활하게 작동하도록 설계됨

2) React-query의 필요성

  1. 서버의 데이터와 클라이언트의 데이터 구분의 모호함

서버의 응답을 Store에 저장할 필요가 없는 상황에서도 구조화를 위해 Action Types, Actions, Reducer를 만들고, Action을 호출하는 등의 불필요한 상황, 캐싱 처리 및 관리 문제 등이 존재

  1. Store의 불필요한 규모 확장

비동기 통신을 위해 Store가 사용되어 Store의 본질과 다르게 규모가 비대해지는 경우가 발생한다. 기능이 추가될 수록 Store가 비대해진다.

서버 상태(Server State)란?

  • Client가 제어하거나 소유하지 않는 위치에서 원격으로 유지 됨.
  • fetching 및 updating을 위한 비동기 API를 필요로 함.
  • 상태가 공유되며 사용자 모르게 변경될 수 있음.
  • 주의하지 않으면 애플리케이션이 잠재적으로 "out of date" 상태가 될 수 있음.

React-query 전환 사례

My구독의 React Query 전환기

Store에서 비동기 통신 분리하기 (feat. React Query) | 우아한형제들 기술블로그

3) 언제 React-query를 사용해야 하는가

클라이언트 데이터보다 서버 데이터에 대한 관리가 더 많을 때

Admin 페이지와 같은 관리형 페이지에서는 클라이언트의 전역 상태 데이터는 많이 필요하지 않을 수 있다. 이러한 페이지에서 Ducks 구조보다는 React Query를 적용하면 구조를 더 단순화 시킬 수 있다.

4) 장단점

장점

  1. 외부 데이터 관리
    • React Query는 외부 API로부터 데이터를 가져오고, 캐싱하고, 업데이트 할 수 있도록 해줍니다.
  2. 오류 처리와 네트워크 상태 관리
    • React Query는 오류 처리와 네트워크 상태를 자동으로 관리합니다.
  3. 페이지네이션
    • React Query는 페이지네이션을 지원하여 데이터를 쪼개서 가져올 수 있도록 해줍니다.
  4. 실시간 업데이트
    • React Query는 실시간 업데이트를 지원하여 데이터가 실시간으로 업데이트될 수 있도록 해줍니다.
  5. 쉬운 관리
    • React Query는 리액트에서 사용하기 쉽고, 코드를 깔끔하게 유지할 수 있도록 해줍니다.
  6. 캐싱 정책
    • React Query는 캐싱 정책을 지원하여 데이터를 언제 캐싱할지, 언제 업데이트 할지를 설정할 수 있습니다.
  7. 일관된 API
    • React Query는 일관된 API를 제공하여 데이터를 가져오는 것을 쉽게 해줍니다.
  8. 쉬운 테스트
    • React Query는 테스트를 쉽게 할 수 있도록 해줍니다.

단점

  • 러닝 커브가 높아 컨셉을 이해하고 초기 설정하는 데 시간이 걸릴 수 있음
  • 비교적 새로운 라이브러리이기 때문에 버그나 누락된 기능이 있을 수 있음
  • 소규모 프로젝트에서 사용 시 프로젝트 규모에 비해 복잡성이 추가될 수 있음
  • 모든 케이스에서 적합한 것은 아니므로 React-query를 도입하는 정당한 가치를 평가해야함

캐싱

  • 데이터를 임시로 저장하여 다음에 필요할 때 빠르게 가져올 수 있도록 하는 기법
  • 데이터를 요청할 때마다 API에 요청하지 않고, 이전에 저장해둔 데이터를 사용할 수 있어서 응답 속도가 빠름
  • React Query는 캐싱 기능을 제공하여, 외부 API로부터 가져온 데이터를 캐싱하고 이를 재사용할 수 있음

2. 시작하기

현재 시점(2023년 1월 26일) 기준으로 가장 최신 버전의 React Query는 v4다. react-query라는 이름으로 설치받으면 v3이 설치된다. 가장 최신 버전인 v4를 설치받고 싶다면 **@tanstack/react-query로 다운받아야한다.**

v4 참고 문서

@tanstack/react-query

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

1) 설치

npm i @tanstack/react-query

2) index.js 세팅

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from "react-query/devtools";

// Create a client
const queryClient = new QueryClient();

ReactDOM.render(
  // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>,
  document.getElementById("root")
);

QueryClientProvider는 리액트 애플리케이션에서 비동기 요청을 처리하기위한 Context Provider로 동작하며 하위 컴포넌트에서 QueryClient를 사용할 수 있게 해준다.

3. 사용하기

1) useQuery

useQuery | TanStack Query Docs

  • get API 요청을 수행하기 위한 hook
  • post, update 등은 useMutations을 사용해야함
  • 기본적으로 비동기로 동작하며 여러 useQuery를 사용하고 싶다면 useQueries를 사용하는 것을 추천
  • enabled 옵션을 사용하면 useQuery를 동기적으로 사용 가능

파라미터

  • unique key : 해당 쿼리의 고유 식별자
  • queryFn : 쿼리에 사용할 promise 기반의 비동기 API 함수 (API 요청 보내는 함수)
  • options : 쿼리에 사용할 옵션

반환값

  • data: fetch한 데이터. 데이터가 fetch될 때까지 undefined 의 값을 가진다.
  • status: 쿼리의 상태. "loading", "error", 혹은"success"의 세가지 종류가 있다.
  • error: 발생한 오류. 발생한 오류가 없다면 undefined 의 값을 가진다.
  • isFetching: 쿼리가 fetching 중인지 여부를 나타내는 값으로 Boolean 값을 가진다.

예시

const Todos = () => {
  const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList, {
    refetchOnWindowFocus: false, // react-query는 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행합니다. 그 재실행 여부 옵션 입니다.
    retry: 0, // 실패시 재호출 몇번 할지
    onSuccess: data => {
      // 성공시 호출
      console.log(data);
    },
    onError: e => {
      // 실패시 호출 (401, 404 같은 error가 아니라 정말 api 호출이 실패한 경우만 호출됩니다.)
      // 강제로 에러 발생시키려면 api단에서 throw Error 날립니다. (참조: https://react-query.tanstack.com/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default)
      console.log(e.message);
    }
  });

  if (isLoading) {
    return <span>Loading...</span>;
  }

  if (isError) {
    return <span>Error: {error.message}</span>;
  }

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};
  • isLoading, isSuccess와 같은 상태를 status로 한번에 처리 가능
function Todos() {
  const { status, data, error } = useQuery("todos", fetchTodoList);

  if (status === "loading") {
    return <span>Loading...</span>;
  }

  if (status === "error") {
    return <span>Error: {error.message}</span>;
  }

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

2) useQuery의 동기적 실행

  • enabled 옵션을 사용
  • useQuery의 옵션이 들어가는 자리인 세번째 파라미터에 enabled 를 넣으면 그 값이 true일 때 useQuery를 실행시킴

예시

const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const { data: nextTodo, error, isFetching } = useQuery(
  "nextTodos",
  fetchNextTodoList,
  {
    enabled: !!todoList // true가 되면 fetchNextTodoList를 실행한다
  }
);

3) useQueries

  • 여러개의 쿼리를 fetch할 때 사용
// 아래 예시는 롤 룬과, 스펠을 받아오는 예시입니다.
const result = useQueries(
	queries : [
	  {
	    queryKey: ["getRune", riot.version],
	    queryFn: () => api.getRunInfo(riot.version)
	  },
	  {
	    queryKey: ["getSpell", riot.version],
	    queryFn: () => api.getSpellInfo(riot.version)
	  }
]);

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

4) Query Keys

  • useQuery에서 파라미터로 사용되는 Query Key는 React Query에서 쿼리 캐싱을 관리하기 위한 unique key
  • unique key를 배열로 넣으면 query함수 내부에서 변수로 사용 가능
const riot = {
  version: "12.1.1"
};

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

      return api.getRunInfo(riot.version);
    }
  },
  {
    queryKey: ["getSpell", riot.version],
    queryFn: () => api.getSpellInfo(riot.version)
  }
]);

5) QueryCache

  • 포함된 모든 데이터, 메타 정보 및 쿼리 상태를 저장
  • onError, onSuccess 콜백을 사용하여 애플리케이션 전역에서 이벤트를 핸들링

예시

import { QueryCache } from '@tanstack/react-query'

const queryCache = new QueryCache({
  onError: error => {
    console.log(error)
  },
  onSuccess: data => {
    console.log(data)
  }
})

const query = queryCache.find({ queryKey: ['posts'] })

메소드

  • [find](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefind)
  • [findAll](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefindall)
  • [subscribe](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachesubscribe)
  • [clear](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycacheclear)
  1. **[queryCache.find](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefind)**

    • 캐시에서 기존 쿼리 인스턴스를 가져오는 데 사용
    • 쿼리가 존재하지 않을 때 undefined 가 반환
    const query = queryCache.find({ queryKey })
  2. **[queryCache.findAll](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefindall)**

    • queryKey가 부분적으로 일치하는 모든 쿼리 인스턴스를 가져옴
    • 쿼리가 존재하지 않을 때 빈 배열 반환
    const queries = queryCache.findAll({ queryKey })
  3. **[queryCache.subscribe](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachesubscribe)**

    • 쿼리 캐시 전체를 구독하고 쿼리 상태가 변경되거나 업데이트, 추가 또는 제거시 안전하게 알림을 받을 수 있음
    const callback = event => {
      console.log(event.type, event.query)
    }
    
    const unsubscribe = queryCache.subscribe(callback)
    • 옵션
      • callback 함수 : 쿼리 캐시가 업데이트 될 때마다 해당 쿼리 캐시와 함께 호출됨
    • 반환
      • unsubscribe 함수 : 이 함수는 해당 쿼리 캐시에 대한 콜백을 구독 취소한다.
  4. **[queryCache.clear](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycacheclear)**

    • 캐시를 모두 지우고 새롭게 시작한다.
    queryCache.clear()

6) useMutation

useMutation | TanStack Query Docs

  • query와 달리 mutations는 데이터를 create, update, delete하거나 서버의 side-effects를 수행할 때 사용

프로퍼티

  • isIdle or status === 'idle' : mutation이 실행 되지 않아 아직 캐싱되지 않은 상태.
  • isLoadingor status === 'loading' : mutation이 실행중인 상태
  • isError or status === 'error' : mutation에 에러가 발생한 상태
  • isSuccess or status === 'success' : mutation이 성공적으로 실행되었고 데이터를 사용 가능한 상태능
  • error : mutation이 isError 상태인 경우 에러 정보 확인을 위해 사용하는 프로퍼티
  • data : mutation이 isSucess 상태인 경우 데이터 사용을 위해 사용하는 프로퍼티

파라미터

  • mutationKey : mutation에 사용할 unique key 값.
  • mutationFn : mutation에 사용할 promise 기반의 비동기 API 함수.
  • options : mutation에 사용할 옵션 값.

mutate

  • mutate 함수를 호출하여 mutation을 실행

예시

function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {mutation.isLoading ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

7) mutations후 다시 get

  • react-query 장점으로 update후에 get 함수를 간단히 재실행 할 수 있다
  • mutation 함수가 성공할 때, unique key로 맵핑된 get 함수를 invalidateQueries
    에 넣는다.

예시

const mutation = useMutation(postTodo, {
  onSuccess: () => {
    // postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행합니다.
    queryClient.invalidateQueries({ queryKey: ['todos'] })
    queryClient.invalidateQueries({ queryKey: ['reminders'] })
  }
});
  • mutation에서 return된 값을 이용해서 get 함수의 파라미터를 변경해야할 경우 setQueryData
    를 사용
const queryClient = useQueryClient();

const mutation = useMutation(editTodo, {
  onSuccess: data => {
    // data가 fetchTodoById로 들어간다
    queryClient.setQueryData(["todo", { id: 5 }], data);
  }
});

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

mutation.mutate({
  id: 5,
  name: "nkh"
});
profile
better than more

0개의 댓글