renderToReadableStream

번역:정재남

renderToReadableStream renders a React tree to a Readable Web Stream. renderToReadableStream은 React 트리를 읽기 가능한 웹 스트림으로 렌더링합니다.

const stream = await renderToReadableStream(reactNode, options?)

Note

This API depends on Web Streams. For Node.js, use renderToPipeableStream instead. 이 API는 웹 스트림에 의존합니다. Node.js의 경우, 대신 renderToPipeableStream을 사용하세요.


Reference참조

renderToReadableStream(reactNode, options?)

Call renderToReadableStream to render your React tree as HTML into a Readable Web Stream. renderToReadableStream을 호출하면 React 트리를 HTML로 읽기 가능한 웹 스트림으로 렌더링합니다.

import { renderToReadableStream } from 'react-dom/server';

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

On the client, call hydrateRoot to make the server-generated HTML interactive. 클라이언트에서 hydrateRoot를 호출하면 서버에서 생성된 HTML을 상호작용이 가능하도록 만듭니다.

See more examples below. 아래에서 더 많은 예시를 확인하세요.

Parameters매개변수

  • reactNode: A React node you want to render to HTML. For example, a JSX element like <App />. It is expected to represent the entire document, so the App component should render the <html> tag. reactNode: HTML로 렌더링하려는 React 노드. 예를 들어, <App />과 같은 JSX 엘리먼트입니다. 전체 문서를 나타낼 것이므로, App 컴포넌트는 <html> 태그를 렌더링해야 합니다.

  • optional options: An object with streaming options. 선택적 options: 스트리밍 옵션이 있는 객체.

    • optional bootstrapScriptContent: If specified, this string will be placed in an inline <script> tag. 선택적 bootstrapScriptContent: 지정하면 이 문자열이 인라인 <script> 태그에 배치됩니다.

    • optional bootstrapScripts: An array of string URLs for the <script> tags to emit on the page. Use this to include the <script> that calls hydrateRoot. Omit it if you don’t want to run React on the client at all. 선택적 bootstrapScripts: 페이지에 표시할<script> 태그의 문자열 URL 배열. hydrateRoot를 호출하는 <script>를 포함하고자 할 때 사용하세요. 클라이언트에서 React를 아예 실행하지 않으려면 생략하세요.

    • optional bootstrapModules: Like bootstrapScripts, but emits <script type="module"> instead. 선택적 bootstrapModules: bootstrapScripts와 비슷하지만, 대신 <script type="module">을 출력합니다.

    • optional identifierPrefix: A string prefix React uses for IDs generated by useId. Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix as passed to hydrateRoot. 선택적 identifierPrefix: React가 useId에 의해 생성된 ID에 사용하는 문자열 접두사. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하는 데 유용합니다. hydrateRoot에 전달된 접두사와 동일해야 합니다.

    • optional namespaceURI: A string with the root namespace URI for the stream. Defaults to regular HTML. Pass 'http://www.w3.org/2000/svg' for SVG or 'http://www.w3.org/1998/Math/MathML' for MathML. 선택적 namespaceURI: 스트림의 루트 namespace URI가 포함된 문자열. 기본값은 일반 HTML입니다. SVG의 경우 'http://www.w3.org/2000/svg'를, MathML의 경우 'http://www.w3.org/1998/Math/MathML'을 전달합니다.

    • optional nonce: A nonce string to allow scripts for script-src Content-Security-Policy. 선택적 nonce: script-src 콘텐츠 보안 정책에 대한 스크립트를 허용하는 nonce 문자열.

    • optional onError: A callback that fires whenever there is a server error, whether recoverable or not. By default, this only calls console.error. If you override it to log crash reports, make sure that you still call console.error. You can also use it to adjust the status code before the shell is emitted. 선택적 onError: 복구 가능 혹은 불가능 여부에 관계 없이 서버 오류가 발생할 때마다 실행되는 콜백. 기본적으로 console.error만 호출합니다. 이 함수를 재정의하여 로그 충돌 보고서를 기록하도록 한 경우에도 여전히 console.error를 호출해야 합니다. 셸이 출력되기 전에 상태 코드를 조정하는 데 사용할 수도 있습니다.

    • optional progressiveChunkSize: The number of bytes in a chunk. Read more about the default heuristic. 선택적 progressiveChunkSize: 청크의 바이트 수. 기본 휴리스틱에 대해 자세히 알아보세요.

    • optional signal: An abort signal that lets you abort server rendering and render the rest on the client. 선택적 signal: 서버 렌더링을 중단하고 나머지는 클라이언트에서 렌더링하도록 하는 중단 신호.

