React-Query란?

BRANDY·2023년 9월 5일
7

리액트 쿼리란?

fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리이다.
복잡하고 장황한 코드가 필요하지 않고 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있다.

리액트 쿼리를 사용하는 이유

리액트에서 상태 관리를 위한 라이브러리는 redux, recoil 등이 있는데, 이는 클라이언트 상태와 서버 상태를 함께 담아 관리하는 경우가 많다.

실제로 로켓CV 프로젝트 당시 recoil을 사용했었는데, 이에 담겨진 상태도 클라이언트와 서버 상태를 함께 관리하여 효율적이지 못했는데 리액트 쿼리는 클라이언트 상태를 분리하여 관리할 수 있기 때문에 직관적이고 효율적으로 관리할 수 있다.

이 외에도 다양한 옵션들을 활용해 캐싱, 에러처리, suspense, refresh, data fetching 조건 설정 등 기존에 불편했던 것들을 선언적이고 간편하게 이용할 수 있다.

Data Fetching 단순화

기존의 방식은 Fetching 코드를 작성하고 데이터를 담아 둘 상태 생성, useEffect를 이용해 컴포넌트 Mount시 데이터를 Fetching 한 뒤 상태에 저장하였다. 이는 3가지 단계로 요약할수 있다.

  1. Fetching 코드 작성
  2. 데이터를 담아 둘 상태 생성
  3. useEffect를 이용해 컴포넌트 Mount시 데이터를 Fetching 한 뒤 상태에 저장

이 코드를 예시로 들자면

import { useEffect, useState } from "react";

const getServerData = async () => {
  const data = await fetch(
    "https://jsonplaceholder.typicode.com/posts"
  ).then((response) => response.json());
  return data;
};

export default function App() {
  const [state, setState] = useState<any[]>([]);

  useEffect(() => {
    getServerData()
      .then((dataList) => setState(dataList))
      .catch((e) => setState([]));
  }, []);

  return <div>{JSON.stringify(state)}</div>;
}

이 과정을 useQuery 한줄로 처리할 수 있다.

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

const getServerData = async () => {
  const data = await fetch(
    "https://jsonplaceholder.typicode.com/posts"
  ).then((response) => response.json());
  return data;
};

export default function App() {
  const { data } = useQuery(["data"], getServerData);

  return <div>{JSON.stringify(data)}</div>;
}

이로인해 발생하는 장점은

  1. 코드 수 감소로 인한 Side Effect 제거
  2. Data Fetching 방식 규격화
  3. enabled를 이용한 동기적 실행 등이 있다.

동기적 실행에 대한 예시로는

const [state1, setState1] = useState();
const [state2, setState2] = useState();

useEffect(()=>{
  getServerData().then((dataList)=>{
    setState1(dataList[0]);
  })
},[]);

// state1의 데이터를 파라미터로 넣어 호출하는 API
useEffect(()=>{
  if(state1){
    getAfterData(state1).then((dataList)=>{
      setState2(dataList);
    })
  }
},[state1]);

API 호출을 위한 조건을 추가하여 동기적으로 관리하며 useEffect의 순서가 다른 코드들과 섞이면 어떤 데이터가 언제 어떻게 호출되는지의 흐름, 시점을 파악하기 어려워진다. 하지만 리액트 쿼리의 옵션인 enabled를 사용하면

const { data: data1 } = useQuery(["data1"], getServerData);
const { data: data2 } = useQuery(["data2", data1], getServerData, {
  enabled: !!data1
});

간단하게 동기적 실행이 가능하다.

캐싱

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

또한 리액트 쿼리의 캐싱기능을 이용해 불필요한 API 호출을 막고 캐싱된 데이터 이용이 가능하다.

staletime

호출한 데이터는 리액트 쿼리 자체적으로 저장을 해둡니다.
staletime은 이 캐시데이터의 유통기한을 정해주는 옵션입니다.
default는 0으로, 별다른 옵션 세팅을 해주지 않는다면
데이터가 stale하다고 판단하여 캐싱 기능을 이용할 수 없습니다.

fresh(최신의 데이터)
stale(기존의 데이터)

