728x90
728x90

리액트 쿼리(Tanstack Query, React Query)

들어가며

  • React Query라고 불린 Tanstack Query 라이브러리에 대해 정리해본다.

Tanstack Query(React Query)

개념

  • 리액트 애플리케이션에서 서버 상태(데이터)를 쉽게 관리하고 조작할 수 있도록 도와주는 라이브러리
  • React Query2022년 초 Tanstack Query로 이름이 변경되었다.
    • React Query가 포함된 Tanstack 생태계를 확장하고, 다양한 프레임워크와 플랫폼에서 사용할 수 있는 범용 데이터 페칭 및 상태 관리 라이브러리를 목표로 하기 위해서 였다고 한다.
    • Tanstack Query는 이제 React뿐만 아니라 Vue, Svelte, Solid와 같은 다른 프레임워크에 대한 지원도 포함하고 있으며, Tanstack Query로의 이름 변경은 React Query의 기능적 변화보다는 브랜딩과 생태계 확장을 반영한 것이라고 한다.
  • 자동 백그라운드 리페칭, 캐싱, 상태 관리, 에러 처리, 간단한 페이지네이션, 무한 스크롤 기능을 구현하는데 유용하다.

 

설치

  • 터미널에 아래 명령을 실행하여 설치할 수 있다.
$ npm install @tanstack/react-query   # yarn add @tanstack/react-query

 

주요 기능

데이터 페칭(Fetching Data)
  • 데이터를 서버에서 가져와서 리액트 컴포넌트와 동기화하는 작업을 간단하게 처리할 수 있다.
  • @useQuery@ 훅을 사용하여 데이터를 가져오고, 로딩 상태와 오류 상태를 쉽게 관리할 수 있다.

 

데이터 변조(Mutating Data)
  • 데이터를 서버에 보내서 생성, 수정, 삭제하는 작업을 수행할 수 있다.
  • @useMutation@ 훅을 사용하여 이러한 변조 작업을 간편하게 수행할 수 있다.

 

자동 리페치(Automatic Refetching)
  • 사용자가 탭을 전환하거나 네트워크 상태가 변경될 때 데이터를 자동으로 다시 가져올 수 있다.
  • 특정 이벤트나 시간 간격에 따라 자동으로 데이터를 새로 고칠 수 있다.

 

캐싱(Caching)
  • 가져온 데이터를 캐시에 저장하여 동일한 데이터 요청 시 불필요한 네트워크 요청을 줄일 수 있다.
  • 캐시된 데이터를 사용하면서 배경에서 새로운 데이터를 가져오는 기능을 제공한다.

 

병렬 및 의존 쿼리(Parallel and Dependent Queries)
  • 여러 쿼리를 병렬로 실행하거나, 특정 쿼리가 완료된 후에 다른 쿼리를 실행할 수 있다.

 

쿼리 무효화(Query Invalidation)
  • 특정 이벤트가 발생할 때 캐시된 데이터를 무효화하고, 데이터를 새로 가져오게 할 수 있다.

 

낙관적 업데이트(Optimistic Updates)
  • 데이터를 변경할 때, 서버의 응답을 기다리지 않고 UI를 즉시 업데이트하여 사용자 경험을 향상시킬 수 있다.

 

 

Tanstack Query를 사용해야 하는 이유?

간편함
  • 복잡한 상태 관리 로직을 단순화하여 코드의 가독성과 유지 보수성을 높인다.
  • 내장된을 사용하여 데이터 페칭상태 관리를 쉽게 처리할 수 있다.

 

효율성
  • 캐싱 자동 리페치 기능을 통해 불필요한 네트워크 요청을 줄이고, 애플리케이션의 성능을 향상시킨다.

 

확장성
  • 병렬 및 의존 쿼리, 쿼리 무효화 등 고급 기능을 통해 복잡한 요구사항도 손쉽게 처리할 수 있다.

 

사용자 경험 향상
  • 낙관적 업데이트, 자동 리페치 등의 기능을 통해 사용자에게 더욱 반응성 있는 UI를 제공할 수 있다.

 

설정 방법

  • @main.jsx@ 파일에 다음과 같이 추가한다.
/src/main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

 

사용 방법

/src/utils/utils.js
import axios from 'axios';

const customFetch = axios.create({
  baseURL: 'http://localhost:5001/api/tasks',
});

export default customFetch;

 