Returns반환값

renderToReadableStream returns a Promise: renderToReadeableStream은 프로미스를 반환합니다:

The returned stream has an additional property: 반환된 스트림에는 추가 속성이 있습니다:

  • allReady: A Promise that resolves when all rendering is complete, including both the shell and all additional content. You can await stream.allReady before returning a response for crawlers and static generation. If you do that, you won’t get any progressive loading. The stream will contain the final HTML. allReady: 과 모든 추가 콘텐츠를 포함하여 렌더링이 전부 완료되면 리졸브되는 프로미스. 크롤링 및 정적 생성시 응답을 반환하기 전에 await stream.allReady를 실행할 수 있습니다. 이렇게 하면 점진적 로딩이 동작하지 않고, 스트림에는 최종 HTML이 포함됩니다.

Usage사용법

Rendering a React tree as HTML to a Readable Web StreamReact 트리를 가독성 있는 웹 스트림에 HTML로 렌더링하기

Call renderToReadableStream to render your React tree as HTML into a Readable Web Stream: renderToReadableStream을 호출하여 React 트리를 HTML로 읽기 가능한 웹 스트림에 렌더링합니다.

import { renderToReadableStream } from 'react-dom/server';

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

Along with the root component, you need to provide a list of bootstrap <script> paths. Your root component should return the entire document including the root <html> tag. root component와 함께 bootstrap <script> paths 경로 목록을 제공해야 합니다. 루트 컴포넌트는 루트 <html> 태그를 포함한 전체 문서를 반환해야 합니다.

For example, it might look like this: 예를 들어, 아래와 같을 것입니다:

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 will inject the doctype and your bootstrap <script> tags into the resulting HTML stream: React는 HTML 스트림에 doctype부트스트랩 <script> 태그들을 주입합니다:

<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>

On the client, your bootstrap script should hydrate the entire document with a call to hydrateRoot: 클라이언트에서 부트스트랩 스크립트는 hydrateRoot를 호출하여 전체 document를 hydrate해야 합니다:

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App />);

This will attach event listeners to the server-generated HTML and make it interactive. 이렇게 하면 서버에서 생성된 HTML에 이벤트 리스너가 첨부되어 상호작용이 가능해 집니다.

Deep Dive | 심층 탐구

Reading CSS and JS asset paths from the build output빌드 출력에서 CSS 및 JS asset 경로 읽기

The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead of styles.css you might end up with styles.123456.css. Hashing static asset filenames guarantees that every distinct build of the same asset will have a different filename. This is useful because it lets you safely enable long-term caching for static assets: a file with a certain name would never change content. 최종 에셋 URL(예: JavaScript 및 CSS 파일)은 빌드 후에 해시 처리되는 경우가 많습니다. 예를 들어, styles.css 대신 styles.123456.css로 끝날 수 있습니다. 정적 에셋 파일명을 해시하면 동일한 에셋의 모든 개별 빌드에 다른 파일명이 지정됩니다. 이는 정적 자산에 대한 장기 캐싱을 안전하게 활성화할 수 있기 때문에 유용합니다. 특정 이름을 가진 파일은 콘텐츠를 변경하지 않습니다.

