코린이의 개발 일지

React Query 살펴보기 본문

웹 (web)/프론트엔드

React Query 살펴보기

폴라민 2023. 1. 12. 22:38
반응형

설치

yarn add react-query

 

React-Query가 주장하는 Global State 개념

  • Global State라는 말을 쓰지 말자: 전역 state는 Client와 Server로 분류할 수 있고, 이 두 State는 다른 방식으로 다뤄져야 효율적인 앱을 만들 수 있다.
  • Server State
    • 세션간 지속되는 데이터
    • 비동기적
    • 세션을 진행하는 클라이언트만 소유하는게 아니고 공유되는 데이터도 존재하며 여러 클라이언트에 의해 수정될 수 있음.
    • 클라이언트에서는 서버 데이터의 스냅샷만을 사용하기 때문에 클라이언트에서 보이는 서버 데이터는 항상 최신임을 보장할 수 없다.
    • ex) 비동기 요청으로 받아올 수 있는, 백엔드 DB에 저장되어 있는 데이터
  • Client State
    • 세션간 지속적이지 않은 데이터
    • 동기적
    • 클라이언트가 소유
    • 항상 최신 데이터로 업데이트 (렌더링에 반영)
    • ex) 리액트 컴포넌트의 state, 동기적으로 저장되는 데이터

 

만들어진 이유

  • 서버 데이터는 항상 최신임을 보장하지 않는다. 명시적으로 fetching을 수행해야만 최신 데이터로 전환된다.
  • 네트워크 통신은 최소한으로 줄이는게 좋은데, 복수의 컴포넌트에서 최신 데이터를 받아오기 위해 fetching을 여러번 수행하는 낭비가 발생할 수 있다.

 

Server State 모델링

우선 리액트 쿼리에서 상태 모델링은 다음과 같다.

  • fresh
    • 새롭게 추가된 쿼리 인스턴스
    • active 상태의 시작
    • default staleTime은 0이기 때문에 옵션을 설정해주지 않으면 api 호출이 끝나고 바로 stale 상태로 변한다.
    • staleTime을 늘려줄 경우, 그 시간동안 fresh한 상태가 유지된다. 이때는 쿼리가 다시 마운트되어도 패칭이 발생하지 않고, 기존의 fresh한 값을 반환한다.
  • fetching:
    • 요청을 수행하는 중인 쿼리
  • stale: 데이터가 오래된 상태, Fetching을 통해 Fresh 상태로 유지해줘야함.
    • 특정 쿼리가 stale된 상태에서 같은 쿼리 마운트를 시도한다면 캐싱된 데이터를 반환하면서 리패칭을 시도한다.
  • inactive
    • active 인스턴스가 하나도 없는 쿼리, inactive된 이후에도 cacheTime동안 캐시된 데이터가 유지된다.
    • React Query에서 브라우저 캐시를 관리하는 가비지 컬렉터에 의해 삭제된다고 함
  • deleted: inactive 상태의 데이터가 캐시에서 삭제된 상태

 

 

inactive 상태와 stale 상태의 차이를 모르겠어서 좀 찾아봤는데,

  • stale상태는 렌더링에 사용되는 패칭을 이미 보낸 오래된 상태.
  • 리액트 쿼리는 이전에 패칭한 데이터중 이제 사용하지 않을 데이터 (렌더링 하지 않을 데이터)는 inactive 상태로 바꾼다.

fetch를 해온 데이터중에 다시 렌더링 하지 않을 데이터라는 것은 무엇일까?

예시로 페이지네이션과 토글을 들 수 있다.

페이지네이션의 경우 이전에 불러온 페이지는 현재 렌더링할 대상이 되는 데이터가 아니기 때문에 inactive상태로 전환된다.

 

또다른 예시로는

const [show, setShow] = useState(true);
//...
{
  show && <GitUsers />;
}

위의 코드에서 show라는 상태 토글에 따라 GitUser라는 컴포넌트가 보이거나 안보이는 상황이라고 할때,

show가 false라서 GitUsers 컴포넌트가 안보이는 상황일 경우, 리액트 쿼리는 git user 데이터가 렌더링 되지 않을 데이터라는 걸 알고 상태를 inactive로 바꾼다.

 

