본문 바로가기

Next.js 서버/클라이언트 환경에서 TanStack Query 사용하기

codeConnection 2024. 8. 9.

TanStack Query, devtools 설치

yarn add @tanstack/react-query @tanstack/react-query-devtools

QueryProvider 설정

QueryProvider 컴포넌트를 정의하여 앱의 최상위 컴포넌트에 TanStack Query를 제공하는 설정을 한다.

  • TanStack Query는 리액트에서 데이터 패칭, 캐싱, 동기화를 도와주는 라이브러리이다.
  • devtools는 개발 도구로 현재 데이터의 상태를 웹 브라우저 우측 하단 플로팅 아이콘을 통해 보여주는 라이브러리이다.
// src/utils/QueryProvider.tsx

'use client';
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnWindowFocus: false,
        retry: false,
        staleTime: 60 * 1000,
      },
    },
  });
}

// undefined 로 초기값 설정
let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
  if (typeof window === 'undefined') {
    // 서버: 항상 새 쿼리 클라이언트 만들기
    return makeQueryClient();
  } else {
    if (!browserQueryClient) browserQueryClient = makeQueryClient();
    return browserQueryClient;
  }
}

function QueryProvider({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools
        initialIsOpen={process.env.NEXT_PUBLIC_RUN_MODE === 'local'}
      />
    </QueryClientProvider>
  );
}

export default QueryProvider;
  • <QueryClientProiver> 파일 이름은 자유 작명
  • 본인처럼 utils 폴더에 넣을 거라면 src 레벨에 넣는 것을 권장 - 상관없지만 사이드이펙트가 발생할 수 있음.
  • 위 설정을 하게 되면 각기 다른 환경 (브라우저, 서버)에 따라 적절한 QueryClient 인스턴스를 생성하여 클라이언트와 서버 모두에서 동일한 방식으로 쿼리를 처리할 수 있게 합니다.

주요 코드 상세 설명

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
  • 설치한 TanStack Query 라이브러리에서 QueryClient와 QueryClientProvider를 가져 온다.
function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnWindowFocus: false,
        retry: false,
        staleTime: 60 * 1000,
      },
    },
  });
}
  • makeQueryClient 함수는 새로운 QueryClient 인스턴스를 생성하며, 기본 옵션을 설정한다.
    • refetchOnWindowFocus : false -> 창이 포커스 될 때 쿼리를 다시 가져오지 않음.
    • retry: false -> 쿼리 실패 시 재시도 하지 않음.
    • staleTime : 60 * 1000 -> 쿼리 데이터를 1분 동안 신선한 상태로 유지함.
let browserQueryClient: QueryClient | undefined = undefined;
  • 클라이언트 측에서 사용할 QueryClient를 저장할 변수.
  • 초기값을 undefined로 설정함.
function getQueryClient() {
  if (typeof window === 'undefined') {
    return makeQueryClient();
  } else {
    if (!browserQueryClient) browserQueryClient = makeQueryClient();
    return browserQueryClient;
  }
}
  • 첫번째 if문 : 서버 환경에서는 항상 새로운 QueryClient 인스턴스를 반환함.
  • 두번째 if문 : 클라이언트 환경에서는 브라우저에 QueryClient가 없으면 새로 생성하고, 있으면 기존 것을 사용함.
  • getQuertClient 함수는 위 내용으로 구성되어 있기 때문에 서버나 클라이언트, 환경에 따라 알맞은 QueryClient를 반환함.