However, if you don’t know the asset URLs until after the build, there’s no way for you to put them in the source code. For example, hardcoding "/styles.css" into JSX like earlier wouldn’t work. To keep them out of your source code, your root component can read the real filenames from a map passed as a prop: 하지만 빌드가 끝날 때까지 에셋 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>
);
}

On the server, render <App assetMap={assetMap} /> and pass your assetMap with the asset URLs: 서버에서 <App assetMap={assetMap} />을 렌더링하고 에셋 URL과 함께 assetMap을 전달합니다:

// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

Since your server is now rendering <App assetMap={assetMap} />, you need to render it with assetMap on the client too to avoid hydration errors. You can serialize and pass assetMap to the client like this: 이제 서버가 <App assetMap={assetMap} /> 를 렌더링하고 있으므로, 클라이언트에서도 assetMap 을 사용하여 렌더링해야 하이드레이션 오류를 방지할 수 있습니다. 다음과 같이 assetMap을 직렬화하여 클라이언트에 전달할 수 있습니다:

// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

In the example above, the bootstrapScriptContent option adds an extra inline <script> tag that sets the global window.assetMap variable on the client. This lets the client code read the same assetMap: 위의 예시에서 bootstrapScriptContent 옵션은 클라이언트에서 전역 window.assetMap 변수를 설정하는 별도의 인라인 <script> 태그를 추가합니다. 이렇게 하면 클라이언트 코드가 동일한 assetMap을 읽을 수 있습니다:

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App assetMap={window.assetMap} />);

Both client and server render App with the same assetMap prop, so there are no hydration errors. 클라이언트와 서버 모두 동일한 AssetMap 프로퍼티로 App을 렌더링하므로 하이드레이션 오류가 발생하지 않습니다.


Streaming more content as it loads콘텐츠가 로드되는 동안 더 많은 콘텐츠 스트리밍하기

Streaming allows the user to start seeing the content even before all the data has loaded on the server. For example, consider a profile page that shows a cover, a sidebar with friends and photos, and a list of posts: 스트리밍을 사용하면 모든 데이터가 서버에 로드되기 전에도 콘텐츠를 보기 시작할 수 있습니다. 예를 들어, 표지와 친구 및 사진이 있는 사이드바나, 게시물 목록이 표시되는 프로필 페이지를 생각해 보세요:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}

Imagine that loading data for <Posts /> takes some time. Ideally, you’d want to show the rest of the profile page content to the user without waiting for the posts. To do this, wrap Posts in a <Suspense> boundary: <Posts />에 대한 데이터를 로드하는 데 시간이 좀 걸린다고 가정해 보겠습니다. 이상적으로는 게시물을 기다리지 않고 나머지 프로필 페이지 콘텐츠를 표시하고 싶을 것입니다. 이렇게 하려면 <Posts /><Suspense> 경계로 감싸면 됩니다:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

This tells React to start streaming the HTML before Posts loads its data. React will send the HTML for the loading fallback (PostsGlimmer) first, and then, when Posts finishes loading its data, React will send the remaining HTML along with an inline <script> tag that replaces the loading fallback with that HTML. From the user’s perspective, the page will first appear with the PostsGlimmer, later replaced by the Posts. 이는 Posts가 데이터를 로드하기 전에 HTML 스트리밍을 시작하도록 React에 지시합니다. React는 로딩 폴백(PostsGlimmer)을 위한 HTML을 먼저 전송한 다음, Posts가 데이터 로딩을 완료하면 나머지 HTML를 인라인 <script> 태그(기존 로딩 폴백을 HTML로 대체하는)와 함께 전송합니다. 사용자 관점에서 볼 때, 페이지는 먼저 PostsGlimmer로 표시되었다가 나중에 Posts로 대체될 것입니다.

