부트캠프 시절 때부터 아래와 같이 useEffect
훅을 사용하여 데이터 패칭을 자주 사용했었다.
useEffect(() => {
fetch(`${BASE_URL}/products`, {
headers: {
'Content-type': 'application/json',
},
method: 'GET',
})
.then(response => response.json())
.then(data => {
setProducts(data);
});
}, []);
하지만 프로젝트를 진행하면서 상태관리가 점점 복잡해지며 코드는 스파게티 코드가 되어가고.. 불필요한 네트워크 요청이 많음을 느꼈다.
심지어 리액트 공식 문서에서도 지양하고 있는 방법이었다.
useEffect 내에서 데이터 패칭을 하면 다음과 같은 단점들이 존재할 수 있다.
useEffect
내에서 데이터 패칭을 하면, 로딩 상태, 에러 상태, 데이터 상태를 수동으로 관리해야 한다. 이는 코드가 복잡해지고 에러를 발생시키기 쉬운 패턴을 초래한다.
데이터 패칭 로직이 컴포넌트에 직접적으로 포함되어 있으면, 컴포넌트의 재렌더링이 데이터 패칭 로직에도 영향을 줄 수 있다. 이는 불필요한 네트워크 요청이나 성능 저하를 초래할 수 있다.
useEffect
를 사용하는 전통적인 방법은 데이터의 캐싱을 자동으로 처리하지 않는다. 따라서 동일한 데이터에 대한 중복 요청이 발생할 수 있으며, 사용자 경험과 리소스 사용에 비효율적이다.
여러 useEffect
가 동시에 데이터를 패칭할 때, 응답이 도착하는 순서가 요청된 순서와 다를 수 있다. 이는 예상치 못한 결과를 초래할 수 있는 레이스 컨디션 문제를 발생시킨다.
이에 대한 대안으로 이번에 새로 시작하는 프로젝트 때부터는 리액트 쿼리를 적용해보자 한다. TanStack Query(리액트 쿼리)는 서버 상태 관리를 위한 라이브러리로, 데이터를 효율적으로 가져오고, 캐싱하고, 동기화하고, 업데이트하는 작업을 지원한다.
TS/JS, React, Solid, Vue, Svelte 및 Angular를 위한 강력한 비동기 상태 관리
"복잡한 상태 관리, 수동 리페칭, 끝없는 비동기 스파게티 코드는 이제 버립시다. TanStack Query는 선언적이고 항상 최신 상태를 유지하는 자동 관리 쿼리와 변형을 제공하여 개발자와 사용자 경험을 직접적으로 향상시킵니다.”
TanStack Query v5는 리액트 애플리케이션에서 서버 데이터를 관리하기 위해 사용되는 도구로, HTTP, WebSocket 등을 통해 데이터를 가져오고, 클라이언트 사이드에서 데이터를 캐싱하고 관리합니다. 복잡한 데이터 로드, 리프레시, 동기화 작업을 단순화하여 개발자의 생산성을 높여준다.
리액트 쿼리는 위에서 언급한 useEffect
의 문제점들을 효과적으로 해결해 준다.
리액트 쿼리는 데이터 패칭 과정에서 발생하는 로딩, 에러, 데이터 상태를 자동으로 관리해준다. 이는 개발자가 보일러플레이트 코드 없이 주요 로직에 집중할 수 있게 한다.
리액트 쿼리는 강력한 캐싱 기능을 제공하여, 한 번 불러온 데이터를 메모리에 저장하고, 이후 같은 요청에 대해서는 캐시된 데이터를 반환한다. 또한, 배경에서 데이터를 주기적으로 새로 고침하거나, 관련 데이터가 변경되었을 때 자동으로 데이터를 업데이트한다.
리액트 쿼리는 데이터의 최신 상태를 유지하면서도, 불필요한 네트워크 요청을 최소화한다. 이는 성능과 사용자 경험을 모두 향상시킨다.
리액트 쿼리는 내부적으로 데이터 패칭 작업의 순서를 관리하여, 경쟁 상태를 효과적으로 방지한다.
npm install @tanstack/react-query
# or
pnpm add @tanstack/react-query
# or
yarn add @tanstack/react-query
# or
bun add @tanstack/react-query
TanStack Query v5를 사용하기 위해 QueryClient
인스턴스를 생성하고, 리액트 컴포넌트 트리의 최상위에 QueryClientProvider
를 설정한다. 이 구성은 라이브러리가 상태와 캐시를 관리하는 데 사용된다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyComponent />
</QueryClientProvider>
);
}
useQuery
훅을 사용하여 서버로부터 데이터를 비동기적으로 가져오고, 자동으로 캐싱 및 상태 관리를 할 수 있다. 이 훅은 데이터 로드, 새로고침, 캐시 관리를 간단하게 만든다.
import { useQuery } from '@tanstack/react-query';
function MyComponent() {
const { isLoading, error, data } = useQuery(['todos'], fetchTodos);
if (isLoading) return 'Loading...';
if (error) return `An error occurred: ${error.message}`;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
TanStack Query DevTools를 설치하여 앱의 쿼리 상태와 캐시 데이터를 시각적으로 확인할 수 있다. DevTools에서 확인할 수 있는 상태들은 각 쿼리의 상태를 나타내며, 이 도구는 개발 중에 매우 유용하다.
$ npm i @tanstack/react-query-devtools@4
# or
$ pnpm add @tanstack/react-query-devtools@4
# or
$ yarn add @tanstack/react-query-devtools@4
역시나 NPM이나 Yarn을 이용해서 설치를 해줘야 한다.
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<>
<QueryClientProvider client={queryClient}>
<MyComponent />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</>
);
}
리액트 컴포넌트 트리 최상단에 ReactQueryDevtools
를 애플리케이션의 QueryClientProvider
내부에 포함시킨다.
프로젝트 우측 하단에 야자수가 생긴 것을 확인해볼 수 있다. 야자수 아이콘을 클릭해보면 다음과 같은 상태들을 확인할 수 있다.
Fresh
Fetching
Paused
Stale
Inactive
staleTime은 데이터가 새롭게 간주되는 기간을 설정한다. 이 기간 동안은 추가적인 요청에 대해 데이터를 새로 고침하지 않는다. gcTime은 데이터가 캐시에서 유지되는 기간을 설정하여, 이 시간이 지나면 캐시에서 데이터가 제거된다. 아래 코드를 통해 다시 한번 살펴보자.
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// QueryClient를 생성하고 구성 옵션을 설정
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// staleTime을 5분으로 설정
// 이 설정은 데이터가 5분 동안 '신선(fresh)'으로 간주되어,
// 이 기간 동안에는 새로운 요청에 대해 자동 리페치를 하지 않는다.
staleTime: 5 * 60 * 1000,
// gcTime을 10분으로 설정
// 이 설정은 캐시에서 관찰되지 않는 데이터가 10분 후에 자동으로 삭제된다.
cacheTime: 10 * 60 * 1000
}
}
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyComponent />
</QueryClientProvider>
);
}
// 데이터 패칭을 수행하는 컴포넌트
function MyComponent() {
// useQuery를 사용하여 데이터를 요청
const { isLoading, error, data } = useQuery(['posts'], fetchPosts);
if (isLoading) return 'Loading...';
if (error) return `An error occurred: ${error.message}`;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// 데이터를 패칭하는 함수 (예: 서버에서 글 목록을 불러옴)
async function fetchPosts() {
const response = await fetch('/api/posts');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
기존에 useEffect
를 이용하여 구현했던 데이터 패칭의 여러 문제점을 파악하고, 이를 해결하기 위한 더 나은 방법을 모색하는 과정은 매우 흥미로웠다. 각 프로젝트의 성격에 맞는 최적의 해결책을 찾아가는 과정에서 리액트 쿼리의 유연성과 효율성이 큰 도움이 될 수 있을 것 같다.
이전 글이었던 Zustand의 간결하고 직관적인 상태 관리와 리액트 쿼리의 강력한 데이터 패칭 및 캐싱 기능을 결합하여 우아한 형제들에서 사용한 것처럼 최적의 구조를 구축해나가고 싶다.