① @useQuery@ : GET

  • 다음과 같이 @useQuery@를 @@tanstack/react-query@에서 import 한 후 사용한다.
    • @useQuery@는 React Query에서 서버의 데이터를 가져오는 작업을 관리하는 데 사용하는 훅이다.
    • 주로 서버에서 데이터를 읽기하는 작업(GET 요청)을 수행하고, 그 데이터를 컴포넌트에서 사용할 수 있게 해준다.
  • @queryKey@
    • 고유키(Unique Key)
    • 애플리케이션 전반적으로 리페칭, 캐싱, 쿼리 공유 등을 위해 사용된다.
  • @queryFn@
    • 프로미스(Promise) 객체를 반환하는 함수
    • 해당 함수는 데이터를 @resolve@ 하거나, 에러를 @throw@를 해야 한다.
import { useQuery } from '@tanstack/react-query';

const result = useQuery({
  queryKey: ['tasks'],
  queryFn: () => customFetch.get('/'),
});
console.log(result);

개발자 도구의 콘솔에서 여러개의 속성들을 확인할 수 있다.

 

사용 예제
  • 데이터가 불러와지는 중에는 @Loading...@이라는 문자가 표시되고, 데이터가 모두 불러와졌을 때 불러와진 데이터가 표시된다.
  • @isError@는 에러가 발생했다는 메시지를 표시할 때 사용할 수 있고, @error@는 실제 에러 메시지의 내용을 표시할 때 사용할 수 있다.
import { useQuery } from '@tanstack/react-query';

import customFetch from '../utils/utils';

import SingleItem from './SingleItem';

const Items = () => {
  // cf. React Query V5부터 isLoading 대신 isPending을 사용한다.
  const { isPending, data, isError, error } = useQuery({
    queryKey: ['tasks'],
    queryFn: async () => {
      const { data } = await customFetch.get('/');
      return data;
    },
  });

  if (isPending) {
    return <p style={{ marginTop: '1rem' }}>Loading...</p>;
  }

  // 에러가 발생했다는 메시지 표시하기
  if (isError) {
    return <p style={{ marginTop: '1rem' }}>There was an error...</p>;
  }

  // 실제 에러 메시지 표시하기
  // if (error) {
  //   return <p style={{ marginTop: '1rem' }}>{error.response.data}</p>;
  // }

  let fetchedData = data.taskList;

  return (
    <div className="items">
      {fetchedData.map((item) => {
        return <SingleItem key={item.id} item={item} />;
      })}
    </div>
  );
};

export default Items;

 

② @useMutation@ : POST, PATCH, DELETE

  • 다음과 같이 @useMutation@을 @@tanstack/react-query@에서 import 한 후 사용한다.
    • @useMutation@은 React Query에서 데이터의 변경(생성, 수정, 삭제)과 같은 변경성 작업(Mutation)을 처리하는 데 사용하는 훅이다.
    • 주로 서버에 데이터를 보내거나, 서버의 상태를 변경하는 요청을 보낼 때 사용된다.
    • @useMutation@은 데이터를 읽기하는 @useQuery@와 달리, 수정/변경하는 작업을 관리한다.
  • @mutationFn@
    • 실제 데이터를 변경하는 함수
  • @onSuccess@
    • Helper Option
    • 요청 성공 시 수행할 내용들 넣기
  • @onError@
    • Helper Option
    • 요청 실패 시 수행할 내용들 넣기
import { useMutation } from '@tanstack/react-query';

const { mutate, isPending } = useMutation({
  mutationFn: () => customFetch.post('/', { data }),
  onSuccess: () => { ... },
  onError: () => { ... },
});

 

사용 예제 : POST
  • @useQueryClient@를 import 하여 다른 컴포넌트에서 @useQuery@를 통해 캐싱한 데이터에 접근할 수 있다.
import { useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

const { mutate: createTask, isPending } = useMutation({
    // ...
 
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    },
    
    // ...
  });

 

import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';

import customFetch from '../utils/utils';

