(React) 리액트 쿼리

호두파파·2022년 2월 27일
2

React

목록 보기
30/38
post-thumbnail


리액트에서 비동기 로직, 대표적으로 api를 이용하기 위한 서버와의 데이터 통신 등을 리액트스럽게 다룰 수 있게 해주는 라이브러리이다.

React Query 공식문서 보기

많은 상태관리 라이브러리 중에서도 리액트 쿼리는 특히나 server state를 아주 효율적으로 관리할 수 있다. 기존에는 isLoading, isError, refetch, 데이터 캐싱 등을 개발자가 직접 구현하려면 꽤나 까다로웠던 반면, 리엑트 쿼리에서는 아주 간단하게 제공할 수 있다.

🌟 Server-State와 Client-State

  • Server State: 세션간 지속되는 데이트를 말하며 비동기적이다. 세션을 진행하는 클라이언트에서만 소유되는 데이터가 아니다. 클라이언트에서는 서버 데이터의 스냅샷(특정한 시점에서 정지된 데이터)을 사용하기 때문에, 클라이언트에서 보이는 서버 데이터가 항상 최신이리라 보장할 수 없다.
    예) 리액트 앱에서는 비동기 요청으로 받아올 수 있는 백엔드 DB의 데이터

  • Client State: 세션간 지속적이지 않은 데이터이다. 동기적이며, 클라이언트가 소유한다. 항상 최신 데이터로 업데이트되며 이는 렌더링에 반영한다.
    예) 리액트 컴포넌트의 state, 동기적으로 저장되는 redux store의 데이터

Redux 같은 전역 상태관리 라이브러리들이 클라이언트 상태값에 대해서는 잘 작동하지만, 서버 상태에서는 그러지 않을 수 있다. 서버 데이터는 항상 최신 상태임을 보장하지 않기 때문이다. 네트워크 통신은 최소한으로 줄이는게 좋은데, 복수의 컴포넌트에서 최신 데이터를 받아오기 위해 fetching을 여러번 수행하는 낭비가 발생할 수 있다.

사용방법

QueryClientProvider

  • 리액트 쿼리 사용을 위해 QueryClientProvider를 최상단에서 감싸줘야 한다.
  • 쿼리 인스턴스를 생성한 후 client={queryClient}를 작성해준다.
import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

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

이를 통해 앱에서 비동기 요청을 알아서 처리하는 background 계층을 이루게 된다.

✨ 반드시 알고 넘어가야할 중요 개념

  • stale: 인스턴스가 존재하지만 이미 패칭이 완료된 쿼리. 특정 쿼리가 stale된 상태에서 같은 쿼리 마운트를 시도한다면 캐싱된 데이터를 반환하면서 리패칭을 시도한다.
  • fetching: 요청을 수행하는 중인 쿼리
  • fresh: active 상태의 시작이다.기본 staleTime은 0인데, 이를 늘려줄 경우 fresh한 상태가 유지되는데, 이때는 쿼리가 다시 마운트되어도 패칭이 발생하지 않고 fresh한 값을 반환한다.
  • inactive: active 인스턴스가 하나도 없는 쿼리. cacheTime 동안 캐시된 데이터가 유지된다. 페이지네이션을 할때마다 컴포넌트가 재랜더링되면서 새로운 쿼리가 만들어지고, 저번 랜더링에서 호출됐던 쿼리들은 inactive된다.

useQeury

import { useQuery } from 'react-query';
// 주로 사용되는 3가지 return 값 외에도 더 많은 return 값들이 있다. 
const { data, isLoading, error } = useQuery(queryKey, queryFn, options)

QeuryKey

  • QueryKey를 기반으로 데이터 캐싱을 관리한다.
  • 문자열 또는 배열로 지정할 수 있으나, 새롭게 배포될 버전에서는 배열로만 지원할 예정이라고 한다.
useQuery('todos', ...) // 문자열 
useQuery(['todos'], ...) //  배열
  • 쿼리가 변수에 의존하는 경우에는 QueryKey에도 해당 변수를 추가해주어야 한다.
const { data, isLoading, error } = useQuery(['todos', id], () => axios.get(`http://.../${id}`));

Query Functions

  • useQuery의 두 번째 인자에는 promise를 반환하는 함수를 넣어주어야 한다.
useQuery(['todos', todoId], () => fetchTodoById(todoId));

혹은 이렇게도 표현할 수 있다.

