10. 리액트 쿼리 기초

 
 
리액트 쿼리를 아시나요?
 
 예전에 이 글 을 통해 리액트 상태관리의 역사를 공부하면서 알게되었는데요.
코드잇의 강의를 통해 그 사용법을 학습한 내용을 정리하고자 합니다.
 
공식문서는 여기 서 확인할 수 있습니다.
 
 우리가 방문하는 웹사이트들은 굉장히 복잡합니다. 수많은 데이터를 다루게 되는데, 편리하게 이 데이터들을 사용할 수 있어야겠죠.
 
 그래서 여러 상태관리 라이브러리들이 등장했습니다. 대표적으로 많이들 사용하는 리덕스가 있겠네요.
 그러나 리덕스는 클라이언트 상태 데이터(사용자가 버튼을 눌렀는지, 사용자가 값을 입력했는지 등)를 관리하는데 용이하여, 서버 상태 데이터를 관리하기엔 좋지 못한 부분도 있고 코드가 복잡해질 수도 있다고 합니다.
 
 

그래서 리액트 쿼리는 Redux의 과사용으로 복잡성 증가, 비동기 처리의 어려움을 해결하고자 나온
리액트의 상태관리 기술입니다.
 

서버 상태 데이터란

 
서버에서 전달 받는 여러 데이터를 말합니다. 이 블로그의 글 목록, 본문 등도 서버에서 받아오는 데이터가 되겠죠.이런 데이터를 불러오는 데 오랜 시간이 걸릴 수도, 혹은 불러오다 에러가 발생할 수도 있습니다.또 데이터를 거의 실시간으로 동기화해야 할 수도 있습니다.
 
서버 상태 데이터는 클라이언트 상태 데이터와 다르게 이러한 특징들을 가지고 있어, 고민할 거리가 많습니다.이 고민거리들을 몽땅 해결해주는게 리액트쿼리구요.
 
자 그럼, 어떤식으로 사용되는지 확인해봅시다.
 

useQuery() : 서버에서 데이터 불러오기

 
api에 fetch 함수를 통해 getPosts 함수를 작성합니다.
이후 다음과 같이 useQuery()를 통해 데이터를 불러오고 관리할 수 있습니다.

import { useQuery } from '@tanstack/react-query';
import { getPosts } from './api';

function HomePage() {
  const result = useQuery({ queryKey: ['posts'], queryFn: getPosts });
  console.log(result);

  return <div>홈페이지</div>;
}

export default HomePage;

 
이렇게 받아온 useQuery의 리턴에는 다양한 값들이 있습니다.
받아온 데이터가 담겨 있는 data, 여러 상태를 알려주는 isError, isFetched, 받아온 시간인 dataUpdateAt 등이 있습니다!
 
그리고 status라는 상태도 있습니다.
 

Status : 값의 유무와 함수 실행 유무

 
리액트 쿼리에는 Query Status, Fetch Status 이렇게 2개의 상태가 있습니다.
전자는 데이터를 받아왔는지 그 유무를 표시하고, 후자는 쿼리함수가 실행중인지를 표시합니다.
 
query status의 상태는 pending, success, error로 이전에 js를 공부할 때 본 promise 객체의 상태(pending, fulfilled, rejected)와도 유사합니다.
 
useQuery가 실행되면서 데이터를 받아오기 전엔 pending, 받아온 후엔 success 상태가 됩니다.
 
 
fetch status도 fetching, paused, idle이라는 세가지 상태를 가집니다.
쿼리 함수가 실행중이라면 fetching, 시작후 중지(네트워크 이슈 등의 이유로)되었다면 paused, 아무것도 하지 않고 있다면 idle 상태입니다.
 
처음 useQuery()가 실행되면 아직 쿼리함수가 실행되지 않았으니 paused 상태로 시작해서, 쿼리 함수 실행시 fetching 상태가 되고, 쿼리 함수 실행이 끝나면 idle 상태가 되고, 이후 다시 또 받아올 작업이 발생하면 쿼리 함수가 재실행 되면서 fetching 상태가 되는 그런 흐름입니다.
 
이렇게 두 상태는 다양한 조합을 이룰 수 있습니다.
 
 

Cache : 우리가 아는 그 캐싱. 비용 감소!

 
useQuery()는 데이터를 받아오고, 그 데이터를 캐시에 저장합니다.
만약 다시 useQuery()가 실행되었을 때, 이미 저장된 데이터가 캐시에 있으면 어떻게 될까요?
 
캐시에 저장된 데이터는 staletime(디폴트는 0) 이라는 시간이 지나기 전엔 fresh, 지나면 stale, 컴포넌트가 언마운트 되면 inactive 상태가 됩니다.
따라서 fresh 상태라면 캐시에 있는 데이터를 리턴하고, stale 상태라면 다시 refetch하게 되는거죠.
 
