본문 바로가기

클라이언트/React
[리액트(React)] Tanstack Query(React Query)

// Tanstack Query

- 서버 상태 관리를 용이하게 해주는 라이브러리

- React Query로 알려져 있었다.

- 리액트 앱 내부에서 서버에 HTTP 요청을 간편하게 보낼 수 있게 해준다.

- HTTP 요청을 직접 전송하는 로직이 내장된 것이 아니라, 요청을 관리하는 로직을 제공해준다.

- 공식 문서: https://tanstack.com/query/latest/docs/framework/react/overview

 

Overview | TanStack Query Docs

 

tanstack.com


- 서버에서 가져온 데이터를 cache 처리하고 메모리에 저장해서 필요할 때 다시 사용할 수 있다.


1. 설치

npm install @tanstack/react-query

2. Tanstack query 사용할 컴포넌트 감싸기

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  );
}


- queryClient를 공유하면, queryKey를 이용해 원하는 query를 만료시키고 refetch 시킬 수 있다.

queryClient.invalidateQueries({ queryKey: [''], exact:true });
  • exact: true로 설정하면 queryKey가 정확히 일치하는 것만 만료시킬 수 있다.

// 데이터 가져오기 (useQuery)

- useQuery hook은 자체적으로 HTTP request를 전송하여 response를 받는다.

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

const { data, isPending, isError, error } = useQuery({
  queryKey: [],
  queryFn: query함수,
});
  • data : response data를 받는다.
  • isPending: request 진행 상태를 받는다.
  • isError: 오류 response 여부를 받는다.
  • error: 발생한 오류의 정보를 받는다.

  • queryKey: request로 생성된 data의 cache 처리에 활용한다.
      > 값은 배열인데, 요소가 여러 개일 수도 있고, 문자열이 아니어도 된다.
  • queryFn : 실제 request를 전송할 때 실행할 코드를 정의한다.

import {
  createBrowserRouter,
  Navigate,
  RouterProvider,
} from 'react-router-dom';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  );
}

export default App;

 

import LoadingIndicator from '../UI/LoadingIndicator.jsx';
import ErrorBlock from '../UI/ErrorBlock.jsx';
import EventItem from './EventItem.jsx';
import { useQuery } from '@tanstack/react-query';
import { fetchEvents } from '../../util/http.js';

export default function NewEventsSection() {
  const { data, isPending, isError, error } = useQuery({
    queryKey: ['events'],
    queryFn: fetchEvents,
  });

  let content;

  if (isPending) {
    content = <LoadingIndicator />;
  }

  if (error) {
    content = (
      <ErrorBlock
        title="An error occurred"
        message={error.info?.message || 'Failed to fetch events.'}
      />
    );
  }

  if (data) {
    content = (
      <ul className="events-list">
        {data.map((event) => (
          <li key={event.id}>
            <EventItem event={event} />
          </li>
        ))}
      </ul>
    );
  }

  return (
    <section className="content-section" id="new-events-section">
      <header>
        <h2>Recently added events</h2>
      </header>
      {content}
    </section>
  );
}

 

import axios from 'axios';

export async function fetchEvents() {
  try {
    const { data } = await axios.get('http://localhost:3000/events');
    return data.events;
  } catch (error) {
    if (error.response) {
      const customError = new Error(
        'An error occurred while fetching the events',
      );
      customError.code = error.response.status;
      customError.info = error.response.data;
      throw customError;
    } else if (error.request) {
      throw new Error('No response was received');
    } else {
      throw new Error('Error in setting up the request');
    }
  }
}

// Cache 처리

- request를 통해 받은 response data를 cache 처리하고, 나중에 동일한 queryKey 를 가진 또다른 useQuery가 실행되면 이 data를 재사용한다.

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

const { data, isPending, isError, error } = useQuery({
  queryKey: [],
  queryFn: query함수,
  staleTime: ms,
  gcTime: ms,
});
  • staleTime: 업데이트된 데이터를 가져오기 위한 request를 전송하기 전 기다릴 시간
    > 기다리는 동안은 cache에 있는 data를 사용한다.
  • gcTime: data와 cache를 보관할 시간(garbage collector)

// 동적 query

- query key를 동적으로 사용하면, 같은 query를 사용하더라도 key마다 각기 다른 data를 cache 처리할 수 있다.

