본문 바로가기

TanStack Query 기본 사용법

codeConnection 2024. 6. 24.

TanStack Query란?

TanStack Query(FKA React 쿼리)는 흔히 웹 애플리케이션을 위한 데이터 가져오기 라이브러리정도로 설명되지만 보다 기술적인 측면에서 보면 웹 애플리케이션에서 서버 상태를 쉽게 가져오기, 캐싱, 동기화 및 업데이트를 할 수 있게 도와주는 라이브러리이다.

 

TanStack Query의 필요성

리액트와 같은 라이브러리 등은 데이터를 전반적으로 가져 오거나 업데이트를 하는 방법에 대해서는 명확한 가이드를 제공해주지 않고 있다. 그래서 데이터를 가져오는 방법을 담은 메타 프레임워크를 만들 거나 자신만의 방법을 만들어서 사용하기도 한다.

 

기존 상태 관리 라이브러리는 클라이언트 상태 관리에는 적합하지만 서버 상태를 관리하기에는 적합하지 않다.

 

일반적으로 서버의 특성은 다음과 같다.

  • 클라이언트가 제어하거나 소유할 수 없다. 서버는 다른 위치에 존재하며 원격으로 유지된다.
  • 데이터를 가져오거나 업데이트 하기 위해서는 비동기 API가 필요하다.
  • 서버는 클라이언트의 소유가 아니며, 클라이언트가 모르게 다른 사람이 변경할 수도 있다.
  • 클라이언트가 서버의 정보를 계속 실시간으로 받아오지 않는 이상 클라이언트가 보고 있는 앱에서는 실제 서버의 데이터와 다르게 오래된 정보를 보고 있을 수도 있다.

아래의 일들은 클라이언트가 하기 어려운 일들이다. 그리고 탄스택 쿼리가 제공하는 기능이기도 하다.

  • 캐싱
  • 동일한 데이터 요청을 단일 요청으로 중복을 제거하기
  • 클라이언트의 앱 백그라운드에서 오래된 데이터 업데이트하기
  • 데이터가 오래된 시기 파악하기
  • 가능한 한 빨리 데이터 업데이트 하기
  • 페이지네이션, 지연 로딩과 같은 성능 최적화
  • 서버 상태의 메모리 및 가비지 컬렉션 관리
  • 구조적 공유를 통한 쿼리 결과 메모화

TanStack Query 라이브러리 설치하기

yarn add @tanstack/react-query

 

TanStack Query는 React 18 버전 이상부터 호환이 가능하다.

 

TanStack Query용으로 버그를 잡는 데 도움이 되는 ESLint도 제공한다.

yarn add -D @tanstack/eslint-plugin-query

 

DevTools를 설치하면 데이터가 어떤 상태인지 쉽게 파악할 수 있다.

yarn add @tanstack/react-query-devtools

 

DevTools는 설치 후 약간의 설정을 더 해주어야 한다.

 

일단 탄스택 쿼리를 사용하려면 Provider로 감싸주어야 한다.

데브툴즈는 추후 설명하겠다.

// main.jsx

import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

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

주요 개념

  • Queries : 서버에서 데이터를 가져오는 작업을 의미한다. 쿼리를 사용하면 데이터가 자동으로 캐싱되고, 새로고침이나 네트워크 재연결 시 데이터를 다시 가져온다. useQuery 훅을 사용하여 쿼리를 정의할 수 있다.
  • Mutations : 서버 데이터를 보내는 작업을 의미한다. 데이터를 추가하거나 업데이트하는, CURD 중 CUD에 해당하는 작업을 할 때 사용한다. useMutation 훅을 사용하여 뮤테이션을 정의할 수 있다.
  • Query Invalidation : 이미 캐싱된 쿼리를 무효화하고 이를 통해서 새로운데이터를 다시 가져오도록 하는 방법이다. 이것은 데이터가 변경되었을 때, 예를 들어 사용자가 CUD 를 했을 때 캐싱된 데이터를 무효화하고 새로운 데이터로 보게 하는 방법이다.
  • Cacing : 탄스택 쿼리는 데이터를 캐싱하여 성능을 향상시킨다. 동일한 쿼리가 여러 번 실행되더라도 요청을 최소화하고 캐시된 데이터를 반환한다.
  • Automatic Refeching : 데이터가 변경될 때마다 자동으로 최신 데이터를 가져온다. 뮤테이션 후 쿼리가 자동으로 다시 실행되어 최신 데이터를 가져오는 방식이다.
  • Background Fetching : 사용자가 앱을 사용하는 동안에도 백그라운드에서 데이터가 업데이트 할 수 있다. 이렇게 되면 사용자가 앱을 켜놓고 오래 지나더라도 사용자가 항상 최신 데이터를 보게 할 수 있다.
  • DevTools : 탄스택 쿼리에서는 개발자 편의를 돕는 라이브러리를 제공하기 때문에 데이터가 어떤 상태인지 쉽게 모니터링 할 수 있다.

