streaming SSR
dynamic rendering으로 빌드된 페이지에서 10초 걸리는 api를 서버에서 호출하면 사용자의 첫 로드가 그만큼 느려진다. 이를 해결하기 위해 nextjs13 이상부터는 async component, 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>;
}
이렇게 오래 걸리는 컴포넌트를 async, suspense로 묶어 예약해 두고 나머지를 먼저 rendering 해서 클라이언트에게 내려 주면 된다. 대신 서버 리소스 사용이 높다. 예약된 컴포넌트는 완성하면 객체로 클라이언트로 내려준다.
nextjs 서버컴포넌트에서 fetch 시 기본 옵션으로 캐싱되니 이를 사용해 리소스를 아낄 수 있겠다.
하지만 이 방법은 static export에서는 사용할 수 없다. static export했을 시 aysnc fetch를 하던 fetch를 하던 빌드 당시의 리턴값으로 고정되어 버린다. 그럴 땐 아래 방법을 사용해 보자
CSR
이 방식은 static export 된 페이지에서도 사용 가능하다. suspense로 감싸진 컴포넌트를 서버에서 빈 값으로 두고 클라이언트에서 렌더링 하는 방식이다.
mejai.kr 프로젝트에서 프리티어 ec2로 올린 nextjs서버 자원을 아끼기 위해 이 방식을 사용했다.
"use client";
import React, { Suspense } from "react";
import UserInfoBox from "@/app/summoner-page/_components/user-info-box";
import TierBox from "@/app/summoner-page/_components/tier-box";
import { useSearchParams } from "next/navigation";
import { useSuspenseQuery } from "@tanstack/react-query";
import { fetchUserInfo } from "@/lib/fetch-func";
import ErrorPage from "@/app/summoner-page/_components/error-page";
import JandiBox from "@/app/summoner-page/_components/jandi-box";
import Spinner from "@/components/ui/spinner";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
function AwaitPage() {
const params = useSearchParams();
const id = params?.get("id") || "";
const tag = params?.get("tag") || "";
const { error, isFetching } = useSuspenseQuery({
queryKey: ["userInfo", { id, tag }],
queryFn: fetchUserInfo,
staleTime: 1000 * 60 * 15,
gcTime: 1000 * 60 * 15,
});
if (error && !isFetching) {
throw error;
}
return (
<>
<UserInfoBox id={id} tag={tag} />
<TierBox id={id} tag={tag} />
<JandiBox />
</>
);
}
function ErrorFallback({ error }: FallbackProps) {
return <ErrorPage error={error} />;
}
export default function SummonerPage() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Spinner />}>
<AwaitPage />
</Suspense>
</ErrorBoundary>
);
}
이렇게 클라이언트 컴포넌트에서 suspense를 사용하면 서버에선 dynamic rendering시에도 클라이언트에서 fetch 하게 된다.
정리하자면
- 서버 컴포넌트에서의 async, Suspense:
- 서버에서 렌더링이 시작되고, Suspense로 감싼 부분을 만나면 해당 부분의 데이터가 준비될 때까지 기다린다.
- 준비된 데이터와 함께 HTML을 생성하여 클라이언트로 스트리밍 한다.
- 클라이언트는 부분적으로 완성된 HTML을 받아 렌더링 하고, 나머지 부분은 서버에서 추가로 전송된다.
- 클라이언트 컴포넌트에서의 Suspense:
- 초기 HTML은 서버에서 생성되지만, 데이터 fetching은 클라이언트에서 이루어진다.
물론 두 방식 모두 클라이언트에 도착한 html엔 suspense로 감싸진 부분은 텅 비어있으니 SEO를 고려해야 한다.
렌더링 방식, 원하는 사용자 경험에 따라 적절히 suspense를 사용해 보자.
++7/13)
react-query를 suspense와 함께 사용하려면 useSuspenseQuery를 사용하도록 권한다.
https://tanstack.com/query/latest/docs/framework/react/guides/suspense#suspense
'<frontend> > next.js' 카테고리의 다른 글
zero runtime css-in-js PandaCSS (2) | 2024.07.29 |
---|---|
nextjs 렌더링 예제 (0) | 2024.07.29 |
nextjs api기능으로 목업api만들기 (0) | 2024.07.01 |
Next.js프로젝트에 구글 애드센스 달기 (3) | 2024.06.28 |
next.js 블로그 제작 후기 (5) | 2023.12.25 |