Streaming SSR은 가장 빠르게 그릴 수 있는 부분을 먼저 랜더링을 진행하고 이후는 점진적으로 렌더링 하는 방식
┌────────────────────────────────────────────────────────────┐
│ 2016 │ React v15: renderToString │
│ │ Next.js v1~v2: SSR + pages router |
├──────────┼─────────────────────────────────────────────────┤
│ 2017 │ React v16: renderToNodeStream (Streaming SSR) │
├──────────┼─────────────────────────────────────────────────┤
│ 2022.03 │ React v18: Concurrent + Suspense + │
│ │ renderToPipeableStream / renderToReadableStream │
├──────────┼─────────────────────────────────────────────────┤
│ 2022.10 │ Next.js 13: App Router 도입 (React 18 기반) │
├──────────┼─────────────────────────────────────────────────┤
│ 2024~ │ Next.js 14+: App Router 안정화 및 실전 적용 확대 │
└──────────┴─────────────────────────────────────────────────┘
import ReactDOMServer from "react-dom/server";
function ChildrenComponent(){
...
}
function SampleComponent(){
return(
<>
<div>hello</div>
<ChildrenComponent/>
</>
)
}
const result = ReactDOMServer.renderToString(
React.createElement('div', { id: 'root'}, <SampleComponent />),
)
위 코드 result는 다음과 같은 문자열을 반환
<div id="root" data-reactroot="">
<div>hello</div>
<ul>
<li>apple</li>
...
</ul>
</div>
*data-reactroot 속성이란?
리액트 컴포넌트의 루트 엘리먼트가 무엇인지 식별하는 역할
이 속성은 이후 자바스크립트를 실행하기 위한 hydrate 함수에서 루트를 식별하는 기준이 됨
import { renderToNodeStream } from "react-dom/server";
const stream = renderToNodeStream(<App />);
stream.pipe(res);
*pipe: pipe는 읽기 스트림에서 흘러나오는 데이터를 자동으로 쓰기 스트림으로 전달해 주는 메서드
-readableStream에서 데이터 청크가 준비되면 pipe가 이를 writableStream에 자동으로 전송
-writableStream은 받은 데이터를 처리하거나 저장
Concurrent SSR + Server Component
⇒ react 18이상부터 사용할 수 있고 Nextjs App Router를 사용하고 있다면 해당 API를 따로 설정할 필요가 없습니다. Next.js는 React 18의 Streaming SSR API를 내부적으로 사용해서 페이지를 스트리밍 처리
import { renderToPipeableStream } from "react-dom/server";
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
pipe(res);
},
});
*onShellReady: React가 최소한의 스트리밍 HTML(쉘)을 준비했을 때 호출됨
renderToPipeableStreamnpm install react react-dom express
project/
├── App.jsx
├── server.js
├── package.json
// App.jsx
import React from "react";
export default function App() {
return (
<div>
<h1>Streaming SSR Example</h1>
<p>This part is streamed!</p>
</div>
);
}
// server.js
import express from "express";
import React from "react";
import ReactDOMServer from "react-dom/server";
import App from "./App.jsx";
import { PassThrough } from "stream";
const app = express();
const PORT = 3000;
app.get("*", (req, res) => {
let didError = false;
const { pipe } = ReactDOMServer.renderToPipeableStream(
<html>
<head>
<title>React Streaming</title>
</head>
<body>
<div id='root'>
<App />
</div>
</body>
</html>,
{
onShellReady() {
res.statusCode = didError ? 500 : 200;
res.setHeader("Content-Type", "text/html");
const body = new PassThrough();
pipe(body); // stream을 response로 연결, 스트리밍할 HTML을 PassThrough 스트림으로 처리
body.pipe(res);
},
onError(err) {
didError = true;
console.error("Render error:", err);
},
}
);
});
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});
*PassThrough: 스트림을 Express 응답과 연결하기 위한 Node.js stream 유틸리티
renderToReadableStream이건 Node.js 환경이 아닌 Web Streams 기반 플랫폼에서 사용
React + Deno, Cloudflare Workers
// Cloudflare Workers 예시
export default {
async fetch(request: Request) {
const stream = await renderToReadableStream(<App />);
return new Response(stream, {
headers: {
"Content-Type": "text/html; charset=utf-8",
},
});
},
};