SSR 환경에서 React Query 사용하기 (Hydration)

hzn·2025년 3월 5일

BLUE 🌊

목록 보기
2/2
post-thumbnail

제품 페이지에 SSR을 적용하면서 처음 구현했던 React cache + props 전달 방식.

🫥 React cache + props 전달 방식


// API 호출 함수
const fetchProduct = cache(async (id: number) => {
  try {
    const data = await getDetailUsingGET(id);
	...
    return data;
  } catch (error) {
   ...에러처리
  }
});

// 서버 컴포넌트
const ProductDetailPage = async ({ params }: ProductProps) => {
  const productData = await fetchProduct(params.id); // 서버에서 API 호출
 
  return <ProductDetailScreen productData={productData} />; // props로 전달
};

// 클라이언트 컴포넌트
const ProductDetailScreen = ({ productData }: ProductProps) => {
  // props로 받은 데이터 사용
  return <ProductDetail data={productData} />;
};

상태 관리 측면에서 불완전한 면이 많다.
React cache로 동일 요청 중복은 방지해 주지만 캐시된 데이터가 메모리에 계속 유지되고 캐시 무효화도 어렵다. (상품 데이터가 업데이트 될 때마다 새로고침 필요) 로딩이나 에러 상태 등도 관리가 불가능하다.


✨ (React Query) Hydration 방식

보다 완전한 상태 관리를 위해서 React Query의 hydration 방식으로 변경.


// API 호출 함수
const fetchProduct = async (id: number) => { // react cache 삭제
  try {
    const data = await getDetailUsingGET(id);
	...
    return data;
  } catch (error) {
   ...에러처리
  }
};

// 서버 컴포넌트
const ProductDetailPage = async ({ params }: ProductProps) => {
// 새 queryClient 생성
  const queryClient = getQueryClient(); // gcTime: Infinity

  // React Query에 데이터 미리 설정
  await queryClient.prefetchQuery({
    queryKey: ['product', params.id],
    queryFn: () => fetchProduct(params.id),
  });
 
  return  (
  		<HydrationBoundary state={dehydrate(queryClient)}> // react query 상태(해당 query client)를 직렬화해서 전달
        	<ProductDetailScreen params={params} />
      	</HydrationBoundary>
)};

// 클라이언트 컴포넌트
const ProductDetailScreen = ({ params }: ProductProps) => {
 const { data } = useGetDetailUsingGET(params.id); // 캐시된 데이터 사용

return <ProductDetail data={data} />;
};
  • 자동 캐시 관리 및 무효화 가능
  • 메모리 최적화: 서버에서 자동 가비지 컬렉션 (React Query의 수동 가비지 컬렉션이 아닌 Node.js의 가비지 컬렉션에 의한 정리 : 요청 완료 후 QueryClient에 대한 참조가 사라짐)
  • gcTime : Infinity (서버 기본값)

🌿 서버 환경에서 QueryClient의 gcTime

  • 클라이언트에서 필요에 따라 적절하게 gcTime을 설정하는 것과 달리 서버에서는 gcTime : Infinity가 안전하다. (React Query 기본값 : 클라이언트는 5분 / 서버에서는 Infinity)
    ➡️ 만약 gcTime이 짧다면 hydration 에러가 발생할 수 있다.

❌ gcTime이 짧을 경우 : hydration 에러 발생

const ProductDetailPage = async ({ params }: ProductProps) => {
  const queryClient = getQueryClient(); // gcTime: 1000ms

  await queryClient.prefetchQuery({
    queryKey: ['product', params.id],
    queryFn: () => fetchProduct(params.id),
  });

  // 1초(1000ms) 후 가비지 컬렉터가 데이터를 제거할 수 있음..!

  // 이 시점에 데이터가 없을 수 있음 => hydration 에러!
  const dehydratedState = dehydrate(queryClient);
 
  return (
    <HydrationBoundary state={dehydratedState}>
      <ProductDetailScreen params={params} />
    </HydrationBoundary>
  );
};
  • 서버 / 클라이언트 환경에 따라 gcTime 다르게 QueryClient를 생성하는 유틸 함수를 생성해서 사용.

getQueryClient

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

export const getQueryClient = cache(
  () =>
    new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 5 * 60 * 1000, // 5분
          gcTime: typeof window === 'undefined' ? Infinity : 10 * 60 * 1000, // 클라이언트에서 10분으로 사용 위해 설정 / 서버는 기본값 Infinity
          retry: 1, 
          refetchOnWindowFocus: false, 
        },
      },
    })
);

0개의 댓글