실습을 위한 준비물

서버에서 가져올 데이터를 json-server를 통해 만들어보겠다.

yarn add json-server

 

이후 루트 폴더에 db.json 폴더를 만들고 아래의 더미 데이터를 삽입한다. 아래는 더미 데이터이고 실제가 아니다.

{
    "users": [
        {
            "_id": "1f2ca396-acdf-4970-Aa82-cd6e49077d6b",
            "index": "1",
            "name": "장태겸",
            "email": "user-xqx3e26@sem.biz",
            "phone": "010-2020-4018",
            "country": "라트비아",
            "address": "양평로 85-1",
            "job": "변리사"
        },
        {
            "_id": "398f5d18-10a1-4078-Ab80-d15e90095e99",
            "index": "2",
            "name": "대예율",
            "email": "user-9l4ps24@Pulvinar.biz",
            "phone": "010-2860-9492",
            "country": "마르티니크",
            "address": "동일로 42-3",
            "job": "전자공학기술자"
        },
        {
            "_id": "d9f644a0-653c-4409-Aab9-b69f09b7356d",
            "index": "3",
            "name": "장승희",
            "email": "user-ukv0z50@Sagittis.biz",
            "phone": "010-6474-7275",
            "country": "요르단",
            "address": "노량진로 5-8",
            "job": "프로게이머"
        },
        {
            "_id": "4c3b9c47-9a63-4bac-B2aa-3383f059d64c",
            "index": "4",
            "name": "옹호연",
            "email": "user-5hvjvia@wants.io",
            "phone": "010-6453-5288",
            "country": "대한민국",
            "address": "망우로 22-3",
            "job": "북디자이너"
        },
        {
            "_id": "417345f1-6eae-432c-C659-49858364d606",
            "index": "5",
            "name": "요려원",
            "email": "user-vbekikf@consectetur.io",
            "phone": "010-9003-8890",
            "country": "몰도바",
            "address": "봉은사로 67-4",
            "job": "만화가"
        }
    ]
}

 

다음 만든 db 서버를 가동시킨다.

// 포트는 마음대로 해도 됨.

yarn json-server db.json --port 4000



// 이 과정이 번거로우면 package.json에 scripts에 json-server라는 명령어에 뒷 내용을 추가한다.
// 그러면 yarn json-server만으로 json-server를 구동할 수 있다.

  "scripts": {
    "json-server": "json-server db.json --port 4000"
  }

Query 기본 사용법 (R)

import './App.css'
import { useQuery } from '@tanstack/react-query'
import axios from 'axios';


function App() {

  // TanStack Query로 쿼리 요청 보내기 (데이터 불러오기)
  
  const fetchUsers = async () => {
    const response = await axios.get('http://localhost:4000/users');
    return response.data;
  }

  const { data: users, isPending, isError } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });

  if (isPending) <div>로딩 중</div>
  if (isError) <div>에러 남</div>
  
   return (
    <>
      <div>
        <form onSubmit={handleAddUser}>
          <input
            type="text"
            placeholder="이름"
            value={userName}
            onChange={(e) => setUserName(e.target.value)}
          />
          <input
            type="text"
            placeholder="전화번호"
            value={userPhone}
            onChange={(e) => setUserPhone(e.target.value)}
          />
          <button>작성하기</button>
        </form>
      </div>
      <div>
        {users && users.map((user, index) => (
          <ul key={index}>
            <li> 번호 : {user.index} </li>
            <li> 이름 : {user.name} </li>
            <li> 전화번호 : {user.phone} </li>
          </ul>
        ))}
      </div>
    </>
  )
}