You can further nest <Suspense> boundaries to create a more granular loading sequence: <Suspense>경계를 더 중첩하여 더 세분화된 로딩 시퀀스를 만들 수도 있습니다:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}

In this example, React can start streaming the page even earlier. Only ProfileLayout and ProfileCover must finish rendering first because they are not wrapped in any <Suspense> boundary. However, if Sidebar, Friends, or Photos need to load some data, React will send the HTML for the BigSpinner fallback instead. Then, as more data becomes available, more content will continue to be revealed until all of it becomes visible. 이 예제에서 React는 페이지 스트리밍을 더 일찍 시작할 수 있습니다. ProfileLayoutProfileCover<Suspense> 경계로 둘러싸여 있지 않기 때문에 먼저 렌더링을 완료해야 합니다. 그러나 Sidebar, Friends, 혹은 Photos에 일부 데이터를 로드해야 하는 경우, React는 대신 BigSpinner 폴백을 위한 HTML을 전송할 것입니다. 이후로는 가용 데이터가 늘어날 때마다 더 많은 컨텐츠가 표시되다가, 결국 모든 데이터가 전부 표시될 것입니다.

Streaming does not need to wait for React itself to load in the browser, or for your app to become interactive. The HTML content from the server will get progressively revealed before any of the <script> tags load. 스트리밍은 브라우저에서 React 자체가 로드되거나 상호작용이 가능해질 때까지 기다릴 필요가 없습니다. 서버의 HTML 콘텐츠는 <script> 태그가 로드되기 전에도 점진적으로 표시됩니다.

Read more about how streaming HTML works. 스트리밍 HTML의 작동 방식에 대해 자세히 알아보세요.

Note

Only Suspense-enabled data sources will activate the Suspense component. They include: 오직 Suspense를 도입한 데이터 소스에서만 Suspense 컴포넌트를 활성화할 수 있습니다. 여기에는 다음이 포함됩니다:

  • Data fetching with Suspense-enabled frameworks like Relay and Next.js RelayNext.js와 같은 Suspense 도입 프레임워크를 사용한 데이터 페칭

  • Lazy-loading component code with lazy lazy를 사용한 지연 로딩 컴포넌트 코드

  • Reading the value of a Promise with use

Suspense does not detect when data is fetched inside an Effect or event handler. Suspense는 Effect나 이벤트 핸들러 내부에서 페칭하는 경우를 감지하지 않습니다.

The exact way you would load data in the Posts component above depends on your framework. If you use a Suspense-enabled framework, you’ll find the details in its data fetching documentation. 위의 Posts 컴포넌트에서 데이터를 로드하는 정확한 방법은 프레임워크에 따라 다릅니다. Suspense를 도입한 프레임워크를 사용하는 경우, 해당 프레임워크의 데이터 페칭 문서에서 자세한 내용을 확인할 수 있을 것입니다.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React. 잘 알려진 프레임워크를 사용하지 않고 데이터 페칭에 Suspense를 도입하는 방법은 아직 지원되지 않습니다. Suspense를 도입한 데이터 소스를 구현하기 위한 요구 사항은 불안정하고 문서화되지 않았습니다. 데이터 소스를 Suspense와 통합하기 위한 React 공식 API는 미래에 출시할 계획입니다.


Specifying what goes into the shell셸에 들어갈 내용 지정하기

The part of your app outside of any <Suspense> boundaries is called the shell: 앱의 <Suspense> 경계 밖에 있는 부분을 이라고 합니다:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}

It determines the earliest loading state that the user may see: 이는 사용자가 볼 수 있는 가장 빠른 로딩 state를 결정합니다:

<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>

