prerenderToNodeStream
은 Node.js Stream을 사용하여 React 트리를 정적 HTML 문자열로 렌더링합니다.
const {prelude, postponed} = await prerenderToNodeStream(reactNode, options?)
레퍼런스
prerenderToNodeStream(reactNode, options?)
prerenderToNodeStream
을 호출해 앱을 정적 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을 인터랙티브하게 만듭니다.
매개변수
-
reactNode
: HTML로 렌더링할 React 노드입니다. 예를 들어<App />
과 같은 JSX 노드가 해당됩니다. 전체 문서를 나타내야 하므로, App 컴포넌트는<html>
태그를 렌더링해야 합니다. -
optional
options
: 정적 생성 옵션을 가진 객체입니다.- optional
bootstrapScriptContent
: 지정될 경우, 이 문자열이 인라인<script>
태그에 삽입됩니다. - optional
bootstrapScripts
: 페이지에 출력할<script>
태그의 문자열 URL 배열입니다.hydrateRoot
를 호출하는<script>
를 포함하려면 이 옵션을 사용하세요. 클라이언트에서 React를 전혀 실행하지 않으려면 생략하세요. - optional
bootstrapModules
:bootstrapScripts
와 같지만,<script type="module">
을 출력합니다. - optional
identifierPrefix
:useId
로 생성된 ID에 React가 사용하는 문자열 접두사입니다. 한 페이지에서 여러 개의 루트를 사용할 때 충돌을 피하는 데 유용합니다.hydrateRoot
에 전달한 접두사와 동일해야 합니다. - optional
namespaceURI
: 스트림의 루트 namespace URI를 담은 문자열입니다. 기본값은 일반 HTML입니다. SVG의 경우'http://www.w3.org/2000/svg'
, MathML의 경우'http://www.w3.org/1998/Math/MathML'
을 전달하세요. - optional
onError
: 서버 오류가 발생할 때마다, 복구 가능 불가능 여부와 관계없이 호출되는 콜백입니다. 기본적으로console.error
만 호출합니다. 충돌 보고를 기록하도록 재정의하는 경우에도 반드시console.error
를 호출해야 합니다. 셸이 출력되기 전에 상태 코드를 설정하는 데에도 사용할 수 있습니다. - optional
progressiveChunkSize
: 청크의 바이트 수입니다. 기본 휴리스틱에 대해 더 읽어보세요. - optional
signal
: 프리렌더링을 중단하고 나머지를 클라이언트에서 렌더링할 수 있게 하는 중단 신호입니다.
- optional
반환값
prerenderToNodeStream
returns a Promise:
- If rendering the is successful, the Promise will resolve to an object containing:
prelude
: a Node.js Stream. of HTML. You can use this stream to send a response in chunks, or you can read the entire stream into a string.postponed
: a JSON-serializeable, opaque object that can be passed toresumeToPipeableStream
ifprerenderToNodeStream
did not finish. Otherwisenull
indicating that theprelude
contains all the content and no resume is necessary.
- If rendering fails, the Promise will be rejected. Use this to output a fallback shell.
주의 사항
프리렌더링 시 nonce
옵션은 사용할 수 없습니다. Nonce는 요청마다 고유해야 하며, CSP로 애플리케이션을 보호할 때 Nonce 값을 프리렌더링 결과에 포함하는 것은 부적절하고 안전하지 않습니다.
사용법
React 트리를 정적 HTML 스트림으로 렌더링하기
prerenderToNodeStream
를 호출해 React 트리를 정적 HTML로 렌더링하고, 이를 Node.js Stream에 출력합니다.
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);
});
루트 컴포넌트와 함께, 부트스트랩 <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과 부트스트랩 <script>
태그를 결과 HTML 스트림에 삽입합니다.
<!DOCTYPE html>
<html>
<!-- ... 컴포넌트에서 생성된 HTML ... -->
</html>
<script src="/main.js" async=""></script>
클라이언트에서 부트스트랩 스크립트는 hydrateRoot
를 호출해 document
전체를 hydrate해야 합니다.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
이는 정적 서버 생성 HTML에 이벤트 리스너를 연결해 인터랙티브하게 만듭니다.
자세히 살펴보기
최종 에셋 URL(예: JavaScript와 CSS 파일)은 빌드 후 종종 해시Hash가 추가됩니다. 예를 들어 styles.css
대신 styles.123456.css
와 같은 파일명이 될 수 있습니다. 정적 에셋 파일명에 해시를 추가하면 동일한 에셋이라도 빌드마다 다른 파일명을 갖게 됩니다. 이는 정적 에셋에 대해 장기 캐싱을 안전하게 활성화할 수 있게 해줍니다. 특정 이름을 가진 파일은 그 내용이 절대 변경되지 않기 때문입니다.
그러나 빌드가 끝난 후에야 에셋 URL을 알 수 있다면, 이를 소스 코드에 넣을 방법이 없습니다. 예를 들어 앞서처럼 JSX에 "/styles.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} />
을 렌더링하고 에셋 URL이 포함된 assetMap
을 전달합니다.
// 이 JSON은 빌드 도구에서 가져와야 합니다. 예를 들어 빌드 출력에서 읽어올 수 있습니다.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: [assetMap['/main.js']]
});
response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});
서버에서 <App assetMap={assetMap} />
을 렌더링하고 있으므로, Hydration 오류를 피하려면 클라이언트에서도 assetMap
과 함께 렌더링해야 합니다. 다음과 같이 assetMap
을 직렬화해 클라이언트로 전달할 수 있습니다.
// 이 JSON은 빌드 도구에서 가져와야 합니다.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
// 주의: 이 데이터는 사용자가 생성한 것이 아니므로 `stringify()`해도 안전합니다.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});
위 예시에서 bootstrapScriptContent
옵션은 클라이언트에서 전역 변수 window.assetMap
을 설정하는 인라인 <script>
태그를 추가합니다. 이렇게 하면 클라이언트 코드에서 동일한 assetMap
을 읽을 수 있습니다.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
클라이언트와 서버 모두 App
을 동일한 assetMap
Prop으로 렌더링하므로 하이드레이션 오류가 발생하지 않습니다.
React 트리를 정적 HTML 문자열로 렌더링하기
prerenderToNodeStream
을 호출해 앱을 정적 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을 Hydrate하고 상호작용하게 만들어야 합니다.
모든 데이터가 로드될 때까지 기다리기
prerenderToNodeStream
는 모든 데이터가 로드될 때까지 기다린 뒤 정적 HTML 생성을 완료하고 Promise를 해결합니다. 예를 들어 표지 이미지, 친구와 사진이 포함된 사이드바, 게시물 목록을 표시하는 프로필 페이지를 생각해 보겠습니다.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
예를 들어 <Posts />
가 데이터를 로드해야 하고, 이 과정에 시간이 걸린다고 가정해 보겠습니다. 이 경우, 이상적으로는 게시물 데이터가 모두 로드된 뒤 HTML에 포함되기를 원할 것입니다. 이를 위해 Suspense를 사용해 데이터 로드가 완료될 때까지 렌더링을 일시 중단할 수 있으며, prerenderToNodeStream
는 해당 중단된 콘텐츠가 완료될 때까지 기다린 후 정적 HTML로 변환을 완료합니다.
사전 렌더링 중단하기
타임아웃 이후 사전 렌더링을 “포기”하도록 강제할 수 있습니다.
async function renderToString() {
const controller = new AbortController();
setTimeout(() => {
controller.abort()
}, 10000);
try {
// Prelude에는 컨트롤러가 중단하기 전에
// 사전렌더링된 모든 HTML이 포함됩니다.
const {prelude} = await prerenderToNodeStream(<App />, {
signal: controller.signal,
});
//...
불완전한 자식을 가진 모든 Suspense 경계는 풀백 상태로 Prelude에 포함됩니다.
This can be used for partial prerendering together with resumeToPipeableStream
or resumeAndPrerenderToNodeStream
.
문제 해결
전체 앱이 렌더링될 때까지 스트림이 시작되지 않았습니다.
prerenderToNodeStream
응답은 모든 Suspense 경계가 해결될 때까지 기다리는 것을 포함하여 전체 앱이 렌더링이 완료될 때까지 기다린 후 완료됩니다. 이 API는 정적 사이트 생성(SSG)을 위해 설계되었으며 콘텐츠가 로드되면서 더 많은 콘텐츠를 스트리밍하는 것을 지원하지 않습니다.
콘텐츠가 로드되면서 스트리밍하려면 renderToPipeableStream
과 같은 스트리밍 서버 렌더링 API를 사용하세요.