본문 바로가기

카테고리 없음

서버에서 데이터 프리패칭을 위한 InitialData vs Hydration API

 

 

- 앞서 Next.js 프로젝트에서 초기 로딩 성능 최적화를 위한 서버 데이터 프리패칭을 적용한 것을 정리한 글(Hydration API를 사용한 데이터 프리패칭을 통한 초기 로딩 속도 개선)에서 언급했던 initialData 방식과 Hydration API 방식에 대해 정리해보고자 한다.

 

- 서버 데이터 프리패칭이란 서버 데이터 프리패칭은 클라이언트가 페이지를 요청했을 때, 서버에서 해당 페이지를 렌더링하기 전에 데이터를 미리 패칭하여, 클라이언트에게 데이터를 함께 전달하는 방식을 말한다.

- 주로 서버사이드 렌더링(SSR)에서 사용되며, 클라이언트가 페이지를 로드할 때 이미 서버에서 필요한 데이터를 미리 준비하여 제공하기 때문에 초기 로딩 속도를 크게 개선할 수 있다.

 

- React Query 공식문서에서는 서버에서 데이터 프리패칭(prefetching) 및 클라이언트에서 해당 데이터 사용을 위한 2가지 방식(initialData 사용 혹은 Hydration API 사용)을 제공하고 있다.

 

- Hydration API를 사용한 방식은 앞선 글(https://pocoding.tistory.com/231)소개하고 설명했고 여기서는 initialData 방식을 적용했을 때와 왜 initialData 대신 Hydration API를 사용했는지에 대해서 다루겠다.

수정 전 코드

// app/page.tsx

import NavBar from '@/components/navbar';
import RoomTable from '@/components/room-table';
import { Suspense } from 'react';

const HomePage = () => {
  return (
    <>
      <NavBar isHomePage={true} />
      <div className="flex justify-center items-center px-40">
        <Suspense>
          <RoomTable />
        </Suspense>
      </div>
    </>
  );
};

export default HomePage;
// RoomTable.tsx
'use client';

import { useForm, Controller } from 'react-hook-form';
import { useDebounce } from '@/hooks/use-debounce';
import { zodResolver } from '@hookform/resolvers/zod';
import { useGetRooms } from '@/hooks/use-get-rooms';
import { useIntersectionObserver } from '@/hooks/use-intersection-observer';
import { TRoomSearchPayload, roomSearchSchema } from '@/schemas/rooms';

const RoomTable = () => {
  const {
    control,
    watch,
    setValue,
    clearErrors,
    formState: { errors },
  } = useForm<TRoomSearchPayload>({
    resolver: zodResolver(roomSearchSchema),
    defaultValues: { searchKeyword: '' },
    mode: 'onChange',
  });

  const searchKeyword = watch('searchKeyword', '');
  const debouncedKeyword = useDebounce(
    errors.searchKeyword ? '' : searchKeyword ?? '',
    300
  );

  const {
    data: roomData,
    isPending,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = useGetRooms(debouncedKeyword);

  const router = useRouter();

  useIntersectionObserver({
    targetId: 'load-more-trigger',
    onIntersect: fetchNextPage,
    enabled: hasNextPage && !isFetchingNextPage,
  });

  //...

- 이전 글에서도 설명했지만, 수정 전 코드에서는 먼저 app/page.tsx 파일에는 HomePage가 있고, RoomTable에서는 useGetRooms 훅을 사용하여 서버에서 방 목록을 받아오고 useIntersectionObserver를 통해 스크롤이 일정 위치에 도달했을 때, 추가 방 목록 데이터를 받아오는 API를 호출하고 있다.

InitialData 방식 적용

// app/page.tsx

import NavBar from '@/components/navbar';
import RoomTable from '@/components/room-table';
import { Suspense } from 'react';
import { getRooms } from '@/api/get-rooms';

export default async function HomePage() {
  const initialRooms: TRoomsListData = await getRooms(0);

  return (
    <>
      <NavBar isHomePage={true} />
      <div className="flex justify-center items-center px-40">
        <Suspense>
          <RoomTable initialData={initialRooms} />
        </Suspense>
      </div>
    </>
  );
}
// RoomTable.tsx

'use client'

import { useForm, Controller } from 'react-hook-form';
import { useDebounce } from '@/hooks/use-debounce';
import { zodResolver } from '@hookform/resolvers/zod';
import { useGetRooms } from '@/hooks/use-get-rooms';
import { useIntersectionObserver } from '@/hooks/use-intersection-observer';
import { TRoomSearchPayload, roomSearchSchema } from '@/schemas/rooms';

type RoomTableProps = {
  initialData: TRoomsListData;
};

const RoomTable = ({ initialData }: RoomTableProps) => {
  console.log(initialData);
  const {
    control,
    watch,
    setValue,
    clearErrors,
    formState: { errors },
  } = useForm<TRoomSearchPayload>({
    resolver: zodResolver(roomSearchSchema),
    defaultValues: { searchKeyword: '' },
    mode: 'onChange',
  });

  const searchKeyword = watch('searchKeyword', '');
  const debouncedKeyword = useDebounce(
    errors.searchKeyword ? '' : searchKeyword ?? '',
    300
  );

  const {
    data: roomData,
    isPending,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = useGetRooms(debouncedKeyword, initialData);

  const router = useRouter();

  useIntersectionObserver({
    targetId: 'load-more-trigger',
    onIntersect: fetchNextPage,
    enabled: hasNextPage && !isFetchingNextPage,
  });
//...

- 위 코드를 보면, HomePage 컴포넌트는 서버에서 getRooms(0)으로 0번째 페이지 데이터 즉, 첫 번째 페이지의 데이터를 GET하는 API를 호출하고 RoomTable에 props로 넘겨준다.

- RoomTable에서는 props로 전달받은 initialData를 초기 로딩에 사용하여 초기 로딩 성능을 최적화 할 수 있다.

 

initialData 방식을 사용하지 않은 이유

- initialData 방식에는 몇 가지 고려해야할 상황이 있다.

1. 만약 initialData를 사용하는 컴포넌트가 트리의 안쪽에 위치하는 경우에는 initialData를 해당 지점까지 전달하는 과정이 필요하다.

2. 쿼리에 대한 데이터가 캐시에 이미 존재하는 경우에는, initialData가 캐시된 데이터보다 최신 데이터("신선한" 상태)여도 initialData는 캐시된 덮어쓰지 않는다. 

 

- 1번 항목의 경우에는 HomePage에서 한 단계 하위 컴포넌트인 RoomTable로 전달할 수 있기 때문에 큰 고민이 되지 않았지만, 내가 가장 많은 고민을 하고 결국 initialData 방식을 사용하지 않은 이유는 2번 항목 때문이었다.

- initialData로 사용되는 방 목록의 경우에는 초기 로딩 성능 최적화 이후에는 사용자에게 최신 데이터를 제공해야 했다.

- 물론, staleTime 조절을 통해 해결 방법이 있을 수 있지만, React Query의 공식문서에서도 Hydration API의 사용을 권장하고 있고 서버 상태 관리는 React Query를 사용해서 관리하고 있는 프로젝트의 통일성을 지킬 수도 있다고 판단하여 Hydration API 방식을 도입했다.


학습 단계로 잘못된 정보가 있을 수 있습니다. 잘못된 부분에 대해 알려주시면 정정하도록 하겠습니다

참고 : https://soobing.github.io/react/server-rendering-and-react-query/