useQuery(['todos', todoId], async () => {
  const data = await fetchTodoById(todoId);
  return data
});

Queru Options

다양한 옵션이 있지만, 자주 사용되는 옵션을 소개해본다.

enabled (boolean)

  • enabled는 쿼리가 자동으로 실행되지 않게 설정하는 옵션이다.
const { data } = useQuery(['todos', id], () => fetchTodoById(id), {
  enabled: !!id, // id가 존재할 때만 쿼리 요청을 한다는 의미의 코드이다. 
});

retry (boolean | number | (failureCount: number, error: TError) => boolean)

  • retry는 실패한 쿼리를 재시도하는 옵션이다.
  • 기존적으로 쿼리 실패시 3번 재시도한다.
  • true로 설정하면 쿼리 실패시 무한히 재시도하고, false로 설정하면 재시도 하지 않는다.

staleTime (number | Infinity)

  • staleTime은 데이터가 fresh상태로 유지되는 시간이다. 해당 시간이 지나면 stale상태가 된다.
  • default staleTime은 0이다.
  • fresh 상태에서는 쿼리가 다시 mount 되어도 fetch가 실행되지 않는다.

cacheTime (number | Infinity)

  • cacheTimeinactive상태인 캐시 데이터가 메모리에 남아있는 시간이다. 이 시간이 지나면 캐시 데이터는 가비지 컬렉터에 의해 메모리에서 제거된다.
  • default cacheTime은 5분이다.

refetchOnMount (boolean | "always")

  • refetchOnMount는 데이터가 stale 상태일경우 마운트될떄마다 refetch를 실행하는 옵션이다.
  • default값은 true이다.
  • always로 설정하면 마운트시 마다 매번 refetch를 실행한다.

refetchOnWindowFocus (boolean | 'always')

  • refetchOnWindowFocus는 데이터가 stale 상태일 경우 윈도우 포커싱될때마다 refetch를 실행하는 옵션이다.
  • 예를 들어, 크롬에서 다른 탭을 눌렀다가 다시 원래 보던 탭을 눌렀을 경우, 네트워크 탭을 보든 개발자 도구 창을 조작하다가 페이지 내부를 다시 클릭했을때 이 옵션을 발동하면, refetchin된다.
  • default값은 true이다.
  • always로 설정하면 항상 윈도우 포커싱될때마다 refetch를 실행한다는 의미이다.
const { data: userInfo } = useQuery(['user'], getUser, {
  refetchOnWindowFocus: true,
  staleTime: 60 * 1000, // 1분
})
  • QueryClient defaultOptions 설정으로 refetch 기능들을 다 false로 꺼버렸을 경우에는 refetch 기능이 실행되지 않는다. 그럴 경우에는 refetchOnWindowFocus 옵션이 실행되게끔 true로 설정하면 된다.
  • fresh 상태인 1분 동안은 아무리 다른 탭을 왔다갔다해도 fetch 요청을 하지 않는다.

refetchOnReconnect (boolean | 'always')

  • refetchOnReconnect는 데이터가 stale 상태일 경우 재 연결될때 refetch를 실행하는 옵션이다.
  • default는 true이다.
  • always 옵션은 앞선 두 옵션처럼 쿼리가 매번 재 연결될때마다 refetch를 실행한다.

onSuccess ((data: Tddata) => void)

  • onSuccess는 쿼리 성공시 실행되는 함수이다.
  • 매개변수 data는 성공시 서버에서 넘어오는 reponse 값을 말한다.

onError ((error: TError) => void)

  • onError는 쿼리 실패 시 실행되는 함수이다.
  • 매개변수로 에러 값을 받을 수있다.
const { data: userInfo } = useQuery(['user'], getUser, {
  refetchOnWindoFocus: true,
  staleTime: 60 * 1000, 
  onError: (error) => {
    if (error.response?.data.code === 401) {
    //...
    }
  },
})

onSettled ((data?: TData, error: TError) => void)

  • onSettled는 쿼리가 성공해서 성공한 데이터가 전달되거나, 실패해서 에러가 전달될때 실행되는 함수이다.
  • 매개변수로 성공 시에는 성공 데이터, 실패 시에는 에러가 전달된다.

initialData (TData | () => TData)

  • initialData를 설정하면 쿼리 캐시의 초기 데이터로 사용된다. (쿼리가 아직 생성되지 않았거나 캐시되지 않았을 때)
  • staleTime이 설정되지 않은 경우 초기 데이터는 기본적으로 stale 상태로 간주한다.