function QueryProvider({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools
        initialIsOpen={process.env.NEXT_PUBLIC_RUN_MODE === 'local'}
      />
    </QueryClientProvider>
  );
  • 위에서 정의한 getQueryClient 함수를 호출해서 QueryClient를 가져옴.
  • QueryClientProvider로 감싸서 React Query를 사용 가능한 상태로 만듦.
  • ReactQueryDevtools를 포함해서 개발 모드에서 Query의 상태를 쉽게 디버깅 할 수 있게 함.

쿼리 커스텀 훅 작성

데이터 페칭을 위한 커스텀 훅을 작성한다.

/src/hooks/useDonations.ts

import { useQuery } from '@tanstack/react-query';

export const fetchDonations = async () => {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/donations`, {
    method: 'GET',
    cache: 'no-store',
  });
  const result = await response.json();

  if (response.ok) {
    return result.data;
  } else {
    throw new Error(result.error);
  }
};

export const useDonations = () => {
  return useQuery({
    queryKey: ['donations'],
    queryFn: fetchDonations,
  });
};
  • fetchDonatios 함수는 데이터 페칭 함수이다.
  • cache 설정은 별도로 설정하지 않으면 기본적으로 캐싱이 되는 설정이다.
    • no-store 옵션은
  • ${process.env.NEXT_PUBLIC_API_URL}은

페이지 컴포넌트에서 데이터 dyhydrate 하기

// src/app/라우트/page.tsx

import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query';
import HomePage from "./(provider)/home/page";
import { fetchDonations } from "../../hooks/useDonations";
import { Suspense } from "react";

export default async function Home() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryKey: ["donations"],
    queryFn: fetchDonations,
  });
  const dehydratedState = dehydrate(queryClient);

  return (
    <>
      <Suspense fallback={<div>Loading...</div>}>
        <HydrationBoundary state={dehydratedState}>
          <HomePage />
        </HydrationBoundary>
      </Suspense>
    </>
  );
}
  • QueryClient : 새로운 QueryClient 인스턴스를 생서앟여 서버에서 데이터를 미리 페칭한다.
  • prefetchQuery : 서버사이드에서 데이터를 미리 가져와서 클라이언트 사이드에서 사용할 수 있도록 한다. 이를 통해 페이지 로드 시간을 단축하고 사용자 경험을 개선할 수 있다.
  • dehydrate : 서버에서 페칭한 데이터를 직렬화하여 클라이언트에 전달한다.
  • HydrationBoundary : 클라이언트에서 데이터를 재수화(hydrate)하여 서버에서 미리 페칭된 데이터를 사용할 수 있게 한다.
import { useQuery } from '@tanstack/react-query';
  • TanStack Query로 커스텀 훅을 만드려면 이 훅이 필요하다.
export const fetchDonations = async () => {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/donations`, {
    method: 'GET',
    cache: 'no-store',
  });
  
  const result = await response.json();
  
  if (response.ok) {
    return result.data;
  } else {
    throw new Error(result.error);
  }
};
  • fetchDonations라는 데이터 페칭 함수를 정의한다.
  • 이 함수는 Next.js에서 API Route Handler를 구성하고 그 서버로 데이터를 페칭하는 방법이다.
  • fetch 다음 소괄호가 본인이 직접 구성한 API 엔드포인트이다.
  • baseURL은 .env.local.에 NEXT_PUBLIC_API_URL이라는 상수에 값을 담아두었다.
    • 로컬 호스트를 가리키고 있다.  NEXT_PUBLIC_API_URL=http://localhost:3000
    • 이름이 헷갈리면 NEXT_PUBLIC_BASE_URL 등으로 자유작명 해도 된다.
  • cache: 'no-store' 옵션은 브라우저가 이 API 요청을 캐싱하지 않도록 하는 설정. 필요에 따라 변경해도 됨.
  • 이 응답에 대해서 결과값인 response를 비동기적으로 json으로 파싱한 뒤 result라는 변수에 담는다.
    • 이 응답값에는 데이터의 반환값인 data, 에러 결과인 error 등 여러가지가 담겨 있다. 그러나 보통은 여기서 data와 error 정도를 사용한다.
  • response.ok 즉, 응답이  성공적이면 result.data를 반환하도록 한다. result 안에 이미 data가 있지만 매번 점 표기법으로 .data 이렇게 꺼내와야 하니, 호출 구문이 길어져 처음부터 data를 꺼내오는 게 편리하다.
  • 그리고 그게 아니라면 에러 상황일 테니, Error를 result.error에 담아 던진다.
export const useDonations = () => {
  return useQuery({
    queryKey: ['donations'],
    queryFn: fetchDonations,
  });
};
  • 이 부분이 TanStack Query를 작성하는 문법이다.
  • fetchDonations 함수로 데이터 페칭하는 것을 'donations'라는 쿼리 키로 지정하여 useDonations 커스텀 훅을 만든다.
  • 나중에 다른 컴포넌트에서 이 쿼리를 사용할 때는 const { data, isPending, error } = useDonation(); 이런 식으로 호출만 하면 된다.
  • 쿼리 키는 invalidate 할 때 사용한다. (필요한 경우)

최종 컴포넌트에서 데이터 사용

dyhydrate된 데이터를 최정 컴포넌트에서 사용하도록 props로 데이터를 전달한다.

// components/bank/DoanationList.tsx

'use client';

import React from 'react';

const DonationList = ({ donations }: { donations: any }) => {
  return (
    <div className="grid grid-cols-1 gap-4">
      {donations.map((donation: any) => (
        <div key={donation.uuid} className="p-4 border rounded shadow">
          <h2 className="text-lg font-bold">일련번호: {donation.serielnumbers}</h2>
          <p>후원일자: {new Date(donation.datetime).toLocaleDateString()}</p>
          <p>후원자명: {donation.name}</p>
          <p>후원액: {donation.amounts.toLocaleString()}원</p>
        </div>
      ))}
    </div>
  );
};

export default DonationList;

댓글