export default App

 

  • fetch 받아오는 함수 정의
  • useQuery 사용
    • 반환 값은 data, isPending, isError가 대표적. 
    • 쿼리 키 지정, 쿼리 펑션으로 fetch 받아오는 함수 할당.
  • 반환된 data를 바인딩하여 사용하면 됨. 이름을 바꿔도 됨. (data : users)

Mutation 기본 사용법 (CUD)

import './App.css'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import axios from 'axios';
import { useState } from 'react';


function App() {

  // TanStack Query로 쿼리 요청 보내기 (데이터 불러오기)
  
  const fetchUsers = async () => {
    const response = await axios.get('http://localhost:4000/users');
    return response.data;
  }

  const { data: users, isPending, isError } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });

  if (isPending) <div>로딩 중</div>
  if (isError) <div>에러 남</div>

  // TanStack Query로 쿼리 수정 요청 보내기 (데이터 수정하기)

  const queryClient = useQueryClient();

  const [userName, setUserName] = useState('');
  const [userPhone, setUserPhone] = useState('');

  const addUser = async (newUser) => {
    await axios.post('http://localhost:4000/users', newUser)
  }

  const mutation = useMutation({
    mutationFn: addUser
  });

  const handleAddUser = (e) => {
    e.preventDefault();
    mutation.mutate({
      name: userName,
      phone: userPhone
    })
    setUserName('');
    setUserPhone('');
  }

  return (
    <>
      <div>
        <form onSubmit={handleAddUser}>
          <input
            type="text"
            placeholder="이름"
            value={userName}
            onChange={(e) => setUserName(e.target.value)}
          />
          <input
            type="text"
            placeholder="전화번호"
            value={userPhone}
            onChange={(e) => setUserPhone(e.target.value)}
          />
          <button>작성하기</button>
        </form>
      </div>
      <div>
        {users && users.map((user, index) => (
          <ul key={index}>
            <li> 번호 : {user.index} </li>
            <li> 이름 : {user.name} </li>
            <li> 전화번호 : {user.phone} </li>
          </ul>
        ))}
      </div>
    </>
  )
}

export default App

 

  • 사용자에게 입력 받을 input 값을 처리
    • 비제어 컴포넌트 : 입력 필드 별 상태 정의
    • input 필드를 만들고 form 태그로 버튼까지 품어주기
    • form 여는 태그에는 onSubmit 이벤트 핸들러로 폼 제출용 함수 연결하기
  • POST fetch 요청 보내는 함수 작성
    • useMutation() 훅으로 mutationFn에 POST 요청 함수 할당
  • 유저 폼 제출 버튼에 할당할 함수 선언
    • e.preventDefault() : 폼 제출 기본 동작 막기
    • mutation.mutate({ 입력할컬럼:보낼상태, 입력할컬럼2:보낼상태2, ... })
    • 상태 비워주기

 

그런데 여기서 문제가 있다. mutation으로 POST 요청을 보내도 화면에 그려지는 리스트가 추가되지 않는다는 것이다.

왜냐면 유저가 데이터를 보내어 수정했음에도 처음 GET 요청으로 받아 온 데이터는 캐싱되어 변하지 않기 때문이다.

 

CUD 요청이 있었다는 건, 원본 데이터에 수정이 가해졌다는 뜻이므로 유저가 최초 캐싱받은 데이터는 최신의 데이터가 아니라는 뜻이다. 따라서 쿼리로 받은 데이터를 무효화 해주어야 한다.

 

useMutation() 훅에 onSuccess 프로퍼티를 추가해주면 된다.

 

const mutation = useMutation({
  mutationFn: addUser,
  onSuccess: () => {
    alert('데이터를 성공적으로 추가하였습니다.');
    queryClient.invalidateQueries(['users']);
  }
})

 

onSuccess 속성은 데이터 CUD 요청이 성공했을 때 수행할 동작을 말한다.

특히 invalidateQueries는 ['users']라는 쿼리 키(GET요청 받아오는 함수)가 더 이상 최신의 정보가 아니니 캐싱한 것을 무효화하라는 의미이다. 즉 CUD 요청이 성공하고 나면 fetch를 다시 받아오라는 의미이다.

 

