React

React query란 무엇일까?

shuai 2025. 1. 9. 18:31

React query에 대해 알아보았다. 처음 접하는 개념이라서 간단하게 큰 그림 정도만 그려보고자 한다.

 

설치

npm i @tanstack/react-query
npm i @tanstack/react-query-devtools

 

사용법

 

React query에는 크게 두가지가 있다고 한다. 바로 Query와 mutation이다. 정리해보면 다음과 같다.

  • query: getting data from somewhere ex) getting posts
  • mutation: changing some type of data ex) create new post
// main.jsx
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient();

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

// /posts -> ["posts"]
// /posts/1 -> ["posts", post.id]
// /posts?authorId=1 -> ["posts", {authorId: 1}]
// /posts/2/comments -> ["posts", post.id, "comments"]

import React from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

function App() {
  const queryClient = useQueryClient();

  // Fetch posts
  const { data: posts, isLoading } = useQuery({
    queryKey: ["posts"],
    queryFn: async () => {
      const response = await fetch("<http://localhost:3001/posts>");
      if (!response.ok) {
        throw new Error("Failed to fetch posts");
      }
      return response.json();
    },
  });

  // Add new post
  const addPostMutation = useMutation({
    mutationFn: async (newPost) => {
      const response = await fetch("<http://localhost:3001/posts>", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(newPost),
      });
      if (!response.ok) {
        throw new Error("Failed to add post");
      }
      return response.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries(["posts"]);
    },
  });

  // Delete post
  const deletePostMutation = useMutation({
    mutationFn: async (postId) => {
      const response = await fetch(`http://localhost:3001/posts/${postId}`, {
        method: "DELETE",
      });
      if (!response.ok) {
        throw new Error("Failed to delete post");
      }
      return postId;
    },
    onSuccess: () => {
      queryClient.invalidateQueries(["posts"]);
    },
  });

  const handleAddPost = () => {
    const newPost = {
      title: "New Post",
      body: "This is a new post content.",
    };
    addPostMutation.mutate(newPost);
  };

  const handleDeletePost = (postId) => {
    deletePostMutation.mutate(postId);
  };

  if (isLoading) return <h1>Loading...</h1>;

  return (
    <div>
      <h1>Posts</h1>
      {posts.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
          <button onClick={() => handleDeletePost(post.id)}>Delete</button>
        </div>
      ))}
      <button onClick={handleAddPost}>
        {addPostMutation.isLoading ? "Adding Post..." : "Add Post"}
      </button>
    </div>
  );
}

export default App;

 

쓰는 방법은 어렵지 않은것 같은데, 그럼 뭐가 특별한걸까?

 

React query를 사용하는 이유?

 

https://velog.io/@jay/10-minute-react-query-concept

 

해당 글의 내용이 잘 정리되어 있어서 참고했다. 주식이나 코인 거래소처럼, 10초 뒤에 앱에서 표현하는 데이터가 더 이상 유효하지 않은 낡은 데이터라고 한다면 어떨까? 클라이언트에서는 polling방식으로 주기적으로 데이터를 받아오거나 실시간성이 중요한 데이터라면 웹소켓을 통해 서버의 상태 값이 변경되었을 때 서버에게 통지받아야 한다.

리엑트 쿼리는 데이터의 캐시 처리를 간편하게 할 수 있는 인터페이스를 제공한다.

  • 몇 초 이후에는 데이터가 유효하지 않은 것으로 간주하고 데이터를 다시 불러온다.
  • 데이터에 변경점이 있는 경우에만 리렌더링을 유발한다.
  • 유저가 탭을 이동했다가 다시 돌아왔을 때 데이터를 다시 불러온다.
  • 데이터를 다시 호출할때 응답이 오기 전까지는 이전 데이터를 계속 보여준다. 필요에 따라서는 로딩바와 같은 대안 UI를 보여주기 위해 loading state를 기본적으로 제공한다.

fetch api를 사용하여 데이터를 불러올 때 에러처리를 추가적으로 해줘야 한다는 불편함을 axios가 해결해주듯이, 클라이언트와 서버의 상태 값을 일치시켜줘야 하는 요구사항에서 부가적으로 생길 수 있는 로직들을 리엑트 쿼리의 api와 인터페이스로 간단하게 해결할 수 있도록 도와주는 것이다.

 

좀 이해가 되었다. 다시 한번 fetch와 react query의 차이점을 정리해보았다.

 

결론적으로, 비동기 데이터 요청이 빈번하거나 서버 상태와 클라이언트 상태를 분리하고 싶을 때 react query를 사용할 수 있다.

  fetch react query
데이터 캐싱 - 데이터를 요청할 때 마다 API 호출
- 동일한 데이터를 요청할 경우에도 항상 새로운 요청
- 데이터 캐싱을 제공하며, 동일한 queryKey로 요청하면 캐싱된 데이터를 반환하며, 필요 시 API를 호출해 데이터 갱신
- 네트워크 요청 최소화를 통한 성능 향상
데이터 동기화 - 데이터가 변경되었을 때 이를 반영하려면 별도의 로직 작성 필요 ex) setPosts(data) - 데이터가 변경되면 mutation을 통해 자동으로 쿼리 데이터 갱신
ex) addPostMutation.mutate(newPost);
- invalidateQueries, onSuccess
로딩 및 에러 상태 관리 - useState와 useEffect를 조합하여 상태 관리 로직을 직접 작성 - 로딩(isLoading), 에러(isError), 성공(isSuccess) 상태를 기본적으로 제공
- 별도의 로직 작성 필요 없이 상태를 활용하여 UI 동적 업데이트 가능
백그라운드 데이터 업데이트 - setInterval과 같은 로직을 직접 작성 - staleTime이나 refetchInterval 옵션으로 데이터가 갱신되는 시점 제어 가능 (데이터를 일정 간격으로 자동 갱신하거나 필요할 때만 새로 가져올 수 있음)
쿼리 중복 방지 - 동일한 요청이 동시에 발생하면 중복 요청이 모두 서버로 전달 - 동일한 queryKey로 중복 요청이 발생하면 중복 요청을 자동으로 방지하고, 이미 진행 중인 요청의 결과를 반환

 

데이터 캐싱

그러면 react query의 핵심인 데이터 캐싱은 어떻게 동작할까?

데이터 캐싱은 서버에서 데이터를 가져온 후 이를 메모리에 저장해두고, 동일한 요청이 다시 발생했을 때 저장된 데이터를 반환하는 기능이다. React query는 queryKey를 기준으로 데이터를 캐싱한다. React Query는 동일한 queryKey로 요청할 경우, 캐싱된 데이터를 반환한다.

그래서 동작 방식을 정리하자면 다음과 같다. 특정 queryKey로 데이터를 요청하면 서버에서 데이터를 가져와 React Query의 캐시에 저장한다. React Query는 데이터가 "오래된 상태"라고 판단하면 서버에서 데이터를 다시 요청해 캐시를 최신 상태로 유지한다. "오래된 상태"인지 판단하는 기준은 staleTime(기본값: 0ms)이다.