Skip to content

Latest commit

Β 

History

History
607 lines (473 loc) Β· 28 KB

File metadata and controls

607 lines (473 loc) Β· 28 KB
title renderToPipeableStream

renderToPipeableStream은 React 트리λ₯Ό νŒŒμ΄ν”„ κ°€λŠ₯ν•œ Node.js 슀트림으둜 λ Œλ”λ§ν•©λ‹ˆλ‹€.

const { pipe, abort } = renderToPipeableStream(reactNode, options?)

이 APIλŠ” Node.js μ „μš©μž…λ‹ˆλ‹€. Deno 및 μ΅œμ‹  μ—£μ§€ λŸ°νƒ€μž„κ³Ό 같은 Web 슀트림이 μžˆλŠ” ν™˜κ²½μ—μ„œλŠ” renderToReadableStream을 λŒ€μ‹  μ‚¬μš©ν•˜μ„Έμš”.


레퍼런슀 {/reference/}

renderToPipeableStream(reactNode, options?) {/rendertopipeablestream/}

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을 μƒν˜Έμž‘μš©ν•  수 μžˆλ„λ‘ λ§Œλ“­λ‹ˆλ‹€.

μ•„λž˜μ—μ„œ 더 λ§Žμ€ μ˜ˆμ‹œλ₯Ό ν™•μΈν•˜μ„Έμš”.

λ§€κ°œλ³€μˆ˜ {/parameters/}

  • 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-src Content-Security-Policy에 λŒ€ν•œ 슀크립트λ₯Ό ν—ˆμš©ν•˜λŠ” nonce λ¬Έμžμ—΄μž…λ‹ˆλ‹€.
    • onAllReady(선택사항): μ…Έκ³Ό λͺ¨λ“  μΆ”κ°€ μ½˜ν…μΈ λ₯Ό ν¬ν•¨ν•˜μ—¬ λͺ¨λ“  λ Œλ”λ§μ΄ μ™„λ£Œλ˜λ©΄ ν˜ΈμΆœλ˜λŠ” μ½œλ°±μž…λ‹ˆλ‹€. 크둀러 및 정적 생성에 onShellReady λŒ€μ‹  이 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ—¬κΈ°μ„œ μŠ€νŠΈλ¦¬λ°μ„ μ‹œμž‘ν•˜λ©΄ ν”„λ‘œκ·Έλ ˆμ‹œλΈŒ λ‘œλ”©μ΄ λ°œμƒν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μŠ€νŠΈλ¦Όμ—λŠ” μ΅œμ’… HTML이 ν¬ν•¨λ©λ‹ˆλ‹€.
    • onError(선택사항): 볡ꡬ κ°€λŠ₯ λ˜λŠ” λΆˆκ°€λŠ₯에 관계없이 μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν•  λ•Œλ§ˆλ‹€ ν˜ΈμΆœλ˜λŠ” μ½œλ°±μž…λ‹ˆλ‹€. 기본적으둜 console.error만 ν˜ΈμΆœν•©λ‹ˆλ‹€. 이 ν•¨μˆ˜λ₯Ό μž¬μ •μ˜ν•˜μ—¬ ν¬λž˜μ‹œ 리포트λ₯Ό κΈ°λ‘ν•˜λŠ” 경우 console.errorλ₯Ό 계속 ν˜ΈμΆœν•΄μ•Ό ν•©λ‹ˆλ‹€. 셸이 좜λ ₯되기 전에 μƒνƒœ μ½”λ“œλ₯Ό μ‘°μ •ν•˜λŠ” 데 μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
    • onShellReady(선택사항): 초기 셸이 λ Œλ”λ§λœ 직후에 μ‹€ν–‰λ˜λŠ” μ½œλ°±μž…λ‹ˆλ‹€. μ—¬κΈ°μ„œ μƒνƒœ μ½”λ“œλ₯Ό μ„€μ •ν•˜κ³  pipeλ₯Ό ν˜ΈμΆœν•˜μ—¬ μŠ€νŠΈλ¦¬λ°μ„ μ‹œμž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ReactλŠ” HTML λ‘œλ”© 폴백을 μ½˜ν…μΈ λ‘œ λŒ€μ²΄ν•˜λŠ” 인라인 <script> νƒœκ·Έμ™€ ν•¨κ»˜ μ…Έ 뒀에 μΆ”κ°€ μ½˜ν…μΈ λ₯Ό μŠ€νŠΈλ¦¬λ°ν•©λ‹ˆλ‹€.
    • onShellError(선택사항): 초기 셸을 λ Œλ”λ§ν•˜λŠ” 데 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ ν˜ΈμΆœλ˜λŠ” μ½œλ°±μž…λ‹ˆλ‹€. 였λ₯˜λ₯Ό 인자둜 λ°›μŠ΅λ‹ˆλ‹€. μŠ€νŠΈλ¦Όμ—μ„œ 아직 λ°”μ΄νŠΈκ°€ μ „μ†‘λ˜μ§€ μ•Šμ•˜κ³ , onShellReadyλ‚˜ onAllReady도 ν˜ΈμΆœλ˜μ§€ μ•ŠμœΌλ―€λ‘œ 폴백 HTML 셸을 좜λ ₯ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    • progressiveChunkSize(선택사항): 청크의 λ°”μ΄νŠΈ μˆ˜μž…λ‹ˆλ‹€. κΈ°λ³Έ νœ΄λ¦¬μŠ€ν‹±μ— λŒ€ν•΄ μžμ„Ένžˆ μ•Œμ•„λ³΄μ„Έμš”.

