무엇
renderToPipeableStream은 React 트리를 HTML로 그려 Node.js Stream으로 흘려보내는 서버 렌더링 API다. Node.js 서버 전용이다.
reactNode는 문서 전체(<html> 포함) 트리다. 반환값은 pipe와 abort 두 메서드를 가진 객체다.
import { renderToPipeableStream } from 'react-dom/server'
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html')
pipe(response)
},
})
Deno나 엣지 런타임처럼 Web Streams를 쓰는 환경이면 이 API 대신 renderToReadableStream을 쓴다.
무슨 일이 일어나나
<Suspense> 밖의 콘텐츠가 셸이고, 셸이 준비되면 onShellReady가 불린다. 이 콜백 안에서 pipe()를 부르는 순간부터 응답 스트리밍이 시작된다. Suspense 안쪽 콘텐츠는 데이터가 준비되는 대로 뒤이어 흘러 들어온다.
콜백은 네 종류다.
onShellReady— 셸 렌더링 완료. 여기서pipe()를 불러 진행형 스트리밍을 시작한다.onShellError— 셸 렌더링 자체가 실패. 아직 바이트를 하나도 내보내지 않은 시점이라 fallback HTML로 안전하게 대체할 수 있다.onAllReady— 셸+Suspense 콘텐츠까지 전부 완료. 크롤러나 정적 생성처럼 진행형 스트리밍이 필요 없을 때 여기서pipe()를 부른다.onError— 셸이든 아니든 서버에서 에러가 날 때마다 불린다. 셸 밖 에러는 스트림을 끊지 않고 React가 클라이언트에서 복구를 시도한다.
사용법
import { renderToPipeableStream } from 'react-dom/server'
app.use('/', (request, response) => {
let didError = false
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = didError ? 500 : 200
response.setHeader('content-type', 'text/html')
pipe(response)
},
onShellError() {
response.statusCode = 500
response.setHeader('content-type', 'text/html')
response.send('<h1>문제가 발생했다</h1>')
},
onError(error) {
didError = true
console.error(error)
},
})
})
크롤러처럼 완성된 HTML만 필요하면 onShellReady가 아니라 onAllReady에서 pipe()를 부른다. abort()로 렌더링을 중도에 끊고 나머지를 클라이언트에 넘길 수도 있다.
const { abort } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'] })
setTimeout(() => abort(), 10_000)
실무 예시
Express, Fastify처럼 Node.js 서버 위에서 직접 SSR을 구현할 때 쓴다. Next.js의 Node.js 런타임 SSR이 내부적으로 이 계열 API를 쓴다.
왜 중요한가
Node.js 기반 커스텀 SSR 서버를 짤 때가 아니면 직접 호출할 일이 거의 없다. 프레임워크가 감싸서 제공하기 때문이다. 그래도 셸/onShellReady/onAllReady라는 스트리밍 모델은 SSR 동작을 이해하는 데 필요하고, 엣지 런타임이면 이 API가 아니라 renderToReadableStream을 찾아야 한다는 것만 기억해 두면 된다.
Reference