React Query 시작하기 (feat. useEffect로 데이터 패칭하는 것의 단점!)

미키오·2024년 6월 12일
0
post-thumbnail

부트캠프 시절 때부터 아래와 같이 useEffect 훅을 사용하여 데이터 패칭을 자주 사용했었다.

useEffect(() => {
    fetch(`${BASE_URL}/products`, {
      headers: {
        'Content-type': 'application/json',
      },
      method: 'GET',
    })
      .then(response => response.json())
      .then(data => {
        setProducts(data);
      });
  }, []);

하지만 프로젝트를 진행하면서 상태관리가 점점 복잡해지며 코드는 스파게티 코드가 되어가고.. 불필요한 네트워크 요청이 많음을 느꼈다.

심지어 리액트 공식 문서에서도 지양하고 있는 방법이었다.

useEffect 내에서 데이터 패칭을 하면 다음과 같은 단점들이 존재할 수 있다.

useEffect data fetching의 단점

1. 복잡한 상태 관리

useEffect 내에서 데이터 패칭을 하면, 로딩 상태, 에러 상태, 데이터 상태를 수동으로 관리해야 한다. 이는 코드가 복잡해지고 에러를 발생시키기 쉬운 패턴을 초래한다.

2. 컴포넌트 재렌더링의 비효율성

데이터 패칭 로직이 컴포넌트에 직접적으로 포함되어 있으면, 컴포넌트의 재렌더링이 데이터 패칭 로직에도 영향을 줄 수 있다. 이는 불필요한 네트워크 요청이나 성능 저하를 초래할 수 있다.

3. 캐싱의 부재

useEffect를 사용하는 전통적인 방법은 데이터의 캐싱을 자동으로 처리하지 않는다. 따라서 동일한 데이터에 대한 중복 요청이 발생할 수 있으며, 사용자 경험과 리소스 사용에 비효율적이다.

4. 레이스 컨디션

여러 useEffect가 동시에 데이터를 패칭할 때, 응답이 도착하는 순서가 요청된 순서와 다를 수 있다. 이는 예상치 못한 결과를 초래할 수 있는 레이스 컨디션 문제를 발생시킨다.

이에 대한 대안으로 이번에 새로 시작하는 프로젝트 때부터는 리액트 쿼리를 적용해보자 한다. TanStack Query(리액트 쿼리)는 서버 상태 관리를 위한 라이브러리로, 데이터를 효율적으로 가져오고, 캐싱하고, 동기화하고, 업데이트하는 작업을 지원한다.

리액트 쿼리란..?

TS/JS, React, Solid, Vue, Svelte 및 Angular를 위한 강력한 비동기 상태 관리

"복잡한 상태 관리, 수동 리페칭, 끝없는 비동기 스파게티 코드는 이제 버립시다. TanStack Query는 선언적이고 항상 최신 상태를 유지하는 자동 관리 쿼리와 변형을 제공하여 개발자와 사용자 경험을 직접적으로 향상시킵니다.”

TanStack Query v5는 리액트 애플리케이션에서 서버 데이터를 관리하기 위해 사용되는 도구로, HTTP, WebSocket 등을 통해 데이터를 가져오고, 클라이언트 사이드에서 데이터를 캐싱하고 관리합니다. 복잡한 데이터 로드, 리프레시, 동기화 작업을 단순화하여 개발자의 생산성을 높여준다.

리액트 쿼리의 장점

리액트 쿼리는 위에서 언급한 useEffect의 문제점들을 효과적으로 해결해 준다.

1. 자동 상태 관리

리액트 쿼리는 데이터 패칭 과정에서 발생하는 로딩, 에러, 데이터 상태를 자동으로 관리해준다. 이는 개발자가 보일러플레이트 코드 없이 주요 로직에 집중할 수 있게 한다.

2. 캐싱과 자동 업데이트

리액트 쿼리는 강력한 캐싱 기능을 제공하여, 한 번 불러온 데이터를 메모리에 저장하고, 이후 같은 요청에 대해서는 캐시된 데이터를 반환한다. 또한, 배경에서 데이터를 주기적으로 새로 고침하거나, 관련 데이터가 변경되었을 때 자동으로 데이터를 업데이트한다.

3. 최적화된 데이터 동기화

리액트 쿼리는 데이터의 최신 상태를 유지하면서도, 불필요한 네트워크 요청을 최소화한다. 이는 성능과 사용자 경험을 모두 향상시킨다.

4. 경쟁 상태 회피

리액트 쿼리는 내부적으로 데이터 패칭 작업의 순서를 관리하여, 경쟁 상태를 효과적으로 방지한다.

리액트 - 쿼리 설치

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 호출

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

DevTools 설치

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

  • "Fresh" 상태의 쿼리는 최근에 성공적으로 데이터를 가져왔고, 해당 데이터가 아직 "staleTime"으로 설정된 시간 동안 유효한 상태이다.
  • 이 시간 동안 쿼리는 추가적인 데이터 패칭 없이 캐시된 데이터를 반환한다.

Fetching

  • "Fetching" 상태는 쿼리가 현재 데이터를 서버로부터 불러오고 있는 중임을 나타낸다. 이는 새로운 데이터 요청이 시작되었거나, 데이터를 자동 리프레시 중임을 의미할 수 있다.

Paused

  • "Paused" 상태는 쿼리가 일시적으로 비활성화되었음을 나타낸다. 예를 들어, 쿼리가 의존하는 변수가 아직 설정되지 않거나, 페이지가 비활성화 상태일 때 이 상태가 될 수 있다.

Stale

  • "Stale" 상태의 쿼리는 더 이상 신선(fresh)하지 않다는 것을 의미한다. 즉, "staleTime"이 경과하여 데이터가 만료되었고, 다음에 해당 쿼리를 참조할 때 자동으로 데이터를 리프레시해야 할 필요가 있음을 나타낸다.

Inactive

  • "Inactive" 상태는 쿼리가 현재 활성화되지 않았음을 나타낸다. 이 상태는 컴포넌트가 언마운트되어 쿼리에 대한 참조가 더 이상 존재하지 않을 때 발생한다. 설정된 "cacheTime"이 경과하면 쿼리 데이터가 캐시에서 제거된다.

stale Time gc Time

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의 간결하고 직관적인 상태 관리와 리액트 쿼리의 강력한 데이터 패칭 및 캐싱 기능을 결합하여 우아한 형제들에서 사용한 것처럼 최적의 구조를 구축해나가고 싶다.

📚Bibliography

profile
교육 전공 개발자 💻

0개의 댓글