여기가지 추가된 전체코드이다.

 

import './App.css'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import axios from 'axios';
import { useState } from 'react';


function App() {

  // TanStack Query로 쿼리 요청 보내기 (데이터 불러오기)

  const fetchUsers = async () => {
    const response = await axios.get('http://localhost:4000/users');
    return response.data;
  }

  const { data: users, isPending, isError } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });

  if (isPending) <div>로딩 중</div>
  if (isError) <div>에러 남</div>

  // TanStack Query로 쿼리 수정 요청 보내기 (데이터 수정하기)

  const queryClient = useQueryClient();

  const [userName, setUserName] = useState('');
  const [userPhone, setUserPhone] = useState('');

  const addUser = async (newUser) => {
    await axios.post('http://localhost:4000/users', newUser)
  }

  const mutation = useMutation({
    mutationFn: addUser,
    onSuccess: () => {
      alert('데이터를 성공적으로 추가했습니다.');
      queryClient.invalidateQueries(['users']);
    }
  });

  const handleAddUser = (e) => {
    e.preventDefault();
    mutation.mutate({
      name: userName,
      phone: userPhone
    })
    setUserName('');
    setUserPhone('');
  }

  return (
    <>
      <div>
        <form onSubmit={handleAddUser}>
          <input
            type="text"
            placeholder="이름"
            value={userName}
            onChange={(e) => setUserName(e.target.value)}
          />
          <input
            type="text"
            placeholder="전화번호"
            value={userPhone}
            onChange={(e) => setUserPhone(e.target.value)}
          />
          <button>작성하기</button>
        </form>
      </div>
      <div>
        {users && users.map((user, index) => (
          <ul key={index}>
            <li> 번호 : {user.index} </li>
            <li> 이름 : {user.name} </li>
            <li> 전화번호 : {user.phone} </li>
          </ul>
        ))}
      </div>
    </>
  )
}

export default App

TanStack Query의 데이터 흐름 이해하기

캐시라는 개념이 추가되면서 이해해야 하는 개념이 하나 있다.

캐시는 사용자의 메모리에 데이터를 저장하는 것이고, 여기서 데이터를 불러온다고 하더라도 서버에서 불러오는 것이 아니다. 따라서 이는 신선한 최신의 데이터가 아니기도 하지만 장점은 굳이 변경된 데이터가 아닌데 계속해서 서버에 fetch를 요청해서 데이터가 낭비되는 것을 막을 수 있기도 하다.

 

만약 아래와 같이 여러 컴포넌트에서 쿼리와 뮤테이션을 하고 있다고 가정하자. 이 상태에서 데이터의 흐름을 이해하는 것이 중요하다.

 

먼저 쿼리를 여러 컴포넌트에서 재사용하기 위해 별도의 커스텀 훅으로 빼 둔다.

// src/api/useUsersQuery.js

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

const useUsersQuery = () => {
  return useQuery(['users'], fetchUsers);
};

export default useUsersQuery;

 

이를 A 컴포넌트에서 끌어다 쓴다.

// src/components/A.jsx

import useUsersQuery from '../queries/useUsersQuery';

function A() {
  const { data: users, isLoading, isError } = useUsersQuery();

...
}

 

그리고 이어서 B 컴포넌트에서도 끌어다 쓴다.

// src/components/B.jsx

import useUsersQuery from '../queries/useUsersQuery';

const { data: users, isLoading, isError } = useUsersQuery();

 

그리고 C 컴포넌트에서 뮤테이션을 하고 무효화를 한다.

 

// src/components/C.jsx

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

const queryClient = useQueryClient();

const addUser = async (newUser) => {
  await axios.post('http://localhost:4000/users', newUser);
};

const mutation = useMutation({
  mutationFn: addUser,
  onSuccess: () => {
    alert('데이터를 성공적으로 추가했습니다.');
    queryClient.invalidateQueries(['users']);
  },
});

const handleAddUser = () => {
  mutation.mutate({ name: 'New User', phone: '123-456-7890' });
};

...

 

이런 상황에서 데이터의 흐름은 이렇다.

 