If you wrap the whole app into a <Suspense> boundary at the root, the shell will only contain that spinner. However, that’s not a pleasant user experience because seeing a big spinner on the screen can feel slower and more annoying than waiting a bit more and seeing the real layout. This is why usually you’ll want to place the <Suspense> boundaries so that the shell feels minimal but complete—like a skeleton of the entire page layout. 전체 앱을 루트의 <Suspense> 경계로 감싸면 셸에는 해당 스피너만 포함됩니다. 하지만 이는 사용자 경험상 좋지 않습니다. 화면에 큰 스피너가 표시되었다가 실제 레이아웃으로 전환되는 방식이, 아무것도 없는 화면에서 잠시 기다렸다가 곧바로 실제 레이아웃을 표시하는 방식보다 오히려 더 느리거나 성가시게 느껴질 수 있기 때문입니다. 때문에 일반적으로 셸이 전체 페이지 레이아웃의 골격과 같이 최소한이면서 완성된 무언가로 느껴지도록 <Suspense> 경계를 배치하는 것이 좋습니다.

The async call to renderToReadableStream will resolve to a stream as soon as the entire shell has been rendered. Usually, you’ll start streaming then by creating and returning a response with that stream: renderToReadableStream에 대한 비동기 호출은 전체 셸이 렌더링되는 즉시 stream으로 리졸브됩니다. 보통 해당 stream으로 응답을 생성하고 반환함으로써 스트리밍을 시작합니다:

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

By the time the stream is returned, components in nested <Suspense> boundaries might still be loading data. stream이 반환될 때 중첩된 <Suspense> 경계에 있는 컴포넌트는 여전히 데이터를 로드하고 있을 수 있습니다.


Logging crashes on the server서버에서의 충돌을 기록하기

By default, all errors on the server are logged to console. You can override this behavior to log crash reports: 기본적으로 서버의 모든 오류는 콘솔에 기록됩니다. 이 동작을 재정의하여 충돌 보고서를 기록할 수 있습니다:

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

If you provide a custom onError implementation, don’t forget to also log errors to the console like above. 사용자 정의 onError 구현을 제공하는, 경우 위와 같이 콘솔에도 오류를 기록하는 것을 잊지 마세요.


Recovering from errors inside the shell셸 내부에서 오류 복구하기

In this example, the shell contains ProfileLayout, ProfileCover, and PostsGlimmer: 다음 예제에서 셸에는 ProfileLayout, ProfileCover, PostsGlimmer가 포함되어 있습니다:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

If an error occurs while rendering those components, React won’t have any meaningful HTML to send to the client. Wrap your renderToReadableStream call in a try...catch to send a fallback HTML that doesn’t rely on server rendering as the last resort: 이 컴포넌트들을 렌더링하는 동안 오류가 발생하면 React는 클라이언트에 보낼 의미 있는 HTML을 갖지 못할 것입니다. 이럴 경우에 대한 마지막 수단으로 서버 렌더링에 의존하지 않는 폴백 HTML을 보내려면 onShellError를 재정의하세요:

async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

If there is an error while generating the shell, both onError and your catch block will fire. Use onError for error reporting and use the catch block to send the fallback HTML document. Your fallback HTML does not have to be an error page. Instead, you may include an alternative shell that renders your app on the client only. 셸을 생성하는 동안 오류가 발생하면 onErroronShellError가 모두 실행됩니다. 오류 보고에는 onError를 사용하고, 폴백 HTML 문서 전송에는 onShellError를 사용하세요. 폴백 HTML은 반드시 오류 페이지일 필요는 없습니다. 대신 클라이언트에서만 앱을 렌더링하는 대체 셸을 포함할 수도 있습니다.


Recovering from errors outside the shell셸 외부에서 오류 복구하기

In this example, the <Posts /> component is wrapped in <Suspense> so it is not a part of the shell: 다음 예제에서 <Posts /> 컴포넌트는 <Suspense>로 래핑되어 있으므로 셸의 일부가 아닙니다:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