const { data, isPending, isError, error } = useQuery({
  queryKey: ['', { 동적 value }],
  queryFn: ({ signal }) => 비동기함수({ signal, 동적 value }),
  enabled: Boolean값
});


- 요청 취소 정보인 signal도 queryFn의 매개변수로 받아서 request의 config로 넘겨주어야 동적 query를 사용하지 않는 query도 문제없이 작동한다.

  • enabled: 쿼리 활성화/비활성화를 설정할 수 있다.
    - false 시 비활성화, true 시 활성화

- 쿼리가 비활성화 되면 상태를 대기 중으로 처리하기 때문에 isPending이 true가 된다.
   > isPending이 아닌 isLoading을 사용하면 쿼리가 비활성화 되더라도 true가 되지 않는다.


// 데이터 보내기 (useMutation)

- useQuery hook은 자체적으로 HTTP request를 전송하여 response를 받는다.

import { useMutation } from '@tanstack/react-query';

const { mutate, isPending, isError, error } = useMutation({
    mutationFn: 함수,
    onSuccess: () => {},
    onMutate:async  (data) => {
      const 기존data = queryClient.getQueryData([update하려는queryKey]);
      
      await queryClient.cancelQueries({ queryKey: [update하려는queryKey]});
      queryClient.setQueryData([update하려는queryKey], data);
      
      return {기존data};
    }
    onError: (error, data, context) => {
      queryClient.setQueryData([update하려는queryKey], context.기존data);
    },
  });
    

mutate(data);
  • mutate : 해당 컴포넌트 어디서든 mutationFn을 호출해서 요청을 전송할 수 있게 해준다.
  • isPending: request 진행 상태를 받는다.
  • isError: 오류 response 여부를 받는다.
  • error: 발생한 오류의 정보를 받는다.

  • mutationKey: request로 생성된 data의 cache 처리에 활용한다.
      > queryKey와 다르게 mutation은 response data를 cache 처리하지 않기 때문에 필수X
  • mutationFn : 실제 request를 전송할 때 실행할 코드를 정의한다.
  • onSuccess : request 성공 시 처리할 내용을 정의한다.
  • onMutate :  mutate를 호출하는 즉시 실행된다.
    > react-query가 자동으로 mutate 함수에 넘긴 data를 받아온다.
  • onError: onMutate의 반환값을 context로 받아온다.
  • onSettled: 성공 여부와 관계 없이 mutationFn이 완료될 때마다 호출된다.

import { Link, useNavigate } from 'react-router-dom';

import Modal from '../UI/Modal.jsx';
import EventForm from './EventForm.jsx';
import { useMutation } from '@tanstack/react-query';
import { createNewEvent, queryClient } from '../../util/http.js';
import ErrorBlock from '../UI/ErrorBlock.jsx';

export default function NewEvent() {
  const navigate = useNavigate();

  const { mutate, isPending, isError, error } = useMutation({
    mutationFn: createNewEvent,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['events'] });
      navigate('/events');
    },
  });

  function handleSubmit(formData) {
    mutate({ event: formData });
  }

  return (
    <Modal onClose={() => navigate('../')}>
      <EventForm onSubmit={handleSubmit}>
        {isPending && 'Submitting...'}
        {!isPending && (
          <>
            <Link to="../" className="button-text">
              Cancel
            </Link>
            <button type="submit" className="button">
              Create
            </button>
          </>
        )}
      </EventForm>
      {isError && (
        <ErrorBlock
          title="Failed to create event"
          message={
            error.info?.message ||
            'Failed to create event. Please check your inputs and try agin later'
          }
        />
      )}
    </Modal>
  );
}

 

export async function createNewEvent(eventData) {
  try {
    const { data } = await axios.post(
      'http://localhost:3000/events',
      eventData,
    );
    return data.event;
  } catch (error) {
    if (error.response) {
      const customError = new Error(
        'An error occurred while creating the event',
      );
      customError.code = error.response.status;
      customError.info = error.response.data;
      throw customError;
    } else if (error.request) {
      throw new Error('No response was received');
    } else {
      throw new Error('Error in setting up the request');
    }
  }
}

※ react-query v3 이상에서 useQuery는 isPending 대신 isLoading을 주로 사용한다.

※ react-query v5 이상에서 useMutation은 isLoading 대신 isPending을 사용한다.