React

React query 적용 구조적 패턴

shuai 2025. 2. 24. 21:08

저번에 react query를 써보고 포스팅을 한번 했었다. 이번에는 왜 해당 기술을 써야 하는지와 더 좋은 적용 구조적 패턴에 대해 알게 되어서 다시 정리를 해보려고 한다.

 

우리는 일반적으로 API 호출을 할 때 아래와 같이 구현할 것이다. 그렇지만, 이로 인한 문제점도 분명히 존재한다.

일반적인 API 호출 처리

  • loading, error 처리를 위한 state 값 선언과 값에 따른 분기 처리가 필요하다.
  • API 호출이 여러개 이루어질 경우, state 선언이 n개 늘어나게 된다. ex) isWebtoonLoading, isNewsLoading
import { useEffect, useState } from 'react';

function App() {
  const [list, setList] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    setIsLoading(true);
    setIsError(false);

    // 1. api 호출
    fetch(
      'https://ko-webtoon-api-cc7dda2f0d77.herokuapp.com/webtoons?page=1&perPage=30&sort=ASC'
    )
      .then((res) => res.json())
      .then((data) => {
        setList(data);
      })
      .catch(() => {
        setIsError(true);
      })
      .finally(() => {
        setIsLoading(false);
      });
    // 2. setList
  }, []);

  if (isError === true) {
    return <h1>Error!</h1>;
  }

  if (isLoading === true || list == null) {
    return <h1>Loading...</h1>;
  }

  return (
    <>
      {list.webtoons.map(({ title }) => {
        return <li>{title}</li>;
      })}
    </>
  );
}

export default App;

 

이러한 문제점을 해결할 수 있는 것이 react query이기도 하다.

React Query

https://tanstack.com/query/v5/docs/framework/react/overview

React Query에 대해 이해하기 위해서는 QueryClientProvider에 대해 알아야 한다. QueryClientProvider는 QueryClient 인스턴스를 client 속성으로 받고, 이를 통해 어플리케이션 내에서 useQuery(), useMutation()을 사용할 수 있도록 한다.

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import './index.css';
import App from './App.tsx';

const client = new QueryClient();

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

 

 

React query의 기본적인 사용 방법은 아래와 같다.

 

1. 데이터 fetching

  • useQuery()를 사용한다. (GET 요청)
  • 자동으로 loading, error, success 상태를 관리한다.
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
});

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

return (
    <div>
      <button onClick={() => refetch()}>🔄 새로고침</button>
      ...
    </div>
);

 

2. 데이터 변경

  • useMutation()을 사용한다. (POST, PUT, DELETE 요청)
const mutation = useMutation({ // 객체가 아닌 함수 자체를 반환
   mutationFn: addPost,
   onSuccess: () => {
     queryClient.invalidateQueries(['posts']);
     // 새로 데이터를 추가하면 기존 posts 데이터가 오래된 상태가 되므로, 최신 데이터를 가져오도록 요청
   },
});

 

 

React Query는 다음과 같은 기능을 가진다.

 

1. 데이터 캐싱

  • useQuery()는 캐싱을 지원하여 불필요한 네트워크 요청을 줄인다.
  • queryKey를 기준으로 캐시를 관리하며, staleTime(신선도, 데이터 유효기간)을 설정하면 특정 시간이 지나기 전에는 캐시된 데이터를 사용하게 된다.
const { data, refetch } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 5000, // 5초 동안은 캐시된 데이터 사용
});

 

2. 데이터 refetching

  • useQuery()는 자동으로 데이터 새로고침을 지원한다.
  • refetchInterval을 설정하면 특정 간격으로 자동 fetching 된다.
const { data, refetch } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  refetchInterval: 10000, // 10초마다 자동 갱신
});

 

React query 적용 방법 (핵심!)

어떻게 하면 클린 코드를 유지하며 react query를 적용할 수 있을까 고민했는데, 멘토님께서 좋은 방안을 제시해주셨다. API를 fetch()하는 코드와 useQuery()와 관련된 코드, 사용처를 모두 분리하는 것이다. 이렇게 하니까 명확하게 관심사가 분리되는 것 같다. 최종 코드는 아래와 같다.

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

const queryClient = new QueryClient();

createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </QueryClientProvider>
);
// App.jsx
import "./App.css";
import { Routes, Route } from "react-router-dom";
import Home from "./pages/home/Home";

function App() {
  return (
    <>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </>
  );
}

export default App;
// Home.jsx
import useWebtoons from "../../hooks/home/useWebtoons";const Home = () => {  const { data, isLoading, isError, refetch } = useWebtoons({ page: 1 });  if (isLoading && data == null) {    return <h1>로딩중...</h1>;  }  if (isError === true) {    return <h1>Error!</h1>;  }  return (    <div>      <h1>Home Page</h1>      <div>        {data.webtoons.map(({ id, title }) => (          <li key={id}>{title}</li>        ))}      </div>      <div>        <button onClick={refetch}>refetch</button>      </div>    </div>  );};export default Home;import useWebtoons from "../../hooks/home/useWebtoons";

const Home = () => {
  const { data, isLoading, isError } = useWebtoons({ page: 1 });

  if (isLoading && data == null) {
    return <h1>로딩중...</h1>;
  }

  if (isError === true) {
    return <h1>Error!</h1>;
  }

  return (
    <div>
      <h1>Home Page</h1>
      <div>
        {data.webtoons.map(({ id, title }) => (
          <li key={id}>{title}</li>
        ))}
      </div>
    </div>
  );
};

export default Home;
// useWebtoons.js
import { useQuery } from "@tanstack/react-query";
import { getWebtoons } from "../../remote/webtoons";

function useWebtoons({ page }) {
  return useQuery({
    queryKey: useWebtoons.getKey({ page }),
    queryFn: () => getWebtoons(page),
  });
}

useWebtoons.getKey = ({ page }) => ["home", "/webtoons", page];

export default useWebtoons
// webtoon.ts
export function getWebtoons(page) {
  return fetch(
    `https://korea-webtoon-api-cc7dda2f0d77.herokuapp.com/webtoons?provider=NAVER&page=${page}&perPage=30&sort=ASC`
  ).then((res) => res.json());
}