cachetime

cashetime은 저장한 데이터를 얼마나 유지할지 정해주는 옵션입니다.
default는 5분입니다.

const { data } = useQuery(['data', getServerData,{
  staletime: 10 * 60 * 1000;
  cashetime: 10 * 60 * 1000;
})

위와 같은 옵션을 통해 별다른 refresh가 없을 때, 10분 내 재호출시 API를 호출하지 않고 캐싱된 데이터를 제공해준다.

또한 데이터 갱신의 시점을 옵션으로 설정할 수 있는데,

  1. 브라우저에 포커스가 들어온 경우
  2. 새로운 컴포넌트 마운트가 발생한 경우
  3. 네트워크 재연결이 발생한 경우

로 나눌 수 있고, 옵션은 다음과 같다.

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

클라이언트 데이터와 서버 데이터 간의 분리

const { data, isLoading } = useQueries(
	['unique-key'],
	() => {
		return api({
			url: URL,
			method: 'GET',
		});
	},
	{
		onSuccess: (data) => {
			// data로 이것저것 하는 로직
		}
	},
	{
		onError: (error) => {
			// error로 이것저것 하는 로직
		}
	}
)

서버의 데이터를 가지고 올때, onSuccess와 onError 함수를 통해 fetch 성공과 실패의 분기를 아주 간단하게 구현할 수 있다.

Client 데이터는 상태 관리 라이브러리가 관리하고, Server 데이터는 React-Query가 관리하는 구조를 통해 우리는 Client 데이터와 Server 데이터를 온전하게 분리할 수 있다.

대표적인 기능들

기본적으로 GET 에는 useQuery가, PUT, UPDATE, DELETE에는 useMutation이 사용된다.

useQuery

  1. 첫 번째 파라미터로 unique key를 포함한 배열이 들어간다. 이후 동일한 쿼리를 불러올 때 유용하게 사용된다.
  2. 첫 번째 파라미터에 들어가는 배열의 첫 요소는 unique key로 사용되고, 두 번째 요소부터는 query 함수 내부의 파라미터로 값들이 전달된다.
  3. 두 번째 파라미터로 실제 호출하고자 하는 비동기 함수가 들어간다. 이때 함수는 Promise를 반환하는 형태여야 한다.
  4. 최종 반환 값은 API의 성공, 실패 여부, 반환값을 포함한 객체이다.
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const { isLoading, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/tannerlinsley/react-query').then(
        (res) => res.json(),
      ),
  })

  if (isLoading) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>✨ {data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  )
}

useQuery 함수가 반환하는 객체를 보면 isLoading 을 통해 로딩 여부를, error 를 통해 에러 발생 여부를, data를 통해 성공 시 데이터를 반환할 수 있다.

isLoading과 error를 이용하여 각 상황 별 분기를 쉽게 진행할 수 있다.

useQueries

여러개의 useQuery를 한 번에 실행하고자 하는 경우 기존의 Promise.all()처럼 묶어서 실행할 수 있다.

const results = useQueries({
  queries: [
    { queryKey: ['post', 1], queryFn: fetchPost, staleTime: Infinity},
    { queryKey: ['post', 2], queryFn: fetchPost, staleTime: Infinity}
  ]
})

// 두 query에 대한 반환값이 배열로 묶여 반환된다

useMutation

위에서 언급한 것처럼 PUT, UPDATE, DELETE 와 같이 값을 변경할 때 사용하는 API다. 반환값은 useQuery와 동일하다.

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>
  )
}

위의 코드에서 볼 수 있듯이 반환값은 useQuery와 동일하지만, 처음 사용 시에 post 비동기 함수를 넣어주었다. 이때 useMutation의 첫 번째 파라미터에 비동기 함수가 들어가고, 두 번째 인자로 상황 별 분기 설정이 들어간다는 점이 차이이다.

실제 사용 시에는 mutation.mutate 메서드를 사용하고, 첫 번째 인자로 API 호출 시에 전달해주어야하는 데이터를 넣어주면 된다.

profile
프런트엔드 개발자

0개의 댓글