그럴듯한 개발 블로그
반응형

streaming SSR

dynamic rendering으로 빌드된 페이지에서 10초 걸리는 api를 서버에서 호출하면 사용자의 첫 로드가 그만큼 느려진다. 이를 해결하기 위해 nextjs13 이상부터는 suspense를 사용해 쉽게 streaming SSR을 적용할 수 있다.

import { Suspense } from 'react';
import SlowComponent from './SlowComponent';

export default function Home() {
  return (
    <div>
      <h1>Welcome to my page</h1>
      <Suspense fallback={<div>Loading slow component...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

// app/SlowComponent.js
async function fetchSlowData() {
  // 10초 지연을 시뮬레이션
  await new Promise((resolve) => setTimeout(resolve, 10000));
  return "Data loaded after 10 seconds";
}

export default async function SlowComponent() {
  const data = await fetchSlowData();
  return <div>{data}</div>;
}

이렇게 오래 걸리는 컴포넌트를 suspense로 묶어 예약해 두고 나머지를 먼저 rendering 해서 클라이언트에게 내려 주면 된다. 대신 서버 리소스 사용이 높다. 예약된 컴포넌트는 완성하면 객체로 클라이언트로 내려준다.

nextjs 서버컴포넌트에서 fetch 시 기본 옵션으로 캐싱되니 이를 사용해 리소스를 아낄 수 있겠다.

CSR

이 방식은 static rendering 된 페이지에서도 사용 가능하다. suspense로 감싸진 컴포넌트를 서버에서 빈 값으로 두고 클라이언트에서 렌더링 하는 방식이다.

mejai.kr 프로젝트에서 프리티어 ec2로 올린 nextjs서버 자원을 아끼기 위해 이 방식을 사용했다.

"use client";

import React, { Suspense } from "react";
import UserInfoBox from "@/app/summoner-page/_components/userInfoBox";
import TierBox from "@/app/summoner-page/_components/tierBox";
import { useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { fetchUserInfo } from "@/lib/fetchFunc";
import ErrorPage from "@/app/summoner-page/_components/errorPage";
import JandiBox from "@/app/summoner-page/_components/jandiBox";
import Spinner from "@/components/ui/spinner";
import { AxiosError } from "axios";

function AwaitPage() {
  const params = useSearchParams();
  const id = params.get("id") || "";
  const tag = params.get("tag") || "";
  const { error, isLoading } = useQuery({
    queryKey: ["userInfo", { id, tag }],
    queryFn: fetchUserInfo,
    staleTime: 1000 * 60 * 15, // 15분으로 staletime 설정
    gcTime: 1000 * 60 * 15,
  });

  if (error) {
    return <ErrorPage error={error as AxiosError} />;
  }
  if (isLoading) {
    return (
      <div className="w-full h-full flex justify-center items-center">
        <Spinner />
      </div>
    );
  }
  return (
    <>
      <UserInfoBox id={id} tag={tag} />
      <TierBox id={id} tag={tag} />
      <JandiBox />
    </>
  );
}
export default function SummonerPage() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <AwaitPage />
    </Suspense>
  );
}

이렇게 클라이언트 컴포넌트에서 suspense를 사용하면 서버에선 dynamic rendering시에도 클라이언트에서 fetch 하게 된다.

정리하자면

  • 서버 컴포넌트에서의 Suspense:
    • 서버에서 렌더링이 시작되고, Suspense로 감싼 부분을 만나면 해당 부분의 데이터가 준비될 때까지 기다린다.
    • 준비된 데이터와 함께 HTML을 생성하여 클라이언트로 스트리밍한다.
    • 클라이언트는 부분적으로 완성된 HTML을 받아 렌더링 하고, 나머지 부분은 서버에서 추가로 전송된다.
  • 클라이언트 컴포넌트에서의 Suspense:
    • 초기 HTML은 서버에서 생성되지만, 데이터 fetching은 클라이언트에서 이루어진다.

물론 두 방식 모두 클라이언트에 도착한 html엔 suspense로 감싸진 부분은 텅 비어있으니 SEO를 고려해야 한다.

렌더링 방식, 원하는 사용자 경험에 따라 적절히 suspense를 사용해 보자.

반응형
profile

그럴듯한 개발 블로그

@donghyk2

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!