그러니까 순서대로 생각해보면

  • 컴포넌트 렌더링이 발생하면, react query는 git user 목록을 fetch 해오고 렌더링. (fresh 상태)
  • 정해놓은 staleTime이 지나면 git user 목록은 stale 상태가 된다.
  • 사용자가 버튼을 눌러 show가 false가 되면 git user목록은이제 렌더링 되지 않을 (사용되지 않을)데이터 이기 때문에 inactive상태가 된다. (inactive가 상태가 되어도 cacheTime동안 react query in-memory cache에 저장된다.)
    • 여기서 핵심은 만약 git user 목록이 여러 컴포넌트에서 사용이 되며, 모든 쿼리 인스턴스가 언마운트 되고 사용되지 않을 상황이어야 해당 쿼리가 inactive 상태가 된다.
  • 만약 5분이 지나지 않은상태에서 git user 쿼리가 다시 마운트 되면, 쿼리는 즉시 캐시된 데이터를 리턴하고 띄운 후, 그동안 fetch함수를 background에서 실행해 fresh value를 가져온다.
  • inactive 상태에서 5분이 지났다면 해당 쿼리는 삭제된 후 garbage collected된다.

 

 

Refs.

캐싱관련 React Query 공식문서: Caching Examples

Stale state와 Inactive State 차이점: What Is The Difference Between Stale State and Inactive State in React Query

 

 

 

 

음 각각의 상태는 좀더 공부를 해봐야 잘 이해가 될거 같다.

일단 사용을 해보자

 

사용전에 refetching 조건을 살펴 보면 아래 4가지 조건이 있다.

Refetching이 일어나는 조건

  • refetchOnMount: 데이터가 stale 상태일 경우 마운트할 때(옵션으로 끄기 가능, default는 true)
  • refetchOnWindowFocus: window가 다시 포커스 되었을 때(옵션으로 끄기 가능, default는 true)
  • refetchOnReconnect: 네트워크가 다시 연결되었을 때(옵션으로 끄기 가능, default는 true)
  • retry: refetch interval이 있을때 ⇒ 요청 실패한 쿼리는 default로 3번 더 백그라운드 단에서 요청하며, retry, retryDelay 옵션으로 간격과 횟수를 커스텀 할 수 있다.

 

 

React Query 사용

우선 가장 상단 컴포넌트를 QueryClientProvider로 감싼다.

  • QueryClientProvider는 비동기 요청을 처리하기위한 Context Provider로 동작
import React from 'react';
...
import { QueryClient, QueryClientProvider } from 'react-query';

function App() {
  const queryClient = new QueryClient();
  return (
    <ThemeProvider theme={AppTheme}>
        <QueryClientProvider client={queryClient}>
          <Global styles={globalStyle} />
          <AppStyle>
            <Router>
              ...
            </Router>
          </AppStyle>
          <Modal />
        </QueryClientProvider>
    </ThemeProvider>
  );
}

그럼 이제 하위 컴포넌트 들에서 React Query를 사용할 수 있다.

 

 

useQuery

  • 리액트 쿼리는 GET 요청을 보낼 때 useQuery훅을 사용한다.
  • 서버 데이터를 바꿀 수 있는 create update delete 요청이라면 mutation 쓰는게 더 추천된다. (useQuery로도 가능하긴하다)

아래의 코드는 투두리스트를 띄우는 로직이다.

import React, { useState } from 'react';
import styled from '@emotion/styled';
import { useQuery } from 'react-query';
import type todoResponseType from '../../types/TodoResponse';
import TodoInputBox from './TodoInputBox';
import TodoItemBox from './TodoItemBox';
import { httpGet } from '../../util/http';
import Loading from '../Loading';
import useModal from '../../hooks/useModal';

function TodoList() {
  const [currentTodoList, setCurrentTodoList] = useState<todoResponseType[]>([]);
  const { setContent, closeModal } = useModal();
  const { isLoading } = useQuery(['todos'], () => httpGet('/todos'), {
    refetchOnWindowFocus: true,
    staleTime: 60 * 1000,
    onSuccess: (data) => setCurrentTodoList([...data]),
    onError: (error) => setContent(`${error}`, [{ name: '확인', handler: closeModal }]),
  });
  return (
    <Wrapper>
      <Header>Todo List</Header>
      <ListBox>
        <TodoInputBox />
        {isLoading ? <Loading /> : currentTodoList.map((item) => <TodoItemBox key={item.id} currentTodo={item} />)}
      </ListBox>
    </Wrapper>
  );
}

 

 

const { isLoading } = useQuery(['todos'], () => httpGet('/todos'), {
  refetchOnWindowFocus: true,
  staleTime: 60 * 1000,
  onSuccess: (data) => setCurrentTodoList([...data]),
  onError: (error) => setContent(`${error}`, [{ name: '확인', handler: closeModal }]),
});

 

 