const Form = () => {
  const [newItemName, setNewItemName] = useState('');

  const queryClient = useQueryClient();

  const { mutate: createTask, isPending } = useMutation({
    mutationFn: (taskTitle) => {
      return customFetch.post('/', { title: taskTitle });
    },
    onSuccess: () => {
      if (newItemName) {
        // 기존의 캐시된 데이터를 무효화하고, 해당 쿼리에 대한 데이터를 새로고침하기
        queryClient.invalidateQueries({ queryKey: ['tasks'] });

        // 성공 메시지 표시하기
        toast.success('task added.');

        // 입력창 비우기 및 화면 리랜더링
        setNewItemName('');
      }
    },
    onError: (error) => {
      // 에러 메시지 표시하기
      toast.error(error.response.data.msg);
    },
  });

  const handleSubmit = async (e) => {
    e.preventDefault();

    // 작업 추가하기
    createTask(newItemName);
  };

  return (
    <form onSubmit={handleSubmit}>
      <h4>task bud</h4>
      <div className="form-control">
        <input
          type="text "
          className="form-input"
          value={newItemName}
          onChange={(event) => setNewItemName(event.target.value)}
        />
        <button type="submit" className="btn" disabled={isPending}>
          add task
        </button>
      </div>
    </form>
  );
};
export default Form;

 

queryClient.invalidateQueries({ queryKey: ['tasks'] });는 React Query에서 캐시된 데이터를 무효화하고, 해당 쿼리에 대한 데이터를 새로고침하도록 지시한다.

 

사용 예제 : PATCH, DELETE
import { useMutation, useQueryClient } from '@tanstack/react-query';

import customFetch from '../utils/utils';

const SingleItem = ({ item }) => {
  const queryClient = useQueryClient();

  // PATCH : 수정
  const { mutate: editTask } = useMutation({
    mutationFn: ({ taskId, isDone }) => {
      return customFetch.patch(`/${taskId}`, { isDone });
    },
    onSuccess: () => {
      // 기존의 캐시 무효화 및 새로고침
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    },
  });

  // DELETE : 삭제
  const { mutate: deleteTask, isPending } = useMutation({
    mutationFn: ({ taskId }) => {
      return customFetch.delete(`/${taskId}`);
    },
    onSuccess: () => {
      // 기존의 캐시 무효화 및 새로고침
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    },
  });

  return (
    <div className="single-item">
      <input
        type="checkbox"
        checked={item.isDone}
        onChange={() => editTask({ taskId: item.id, isDone: !item.isDone })}
      />
      <p
        style={{
          textTransform: 'capitalize',
          textDecoration: item.isDone && 'line-through',
        }}
      >
        {item.title}
      </p>
      <button
        className="btn remove-btn"
        type="button"
        disabled={isPending}
        onClick={() => deleteTask({ taskId: item.id })}
      >
        delete
      </button>
    </div>
  );
};

export default SingleItem;

 

(참고) 커스텀 훅으로 만들어서 사용하기

  • 위의 코드들을 다음과 같이 커스텀 훅으로 만들어서 사용할 수 있다.
/src/hooks/reactQueryCustomHooks.jsx
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';

import customFetch from '../utils/utils';

// GET : 데이터 불러오기
export const useFetchTasks = () => {
  const { isPending, data, isError, error } = useQuery({
    queryKey: ['tasks'],
    queryFn: async () => {
      const { data } = await customFetch.get('/');

      return data;
    },
  });
  return { isPending, isError, error, data };
};

// POST : 데이터 생성
export const useCreateTask = () => {
  const queryClient = useQueryClient();

  const { mutate: createTask, isPending: createTaskLoading } = useMutation({
    mutationFn: (taskTitle) => {
      return customFetch.post('/', { title: taskTitle });
    },
    onSuccess: () => {
      // 기존의 캐시된 데이터를 무효화하고, 해당 쿼리에 대한 데이터를 새로고침하기
      queryClient.invalidateQueries({ queryKey: ['tasks'] });

      // 성공 메시지 표시하기
      toast.success('task added.');
    },
    onError: (error) => {
      // 에러 메시지 표시하기
      toast.error(error.response.data.msg);
    },
  });

  return { createTask, createTaskLoading };
};

// PATCH : 데이터 수정
export const useEditTask = () => {
  const queryClient = useQueryClient();

  const { mutate: editTask } = useMutation({
    mutationFn: ({ taskId, isDone }) => {
      return customFetch.patch(`/${taskId}`, { isDone });
    },
    onSuccess: () => {
      // 기존의 캐시 무효화 및 새로고침
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    },
  });

  return { editTask };
};

