| title | renderToPipeableStream |
|---|
renderToPipeableStreamμ React νΈλ¦¬λ₯Ό νμ΄ν κ°λ₯ν Node.js μ€νΈλ¦ΌμΌλ‘ λ λλ§ν©λλ€.
const { pipe, abort } = renderToPipeableStream(reactNode, options?)μ΄ APIλ Node.js μ μ©μ
λλ€. Deno λ° μ΅μ μ£μ§ λ°νμκ³Ό κ°μ Web μ€νΈλ¦Όμ΄ μλ νκ²½μμλ renderToReadableStreamμ λμ μ¬μ©νμΈμ.
renderToPipeableStreamμ νΈμΆνμ¬ React νΈλ¦¬λ₯Ό HTMLλ‘ Node.js μ€νΈλ¦Όμ λ λλ§ν©λλ€.
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});ν΄λΌμ΄μΈνΈμμ hydrateRootλ₯Ό νΈμΆνμ¬ μλ²μμ μμ±λ HTMLμ μνΈμμ©ν μ μλλ‘ λ§λλλ€.
μλμμ λ λ§μ μμλ₯Ό νμΈνμΈμ.
-
reactNode: HTMLλ‘ λ λλ§νλ €λ React λ Έλ. μλ₯Ό λ€μ΄,<App />κ³Ό κ°μ JSX μ리먼νΈμ λλ€. μ 체 λ¬Έμλ₯Ό λνλΌ κ²μΌλ‘ μμλλ―λ‘Appμ»΄ν¬λνΈλ<html>νκ·Έλ₯Ό λ λλ§ν΄μΌ ν©λλ€. -
options(μ νμ¬ν): μ€νΈλ¦¬λ° μ΅μ μ΄ μλ κ°μ²΄μ λλ€.bootstrapScriptContent(μ νμ¬ν): μ§μ νλ©΄ μ΄ λ¬Έμμ΄μ΄ μΈλΌμΈ<script>νκ·Έμ λ°°μΉλ©λλ€.bootstrapScripts(μ νμ¬ν): νμ΄μ§μ νμν<script>νκ·Έμ λν λ¬Έμμ΄ URL λ°°μ΄μ λλ€. μ΄λ₯Ό μ¬μ©νμ¬hydrateRootλ₯Ό νΈμΆνλ<script>λ₯Ό ν¬ν¨νμΈμ. ν΄λΌμ΄μΈνΈμμ Reactλ₯Ό μ ν μ€ννμ§ μμΌλ €λ©΄ μλ΅νμΈμ.bootstrapModules(μ νμ¬ν):bootstrapScriptsμ κ°μ§λ§ λμ<script type="module">λ₯Ό μΆλ ₯ν©λλ€.identifierPrefix(μ νμ¬ν): Reactκ°useIdμ μν΄ μμ±λ IDμ μ¬μ©νλ λ¬Έμμ΄ μ λμ¬μ λλ€. κ°μ νμ΄μ§μμ μ¬λ¬ 루νΈλ₯Ό μ¬μ©ν λ μΆ©λμ νΌνλ λ° μ μ©ν©λλ€.hydrateRootμ μ λ¬λ κ²κ³Ό λμΌν μ λμ¬μ¬μΌ ν©λλ€.namespaceURI(μ νμ¬ν): μ€νΈλ¦Όμ λ£¨νΈ λ€μμ€νμ΄μ€ URIκ° ν¬ν¨λ λ¬Έμμ΄μ λλ€. κΈ°λ³Έκ°μ μΌλ° HTMLμ λλ€. SVGμ κ²½μ°'http://www.w3.org/2000/svg'λ₯Ό, MathMLμ κ²½μ°'http://www.w3.org/1998/Math/MathML'λ₯Ό μ λ¬ν©λλ€.nonce(μ νμ¬ν):script-srcContent-Security-Policyμ λν μ€ν¬λ¦½νΈλ₯Ό νμ©νλnonceλ¬Έμμ΄μ λλ€.onAllReady(μ νμ¬ν): μ Έκ³Ό λͺ¨λ μΆκ° μ½ν μΈ λ₯Ό ν¬ν¨νμ¬ λͺ¨λ λ λλ§μ΄ μλ£λλ©΄ νΈμΆλλ μ½λ°±μ λλ€. ν¬λ‘€λ¬ λ° μ μ μμ±μonShellReadyλμ μ΄ ν¨μλ₯Ό μ¬μ©ν μ μμ΅λλ€. μ¬κΈ°μ μ€νΈλ¦¬λ°μ μμνλ©΄ νλ‘κ·Έλ μλΈ λ‘λ©μ΄ λ°μνμ§ μμ΅λλ€. μ€νΈλ¦Όμλ μ΅μ’ HTMLμ΄ ν¬ν¨λ©λλ€.onError(μ νμ¬ν): 볡ꡬ κ°λ₯ λλ λΆκ°λ₯μ κ΄κ³μμ΄ μλ² μ€λ₯κ° λ°μν λλ§λ€ νΈμΆλλ μ½λ°±μ λλ€. κΈ°λ³Έμ μΌλ‘console.errorλ§ νΈμΆν©λλ€. μ΄ ν¨μλ₯Ό μ¬μ μνμ¬ ν¬λμ 리ν¬νΈλ₯Ό κΈ°λ‘νλ κ²½μ°console.errorλ₯Ό κ³μ νΈμΆν΄μΌ ν©λλ€. μ Έμ΄ μΆλ ₯λκΈ° μ μ μν μ½λλ₯Ό μ‘°μ νλ λ° μ¬μ©ν μλ μμ΅λλ€.onShellReady(μ νμ¬ν): μ΄κΈ° μ Έμ΄ λ λλ§λ μ§νμ μ€νλλ μ½λ°±μ λλ€. μ¬κΈ°μ μν μ½λλ₯Ό μ€μ νκ³pipeλ₯Ό νΈμΆνμ¬ μ€νΈλ¦¬λ°μ μμν μ μμ΅λλ€. Reactλ HTML λ‘λ© ν΄λ°±μ μ½ν μΈ λ‘ λ체νλ μΈλΌμΈ<script>νκ·Έμ ν¨κ» μ Έ λ€μ μΆκ° μ½ν μΈ λ₯Ό μ€νΈλ¦¬λ°ν©λλ€.onShellError(μ νμ¬ν): μ΄κΈ° μ Έμ λ λλ§νλ λ° μ€λ₯κ° λ°μνλ©΄ νΈμΆλλ μ½λ°±μ λλ€. μ€λ₯λ₯Ό μΈμλ‘ λ°μ΅λλ€. μ€νΈλ¦Όμμ μμ§ λ°μ΄νΈκ° μ μ‘λμ§ μμκ³ ,onShellReadyλonAllReadyλ νΈμΆλμ§ μμΌλ―λ‘ ν΄λ°± HTML μ Έμ μΆλ ₯ ν μ μμ΅λλ€.progressiveChunkSize(μ νμ¬ν): μ²ν¬μ λ°μ΄νΈ μμ λλ€. κΈ°λ³Έ ν΄λ¦¬μ€ν±μ λν΄ μμΈν μμ보μΈμ.
renderToPipeableStreamμ λ κ°μ λ©μλκ° μλ κ°μ²΄λ₯Ό λ°νν©λλ€.
pipeλ HTMLμ μ 곡λ μ°κΈ° κ°λ₯ν Node.js μ€νΈλ¦ΌμΌλ‘ μΆλ ₯ν©λλ€. μ€νΈλ¦¬λ°μ νμ±ννλ €λ©΄onShellReadyμμ, ν¬λ‘€λ¬μ μ μ μμ±μ μ¬μ©νλ €λ©΄onAllReadyμμpipeλ₯Ό νΈμΆνμΈμ.abortλ₯Ό μ¬μ©νλ©΄ μλ² λ λλ§μ μ€λ¨νκ³ λλ¨Έμ§λ ν΄λΌμ΄μΈνΈμμ λ λλ§ν μ μμ΅λλ€.
React νΈλ¦¬λ₯Ό HTMLλ‘ Node.js μ€νΈλ¦Όμ λ λλ§νκΈ° {/rendering-a-react-tree-as-html-to-a-nodejs-stream/}
renderToPipeableStreamμ νΈμΆνμ¬ React νΈλ¦¬λ₯Ό HTMLλ‘ Node.js μ€νΈλ¦Όμ λ λλ§ν©λλ€.
import { renderToPipeableStream } from 'react-dom/server';
// κ²½λ‘ νΈλ€λ¬ λ¬Έλ²μ λ°±μλ νλ μμν¬μ λ°λΌ λ€λ¦
λλ€.
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
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λ₯Ό νμ΄λλ μ΄νΈν΄μΌ ν©λλ€.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);μ΄λ κ² νλ©΄ μλ²μμ μμ±λ HTMLμ μ΄λ²€νΈ 리μ€λκ° μ²¨λΆλμ΄ μνΈμμ©μ΄ κ°λ₯ν΄μ§λλ€.
λΉλ μΆλ ₯μμ CSS λ° JS μμ κ²½λ‘ μ½κΈ° {/reading-css-and-js-asset-paths-from-the-build-output/}
μ΅μ’
μμ
URL(μ: μλ°μ€ν¬λ¦½νΈ λ° CSS νμΌ)μ λΉλ νμ ν΄μ μ²λ¦¬λλ κ²½μ°κ° λ§μ΅λλ€. μλ₯Ό λ€μ΄ styles.css λμ styles.123456.cssλ‘ λλ μ μμ΅λλ€. μ μ μμ
νμΌλͺ
μ ν΄μνλ©΄ λμΌν μμ
μ λͺ¨λ λ³κ°μ λΉλμμ λ€λ₯Έ νμΌλͺ
μ κ°μ§ μ μμ΅λλ€. μ΄λ μ μ μμ°μ λν μ₯κΈ° μΊμ±μ μμ νκ² νμ±νν μ μκΈ° λλ¬Έμ μ μ©ν©λλ€. νΉμ μ΄λ¦μ κ°μ§ νμΌμ μ½ν
μΈ κ° λ³κ²½λμ§ μμ΅λλ€.
νμ§λ§ λΉλκ° λλ λκΉμ§ μμ
URLμ λͺ¨λ₯΄λ κ²½μ° μμ€ μ½λμ λ£μ λ°©λ²μ΄ μμ΅λλ€. μλ₯Ό λ€μ΄, μμμ²λΌ "/styles.css"λ₯Ό JSXμ νλμ½λ©νλ©΄ μλνμ§ μμ΅λλ€. μμ€ μ½λμ ν¬ν¨λμ§ μλλ‘ νλ €λ©΄ λ£¨νΈ μ»΄ν¬λνΈκ° νλ‘νΌν°λ‘ μ λ¬λ λ§΅μμ μ€μ νμΌλͺ
μ μ½μ μ μμ΅λλ€.
export default function App({ assetMap }) {
return (
<html>
<head>
...
<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('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});μ΄μ μλ²μμ <App assetMap={assetMap} />λ₯Ό λ λλ§νκ³ μμΌλ―λ‘ ν΄λΌμ΄μΈνΈμμλ assetMapμ μ¬μ©νμ¬ λ λλ§ν΄μΌ νμ΄λλ μ΄μ
μ€λ₯λ₯Ό λ°©μ§ν μ μμ΅λλ€. λ€μκ³Ό κ°μ΄ assetMapμ μ§λ ¬ννμ¬ ν΄λΌμ΄μΈνΈμ μ λ¬ν μ μμ΅λλ€.
// λΉλ λꡬμμ μ΄ JSONμ κ°μ ΈμμΌ ν©λλ€.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
// μ‘°μ¬νμΈμ: μ΄ λ°μ΄ν°λ μ¬μ©μκ° μμ±ν κ²μ΄ μλλ―λ‘ stringify()νλ κ²μ΄ μμ ν©λλ€.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});μ μμμμ bootstrapScriptContent μ΅μ
μ ν΄λΌμ΄μΈνΈμμ μ μ window.assetMap λ³μλ₯Ό μ€μ νλ μΆκ° μΈλΌμΈ <script> νκ·Έλ₯Ό μΆκ°ν©λλ€. μ΄λ κ² νλ©΄ ν΄λΌμ΄μΈνΈ μ½λκ° λμΌν assetMapμ μ½μ μ μμ΅λλ€.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);ν΄λΌμ΄μΈνΈμ μλ² λͺ¨λ λμΌν assetMap νλ‘νΌν°λ‘ Appμ λ λλ§νλ―λ‘ νμ΄λλ μ΄μ
μ€λ₯κ° λ°μνμ§ μμ΅λλ€.
μ½ν μΈ κ° λ‘λλλ λμ λ λ§μ μ½ν μΈ μ€νΈλ¦¬λ°νκΈ° {/streaming-more-content-as-it-loads/}
μ€νΈλ¦¬λ°μ μ¬μ©νλ©΄ λͺ¨λ λ°μ΄ν°κ° μλ²μ λ‘λλκΈ° μ μλ μ¬μ©μκ° μ½ν μΈ λ₯Ό λ³Ό μ μμ΅λλ€. μλ₯Ό λ€μ΄ νμ§μ μΉκ΅¬ λ° μ¬μ§μ΄ μλ μ¬μ΄λλ°, κΈ λͺ©λ‘μ΄ νμλλ νλ‘ν νμ΄μ§λ₯Ό μκ°ν΄ 보μΈμ.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}<Posts />μ λν λ°μ΄ν°λ₯Ό λ‘λνλ λ° μκ°μ΄ κ±Έλ¦°λ€κ³ κ°μ ν΄ λ³΄κ² μ΅λλ€. μ΄μμ μΌλ‘λ κ²μλ¬Όμ κΈ°λ€λ¦¬μ§ μκ³ λλ¨Έμ§ νλ‘ν νμ΄μ§ μ½ν
μΈ λ₯Ό μ¬μ©μμκ² νμνκ³ μΆμ κ²μ
λλ€. μ΄λ κ² νλ €λ©΄, <Posts>λ₯Ό <Suspense> κ²½κ³λ‘ κ°μΈλ©΄ λ©λλ€.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}μ΄κ²μ Postsκ° λ°μ΄ν°λ₯Ό λ‘λνκΈ° μ μ Reactκ° HTML μ€νΈλ¦¬λ°μ μμνλλ‘ μ§μν©λλ€. Reactλ λ‘λ© ν΄λ°±(PostsGlimmer)μ μν HTMLμ λ¨Όμ μ μ‘ν λ€μ, Postsκ° λ°μ΄ν° λ‘λ©μ μλ£νλ©΄ λλ¨Έμ§ HTMLμ μΈλΌμΈ <script> νκ·Έμ ν¨κ» μ μ‘νμ¬ λ‘λ© ν΄λ°±μ ν΄λΉ HTMLλ‘ λ체ν κ²μ
λλ€. μ¬μ©μ μ
μ₯μμλ νμ΄μ§κ° λ¨Όμ PostsGlimmerλ‘ νμλκ³ λμ€μ Postsλ‘ λ체λ©λλ€.
<Suspense> κ²½κ³λ₯Ό λ μ€μ²©νμ¬ λ³΄λ€ μΈλΆνλ λ‘λ© μνμ€λ₯Ό λ§λ€ μ μμ΅λλ€.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}μ΄ μμμμ Reactλ νμ΄μ§ μ€νΈλ¦¬λ°μ λ μΌμ° μμν μ μμ΅λλ€. ProfileLayoutκ³Ό ProfileCoverλ§ <Suspense> κ²½κ³λ‘ λλ¬μΈμ¬ μμ§ μκΈ° λλ¬Έμ λ¨Όμ λ λλ§μ μλ£ν΄μΌ ν©λλ€. νμ§λ§ Sidebar, Friends, Photosκ° μΌλΆ λ°μ΄ν°λ₯Ό λ‘λν΄μΌ νλ κ²½μ°, Reactλ λμ BigSpinner ν΄λ°±μ μν HTMLμ μ μ‘ν©λλ€. κ·Έλ¬λ©΄ λ λ§μ λ°μ΄ν°λ₯Ό μ¬μ©ν μ μκ² λλ©΄ λͺ¨λ λ°μ΄ν°κ° νμλ λκΉμ§ λ λ§μ μ½ν
μΈ κ° κ³μ νμλ©λλ€.
μ€νΈλ¦¬λ°μ λΈλΌμ°μ μμ React μμ²΄κ° λ‘λλκ±°λ μ±μ΄ μνΈμμ© κ°λ₯ν΄μ§ λκΉμ§ κΈ°λ€λ¦΄ νμκ° μμ΅λλ€. μλ²μ HTML μ½ν
μΈ λ <script> νκ·Έκ° λ‘λλκΈ° μ μ μ μ§μ μΌλ‘ νμλ©λλ€.
μ€νΈλ¦¬λ° HTMLμ μλ λ°©μμ λν΄ μμΈν μμ보μΈμ.
Suspenseλ₯Ό μ§μνλ λ°μ΄ν° μμ€λ§ Suspense μ»΄ν¬λνΈλ₯Ό νμ±νν©λλ€. μ΄λ λ€μκ³Ό κ°μ΅λλ€.
- Relayμ Next.js κ°μ Suspenseκ° κ°λ₯ν νλ μμν¬λ₯Ό μ¬μ©ν λ°μ΄ν° κ°μ Έμ€κΈ°.
lazyλ₯Ό νμ©ν μ§μ° λ‘λ© μ»΄ν¬λνΈ.useλ₯Ό μ¬μ©ν΄μ Promise κ° μ½κΈ°.
Suspenseλ Effect λλ μ΄λ²€νΈ νΈλ€λ¬ λ΄λΆμμ λ°μ΄ν°λ₯Ό κ°μ Έμ¬ κ²½μ°, μ΄λ₯Ό κ°μ§νμ§ λͺ»ν©λλ€.
Posts μ»΄ν¬λνΈμμ λ°μ΄ν°λ₯Ό λΆλ¬μ€λ μ νν λ°©λ²μ μμ μ€λͺ
ν νλ μμν¬μ λ°λΌ λ€λ¦
λλ€. Suspenseλ₯Ό μ§μνλ νλ μμν¬λ₯Ό μ¬μ©νλ κ²½μ°, λ°μ΄ν°λ₯Ό κ°μ Έμ€λ μμΈν λ°©λ²μ ν΄λΉ νλ μμν¬ λ¬Έμμμ μ°Ύμ μ μμ΅λλ€.
λ μμ μΈ νλ μμν¬λ₯Ό μ¬μ©νμ§ μλ Suspense μ§μ λ°μ΄ν° κ°μ Έμ€κΈ°λ μμ§ μ§μνμ§ μμ΅λλ€. Suspenseλ₯Ό μ§μνλ λ°μ΄ν° μμ€λ₯Ό ꡬννκΈ° μν μꡬ μ¬νμ λΆμμ νκ³ λ¬Έμνλμ§ μμμ΅λλ€. λ°μ΄ν° μμ€λ₯Ό Suspenseμ ν΅ν©νκΈ° μν 곡μ APIλ Reactμ ν₯ν λ²μ μμ μΆμν μμ μ λλ€.
μ±μ <Suspense> κ²½κ³ λ°μ μλ λΆλΆμ μ
Έμ΄λΌκ³ ν©λλ€.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}μ¬μ©μκ° λ³Ό μ μλ κ°μ₯ λΉ λ₯Έ λ‘λ© μνλ₯Ό κ²°μ ν©λλ€.
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>μ 체 μ±μ 루νΈμ <Suspense> κ²½κ³λ‘ κ°μΈλ©΄ μ
Έμλ ν΄λΉ μ€νΌλλ§ ν¬ν¨λ©λλ€. νμ§λ§ νλ©΄μ ν° μ€νΌλκ° νμλλ©΄ μ‘°κΈ λ κΈ°λ€λ Έλ€κ° μ€μ λ μ΄μμμ 보λ κ²λ³΄λ€ λλ¦¬κ³ μ±κ°μκ² λκ»΄μ§ μ μμΌλ―λ‘ μ¬μ©μ κ²½νμ΄ μ’μ§ μμ΅λλ€. κ·Έλ κΈ° λλ¬Έμ μΌλ°μ μΌλ‘ μ
Έμ΄ μ 체 νμ΄μ§ λ μ΄μμμ μ€μΌλ ν€μ²λΌ μ΅μνμ μμ ν¨μ λλ μ μλλ‘ <Suspense> κ²½κ³λ₯Ό λ°°μΉνλ κ²μ΄ μ’μ΅λλ€.
μ 체 μ
Έμ΄ λ λλ§λλ©΄ onShellReady μ½λ°±μ΄ μ€νλ©λλ€. λ³΄ν΅ μ΄λ μ€νΈλ¦¬λ°μ΄ μμλ©λλ€.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});onShellReadyκ° μ€νλ λ μ€μ²©λ <Suspense> κ²½κ³μ μλ μ»΄ν¬λνΈλ μ¬μ ν λ°μ΄ν°λ₯Ό λ‘λνκ³ μμ μ μμ΅λλ€.
κΈ°λ³Έμ μΌλ‘ μλ²μ λͺ¨λ μ€λ₯λ μ½μμ κΈ°λ‘Loggingλ©λλ€. μ΄ λμμ μ¬μ μνμ¬ μΆ©λCrash λ³΄κ³ μλ₯Ό κΈ°λ‘ν μ μμ΅λλ€.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});μ¬μ©μ μ μ onError ꡬνμ μ 곡νλ κ²½μ° μμ κ°μ΄ μ½μμ μ€λ₯λ₯Ό κΈ°λ‘νλ κ²λ μμ§ λ§μΈμ.
μ΄ μμμμλ μ
Έμ ProfileLayout, ProfileCover, PostsGlimmerκ° ν¬ν¨λμ΄ μμ΅λλ€.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}μ΄λ¬ν μ»΄ν¬λνΈλ₯Ό λ λλ§νλ λμ μ€λ₯κ° λ°μνλ©΄ Reactλ ν΄λΌμ΄μΈνΈμ λ³΄λΌ μλ―Έ μλ HTMLμ κ°μ§ λͺ»ν©λλ€. λ§μ§λ§ μλ¨μΌλ‘ μλ² λ λλ§μ μμ‘΄νμ§ μλ ν΄λ°± HTMLμ 보λ΄λ €λ©΄ onShellErrorλ₯Ό μ¬μ μνμΈμ.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});μ
Έμ μμ±νλ λμ μ€λ₯κ° λ°μνλ©΄ onErrorμ onShellErrorκ° λͺ¨λ μ€νλ©λλ€. μ€λ₯ λ³΄κ³ μλ onErrorλ₯Ό μ¬μ©νκ³ , λ체 HTML λ¬Έμλ₯Ό 보λ΄λ €λ©΄ onShellErrorλ₯Ό μ¬μ©ν©λλ€. ν΄λ°± HTMLμ΄ μ€λ₯ νμ΄μ§μΌ νμλ μμ΅λλ€. λμ ν΄λΌμ΄μΈνΈμμλ§ μ±μ λ λλ§νλ λ체 μ
Έμ ν¬ν¨ν μ μμ΅λλ€.
μ΄ μμμμλ <Posts /> μ»΄ν¬λνΈκ° <Suspense>λ‘ λνλμ΄ μμΌλ―λ‘ μ
Έμ μΌλΆκ° μλλλ€.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}Posts μ»΄ν¬λνΈ λλ κ·Έ λ΄λΆ μ΄λκ°μμ μ€λ₯κ° λ°μνλ©΄ Reactλ μ΄λ₯Ό 볡ꡬνλ €κ³ μλν©λλ€.
- κ°μ₯ κ°κΉμ΄
<Suspense>κ²½κ³(PostsGlimmer)μ λν λ‘λ© ν΄λ°±μ HTMLλ‘ λ°©μΆν©λλ€. - λ μ΄μ μλ²μμ
Postsμ½ν μΈ λ₯Ό λ λλ§νλ κ²μ "ν¬κΈ°"ν©λλ€. - μλ°μ€ν¬λ¦½νΈ μ½λκ° ν΄λΌμ΄μΈνΈμμ λ‘λλλ©΄ Reactλ ν΄λΌμ΄μΈνΈμμ
Postsλ λλ§μ μ¬μλν©λλ€.
ν΄λΌμ΄μΈνΈμμ Posts λ λλ§μ λ€μ μλν΄λ μ€ν¨νλ©΄ Reactλ ν΄λΌμ΄μΈνΈμμ μ€λ₯λ₯Ό λμ§λλ€. λ λλ§ μ€μ λ°μνλ λͺ¨λ μ€λ₯μ λ§μ°¬κ°μ§λ‘, κ°μ₯ κ°κΉμ΄ λΆλͺ¨ μ€λ₯ κ²½κ³μ λ°λΌ μ¬μ©μμκ² μ€λ₯λ₯Ό νμνλ λ°©λ²μ΄ κ²°μ λ©λλ€. μ€μ λ‘λ μ€λ₯λ₯Ό 볡ꡬν μ μλ€λ κ²μ΄ νμ€ν΄μ§ λκΉμ§ μ¬μ©μμκ² λ‘λ© νμκΈ°κ° νμλλ€λ μλ―Έμ
λλ€.
ν΄λΌμ΄μΈνΈμμ Posts λ λλ§μ λ€μ μλνμ¬ μ±κ³΅νλ©΄ μλ²μ λ‘λ© ν΄λ°±μ΄ ν΄λΌμ΄μΈνΈ λ λλ§ μΆλ ₯μΌλ‘ λ체λ©λλ€. μ¬μ©μλ μλ² μ€λ₯κ° λ°μνλ€λ μ¬μ€μ μ μ μμ΅λλ€. κ·Έλ¬λ μλ² onError μ½λ°± λ° ν΄λΌμ΄μΈνΈ onRecoverableError μ½λ°±μ΄ μ€νλμ΄ μ€λ₯μ λν μλ¦Όμ λ°μ μ μμ΅λλ€.
μ€νΈλ¦¬λ°μλ μ₯λ¨μ μ΄ μμ΅λλ€. μ¬μ©μκ° μ½ν μΈ λ₯Ό λ 빨리 λ³Ό μ μλλ‘ κ°λ₯ν ν 빨리 νμ΄μ§ μ€νΈλ¦¬λ°μ μμνκ³ μΆμ μ μμ΅λλ€. κ·Έλ¬λ μ€νΈλ¦¬λ°μ μμνλ©΄ λ μ΄μ μλ΅ μν μ½λλ₯Ό μ€μ ν μ μμ΅λλ€.
μ±μ μ
Έ(νΉν <Suspense> κ²½κ³ λ°κΉ₯)κ³Ό λλ¨Έμ§ μ½ν
μΈ λ‘ λλλ©΄ μ΄ λ¬Έμ μ μΌλΆλ₯Ό μ΄λ―Έ ν΄κ²°ν κ²μ
λλ€. μ
Έμ μ€λ₯κ° λ°μνλ©΄ μ€λ₯ μν μ½λλ₯Ό μ€μ ν μ μλ onShellError μ½λ°±μ λ°κ² λ©λλ€. κ·Έλ μ§ μμΌλ©΄ μ±μ΄ ν΄λΌμ΄μΈνΈμμ 볡ꡬλ μ μμΌλ―λ‘ "OK"λ₯Ό λ³΄λΌ μ μμ΅λλ€.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});μ
Έ μΈλΆ(μ¦, <Suspense> κ²½κ³ μμͺ½)μ μλ μ»΄ν¬λνΈκ° μ€λ₯λ₯Ό λμ Έλ Reactλ λ λλ§μ λ©μΆμ§ μμ΅λλ€. μ¦, onError μ½λ°±μ΄ μ€νλμ§λ§ onShellError λμ onShellReadyκ° λ°νλ©λλ€. μ΄λ μμμ μ€λͺ
ν κ²μ²λΌ Reactκ° ν΄λΌμ΄μΈνΈμμ ν΄λΉ μ€λ₯λ₯Ό 볡ꡬνλ €κ³ μλνκΈ° λλ¬Έμ
λλ€.
κ·Έλ¬λ μνλ κ²½μ° μ€λ₯κ° λ°μνλ€λ μ¬μ€μ μ¬μ©νμ¬ μν μ½λλ₯Ό μ€μ ν μ μμ΅λλ€.
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(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});μ΄λ μ΄κΈ° μ Έ μ½ν μΈ λ₯Ό μμ±νλ λμ λ°μν μ Έ μΈλΆμ μ€λ₯λ§ ν¬μ°©νλ―λ‘ μμ ν κ²μ μλλλ€. μΌλΆ μ½ν μΈ μμ μ€λ₯κ° λ°μνλμ§ μ¬λΆλ₯Ό νμ νλ κ²μ΄ μ€μν κ²½μ° ν΄λΉ μ½ν μΈ λ₯Ό μ Έλ‘ μ΄λνλ©΄ λ©λλ€.
λ€μν μ€λ₯λ₯Ό μλ‘ λ€λ₯Έ λ°©μμΌλ‘ μ²λ¦¬νκΈ° {/handling-different-errors-in-different-ways/}
μμ λ§μ Error μλΈ ν΄λμ€λ₯Ό μμ±νκ³ instanceof μ°μ°μλ₯Ό μ¬μ©ν΄ μ΄λ€ μ€λ₯κ° λ°μνλμ§ νμΈν μ μμ΅λλ€. μλ₯Ό λ€μ΄, μ¬μ©μ μ μ NotFoundErrorλ₯Ό μ μνκ³ μ»΄ν¬λνΈμμ μ΄λ₯Ό λμ§ μ μμ΅λλ€. κ·Έλ¬λ©΄ μ€λ₯ μ νμ λ°λΌ onError, onShellReady, onShellError μ½λ°±μ΄ λ€λ₯Έ μμ
μ μνν μ μμ΅λλ€.
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});μ Έμ λ΄λ³΄λ΄κ³ μ€νΈλ¦¬λ°μ μμνλ©΄ μν μ½λλ₯Ό λ³κ²½ν μ μλ€λ μ μ μ μνμΈμ.
ν¬λ‘€λ¬ λ° μ μ μμ±μ μν΄ λͺ¨λ μ½ν μΈ κ° λ‘λλ λκΉμ§ κΈ°λ€λ¦¬κΈ° {/waiting-for-all-content-to-load-for-crawlers-and-static-generation/}
μ€νΈλ¦¬λ°μ μ½ν μΈ κ° μ 곡λ λ λ°λ‘ λ³Ό μ μκΈ° λλ¬Έμ λ λμ μ¬μ©μ κ²½νμ μ 곡ν©λλ€.
κ·Έλ¬λ ν¬λ‘€λ¬κ° νμ΄μ§λ₯Ό λ°©λ¬Ένκ±°λ λΉλ μμ μ νμ΄μ§λ₯Ό μμ±νλ κ²½μ° λͺ¨λ μ½ν μΈ λ₯Ό μ μ§μ μΌλ‘ νμνλ λμ λͺ¨λ μ½ν μΈ λ₯Ό λ¨Όμ λ‘λν λ€μ μ΅μ’ HTML μΆλ ₯μ μμ±νλ κ²μ΄ μ’μ μ μμ΅λλ€.
onAllReady μ½λ°±μ μ¬μ©νμ¬ λͺ¨λ μ½ν
μΈ κ° λ‘λλ λκΉμ§ κΈ°λ€λ¦΄ μ μμ΅λλ€.
let didError = false;
let isCrawler = // ... λ΄ νμ§ μ λ΅μ λ°λΌ λ¬λΌμ§λλ€ ...
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
if (!isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onAllReady() {
if (isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});μΌλ° λ°©λ¬Έμλ μ μ§μ μΌλ‘ λ‘λλλ μ½ν μΈ μ€νΈλ¦Όμ λ°κ² λ©λλ€. ν¬λ‘€λ¬λ λͺ¨λ λ°μ΄ν°κ° λ‘λλ ν μ΅μ’ HTML μΆλ ₯μ λ°κ² λ©λλ€. κ·Έλ¬λ μ΄λ ν¬λ‘€λ¬κ° λͺ¨λ λ°μ΄ν°λ₯Ό κΈ°λ€λ €μΌ νλ€λ κ²μ μλ―Ένλ©°, κ·Έμ€ μΌλΆλ λ‘λ μλκ° λ리거λ μ€λ₯κ° λ°μν μ μμ΅λλ€. μ±μ λ°λΌ ν¬λ‘€λ¬μλ μ Έμ 보λ΄λλ‘ μ νν μ μμ΅λλ€.
μκ° μ΄κ³Ό ν μλ² λ λλ§μ κ°μ λ‘ 'ν¬κΈ°'ν μ μμ΅λλ€.
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);Reactλ λλ¨Έμ§ λ‘λ© ν΄λ°±μ HTMLλ‘ νλ¬μνκ³ λλ¨Έμ§λ ν΄λΌμ΄μΈνΈμμ λ λλ§μ μλν©λλ€.