A 컴포넌트가 users 쿼리 키에 해당하는 fetch 함수를 호출하여 서버에 GET 요청을 보냈다. 따라서 서버로부터 데이터를 반환받았다.

 

이어서 B 컴포넌트도 같은 users 쿼리 키를 사용했지만, 앞에서 A 컴포넌트가 이 쿼리 키로 서버에서 데이터를 받아온 것이 있기 때문에 이 데이터는 캐싱되어서 B 컴포넌트에게 값을 반환해준다. 즉 B 컴포넌트는 캐싱된 데이터를 사용하는 것이지 서버에서 받은 데이터가 아니라는 의미이다.

 

다음으로 C 컴포넌트가 뮤테이션 이후 이 쿼리 키를 무효화했다. 따라서 C 컴포넌트는 서버로부터 users 쿼리 키에 해당하는 데이터를 새롭게 C 컴포넌트에 반환해준다.

그리고 A 컴포넌트와 B 컴포넌트도 이 쿼리 키를 사용하고 있기에 새로운 데이터로 다시 캐싱하면서 데이터가 새롭게 업데이트 된다.

 

TanStack Query의 데이터 상태 흐름 관계도

 

위 관계도는 TanStack Query에서 데이터의 상태와 그 사이의 전이를 설명하고 있다.

 

주요 개념은 아래와 같다.

  • Fetching : 데이터 요청 중. 서버에서 데이터를 가져오는 상태.
  • Fresh : 데이터가 최신 상태임. staleTime이 0이 아닐 때 데이터가 최신 상태로 유지되는 시간 동안을 Fresh 상태라고 봄.
  • Stale : 데이터가 신선하지 않은 상태, 즉 오래됐을 수 있을 상태임. staleTime이 만료되면 상태가 Fresh에서 Stale로 변경됨.
  • Inactive : 현재 페이지에서 데이터가 사용되지 않는 상태. 컴포넌트가 언마운트 되면 상태가 Stale에서 Inactive로 변경됨.
  • Deleted : 캐시된 데이터가 삭제된 상태. cacheTime이 만료되면 Inactive에서 Deleted 상태로 변경됨.

상태 전이는 아래와 같은 경우의 수가 있다.

  • Fetching -> Fresh : 데이터가 성공적으로 가져와지면 상태가 Fresh로 변경됨.
  • Fresh -> Stale : staleTime이 만료되면 상태가 Fresh에서 Stale로 변경됨.
  • Stale -> Fetching : staleTime이 0이거나, refetchOnMount... 등의 조건이 충족되면 상태가 Fetching으로 돌아가서 데이터를 다시 가져옴. 데이터를 새로 가져오는 설정은 아래와 같음.
    • staleTime : 데이터가 Fresh 상태로 유지되는 시간을 설정. staleTime이 0으로 하면 데이터를 가져오자 마자 Stale 상태로 전환함.
    • cacheTime : Inactive 상태에서 데이터가 캐시된 상태로 유지되는 시간. cacheTime이 만료되면 데이터가 Deleted 됨.
    • refetchOnMount : 컴포넌트가 다시 마운트 됐을 때 데이터를 다시 가져오는 설정.
    • refetchOnWindowFocus : 창이 포커스를 얻을 때 데이터를 다시 가져오는 설정.
    • refetchOnReconnect : 네트워크가 다시 연결 됐을 때 데이터를 다시 가져오는 설정.
  • stale -> Inactive : 현재 페이지에서 데이터가 더이상 사용되지 않으면 상태가 Inactive로 변경됨.
  • Inactive -> Fetching : Inacitve 상태에서 데이터가 다시 사용되면 Fetching으로 돌아가 데이터를 다시 가져옴.
  • Inactive -> Deleted : 데이터가 사용되지 않다가 cacheTime 마저 만료되면 데이터의 상태가 Deleted로 변경됨.

별도로 설정하지 않으면 query에서 staleTime은 0이 기본값으로 설정되어 있다. 데이터를 fetch 받아 오자마자 Fresh 상태가 아닌 Stale 상태가 된다는 것인데, 서버에서 데이터를 방금 받아왔기 때문에 realtime 데이터가 아닌 이상 당연히 0.1초라도 지나면 서버의 최신 데이터라고 보장할 수 없기 때문이다.

 

 

댓글