If an error happens in the Posts component or somewhere inside it, React will try to recover from it: Posts 컴포넌트 또는 그 내부 어딘가에서 오류가 발생하면 React는 이를 복구하려고 시도합니다:

  1. It will emit the loading fallback for the closest <Suspense> boundary (PostsGlimmer) into the HTML. 가장 가까운 <Suspense> 경계(PostsGlimmer)에 대한 로딩 폴백을 HTML에 방출합니다.

  2. It will “give up” on trying to render the Posts content on the server anymore. 더 이상 서버에서 Posts 콘텐츠를 렌더링하려는 시도를 “포기”합니다.

  3. When the JavaScript code loads on the client, React will retry rendering Posts on the client. JavaScript 코드가 클라이언트에서 로드되면, React는 클라이언트에서 Posts렌더링을 다시 시도합니다.

If retrying rendering Posts on the client also fails, React will throw the error on the client. As with all the errors thrown during rendering, the closest parent error boundary determines how to present the error to the user. In practice, this means that the user will see a loading indicator until it is certain that the error is not recoverable. 클라이언트에서 Posts 렌더링을 다시 시도해도 실패하면, React는 클라이언트에서 오류를 던집니다. 렌더링 중에 발생하는 모든 에러와 마찬가지로, 가장 가까운 상위 에러 경계에 따라 사용자에게 에러를 표시하는 방법이 결정됩니다. 보다 실질적으로는, 오류를 복구할 수 없다는 것이 확실해질 때까지 로딩 표시기가 표시됩니다.

If retrying rendering Posts on the client succeeds, the loading fallback from the server will be replaced with the client rendering output. The user will not know that there was a server error. However, the server onError callback and the client onRecoverableError callbacks will fire so that you can get notified about the error. 클라이언트에서 Posts 렌더링을 다시 시도하여 성공하면 서버의 로딩 폴백이 클라이언트 렌더링 출력물로 대체됩니다. 사용자는 서버 오류가 발생했다는 사실을 알 수 없습니다. 그러나 서버의 onError 콜백 및 클라이언트의 onRecoverableError 콜백이 실행되므로 오류에 대한 알림을 받을 수는 있습니다.


Setting the status code상태 코드 설정하기

Streaming introduces a tradeoff. You want to start streaming the page as early as possible so that the user can see the content sooner. However, once you start streaming, you can no longer set the response status code. 스트리밍에는 장단점이 있습니다. 사용자가 콘텐츠를 더 빨리 볼 수 있도록 가능한 한 빨리 페이지 스트리밍을 시작하고 싶을 수 있습니다. 하지만 스트리밍을 시작하면 더 이상 응답 상태 코드를 설정할 수 없습니다.

By dividing your app into the shell (above all <Suspense> boundaries) and the rest of the content, you’ve already solved a part of this problem. If the shell errors, your catch block will run which lets you set the error status code. Otherwise, you know that the app may recover on the client, so you can send “OK”. 앱을 셸(특히 <Suspense> 경계)과 나머지 콘텐츠로 나누면, 이 문제의 일부를 이미 해결한 것입니다. 셸에서 오류가 발생하면 오류 상태 코드를 설정할 수 있는 onShellError 콜백을 받게 됩니다. 그렇지 않으면 앱이 클라이언트에서 복구될 수 있으므로 “OK”를 보낼 수 있습니다.

async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

If a component outside the shell (i.e. inside a <Suspense> boundary) throws an error, React will not stop rendering. This means that the onError callback will fire, but your code will continue running without getting into the catch block. This is because React will try to recover from that error on the client, as described above. 셸 외부에 있는 컴포넌트(예: <Suspense> 경계 안에 있는 컴포넌트)가 에러를 던져도 React는 렌더링을 멈추지 않습니다. 즉, onError 콜백이 실행됨에도 불구하고 코드는 여전히 catch 블록에 진입하지 않고 계속 실행됩니다. 이는 위에서 설명한 것처럼 React가 클라이언트에서 해당 오류를 복구하려고 시도하기 때문입니다.