λ°˜ν™˜κ°’ {/returns/}

renderToPipeableStream은 두 개의 λ©”μ„œλ“œκ°€ μžˆλŠ” 객체λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.

  • pipeλŠ” HTML을 제곡된 μ“°κΈ° κ°€λŠ₯ν•œ Node.js 슀트림으둜 좜λ ₯ν•©λ‹ˆλ‹€. μŠ€νŠΈλ¦¬λ°μ„ ν™œμ„±ν™”ν•˜λ €λ©΄ onShellReadyμ—μ„œ, ν¬λ‘€λŸ¬μ™€ 정적 생성을 μ‚¬μš©ν•˜λ €λ©΄ onAllReadyμ—μ„œ pipeλ₯Ό ν˜ΈμΆœν•˜μ„Έμš”.
  • abortλ₯Ό μ‚¬μš©ν•˜λ©΄ μ„œλ²„ λ Œλ”λ§μ„ μ€‘λ‹¨ν•˜κ³  λ‚˜λ¨Έμ§€λŠ” ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ‚¬μš©λ²• {/usage/}

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의 ν–₯ν›„ λ²„μ „μ—μ„œ μΆœμ‹œν•  μ˜ˆμ •μž…λ‹ˆλ‹€.


셸에 λ“€μ–΄κ°ˆ λ‚΄μš© μ§€μ •ν•˜κΈ° {/specifying-what-goes-into-the-shell/}

μ•±μ˜ <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-crashes-on-the-server/}

기본적으둜 μ„œλ²„μ˜ λͺ¨λ“  였λ₯˜λŠ” μ½˜μ†”μ— 기둝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 κ΅¬ν˜„μ„ μ œκ³΅ν•˜λŠ” 경우 μœ„μ™€ 같이 μ½˜μ†”μ— 였λ₯˜λ₯Ό κΈ°λ‘ν•˜λŠ” 것도 μžŠμ§€ λ§ˆμ„Έμš”.


μ…Έ λ‚΄λΆ€μ˜ 였λ₯˜λ‘œλΆ€ν„° λ³΅κ΅¬ν•˜κΈ° {/recovering-from-errors-inside-the-shell/}

이 μ˜ˆμ‹œμ—μ„œλŠ” 셸에 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이 였λ₯˜ νŽ˜μ΄μ§€μΌ ν•„μš”λŠ” μ—†μŠ΅λ‹ˆλ‹€. λŒ€μ‹  ν΄λΌμ΄μ–ΈνŠΈμ—μ„œλ§Œ 앱을 λ Œλ”λ§ν•˜λŠ” λŒ€μ²΄ 셸을 포함할 수 μžˆμŠ΅λ‹ˆλ‹€.


μ…Έ μ™ΈλΆ€μ˜ 였λ₯˜λ‘œλΆ€ν„° λ³΅κ΅¬ν•˜κΈ° {/recovering-from-errors-outside-the-shell/}

이 μ˜ˆμ‹œμ—μ„œλŠ” <Posts /> μ»΄ν¬λ„ŒνŠΈκ°€ <Suspense>둜 λž˜ν•‘λ˜μ–΄ μžˆμœΌλ―€λ‘œ μ…Έμ˜ 일뢀가 μ•„λ‹™λ‹ˆλ‹€.

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

