무엇
prerender는 React 트리를 정적 HTML로 미리 그리는 API다. renderToReadableStream과 달리 콘텐츠를 흘려보내지 않고, 모든 렌더링이 끝난 뒤에야 결과를 돌려준다.
Web Streams 환경(Deno, 엣지 런타임) 전용이고 Node.js에서는 대신 prerenderToNodeStream을 쓴다.
import { prerender } from 'react-dom/static'
async function handler(request: Request) {
const { prelude } = await prerender(<App />, { bootstrapScripts: ['/main.js'] })
return new Response(prelude, { headers: { 'content-type': 'text/html' } })
}
무슨 일이 일어나나
renderToReadableStream은 셸을 먼저 보내고 Suspense 콘텐츠를 순서대로 이어 붙이는 스트리밍이지만, prerender는 Suspense로 기다리는 데이터까지 전부 resolve된 뒤에 완성된 HTML을 돌려준다. 그래서 빌드 타임 정적 생성(SSG)에 맞는 API다 — 결과물에 로딩 fallback이 섞여 있지 않다.
반환값은 prelude(HTML을 담은 ReadableStream)와 postponed(부분 프리렌더링을 나중에 resume()으로 이어 그릴 때 쓰는 값, 완전히 끝났으면 null) 두 필드다.
사용법
async function renderToString() {
const { prelude } = await prerender(<App />, { bootstrapScripts: ['/main.js'] })
const reader = prelude.getReader()
let content = ''
while (true) {
const { done, value } = await reader.read()
if (done) return content
content += Buffer.from(value).toString('utf8')
}
}
signal로 중간에 끊을 수 있고, 이 경우 아직 안 끝난 Suspense 경계는 fallback 상태로 결과에 남는다.
const controller = new AbortController()
setTimeout(() => controller.abort(), 10_000)
const { prelude } = await prerender(<App />, { signal: controller.signal })
bootstrapScripts, bootstrapModules, identifierPrefix, namespaceURI, onError, progressiveChunkSize 등 나머지 옵션은 renderToReadableStream과 같다. 다만 nonce는 지원하지 않는다 — nonce는 요청마다 달라야 하는데 프리렌더링 결과는 여러 요청에 재사용되기 때문이다.
실무 예시
빌드 시점에 페이지를 정적 HTML로 미리 뽑아 CDN에 올려 두는 SSG 파이프라인에서 쓴다. Next.js의 정적 생성이 개념적으로 같은 일을 한다.
왜 중요한가
직접 호출할 일은 드물고 프레임워크의 빌드 파이프라인 안에서 쓰인다. 다만 "스트리밍(renderToReadableStream)과 정적 생성(prerender)은 다른 API"라는 구분, 그리고 Node.js에서는 prerenderToNodeStream을 써야 한다는 것 정도는 기억해 둘 만하다.
Reference