However, if you’d like, you can use the fact that something has errored to set the status code: 하지만 원한다면 오류가 발생했다는 사실로부터 상태 코드를 설정할 수도 있습니다:

async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

This will only catch errors outside the shell that happened while generating the initial shell content, so it’s not exhaustive. If knowing whether an error occurred for some content is critical, you can move it up into the shell. 이는 초기 셸 콘텐츠를 생성하는 동안 발생한 셸 외부의 오류만 포착하므로 완전한 것은 아닙니다. 일부 콘텐츠에서 오류가 발생했는지 여부를 파악하는 것이 중요하다면 셸 내부로 이동시키세요.


Handling different errors in different ways다양한 방식으로 오류 처리하기

You can create your own Error subclasses and use the instanceof operator to check which error is thrown. For example, you can define a custom NotFoundError and throw it from your component. Then you can save the error in onError and do something different before returning the response depending on the error type: 자신만의 Error서브클래스를 생성하고 instanceof를 사용하여 어떤 오류가 발생했는지 확인할 수도 있습니다. 예를 들어, 사용자 정의 NotFoundError를 정의하고 컴포넌트에서 이를 던질 수 있습니다. 그런 다음 오류를 onError에 저장하고, 오류 유형에 따라 응답을 반환하기 전에 다른 작업을 수행할 수 있습니다:

async function handler(request) {
let didError = false;
let caughtError = null;

function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}

try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
}
}

Keep in mind that once you emit the shell and start streaming, you can’t change the status code. 셸을 내보내고 스트리밍을 시작하면 상태 코드를 변경할 수 없다는 점에 유의하세요.


Waiting for all content to load for crawlers and static generation크롤러 및 정적 생성시 모든 콘텐츠가 로드될 때까지 기다리기

Streaming offers a better user experience because the user can see the content as it becomes available. 스트리밍은 사용자가 콘텐츠를 사용할 수 있게 되는 즉시 볼 수 있기 때문에 더 나은 사용자 경험을 제공합니다.

However, when a crawler visits your page, or if you’re generating the pages at the build time, you might want to let all of the content load first and then produce the final HTML output instead of revealing it progressively. 그러나 크롤러가 페이지를 방문하거나 빌드 시점에 페이지를 생성하는 경우에는, 점진적으로 표시하는 대신 모든 콘텐츠가 먼저 로드된 다음 최종 HTML 출력을 생성하는 것이 좋을 수 있습니다.

You can wait for all the content to load by awaiting the stream.allReady Promise: onAllReady 프로미스를 사용하면 모든 콘텐츠가 로드될 때까지 기다릴 수 있습니다:

async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
let isCrawler = // ... depends on your bot detection strategy ...
if (isCrawler) {
await stream.allReady;
}
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

A regular visitor will get a stream of progressively loaded content. A crawler will receive the final HTML output after all the data loads. However, this also means that the crawler will have to wait for all data, some of which might be slow to load or error. Depending on your app, you could choose to send the shell to the crawlers too. 일반 방문자는 점진적으로 로드되는 콘텐츠 스트림을 받게 됩니다. 크롤러는 모든 데이터가 로드된 후 최종 HTML 출력을 받게 됩니다. 그러나 이는 크롤러가 모든 데이터를 기다려야 한다는 것을 의미하며, 그 중 일부는 로드 속도가 느리거나 오류가 발생할 수도 있습니다. 앱에 따라서는 크롤러에도 셸을 보내도록 선택할 수 있습니다.


Aborting server rendering서버렌더링 중단하기

You can force the server rendering to “give up” after a timeout: 시간 초과 후 서버 렌더링을 “포기”하도록 강제할 수 있습니다:

async function handler(request) {
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 10000);

const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
// ...

React will flush the remaining loading fallbacks as HTML, and will attempt to render the rest on the client. React는 기존 로딩 폴백을 HTML로 플러시하고 나머지는 클라이언트에서 렌더링을 시도합니다.