useQuery 부분만 따로 떼어서 보면, 인자로 세개가 들어갔다.

 

  • unique key
    • 한 번 fresh가 되었다면 계속 추적이 가능하다.
    • 리패칭, 캐싱, 공유 등을 할때 참조되는 값.
    • 요청 함수가 특정 변수에 의존할 때, 쿼리 키 배열에 객체로 같이 넣어주면 요청 함수 내에서 인자로 객체를 받을 수 있다.
      function Todos({ todoId }) {
        const result = useQuery(['todos', todoId], () => fetchTodoById(todoId));
      }
      
      function Todos({ status, page }) {
        const result = useQuery(['todos', { status, page }], fetchTodoList);
      }
      
      // 쿼리 요청 함수에서 queryKey에 접근할 수 있다
      function fetchTodoList({ queryKey }) {
        const [_key, { status, page }] = queryKey;
        return new Promise();
      }
      
  • 프로미스를 리턴하는 api 요청 함수 (이 함수는 반드시 resolve promise를 리턴하거나 에러를 throw해야 한다.
// /util/http.ts

export async function httpGet(url: string): Promise<ReturnType> {
  const token = localStorage.getItem('token');
  if (token === null) {
    throw new Error('token error');
  }
  const response: ResponseType = await axios.get(url, {
    headers: {
      'Content-Type': 'application/json',
      Authorization: token,
    },
  });
  return response.data.data;
}
  • 옵션
    • 옵션은 진짜 많다. 필요 옵션은 찾아서 사용하자
    • 필수적인 부분은 staleTime이다. 반드시 지정해야 요청 횟수를 줄일 수 있다.

 

 

 

 

useMutations

  • useQuery와는 다르게 create, update, delete 하며 server state에 사이드 이펙트를 일으키는 경우에 사용
const updateTodoMutate = useMutation(
  'updateTodo',
  (todo: todoItemType) => httpPut(`/todos/${currentTodo.id}`, todo),
  {
    onSuccess: () => {
      queryClient.invalidateQueries(['todos']);
    },
    onError: (error: errorResponseType) => {
      setContent(`${error.response.status}: ${error.response.statusText}\\nmessage: ${error.response.data.message}`, [
        { name: '확인', handler: closeModal },
      ]);
    },
  }
);
  • useMutation으로 mutation 객체 정의 후, mutate메서드를 사용해야 요청 함수를 호출해 요청이 보내진다. (useQuery와 가장 큰 차이점)
  • useQuery를 사용할때처럼 실패시 retry가 디폴트는 아니지만, retry 옵션을 줄 수는 있다.

 

 

invalidation

  • stale 쿼리 폐기
  • invalidateQueries 메소드를 사용하면 개발자가 명시적으로 query가 stale되는 지점을 지정할 수 있다.
  • 해당 메소드가 호출되면 쿼리가 바로 stale되고 리패치가 진행된다.
// 캐시가 있는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries();

// 'todos'로 시작하는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries('todos');
  • Mutation이 일어날 때, 관련 query도 invalidate되어야 하기 때문에, mutation 생명주기 콜백 안에서 invalidate 해주는 경우가 많다.

 

 

setQueryData

  • 또한 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);

 

 

Optimistic Update(낙관적 업데이트)

  • mutation시, invalidation이나 setQueryData 방식이 아닌 낙관적 업데이트라는 방식도 있다.

공식문서: Optimistic Updates

낙관적 업데이트 구현한 글: React Query(리액트 쿼리) 개념 및 예제(6) | Kkiri Blog

 

 

쿼리 함수 처리 방식: 병렬 처리

  • 쿼리가 여러개 선언되어 있는 상황에 쿼리 함수들은 그냥 병렬로 요청되서 처리된다.
function App () {
  // 이렇게 주루륵 있을 때 병렬로 처리
  const usersQuery = useQuery('users', fetchUsers)
  const teamsQuery = useQuery('teams', fetchTeams)
  const projectsQuery = useQuery('projects', fetchProjects)
  ...

 

 

 

기타

스크롤 복원이 자동으로 된다

  • react나 next로 개발을 하다보면 스크롤 복원시키가 까다로운데, react query를 사용하면, cache가 유지되는 동안(디폴트 5분)은 자동으로 스크롤 복원이 된다고 한다.

관련 내용 공식문서: Scroll Restoration

 

SSR도 지원한다

  • 서버에서 prefetching한 데이터를 queryClient로 전달하는 방식을 2가지 지원한다.
    • initialData 사용하기
    • Hydration 사용하기

관련 내용 공식문서: SSR

 

 

 

 

이외에도 react query는 관련 내용, api, 옵션들이 정말 많아서 필요한 부분 공식문서 찾아보는 것이 좋을 거 같다.

 

 

 

 

 

 

 

Refs:

https://react-query-v3.tanstack.com/

https://maxkim-j.github.io/posts/react-query-preview/

https://backbencher.dev/react-query-what-is-inactive-query-state

https://velog.io/@kimhyo_0218/React-Query-리액트-쿼리-시작하기-useQuery

https://devkkiri.com/post/7fafd5b1-f034-47a6-8f4b-201701f8f991

반응형
Comments