Site Search

Next.js의 PPR(Partial Prerendering)이란? 동적 콘텐츠를 빠르게 제공하는 새로운 렌더링 방법

Next.js의 PPR(Partial Prerendering)이란? 동적 콘텐츠를 빠르게 제공하는 새로운 렌더링 방법

여러분 안녕하세요, 웹 엔지니어 Y라고 합니다。
이번 기사에서는 PPR(Partial Prerendering)에 대해 설명하겠습니다。

PPR(Partial Prerendering)이란 무엇인가?

Next.js 14에서는 Partial Prerendering(이하 PPR)이라는 새로운 렌더링 방식이 실험적으로 지원되고 있습니다。
기존의 Server Side Rendering(이하 SSR)이 페이지 전체 데이터를 서버에서 생성하고 완료 후에 한 번에 클라이언트로 전송하는 것과 달리, PPR에서는 빌드 시에 정적 데이터를 생성(Static Rendering)하면서, 동적 데이터는 서버가 데이터를 생성(Dynamic Rendering)하며 조금씩 클라이언트로 전송합니다。이를 통해 초기 표시까지의 시간이 크게 단축되어 UX 향상에 기여합니다。
이 기사에서는 PPR의 원리와 그 이점, 구현 방법에 대해 자세히 살펴보겠습니다。

PPR의 이점

PPR의 이점은 페이지의 초기 표시 속도 향상과 사용자 경험의 개선에 있습니다。주요 이점은 다음과 같습니다。

초기 표시까지의 시간 단축

필요한 데이터가 준비되는 대로 순차적으로 클라이언트에 전송되므로, 사용자는 즉시 페이지의 일부를 확인할 수 있어 대기 시간이 단축됩니다。

UX 향상

중요한 부분을 먼저 표시하고 나머지 요소를 순차적으로 불러옴으로써, 사용자에게 원활한 경험을 제공합니다。

성능 최적화

서버에서 일괄 생성하는 것보다 리소스를 효율적으로 활용할 수 있어, 대규모 페이지나 무거운 콘텐츠를 포함한 페이지, API로부터 데이터를 자주 가져오는 경우에 성능이 향상됩니다。

PPR의 작동 원리

PPR에서는 앞서 언급한 대로 페이지를 생성할 때 이미 만들어진 정적 데이터를 반환하면서, 서버에서 데이터가 도착한 순서대로 렌더링을 진행하고 필요한 부분을 클라이언트에 순차적으로 전송합니다。

즉, 동일한 페이지 내에 정적 및 동적 데이터가 혼합되어 있어도 동적 데이터가 생성되기를 기다리지 않고 정적 데이터를 먼저 반환할 수 있습니다。

따라서 사용자는 페이지의 일부를 즉시 확인할 수 있으며, 나머지 데이터가 준비되는 동안에도 원활한 표시가 유지됩니다。

Next.js Streaming with Suspense

Streaming SSR과의 차이점

PPR은 Streaming SSR을 더욱 발전시킨 방법입니다。

PPR은 정적 데이터가 빌드 시에 생성되는 반면, Streaming SSR은 요청 시에 서버에서 생성합니다。

즉, PPR은 정적 데이터가 빌드 시에 생성되어 있으므로 서버에서 콘텐츠 생성을 기다릴 필요가 없어 초기 표시가 더욱 빨라집니다。

PPR의 구현 방법

여기에서는 예로 포케코로 아이템 도감에서의 고객 정보 표시를 소개합니다。

아이템 도감의 콘텐츠는 자주 업데이트되지 않으므로 SSG나 ISR로 정적 데이터를 생성합니다。반면에 고객 정보는 데이터를 가져와서 표시해야 하므로 동적 콘텐츠여야 합니다。

EC 사이트 예시_Next.js Partial Prerendering

여기에서는 SSR → Streaming SSR → PPR 순으로 구현합니다。

SSR

Data Fetching의 옵션에 cache: "no-store"를 추가하는 것만으로 SSR이 가능합니다。
Next.js Partial Data Fetching


// app/page.tsx
import Image from "next/image";
import StaticContents from "@/components/StaticContents";

export default function Home() {
  return (
    <main>
      <DynamicUserInfo />
      <StaticContents />
    </main>
  );
}

