Streaming SSR로 사용자 경험 개선하기

GwangSoo·2025년 9월 25일
1

개인공부

목록 보기
33/34
post-thumbnail

Next.js를 사용하다 보면 서버 사이드 렌더링(SSR) 을 통해 서버에서 데이터를 처리한 뒤 완성된 HTML을 클라이언트에 내려주는 경우가 많다.

문제 상황

아래 예시는 SomeComponent에서 데이터를 가져오느라 3초가 걸리고, 그 동안 페이지 전체가 블로킹되는 상황이다.

// app/api/delay/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const { delay = 3000 } = await request.json();
  
	// 지연 처리를 위한 setTimeout
  await new Promise((resolve) => setTimeout(resolve, delay));
  
  return NextResponse.json({ message: "Delayed Response from API" });
}
// app/page.tsx
import SomeComponent from "@/components/some-component";

export default async function Home() {
  await fetch("http://localhost:3000/api/delay", {
    method: "POST",
    body: JSON.stringify({ delay: 1000 }),
  });

  return (
    <div>
      <h1>Root Component Rendered</h1>
      <SomeComponent />
    </div>
  );
}
// components/some-component.tsx
export default async function SomeComponent() {
  await fetch("http://localhost:3000/api/delay", {
    method: "POST",
    body: JSON.stringify({ delay: 3000 }),
  });

  return <p>SomeComponent Rendered</p>;
}

no-loading-component

이 경우 page.tsx의 1초 딜레이가 끝나도, SomeComponent가 끝날 때까지 최종 HTML이 브라우저에 도착하지 않는다.

즉, 사용자는 화면이 멈춘 것 같은 경험을 하게 되는 것이다.

loading.tsx를 이용한 개선

Next.js에서는 가장 가까운 경로에 loading.tsx를 두면 해당 경로에서 비동기 작업이 진행되는 동안 fallback UI를 보여줄 수 있다.

// app/loading.tsx
export default function Loading() {
  return <div>Loading...</div>;
}

구조는 아래와 같다.

app
|_layout.tsx
|_loading.tsx
|_page.tsx

with-loading-component

이제 페이지 진입 시 Loading...이 먼저 보이기 때문에 최소한 빈 화면은 피할 수 있다.

하지만 여전히 모든 컴포넌트가 다 준비될 때까지 기다려야 최종 페이지가 렌더링된다는 한계가 남아있다.

Streaming SSR로 한 단계 더 개선하기

여기서 등장하는 개념이 바로 Streaming SSR이다.

네트워크를 통해 데이터 특히 오디오나 비디오 같은 미디어를 실시간으로 받아오는 기법을 의미한다. 데이터를 실시간으로 받아오는 과정을 개천 물이 흘러오는 것으로 비유한 것.

출처: 나무위키

서버에서 HTML을 “조각” 단위로 만들어 클라이언트로 흘려보내는 방식이다. 즉, 일부 데이터가 늦게 도착하더라도 먼저 보여줄 수 있는 부분을 우선 렌더링하는 것을 의미한다.

서버: [HTML 조각1] → [HTML 조각2] → [HTML 조각3] → 클라이언트

페이지를 조각으로 분리하여 사용자에게 먼저 보여줄 수 있는 부분은 먼저 보여주는 방식을 제공한다. 이를 통해 사용자는 전체 페이지가 렌더링되기 전에 일부 페이지를 먼저 볼 수 있다.

이 원리를 이용하면 페이지의 일부는 바로 보여주고 늦게 로딩되는 컴포넌트는 나중에 합쳐서 표시할 수 있다.

React에서는 이를 위해 Suspense를 사용한다.

// app/page.tsx
import SomeComponent from "@/components/some-component";
import { Suspense } from "react";

export default async function Home() {
  await fetch("http://localhost:3000/api/delay", {
    method: "POST",
    body: JSON.stringify({ delay: 1000 }),
  });

  return (
    <div>
      <h1>Root Component Rendered</h1>
      <Suspense fallback={<div>Loading SomeComponent</div>}>
        <SomeComponent />
      </Suspense>
    </div>
  );
}

이제 위 코드로 변경 후 실행하면 아래와 같이 1초 후에는 Root Component RenderedLoading SomeComponent가 먼저 나오고 이후에 SomeComponent Rendered가 화면에 보이게 된다.

with-suspense

이렇게 로딩이 완료된 화면을 먼저 보여줌으로써 사용자 경험을 개선하고 이탈 또한 막을 수 있을 것이다.

사용자 경험(UX) 관점에서의 장점

  • 즉각적인 피드백 제공 → “멈춘 것 같은” 경험 방지
  • 중요한 콘텐츠를 먼저 보여주기 → 사용자는 기다리면서도 필요한 정보를 확인 가능
  • 부분적 렌더링 → 네트워크 상황이 좋지 않아도 핵심 UI를 빠르게 확인 가능

추가로 고려할 수 있는 성능 개선 방법

Streaming SSR 외에도 여러 전략을 조합하면 UX를 더 개선할 수 있다.

  • Dynamic Import (next/dynamic): 필요할 때만 컴포넌트를 불러오기
  • TanStack Query의 prefetchQuery: 서버에서 데이터를 미리 받아 캐싱 후 클라이언트에서 즉시 사용
  • 서버 액션(Server Actions): 데이터 요청과 변형을 서버에서 직접 처리하여 클라이언트 전송량 최소화

마무리하며

이번 글에서는 Next.js에서의 SSR의 한계와 이를 극복하는 Streaming SSR 개념을 정리했다.

Suspense와 Streaming을 적절히 활용하면 사용자 이탈을 막고 훨씬 부드러운 사용자 경험을 제공할 수 있다.

0개의 댓글