지금까지 빌드 및 revalidation 시간에 데이터 fetch 및 렌더링을 진행하는 정적 렌더링과 사용자 요청 등이 발생할 때 렌더링을 진행하는 동적 렌더링에 대해 알아보았습니다.
이번 포스팅에서는 PPR(Partial Prerendering)을 사용하여 정적 렌더링, 동적 렌더링, 스트리밍을 동일한 경로로 결합하는 방법을 알아보겠습니다.
오늘날 대부분의 웹 애플리케이션에서는 전체 애플리케이션 또는 특정 경로(route)에 한해 정적 렌더링과 동적 렌더링 중 하나를 선택합니다.
noStore()
, cookies()
등) 해당 경로 전체가 동적으로 변화합니다.하지만 보통 대부분의 경로는 완전히 정적이거나 동적이지 않습니다.
예를 들어, 전자상거래 사이트를 생각해보겠습니다. 제품 정보 페이지의 대부분을 정적으로 렌더링하고 싶을 수 있지만, 사용자의 장바구니와 추천 제품 등은 동적으로 가져와야 합니다.
지금까지 만들어 본 대시보드 페이지를 생각해보면:
<SideNav/>
는 fetch
해오는 데이터에 의존하지 않기 때문에 정적으로 개발할 수 있습니다.fetch
해오는 데이터를 기반으로 화면을 구성하는 <Page/>
내의 컴포넌트들은 동적으로 개발해야 합니다.<SideNav/>
컴포넌트 (정적)import Link from 'next/link';
import NavLinks from '@/app/ui/dashboard/nav-links';
import AcmeLogo from '@/app/ui/acme-logo';
import { PowerIcon } from '@heroicons/react/24/outline';
export default function SideNav() {
return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
<Link
className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
href="/"
>
<div className="w-32 text-white md:w-40">
<AcmeLogo />
</div>
</Link>
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<form>
<button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<PowerIcon className="w-6" />
<div className="hidden md:block">Sign Out</div>
</button>
</form>
</div>
</div>
);
}
cards.tsx
컴포넌트 (동적)import {
BanknotesIcon,
ClockIcon,
UserGroupIcon,
InboxIcon,
} from "@heroicons/react/24/outline";
import { lusitana } from "../font";
import { fetchCardData } from "@/app/lib/data";
const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
export default async function CardWrapper() {
// 동적 데이터 fetch
const {
numberOfCustomers,
numberOfInvoices,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
return (
<>
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
<Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/>
</>
);
}
export function Card({
title,
value,
type,
}: {
title: string;
value: number | string;
type: "invoices" | "customers" | "pending" | "collected";
}) {
const Icon = iconMap[type];
return (
<div className="rounded-xl bg-gray-50 p-2 shadow-sm">
<div className="flex p-4">
{Icon ? <Icon className="h-5 w-5 text-gray-700" /> : null}
<h3 className="ml-2 text-sm font-medium">{title}</h3>
</div>
<p
className={`${lusitana.className}
truncate rounded-xl bg-white px-4 py-8 text-center text-2xl`}
>
{value}
</p>
</div>
);
}
공식 문서에서는 동일한 경로에서 정적 렌더링과 동적 렌더링의 장점을 결합할 수 있는 새로운 렌더링 모델로써 Partial Prerendering을 소개하고 있습니다.
이 모델은 빠른 초기 로드를 보장하면서도 개인화되고 자주 업데이트되는 콘텐츠를 제공하여 성능과 사용자 만족도를 모두 향상시킬 수 있습니다.
사용자가 경로를 방문할 때 다음과 같은 효과를 제공합니다:
인덱싱이란 데이터베이스 또는 검색 엔진에서 데이터 검색 속도를 높이기 위해 사용하는 기술을 의미합니다. 데이터베이스나 검색 엔진에서 인덱스는 특정 키를 기준으로 데이터의 위치를 저장한 구조체로, 대량의 데이터를 효율적으로 조회할 수 있도록 돕습니다.
Partial Prerendering(부분 사전 렌더링)은 React의 Suspense를 활용하여 애플리케이션 일부를 렌더링하는 시점을 데이터 로딩과 같은 조건이 충족될 때까지 지연시키는 방식입니다.
Suspense로 컴포넌트를 감싸는 것은 컴포넌트 자체를 동적으로 만드는 것이 아닙니다. 오히려 Suspense는 정적과 동적 코드 사이의 경계로 사용됩니다.
대시보드 경로에 부분적 프리렌더링(PPR)을 구현하는 방법을 살펴보겠습니다.
먼저 next.config.js
에 다음 코드를 추가하여 PPR을 활성화합니다.
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
ppr: "incremental",
},
};
module.exports = nextConfig;
ppr: "incremental"
옵션을 사용하면 특정 경로에 대해 PPR을 선택적으로 적용할 수 있습니다.
대시보드 레이아웃에서 experimental_ppr
segment 구성 옵션을 추가합니다.
import SideNav from "@/app/ui/dashboard/sidenav";
export const experimental_ppr = true; // PPR 활성화
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64">
<SideNav />
</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
</div>
);
}
Partial Prerendering의 큰 장점 중 하나는 코드를 변경하지 않고도 사용할 수 있다는 것입니다.
특정 경로의 동적 부분을 Suspense로 감싸주기만 하면, Next.js가 어떤 부분이 정적이고 어떤 부분이 동적인지를 확실히 알 수 있도록 할 수 있습니다.
PPR(Partial Prerendering)은 Next.js에서 제공하는 혁신적인 렌더링 모델로, 정적 렌더링과 동적 렌더링의 장점을 모두 활용할 수 있게 해줍니다.
간단한 설정만으로 성능과 사용자 경험을 크게 향상시킬 수 있으니, 여러분의 Next.js 프로젝트에도 한번 적용해보시기 바랍니다!