제품 페이지에 SSR을 적용하면서 처음 구현했던 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 방식으로 변경.
// 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} />; };
❌ 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> ); };
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, }, }, }) );