SSR을 넘어서(Streaming SSR, PPR)

coggiee
8 min read4 days ago

--

SSR이 CSR보다 느리다?

SSR은 CSR 보다 일반적으로 더 빠르게 화면을 볼 수 있다.

SSR은 서버에서 완성된 HTML을 내려주기 때문에 일반적으로 사용자는 CSR보다 화면을 더 빠르게 볼 수 있다.

(만약, tanstack query를 사용한다면 쿼리를 프리페칭 했을 때, 서버에서 API를 호출하고 데이터가 포함된 마크업(HTML)을 사용자에게 내려주기 때문에 더욱 빨라질 수 있다.)

종종, SSR이 CSR보다 느리게 화면이 보여질 때가 있다.

SSR에서는 모든 API 응답이 끝나야 화면을 보여줄 수 있다. (서버에서 API 요청을 처리하고 데이터를 마크업에 넣어야 하기 때문)

그래서 API 속도가 느리다면 CSR 대비, 오히려 화면이 늦게 보여질 수 있다.

과거에 CSR을 도입한 이유도 바로 이 때문이다.

Streaming SSR: SSR을 넘어서

기존 SSR은 화면을 한 번에 그리기 때문에 API 호출 때문에 렌더링이 지연될 수 있다.

반면에, Streaming SSR 방식은 화면을 한 번에 그리지 않고 청크 단위로 나누어서 그리는 방식이기 때문에, API 호출과 관련없는 부분이라면 가장 빠르게 그릴 수 있다.

즉, 빠르게 렌더링 될 수 있는 부분(API와 연관이 없는 부분, 정적 콘텐츠)을 먼저 그리고 API 호출과 같이 외부 요소에 의해 렌더링이 지연될 수 있는 곳을 나중에 그리는 방식이다.

NextJS에서 Streaming SSR 적용

기본 방식은 Suspense를 사용하여 일종의 경계를 설정하는 것이다. 이는 페이지와 컴포넌트에 적용할 수 있다.

route(페이지) 단위

  • route에 해당하는 폴더에 loading.tsx 을 생성한다.
  • 이는 페이지의 로딩 상태에 관여한다.
  • 이는 마치 페이지를 <Suspense> 로 감싼것과 동일하게 동작한다.
<Layout>
<SomeComponent />
<Suspense fallback={<Loading/>}>
<Page />
</Suspense>
</Layout>

컴포넌트 단위

  • <Suspense> 로 로딩이 필요한 컴포넌트를 감싼다.
  • 보통 API 데이터를 필요로 하는 컴포넌트를 <Suspense>로 감싼다.
  • 이는 해당 컴포넌트의 로딩 상태에만 관여한다.
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>

PPR(Partial Pre-rendering): 도입 계기

PPR의 도입 배경을 이해하려면 먼저 ‘Pre-rendering’의 한계를 살펴봐야 한다.

Pre-rendering의 한계

런타임에 결정되는 정보를 사용하는 페이지는 요청을 받기 전까지는 사전에 렌더링할 수 없다.

이는 다음의 고민에 부딪히게 된다.

  • 정적으로 페이지를 미리 생성해둔다. 하지만 빌드 타임에 페이지를 미리 생성하기 때문에, 완전히 개인화된 경험을 제공할 수 없다. (SSG)
    - 이를 보완한 방식인 ISR이 있지만, 이는 주기적으로 데이터를 업데이트하는 경우에 적합하고 실시간성 및 개인화 경험 제공과는 거리가 멀다.
  • 매 요청마다 페이지를 동적으로 생성한다. 하지만, 이는 데이터가 변하지 않는 페이지도 매 요청마다 생성하기 때문에 불필요한 비용이 소모된다. (SSR)

추가: NextJS의 Pre-rendering의 기본 동작

NextJS는 빌드 타임에 페이지를 미리 렌더링한다.

하지만 headers(), cookies()와 같은 동적 API 또는 fetch(’’, { cache: ‘no-store’ }) 를 사용한다면, 정적 콘텐츠를 보여주는 다른 컴포넌트 일지라도 동적으로 렌더링된다.

즉, 컴포넌트 트리에서 특정 컴포넌트가 동적 API를 사용한다면, 루트 컴포넌트가 동적으로 렌더링되고 정적으로 렌더링되던 다른 하위 컴포넌트도 동적으로 렌더링된다.

이 방식은 서버에서 새로 생성될 필요가 없는 정적 콘텐츠도 새로 생성된다는 문제점이 있다.

PPR: 정적, 동적 렌더링의 결합

PPR은 ‘static-rendering’과 ‘dynamic-rendering’의 이점을 결합한, 한 페이지에서 필요에 따라 두 가지의 방식을 유연하게 사용하는 방식이다.

이게 가능한 이유는 PPR은 정적, 동적 콘텐츠를 일종의 경계(boundary)로 구분하기 때문이다. 그래서 동적 API를 사용하더라도 정적 컴포넌트(콘텐츠)의 렌더링 방식에 영향을 주지 않는다.

