무엇
renderToReadableStream은 React 트리를 HTML로 그려 Web Stream으로 돌려주는 서버 렌더링 API다. Node.js가 아니라 Cloudflare Workers, Deno처럼 Web Streams를 쓰는 엣지 런타임에서 돌아간다.
reactNode는 <html> 태그부터 문서 전체를 나타내는 트리다. await하면 ReadableStream을 얻는다.
import { renderToReadableStream } from 'react-dom/server'
async function handler(request: Request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
})
return new Response(stream, { headers: { 'content-type': 'text/html' } })
}
Node.js 서버라면 이 API 대신 renderToPipeableStream을 쓴다.
무슨 일이 일어나나
<Suspense> 밖에 있는 컴포넌트가 **셸(shell)**이다. React는 셸을 먼저 그려 스트림으로 흘려보내고, <Suspense> 안쪽은 데이터가 준비되는 대로 순서대로 이어 붙인다. 셸 렌더링이 성공하면 Promise가 스트림으로 resolve되고, 실패하면 reject된다 — 그래서 await 자체를 try/catch로 감싸 셸 에러를 잡는다.
셸 밖(Suspense 안쪽)에서 나는 에러는 스트림을 끊지 않는다. 대신 onError 콜백이 불리고, React는 그 경계의 fallback을 내보낸 뒤 클라이언트에서 복구를 시도한다.
stream.allReady 프로미스는 셸과 나머지 콘텐츠까지 전부 끝났을 때 resolve된다. 크롤러나 정적 생성처럼 완성된 HTML이 필요할 때 이걸 기다린다.
사용법
import { renderToReadableStream } from 'react-dom/server'
async function handler(request: Request) {
let didError = false
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true
console.error(error)
},
})
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
})
} catch (error) {
// 셸 자체가 실패했을 때만 여기로 온다
return new Response('<h1>문제가 발생했다</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
})
}
}
크롤러처럼 진행형 스트리밍 없이 완성된 HTML이 필요하면 stream.allReady를 기다린 다음 응답한다.
const stream = await renderToReadableStream(<App />, { bootstrapScripts: ['/main.js'] })
if (isCrawler) await stream.allReady
return new Response(stream, { headers: { 'content-type': 'text/html' } })
signal로 렌더링을 중도에 끊고 나머지를 클라이언트에 넘길 수도 있다.
const controller = new AbortController()
setTimeout(() => controller.abort(), 10_000)
const stream = await renderToReadableStream(<App />, { signal: controller.signal })
클라이언트에서는 hydrateRoot(document, <App />)로 이어받는다.
실무 예시
Vercel Edge Functions, Cloudflare Workers처럼 Node.js가 아닌 런타임에서 앱을 서버 렌더링할 때 쓴다. Next.js의 Edge Runtime SSR이 내부적으로 이 계열 API를 쓴다.
왜 중요한가
직접 호출하는 경우는 드물고, 대개 프레임워크(Next.js 등)가 감싸서 쓴다. 다만 셸/Suspense 스트리밍이라는 모델 자체는 프레임워크 SSR 동작을 이해하는 데 필요하다. Node 환경이면 이 API가 아니라 renderToPipeableStream을 찾아야 한다는 것만 기억해 두면 된다.
Reference