Mutation

function App() {
   const mutation = useMutation(newTodo => 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와는 다르게 create, update, delete를 담당하며, server state에 사이드 이펙트를 일으키는 경우에 사용한다.

  • useMutation으로 mutation 객체를 정의하고, mutate 메서드를 사용하면 요청 함수를 호출해 요청이 보내진다.
  • useMutation이 반환하는 객체 프로퍼티로 제공되는 상태 값은 useQuery와 동일하다.
  • 뮤테이션이 성공한다면 높은 확률로 해당 데이터를 다시 패칭해와야 한다. mutation이 일어날때 관련 query도 invalidate되어야 한다.

invalidation

// 캐시가 있는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries()

// 'todos'로 시작하는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries('todos')

queryClient.invalidateQueries({
   predicate: query =>
     query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
 })

쿼리의 데이터가 요청을 통해 서버에서 바뀌었다면, 백그라운드에 남아있는 데이터는 과거의 것이 되어 앱에서 쓸모없어지는 상황이 발생할 수 있다.
invalidateQueries메소드를 사용하면 개발자가 명시적으로 query가 stale되는 지점을 지정해줄 수있다. 해당 메소드가 호출되면 쿼리가 바로 stale되고, 리패치가 진행된다.

import { useMutation, useQueryClient } from 'react-query'
 
 const queryClient = useQueryClient()
 
 // 뮤테이션이 성공한다면, 쿼리의 데이터를 invalidate해 관련된 쿼리가 리패치되도록 만든다.
 const mutation = useMutation(addTodo, {
   onSuccess: () => {
     queryClient.invalidateQueries('todos')
     queryClient.invalidateQueries('reminders')
   },
 })

mutation으로 요청 후 서버에서 받는 response값이 갱신된 새로운 데이터일 경우, mutation을 성공했을 때, 쿼리 데이터를 명시적으로 바꿔주는 queryClient 인스턴스의 setQueryData 메소드를 사용하면 좋다.

const queryClient = useQueryClient()
 
 const mutation = useMutation(editTodo, {
   onSuccess: data => queryClient.setQueryData(['todo', { id: 5 }], data),
 })
 
 mutation.mutate({
   id: 5,
   name: 'Do the laundry',
 })
 
// 뮤테이션의 response 값으로 업데이트된 data를 사용할 수 있다.
 const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoByID)

Catching Process

  1. useQuery의 첫 번째, 새로운 인스턴스가 마운트된다. 런타임간 최초로 fresh한 해당 쿼리가 호출되었다면, 캐싱하고, 패칭이 끝나면 해당 쿼리를 stale한 상태로 바꾼다. (staleTime: 0)
  2. useQuery의 두 번째 인스턴스가 앱 어딘가에서 마운트되었을때, 이미 쿼리가 stale한 상태이므로 이전에 만들어놨던 캐시를 반환하고 리패칭한다. 캐시도 이때 업데이트된다.
  3. 쿼리가 언마운트되거나 더 이상 사용하지 않을 때 마지막 인스턴스가 언마운트되어 inactive 상태가 되었을때 5분이 지나면 자동으로 삭제한다.

😇 react-query를 썼을때 뭐가 좋을까?

  • 비동기 관련한 코드가 대폭 줄어든다.
  • Redux 같은 전역 상태 저장소의 store에 동기적으로 업데이트되는 데이터와 액션만 남길 수 있어 크기를 줄이고, 비동기 데이터를 관리하기 위한 Saga나 thunk는 대체할 수 있다.
  • 캐싱과 리패칭을 개발자가 구현하지 않아도 알아서 지원한다.
  • 풍부한 옵션으로 인해 다양한 custom이 가능하다.
  • 디버깅이 매우 용이하다. React Query도 자체 데브툴을 제공하는데, 쿼리의 호출 상태를 바로 브라우저에서 확인 가능하다.

결론

Redux는 전역상태 관리를, React Query는 서버에서 받아온 데이터 관리를 하면서 역할을 분담할 수 있어, Redux 자체를 좀 더 취지에 맞게 사용할 수 있게 한다.


참고

[React Query] 리액트 쿼리 시작하기 (useQuery)

React-Query 살펴보기

     
profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

0개의 댓글