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>;
}
이 경우 page.tsx
의 1초 딜레이가 끝나도, SomeComponent
가 끝날 때까지 최종 HTML이 브라우저에 도착하지 않는다.
즉, 사용자는 화면이 멈춘 것 같은 경험을 하게 되는 것이다.
Next.js에서는 가장 가까운 경로에 loading.tsx
를 두면 해당 경로에서 비동기 작업이 진행되는 동안 fallback UI를 보여줄 수 있다.
// app/loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}
구조는 아래와 같다.
app
|_layout.tsx
|_loading.tsx
|_page.tsx
이제 페이지 진입 시 Loading...이 먼저 보이기 때문에 최소한 빈 화면은 피할 수 있다.
하지만 여전히 모든 컴포넌트가 다 준비될 때까지 기다려야 최종 페이지가 렌더링된다는 한계가 남아있다.
여기서 등장하는 개념이 바로 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 Rendered와 Loading SomeComponent가 먼저 나오고 이후에 SomeComponent Rendered가 화면에 보이게 된다.
이렇게 로딩이 완료된 화면을 먼저 보여줌으로써 사용자 경험을 개선하고 이탈 또한 막을 수 있을 것이다.
Streaming SSR 외에도 여러 전략을 조합하면 UX를 더 개선할 수 있다.
이번 글에서는 Next.js에서의 SSR의 한계와 이를 극복하는 Streaming SSR 개념을 정리했다.
Suspense와 Streaming을 적절히 활용하면 사용자 이탈을 막고 훨씬 부드러운 사용자 경험을 제공할 수 있다.