stale 상태일 때,
새로운 쿼리 인스턴스가 마운트 되거나, 브라우저 창에 다시 포커스가 가거나, 네트워크가 재연결되거나, refetch 인터벌 시간이 지나면 refetch하게 됩니다.
 
 

쿼리키 : 계층적으로 지정 가능

 
쿼리키는 배열 구조를 갖고 있는데, 이 덕분에 계층적으로 지정할 수 있습니다.
 
만약 특정 user 만의 post를 받아오고 싶다면, fetch api를 아래와 같이 작성할 수 있습니다.

export async function getPostsByUsername(username) {
  const response = await fetch(`${BASE_URL}/posts?username=${username}`);
  return await response.json();
}

 
그리고, 아래와 같이 쿼리키를 지정할 수 있습니다.

function HomePage() {
  const username = 'codeit'; // 임의로 username을 지정
  const { data: postsDataByUsername } = useQuery({
    queryKey: ['posts', username],
    queryFn: () => getPostsByUsername(username),
  });
  console.log(postsDataByUsername);
  
  return <div>홈페이지</div>;
}

 
이렇게 배열을 활용해서 상황에 따라 다양한 파라미터를 가지고 쿼리키를 설정할 수 있습니다.
나만보기 설정이 되어 있다면, 쿼리키에 {status : private} 와 같은 객체를 설정할 수도 있을 것입니다.
 
위 코드를 보면 쿼리함수에서 화살표 함수 형태로 아규먼트에 username을 전달한다. 이렇게 Promise를 리턴한다면 화살표 함수 등 어떤 형태의 함수여도 사용 가능합니다.
 
단, 객체를 쿼리키로 전달한다면 그 객체 내부에서는 순서 상관이 없지만,
그냥 리스트 내부에서 여러 요소를 쿼리 키를 전달하면 순서가 생깁니다. 순서가 다르면 다 다른 쿼리로 인식하게 됩니다.
 
 

에러 및 로딩 처리

아까 봤듯이 useQuery에서는 여러 상태를 리턴받는데,
그 중 isPending을 통해 로딩, isError를 통해 에러를 처리할 수 있습니다.
 
useQuery 함수를 통해 데이터와 함께 isPending, isError를 받아온 후,
 

if (isPending) return '로딩 중입니다...';
if (isError) return '에러가 발생했습니다.';

다음과 같은 구문으로 에러나 오류를 처리할 수 있다.
 
 

useMutation : 데이터 추가(Post)

 
데이터 추가와 같은 사이드이펙트를 실행하기 위해서는 useMutatuon()이라는 Hook을 사용합니다.
 

export async function uploadPost(newPost) {
  const response = await fetch(`${BASE_URL}/posts`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newPost),
  });

  if (!response.ok) {
    throw new Error('Failed to upload the post.');
  }
  return await response.json();
}

위와 같은 Post하는 uploadPost라는 함수(api)가 있다는 가정 하에,
 

  const uploadPostMutation = useMutation({
    mutationFn: (newPost) => uploadPost(newPost),
  });

  const handleInputChange = (e) => {
    setContent(e.target.value);
  }

  const handleSubmit = (e) => {
    e.preventDefault();
    const newPost = { username: 'codeit', content };
    uploadPostMutation.mutate(newPost);
    setContent('');
  };

위와 같이 handleInputChange와 handleSubmit이라는 지극히 평범한 form을 입력하고 업데이트하는 함수를 만들고,
useMutation()을 활용해서 api를 불러온 후 업로드 버튼을 눌렀을 때 mutate() 함수를 실행하도록 합니다.
그럼 새로운 데이터가 추가됩니다.
 
그러나 아직 캐시에 있는 데이터가 업데이트 되지 않았습니다.
이 또한 자동화시켜줘야겠죠?
 

useQueryClient 훅의 invalidateQueries 함수

 
이 함수를 실행하면 캐시의 데이터를 모두 stale 상태로 만들고, 백그라운드에서 refetch하게 만듭니다.

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

const queryClient = useQueryClient();
// ...
queryClient.invalidateQueries();

위와 같이 작성할 수 있습니다.
 

const uploadPostMutation = useMutation({
  mutationFn: (newPost) => uploadPost(newPost),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['posts'] });
  },
});

useMutation 함수의 객체에는 onSuccess, onMutate, onError 등의 옵션이 있기에
onSuccess 상태가 되었을 때 invalidateQueries 함수를 실행해주면 될 것입니다.
 
useMutation()에 등록한 콜백함수 이후에 mutate()에 등록한 콜백 함수들이 실행되므로 거기다 달아주어도 됩니다. 그러나 이 경우엔 컴포넌트가 언마운트 되면 실행되지 않기에 컴포넌트에 종속적인 로직만 mutate()에 달아주어야 합니다.
 

<button
  disabled={uploadPostMutation.isPending || !content}
  type='submit'
>
  업로드
</button>

또한 mutation은 isPending이라는 값이 있기 때문에 위처럼 코드를 짜서 중복 업로드를 막을 수 있습니다.