Posts μ»΄ν¬λ„ŒνŠΈ λ˜λŠ” κ·Έ λ‚΄λΆ€ μ–΄λ”˜κ°€μ—μ„œ 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ ReactλŠ” 이λ₯Ό λ³΅κ΅¬ν•˜λ €κ³  μ‹œλ„ν•©λ‹ˆλ‹€.

  1. κ°€μž₯ κ°€κΉŒμš΄ <Suspense> 경계(PostsGlimmer)에 λŒ€ν•œ λ‘œλ”© 폴백을 HTML둜 λ°©μΆœν•©λ‹ˆλ‹€.
  2. 더 이상 μ„œλ²„μ—μ„œ Posts μ½˜ν…μΈ λ₯Ό λ Œλ”λ§ν•˜λŠ” 것을 "포기"ν•©λ‹ˆλ‹€.
  3. μžλ°”μŠ€ν¬λ¦½νŠΈ μ½”λ“œκ°€ ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ λ‘œλ“œλ˜λ©΄ ReactλŠ” ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ Posts λ Œλ”λ§μ„ μž¬μ‹œλ„ν•©λ‹ˆλ‹€.

ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ Posts λ Œλ”λ§μ„ λ‹€μ‹œ μ‹œλ„ν•΄λ„ μ‹€νŒ¨ν•˜λ©΄ ReactλŠ” ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 였λ₯˜λ₯Ό λ˜μ§‘λ‹ˆλ‹€. λ Œλ”λ§ 쀑에 λ°œμƒν•˜λŠ” λͺ¨λ“  였λ₯˜μ™€ λ§ˆμ°¬κ°€μ§€λ‘œ, κ°€μž₯ κ°€κΉŒμš΄ λΆ€λͺ¨ 였λ₯˜ 경계에 따라 μ‚¬μš©μžμ—κ²Œ 였λ₯˜λ₯Ό ν‘œμ‹œν•˜λŠ” 방법이 κ²°μ •λ©λ‹ˆλ‹€. μ‹€μ œλ‘œλŠ” 였λ₯˜λ₯Ό 볡ꡬ할 수 μ—†λ‹€λŠ” 것이 ν™•μ‹€ν•΄μ§ˆ λ•ŒκΉŒμ§€ μ‚¬μš©μžμ—κ²Œ λ‘œλ”© ν‘œμ‹œκΈ°κ°€ ν‘œμ‹œλœλ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ Posts λ Œλ”λ§μ„ λ‹€μ‹œ μ‹œλ„ν•˜μ—¬ μ„±κ³΅ν•˜λ©΄ μ„œλ²„μ˜ λ‘œλ”© 폴백이 ν΄λΌμ΄μ–ΈνŠΈ λ Œλ”λ§ 좜λ ₯으둜 λŒ€μ²΄λ©λ‹ˆλ‹€. μ‚¬μš©μžλŠ” μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆλ‹€λŠ” 사싀을 μ•Œ 수 μ—†μŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ„œλ²„ onError 콜백 및 ν΄λΌμ΄μ–ΈνŠΈ onRecoverableError 콜백이 μ‹€ν–‰λ˜μ–΄ 였λ₯˜μ— λŒ€ν•œ μ•Œλ¦Όμ„ 받을 수 μžˆμŠ΅λ‹ˆλ‹€.


μƒνƒœ μ½”λ“œ μ„€μ •ν•˜κΈ° {/setting-the-status-code/}

μŠ€νŠΈλ¦¬λ°μ—λŠ” μž₯단점이 μžˆμŠ΅λ‹ˆλ‹€. μ‚¬μš©μžκ°€ μ½˜ν…μΈ λ₯Ό 더 빨리 λ³Ό 수 μžˆλ„λ‘ κ°€λŠ₯ν•œ ν•œ 빨리 νŽ˜μ΄μ§€ μŠ€νŠΈλ¦¬λ°μ„ μ‹œμž‘ν•˜κ³  싢을 수 μžˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μŠ€νŠΈλ¦¬λ°μ„ μ‹œμž‘ν•˜λ©΄ 더 이상 응닡 μƒνƒœ μ½”λ“œλ₯Ό μ„€μ •ν•  수 μ—†μŠ΅λ‹ˆλ‹€.

앱을 μ…Έ(특히 <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 좜λ ₯을 λ°›κ²Œ λ©λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ΄λŠ” ν¬λ‘€λŸ¬κ°€ λͺ¨λ“  데이터λ₯Ό κΈ°λ‹€λ €μ•Ό ν•œλ‹€λŠ” 것을 μ˜λ―Έν•˜λ©°, 그쀑 μΌλΆ€λŠ” λ‘œλ“œ 속도가 λŠλ¦¬κ±°λ‚˜ 였λ₯˜κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. 앱에 따라 ν¬λ‘€λŸ¬μ—λ„ 셸을 보내도둝 선택할 수 μžˆμŠ΅λ‹ˆλ‹€.


μ„œλ²„ λ Œλ”λ§ μ€‘λ‹¨ν•˜κΈ° {/aborting-server-rendering/}

μ‹œκ°„ 초과 ν›„ μ„œλ²„ λ Œλ”λ§μ„ κ°•μ œλ‘œ '포기'ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

const { pipe, abort } = renderToPipeableStream(<App />, {
  // ...
});

setTimeout(() => {
  abort();
}, 10000);

ReactλŠ” λ‚˜λ¨Έμ§€ λ‘œλ”© 폴백을 HTML둜 ν”ŒλŸ¬μ‹œν•˜κ³  λ‚˜λ¨Έμ§€λŠ” ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ λ Œλ”λ§μ„ μ‹œλ„ν•©λ‹ˆλ‹€.