// DELETE : 데이터 삭제
export const useDeleteTask = () => {
  const queryClient = useQueryClient();

  const { mutate: deleteTask, isPending: deleteTaskLoading } = useMutation({
    mutationFn: ({ taskId }) => {
      return customFetch.delete(`/${taskId}`);
    },
    onSuccess: () => {
      // 기존의 캐시 무효화 및 새로고침
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    },
  });

  return { deleteTask, deleteTaskLoading };
};

 

  • 나머지 코드 확인하기
더보기
/src/components/Items.jsx
import { useFetchTasks } from '../hooks/reactQueryCustomHooks';

import SingleItem from './SingleItem';

const Items = () => {
  // 커스텀 훅 불러오기
  const { isPending, isError, error, data } = useFetchTasks();

  if (isPending) {
    return <p style={{ marginTop: '1rem' }}>Loading...</p>;
  }

  // 에러가 발생했다는 메시지 표시하기
  if (isError) {
    return <p style={{ marginTop: '1rem' }}>There was an error...</p>;
  }

  // 실제 에러 메시지 표시하기
  // if (error) {
  //   return <p style={{ marginTop: '1rem' }}>{error.response.data}</p>;
  // }

  const fetchedData = data.taskList;

  return (
    <div className="items">
      {fetchedData.map((item) => {
        return <SingleItem key={item.id} item={item} />;
      })}
    </div>
  );
};

export default Items;

 

/src/components/Form.jsx
import { useState } from 'react';

import { useCreateTask } from '../hooks/reactQueryCustomHooks';

const Form = () => {
  const [newItemName, setNewItemName] = useState('');
  
  const { createTaskLoading, createTask } = useCreateTask();

  const handleSubmit = (e) => {
    e.preventDefault();

    // 작업 추가
    // -> createTask의 두 번째 인자에 작업 수행 후 수행할 작업을 추가할 수 있다.
    createTask(newItemName, {
      onSuccess: () => {
        setNewItemName('');
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h4>task bud</h4>
      <div className="form-control">
        <input
          type="text "
          className="form-input"
          value={newItemName}
          onChange={(event) => setNewItemName(event.target.value)}
        />
        <button type="submit" className="btn" disabled={createTaskLoading}>
          add task
        </button>
      </div>
    </form>
  );
};
export default Form;

 

⇒ @mutate(value, {})@에서 두 번째 인자(@{}@)에 mutate 작업이 수행된 후, 수행할 작업들을 추가할 수 있다. 

 

/src/components/SingleItem.jsx
import { useDeleteTask, useEditTask } from '../hooks/reactQueryCustomHooks';

const SingleItem = ({ item }) => {
  const { editTask } = useEditTask();
  const { deleteTask, deleteTaskLoading } = useDeleteTask();

  return (
    <div className="single-item">
      <input
        type="checkbox"
        checked={item.isDone}
        onChange={() => editTask({ taskId: item.id, isDone: !item.isDone })}
      />
      <p
        style={{
          textTransform: 'capitalize',
          textDecoration: item.isDone && 'line-through',
        }}
      >
        {item.title}
      </p>
      <button
        className="btn remove-btn"
        type="button"
        disabled={deleteTaskLoading}
        onClick={() => deleteTask({ taskId: item.id })}
      >
        delete
      </button>
    </div>
  );
};

export default SingleItem;

 

예제 코드

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

// QueryClient 인스턴스 생성
const queryClient = new QueryClient();

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

function MyComponent() {
  // 데이터 페칭
  const { data, error, isLoading } = useQuery('fetchData', fetchData);

  if (isLoading) return <span>Loading...</span>;
  if (error) return <span>Error: {error.message}</span>;

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

async function fetchData() {
  const response = await fetch('/api/data');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
}

 

참고

  • React Query V4까지 사용되었던 @isLoading@ 속성이 V5부터는 @isPending@으로 바뀌었다.
  • Tanstack Query의 사용은 필수가 아니다. @useEffect@와 @fetch@를 이용하여 동일한 기능을 언제든지 만들 수 있다.

 

참고 사이트

 

TanStack Query

Instead of writing reducers, caching logic, timers, retry logic, complex async/await scripting (I could keep going...), you literally write a tiny fraction of the code you normally would. You will be surprised at how little code you're writing or how much

tanstack.com

 

TanStack | High Quality Open-Source Software for Web Developers

Headless, type-safe, powerful utilities for complex workflows like Data Management, Data Visualization, Charts, Tables, and UI Components.

tanstack.com

 

728x90
728x90