즉, 한 페이지에서 정적, 동적 콘텐츠를 서로에게 영향을 주지 않는 방식으로 보여줄 수 있다.

비교: Streaming SSR ↔ PPR

Streaming SSR

  • 콘텐츠를 부분적으로 스트리밍하는 방식
    - 빠르게 렌더링 되는 콘텐츠를 먼저 표시하고 느린 데이터 로딩이 있는 콘텐츠는 점진적으로 렌더링한다.
    - 즉, 렌더링되는 속도에 따라 컴포넌트를 청크 단위로 쪼개어 부분적으로 클라이언트에 내려준다.
  • 기존 SSR은 전체 페이지가 로딩될 때를 기다려야 했으나, Streaming SSR은 컴포넌트를 쪼개어 보내주기 때문에 일부 콘텐츠(컴포넌트)를 먼저 표시할 수 있다.

PPR: Partial Pre-rendering

  • 빌드타임에 정적 콘텐츠를 미리 생성하고, 동적 콘텐츠는 런타임에 렌더링된다.
    - 페이지를 생성할 때 정적 콘텐츠는 미리 빌드된 데이터를 보여주고 동적 콘텐츠는 실시간으로 데이터를 보여준다.
  • 전체 페이지를 빌드 타임에 생성하지만, Suspense로 감싸진 동적 콘텐츠(컴포넌트)는 런타임에 생성된다.

결국 주요 차이는…

정적 데이터의 생성 시점

  • PPR: 빌드타임에 생성
  • Streaming SSR: 런타임에 생성(=요청 시 서버에서 생성)

즉, PPR은 정적 데이터가 빌드 타임에 이미 생성되어 있으므로, 서버에서의 정적 콘텐츠 생성을 기다릴 필요가 없다.

코드로 살펴보기

SSR: 모든 콘텐츠를 한 번에 표시

// app/page.tsx
export default function Home() {
return (
<section>
<DynamicContents />
<StaticContents />
</section>
);
}
async function DynamicContents() {
const 데이터 = await fetch("API 주소", {
cache: "no-store"
}).then((res) => res.json());

await new Promise((resolve) => setTimeout(resolve, 5000)); // 명확한 차이를 보기 위한 지연
return 컴포넌트 구조;
}

전체 데이터가 모두 로드된 후에 표시된다. (정적 콘텐츠 포함)

Streaming SSR: 콘텐츠를 점진적으로 쪼개서 표시

// app/page.tsx
import { Suspense } from "react";
export default function Home() {
return (
<main>
<Suspense fallback={<Loading />}> // 동적 콘텐츠를 Suspense로 감싼다.
<DynamicContents />
</Suspense>
<StaticContents />
</main>
);
}
async function DynamicContents() { ... }

동적 콘텐츠가 로드되는 동안 Fallback이 표시되며, 이 과정에서 정적 콘텐츠가 동시에 표시된다.

PPR: 컴포넌트를 경계로 구분하여 표시

// app/page.tsx
export const experimental_ppr = true; // PPR 활성화
export default function Page() {
return (
<main>
<Suspense fallback={<Loading />}> // 동적 콘텐츠를 Suspense로 감싼다.
<DynamicContents />
</Suspense>
<StaticContents />
</main>
);
}
async function DynamicContents() { ... }

정적 콘텐츠는 빌드 타임에 미리 생성되기 때문에 더 빠르게 표시된다.

코드에서는 차이가 없다?: Streaming SSR ↔ PPR

현재까지는 두 방식의 코드는 모두 동적 콘텐츠를 Suspense로 감싸기 때문에 코드 구조상 큰 차이는 없다.

명확한 차이는 코드가 아닌 내부적인 동작 원리와 콘텐츠의 생성 시점에 있다.

  • Streaming SSR
    - 정적과 동적 콘텐츠 모두
    요청 시점에 서버에서 생성된다.
    - 서버는 화면에 빠르게 표시할 수 있는 콘텐츠를 먼저 스트리밍하고, 나머지 동적 콘텐츠를 점진적으로 채운다.
    - 결과적으로 모든 콘텐츠를 런타임에 처리하는 방식이다.
  • PPR(Partial Pre-rendering)
    - 정적 콘텐츠
    빌드 타임에 미리 생성된다.
    - 클라이언트 요청 시 정적 콘텐츠를 빠르게 제공하고, 동적 콘텐츠는 비동기적으로 런타임에서 로드한다.
    - Streaming SSR보다 정적 콘텐츠를 더 빠르게 화면에 표시할 수 있다.
    - 즉, SSG의 성능SSR의 유연함을 결합한 방식이다.

✔️ 결론적으로 핵심 차이는 정적 콘텐츠의 생성 시점이다.

--

--

coggiee
coggiee

Written by coggiee

0 Followers

Just doing

No responses yet