출처: https://react.dev/reference/react-dom/static/prerenderToNodeStream
Reference
prerenderToNodeStream(reactNode, options?)
prerenderToNodeStream
를 호출하면 React 트리를 Node.js 스트림을 통해 정적 HTML로 렌더링할 수 있습니다.
import { prerenderToNodeStream } from 'react-dom/static';
// 백엔드 프레임워크에 따라 라우트 핸들러 문법이 다를 수 있습니다.
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js'],
});
response.setHeader('Content-Type', 'text/plain');
prelude.pipe(response);
});
클라이언트에서는 hydrateRoot를 호출해 서버에서 생성된 HTML을 인터랙티브하게 만들 수 있습니다.
Parameters(파라미터)
reactNode
: HTML로 렌더링할 React 노드(예:<App />
와 같은 JSX 노드). 전체 문서를 나타내야 하므로 App 컴포넌트가<html>
태그를 렌더링해야 합니다.- optional
options
: 정적 생성 옵션 객체- optional
bootstrapScriptContent
: 지정 시, 이 문자열이 인라인<script>
태그로 삽입됩니다. - optional
bootstrapScripts
: 페이지에 삽입할<script>
태그의 URL 문자열 배열. hydrateRoot를 호출하는 스크립트를 포함할 때 사용. 클라이언트에서 React를 실행하지 않을 경우 생략 가능. - optional
bootstrapModules
: bootstrapScripts와 유사하지만,<script type="module">
로 삽입됩니다. - optional
identifierPrefix
: useId로 생성되는 id에 사용할 prefix 문자열. 여러 React root를 한 페이지에 사용할 때 id 충돌 방지에 유용합니다. hydrateRoot에 전달한 prefix와 동일해야 합니다. - optional
namespaceURI
: 스트림의 루트 네임스페이스 URI. 기본값은 HTML. SVG는 'http://www.w3.org/2000/svg', MathML은 'http://www.w3.org/1998/Math/MathML' 전달. - optional
onError
: 서버 에러 발생 시 호출되는 콜백. 기본적으로 console.error만 호출. 크래시 리포트 로깅 시에도 console.error는 반드시 호출해야 함. shell이 emit되기 전 상태코드 조정에도 사용 가능. - optional
progressiveChunkSize
: 청크 크기(바이트 단위). 기본값은 내부 휴리스틱 참고. - optional
signal
: AbortSignal. prerendering을 중단하고 나머지는 클라이언트에서 렌더링할 때 사용.
- optional
Returns(반환값)
Promise를 반환합니다:
- 렌더링 성공 시,
{ prelude }
객체를 resolve합니다. prelude는 Node.js HTML 스트림입니다. 이 스트림을 이용해 청크 단위로 응답을 전송하거나, 전체를 문자열로 읽을 수 있습니다. - 렌더링 실패 시, Promise가 reject됩니다. 이때 fallback shell을 출력할 수 있습니다.
Caveats(주의사항)
- prerendering 시 nonce 옵션은 사용할 수 없습니다. nonce는 요청마다 고유해야 하며, CSP 보안을 위해 prerender 결과에 포함하는 것은 부적절하고 위험합니다.
Note
언제 prerenderToNodeStream을 사용해야 하나요?
static prerenderToNodeStream API는 정적 서버 사이드 생성(SSG)에 사용합니다. renderToString과 달리, prerenderToNodeStream은 모든 데이터가 로드될 때까지 대기한 후 resolve합니다. Suspense로 데이터를 fetch하는 경우에도 전체 페이지의 정적 HTML을 생성할 수 있습니다. 콘텐츠가 준비되는 대로 스트리밍하려면 renderToPipeableStream 등 SSR 스트리밍 API를 사용하세요.
이 API는 Node.js 환경에 특화되어 있습니다. Deno, Edge 등 Web Streams 환경에서는 prerender API를 사용하세요.
사용법
React 트리를 정적 HTML 스트림으로 렌더링하기
import { prerenderToNodeStream } from 'react-dom/static';
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js'],
});
response.setHeader('Content-Type', 'text/plain');
prelude.pipe(response);
});
루트 컴포넌트와 함께 bootstrap <script>
경로 목록을 전달해야 합니다. 루트 컴포넌트는 반드시 <html>
태그를 포함한 전체 문서를 반환해야 합니다.
예시:
export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}
React는 doctype과 bootstrap <script>
태그를 결과 HTML 스트림에 자동으로 삽입합니다.
<!DOCTYPE html>
<html>
<!-- ... 컴포넌트에서 생성된 HTML ... -->
</html>
<script src="/main.js" async=""></script>
클라이언트에서는 bootstrap 스크립트에서 hydrateRoot를 호출해 전체 문서를 하이드레이션해야 합니다.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
Deep Dive
빌드 결과에서 CSS/JS 에셋 경로 읽기
빌드 후에는 JS/CSS 파일명이 해시되어 있을 수 있습니다. 이 경우, 에셋 맵을 prop으로 전달해 실제 파일명을 컴포넌트에서 읽도록 구현할 수 있습니다.
export default function App({ assetMap }) {
return (
<html>
<head>
<title>My app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}
서버에서 <App assetMap={assetMap} />
로 렌더링하고, assetMap을 prop으로 전달하세요.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});
클라이언트에서도 동일한 assetMap으로 렌더링해야 hydration 오류가 발생하지 않습니다. assetMap을 직렬화해 클라이언트로 전달하세요.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App assetMap={assetMap} />, {
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});
bootstrapScriptContent 옵션을 사용하면 window.assetMap 전역 변수를 클라이언트에 전달할 수 있습니다.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
React 트리를 정적 HTML 문자열로 렌더링하기
import { prerenderToNodeStream } from 'react-dom/static';
async function renderToString() {
const {prelude} = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Promise((resolve, reject) => {
let data = '';
prelude.on('data', chunk => {
data += chunk;
});
prelude.on('end', () => resolve(data));
prelude.on('error', reject);
});
}
이렇게 하면 React 컴포넌트의 초기 비인터랙티브 HTML이 생성됩니다. 클라이언트에서는 hydrateRoot를 호출해 해당 HTML을 인터랙티브하게 만들 수 있습니다.
모든 데이터가 로드될 때까지 대기하기
prerenderToNodeStream은 모든 데이터가 준비될 때까지 대기한 후 정적 HTML 생성을 완료합니다. 예를 들어, Suspense로 감싼 데이터 fetch가 끝날 때까지 기다립니다.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
⚠️ Suspense가 활성화되는 데이터 소스만 대기합니다. (Relay, Next.js 등 프레임워크의 Suspense 데이터 fetch, lazy, use 등)
Effect나 이벤트 핸들러 내부의 fetch는 Suspense로 감지되지 않습니다.
prerendering 중단하기
타임아웃 등으로 prerendering을 중단할 수 있습니다.
async function renderToString() {
const controller = new AbortController();
setTimeout(() => {
controller.abort()
}, 10000);
try {
// prelude에는 중단 전까지 prerender된 모든 HTML이 포함됩니다.
const {prelude} = await prerenderToNodeStream(<App />, {
signal: controller.signal,
});
// ...
} catch (e) {
// 에러 처리
}
}
중단 시, 완료되지 않은 Suspense 경계는 fallback 상태로 prelude에 포함됩니다.
Troubleshooting(트러블슈팅)
스트림이 전체 렌더가 끝날 때까지 시작되지 않음
prerenderToNodeStream은 전체 앱 렌더가 끝날 때까지(모든 Suspense 경계가 resolve될 때까지) 대기한 후 응답을 반환합니다. SSG(정적 사이트 생성)에 적합하며, 콘텐츠가 준비되는 대로 스트리밍하려면 renderToPipeableStream 등 SSR 스트리밍 API를 사용하세요.
출처: https://react.dev/reference/react-dom/static/prerenderToNodeStream