Next.js PPR(Partial Prerendering) 가이드

지금까지 빌드 및 revalidation 시간에 데이터 fetch 및 렌더링을 진행하는 정적 렌더링과 사용자 요청 등이 발생할 때 렌더링을 진행하는 동적 렌더링에 대해 알아보았습니다.

이번 포스팅에서는 PPR(Partial Prerendering)을 사용하여 정적 렌더링, 동적 렌더링, 스트리밍을 동일한 경로로 결합하는 방법을 알아보겠습니다.

1. Static vs. Dynamic Routes

오늘날 대부분의 웹 애플리케이션에서는 전체 애플리케이션 또는 특정 경로(route)에 한해 정적 렌더링과 동적 렌더링 중 하나를 선택합니다.

  • Next.js에서는 특정 경로(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>
  );
}

2. What is Partial Prerendering?

공식 문서에서는 동일한 경로에서 정적 렌더링과 동적 렌더링의 장점을 결합할 수 있는 새로운 렌더링 모델로써 Partial Prerendering을 소개하고 있습니다.

이 모델은 빠른 초기 로드를 보장하면서도 개인화되고 자주 업데이트되는 콘텐츠를 제공하여 성능과 사용자 만족도를 모두 향상시킬 수 있습니다.

PPR의 주요 효과

사용자가 경로를 방문할 때 다음과 같은 효과를 제공합니다:

  • 빠른 초기 로드: 사전 렌더링된 정적 셸을 제공함으로써 사용자는 빠른 초기 로드 시간을 경험할 수 있습니다.
  • 향상된 사용자 경험: 동적 콘텐츠가 비동기적으로 병렬 로드되기 때문에 사용자는 대기 시간 없이 정적 부분과 상호작용할 수 있으며, 동적 데이터는 준비되자마자 표시됩니다.
  • SEO 최적화: 페이지의 주요 부분이 사전 렌더링되므로 검색 엔진이 정적 콘텐츠를 효과적으로 인덱싱(indexing)할 수 있어 SEO가 향상됩니다.

인덱싱이란?

인덱싱이란 데이터베이스 또는 검색 엔진에서 데이터 검색 속도를 높이기 위해 사용하는 기술을 의미합니다. 데이터베이스나 검색 엔진에서 인덱스는 특정 키를 기준으로 데이터의 위치를 저장한 구조체로, 대량의 데이터를 효율적으로 조회할 수 있도록 돕습니다.

3. How does Partial Prerendering work?

Partial Prerendering(부분 사전 렌더링)은 React의 Suspense를 활용하여 애플리케이션 일부를 렌더링하는 시점을 데이터 로딩과 같은 조건이 충족될 때까지 지연시키는 방식입니다.

동작 원리

  1. Suspense의 fallback은 초기 HTML 파일에 정적 콘텐츠와 함께 삽입됩니다.
  2. 빌드 시간에 (또는 재검증 중에) 정적 콘텐츠는 Prerendering되고 CDN이나 Edge Network에 캐시됩니다.
  3. 동적 콘텐츠의 렌더링은 사용자가 경로를 요청할 때까지 지연됩니다.

중요한 개념: Suspense는 경계선 역할

Suspense로 컴포넌트를 감싸는 것은 컴포넌트 자체를 동적으로 만드는 것이 아닙니다. 오히려 Suspense는 정적과 동적 코드 사이의 경계로 사용됩니다.

정적과 동적 코드의 차이점

  • 정적 코드: 렌더링할 때 변하지 않는 부분 (컴포넌트의 구조나 UI 레이아웃)
  • 동적 코드: 런타임(run-time) 중에 데이터에 따라 변하는 부분 (데이터 fetch)

Suspense의 역할

  • Suspense는 React에서 동적으로 데이터를 불러오거나 다른 비동기 작업을 수행할 때 사용됩니다.
  • Suspense는 이러한 작업이 완료될 때까지 대기하고, 그 사이에 사용자에게 로딩 상태를 보여주는 역할을 합니다. (이를 fallback이라고 합니다)
  • 따라서 Suspense는 애플리케이션에서 정적인 부분과 동적인 부분 사이를 나누는 역할을 하며, 정적인 부분은 바로 렌더링되고, 동적인 부분은 필요할 때까지 지연됩니다.

4. Partial Prerendering(PPR) 구현

대시보드 경로에 부분적 프리렌더링(PPR)을 구현하는 방법을 살펴보겠습니다.

Step 1: PPR 활성화

먼저 next.config.js에 다음 코드를 추가하여 PPR을 활성화합니다.

/** @type {import('next').NextConfig} */

const nextConfig = {
  experimental: {
    ppr: "incremental",
  },
};

module.exports = nextConfig;

ppr: "incremental" 옵션을 사용하면 특정 경로에 대해 PPR을 선택적으로 적용할 수 있습니다.

Step 2: 레이아웃에서 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>
  );
}

설정 완료

  • 개발 중에는 애플리케이션에서 큰 차이를 느끼지 못할 수 있지만, 배포 환경(production)에서 성능 향상을 눈에 띄게 느낄 것입니다.
  • 이렇게 하면 Next.js는 경로의 정적 부분을 미리 렌더링하고, 동적 부분은 사용자가 요청할 때까지 지연시킵니다.

PPR의 큰 장점

Partial Prerendering의 큰 장점 중 하나는 코드를 변경하지 않고도 사용할 수 있다는 것입니다.

특정 경로의 동적 부분을 Suspense로 감싸주기만 하면, Next.js가 어떤 부분이 정적이고 어떤 부분이 동적인지를 확실히 알 수 있도록 할 수 있습니다.

마무리

PPR(Partial Prerendering)은 Next.js에서 제공하는 혁신적인 렌더링 모델로, 정적 렌더링과 동적 렌더링의 장점을 모두 활용할 수 있게 해줍니다.

간단한 설정만으로 성능과 사용자 경험을 크게 향상시킬 수 있으니, 여러분의 Next.js 프로젝트에도 한번 적용해보시기 바랍니다!

profile
프론트엔드 입문 개발자입니다.

0개의 댓글