prerender
는 Web Stream을 사용하여 React 트리를 정적 HTML 문자열로 렌더링합니다.
const {prelude} = await prerender(reactNode, options?)
레퍼런스
prerender(reactNode, options?)
prerender
를 호출하여 앱을 정적 HTML로 렌더링합니다.
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
클라이언트에서 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
: React가useId
에 의해 생성된 ID를 사용하는 문자열 접두사입니다. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하는 데 유용합니다.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
: 사전 렌더링을 중단하고 나머지를 클라이언트에서 렌더링하기 위한 중단 신호Abort Signal를 설정합니다.
- optional
반환값
prerender
는 Promise를 반환합니다.
- 렌더링이 성공하면 Promise는 다음을 포함하는 객체로 해결됩니다.
prelude
: HTML의 Web Stream입니다. 스트림을 사용하여 청크 단위로 응답을 보내거나 전체 스트림을 문자열로 읽을 수 있습니다.
- 렌더링이 실패하면 반환된 Promise는 취소됩니다. 이것을 이용해 실패 결과를 출력하세요.
주의 사항
nonce
는 사전 렌더링할 때 사용할 수 없는 옵션입니다. Nonce는 요청마다 고유해야 하며, CSP로 애플리케이션을 보호하기 위해 Nonce를 사용한다면 Nonce 값을 사전 렌더링 자체에 포함하는 것은 부적절하고 안전하지 않습니다.
사용법
React 트리를 정적 HTML 스트림으로 렌더링하기
prerender
를 호출해 Readable Web Stream을 통해 React 트리를 정적 HTML로 렌더링합니다.
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
루트 컴포넌트와 함께 부트스트랩 <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 from your components ... -->
</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 파일 등)은 빌드 후 해시되는 경우가 많습니다. 예를 들어, styles.css
대신 styles.123456.css
로 끝날 수 있습니다. 정적 에셋 파일명을 해시하면 동일한 에셋의 모든 개별 빌드가 다른 파일명을 갖게 됩니다. 이는 정적 에셋에 대한 장기 캐싱을 안전하게 활성화할 수 있게 해주므로 유용합니다. 특정 이름의 파일은 절대 콘텐츠가 변경되지 않기 때문입니다.
하지만 빌드가 끝날 때까지 에셋 URL을 모르는 경우 소스 코드에 넣을 방법이 없습니다. 예를 들어, JSX에 "/styles.css"
를 하드코딩하는 것은 작동하지 않습니다. 소스 코드에 URL을 넣지 않으려면 루트 컴포넌트는 Props로 전달된 맵에서 실제 파일명을 읽어야 합니다.
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'
};
async function handler(request) {
const {prelude} = await prerender(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
서버에서 <App assetMap={assetMap} />
를 렌더링하고 있으므로, Hydration 오류를 방지하기 위해 클라이언트에서도 assetMap
과 함께 렌더링해야 합니다. 다음과 같이 assetMap
을 직렬화하여 클라이언트에 전달할 수 있습니다.
// 빌드 도구에서 이 JSON을 가져와야 합니다.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const {prelude} = await prerender(<App assetMap={assetMap} />, {
// 주의: 이 데이터는 사용자가 생성한 것이 아니므로 stringify()를 사용해도 안전합니다.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
위 예시에서 bootstrapScriptContent
옵션은 클라이언트에서 전역 window.assetMap
변수를 설정하는 추가 인라인 <script>
태그를 추가합니다. 이를 통해 클라이언트 코드가 동일한 assetMap
을 읽을 수 있습니다.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
클라이언트와 서버 모두 동일한 assetMap
Prop으로 App
을 렌더링하므로 Hydration 오류가 발생하지 않습니다.
React 트리를 정적 HTML 문자열로 렌더링하기
prerender
를 호출하여 앱을 정적 HTML 문자열로 렌더링합니다.
import { prerender } from 'react-dom/static';
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');
}
}
이렇게 하면 React 컴포넌트의 초기 상호작용하지 않는 HTML 출력이 생성됩니다. 클라이언트에서는 hydrateRoot
를 호출하여 서버에서 생성된 HTML을 Hydrate하고 상호작용하게 만들어야 합니다.
모든 데이터 로드 대기
prerender
는 정적 HTML 생성을 완료하고 해결되기 전에 모든 데이터가 로드될 때까지 대기합니다. 예를 들어, 표지, 친구와 사진이 있는 사이드바, 게시물 목록을 보여주는 프로필 페이지를 생각해 보세요.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
<Posts />
가 데이터를 로드해야 하는데 시간이 걸린다고 가정해 보겠습니다. 이상적으로는 게시물이 완료될 때까지 기다려서 HTML에 포함하고 싶을 것입니다. 이를 위해 Suspense를 사용하여 데이터를 일시 중단할 수 있으며, prerender
는 일시 중단된 콘텐츠가 완료될 때까지 기다린 후 정적 HTML로 해결됩니다.
사전 렌더링 중단
타임아웃 후 사전 렌더링을 “포기”하도록 강제할 수 있습니다.
async function renderToString() {
const controller = new AbortController();
setTimeout(() => {
controller.abort()
}, 10000);
try {
// prelude에는 컨트롤러가 중단되기 전에
// 사전 렌더링된 모든 HTML이 포함됩니다.
const {prelude} = await prerender(<App />, {
signal: controller.signal,
});
//...
불완전한 자식을 가진 모든 Suspense 경계는 폴백 상태로 prelude에 포함됩니다.
문제 해결
전체 앱이 렌더링될 때까지 스트림이 시작되지 않습니다
prerender
응답은 모든 Suspense 경계가 해결될 때까지 기다리는 것을 포함하여 전체 앱이 렌더링을 완료할 때까지 기다린 후 해결됩니다. 이는 사전에 정적 사이트 생성(SSG)을 위해 설계되었으며 콘텐츠가 로드되면서 더 많은 콘텐츠를 스트리밍하는 것을 지원하지 않습니다.
콘텐츠가 로드되면서 스트리밍하려면 renderToReadableStream
과 같은 스트리밍 서버 렌더링 API를 사용하세요.