async function DynamicUserInfo() {
  const userInfo: IUserInfo = await fetch("https://dummyjson.com/userInfo", {
    cache: "no-store", // no-store를 추가함으로써 SSR이 됩니다。
  }).then((res) => res.json());
  await new Promise((resolve) => setTimeout(resolve, 5000)); // 5초 지연시킵니다。

  return (
    <section className="colonian-info animate-fade">
      <div className="profile">
        <Image
          src={userInfo.imgSrc}
          width="64"
          height="64"
          className="image"
          alt={"image"}
        />
        <p className="mycode">{`ID:${userInfo.userId}`}</p>
        <p className="nickname">{userInfo.nickName}</p>
      </div>
    </section>
  );
}

interface IUserInfo {
  imgSrc: string;
  nickName: string;
  userId: string;
}

DynamicUserInfo는 고객 정보를 표시하는 동적 컴포넌트입니다。

여기에서는 Data Fetching 후 5초간 지연시켜 보겠습니다。

이처럼 SSR의 경우, Data Fetching을 하는 5초 동안 아무것도 표시되지 않습니다。(흰 화면이 표시됩니다)

그리고 접속 후 약 6초가 지난 시점에서 모든 정보가 한꺼번에 표시되는 것을 알 수 있습니다。

Streaming SSR

다음은 SSR을 Streaming SSR로 변경해 보겠습니다。

Suspense를 추가하는 것만으로 Streaming SSR로 변경할 수 있습니다。
Next.js Streaming with Suspense Example


import { Suspense } from "react"; // ① Suspense를 import합니다
import Image from "next/image";
import Skeleton from "@/components/Skeleton";
import StaticContents from "@/components/StaticContents"

export default function Home() {
  return (
    <main>
        {/*
          ② 동적 콘텐츠를 Suspense로 랩핑합니다。
          fallback에는 동적 콘텐츠가 렌더링되기 전에 표시할 UI 컴포넌트를 넣습니다。
        */}
        <Suspense fallback={<Skeleton />}>
            <DynamicUserInfo />
        </Suspense>
        <StaticContents />
    </main>
  );
}


async function DynamicUserInfo() {
    ...
}

Suspensefallback에는 동적 데이터를 로드할 때 표시할 대체 요소를 넣습니다。

더 쉽게 이해하기 위해 네트워크 속도를 낮추었습니다。

고객 정보가 로드되는 동안 대체 요소(Skeleton)가 표시되며, 동시에 아이템 도감이 표시되는 것을 볼 수 있습니다。

그리고 접속 후 약 6초가 지난 시점에서 고객 정보가 표시되는 것을 알 수 있습니다。

이처럼 실제 고객 정보가 로드될 때까지 사용자에게 시각적인 피드백(아이템 도감의 정보)을 제공할 수 있습니다。

PPR

마지막으로 Streaming SSR을 PPR로 변경해 보겠습니다。

먼저 next.config.mjs에 옵션을 추가합니다。


// next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: "incremental", // ppr: boolean | "incremental" _ true인 경우 모든 페이지에 적용되며, "incremental"인 경우 지정한 Route에 적용됩니다
  },
};

export default nextConfig;

다음으로, PPR을 설정할 Route에 export const experimental_ppr = true;를 추가합니다。


export const experimental_ppr = true; // 추가

export default function Page() {
  ...
  <Suspense fallback={<Loading />}>
    <DynamicContents />
  </Suspense>
  ...
}

이상으로 export const experimental_ppr = true;가 추가된 Route는 PPR이 됩니다。

만약 프로젝트 전체에 적용하고 싶다면, next.config.mjs의 ppr 옵션을 true로 설정합니다。

더 쉽게 이해하기 위해 네트워크 속도를 낮추었습니다。

이번에는 정적 데이터가 매우 가볍기 때문에 Streaming SSR과의 차이는 거의 없습니다。

PPR로 변경함으로써 Suspense 외부의 요소가 빌드 시에 정적화되어, Streaming SSR에 비해 더 빠르게 Suspense 외부의 콘텐츠(아이템 도감의 정보)가 표시됩니다。

SSR과 PPR을 나란히 비교하면 초기 표시까지의 차이가 명확하게 드러납니다。
 

정리

Next.js의 PPR은 동적 페이지나 무거운 콘텐츠를 효율적으로 제공하기 위한 강력한 도구입니다。필요한 데이터가 도착한 단계에서 조금씩 렌더링을 진행함으로써, 사용자에게 원활하고 스트레스 없는 경험을 제공합니다。초기 표시 속도와 UX를 중시하는 프로젝트에서 꼭 활용해 보세요。

참고

Next.js Partial Prerendering
PPR – pre-rendering 신시대의 도래와 SSR/SSG 논쟁의 종언
Youtube – Next.js Visually Explained: Partial Pre-rendering (PPR)

Category

Tag