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());
}