prerenderToNodeStream์€ Node.js Stream์„ ์‚ฌ์šฉํ•˜์—ฌ React ํŠธ๋ฆฌ๋ฅผ ์ •์  HTML ๋ฌธ์ž์—ด๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

const {prelude} = await prerenderToNodeStream(reactNode, options?)

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

์ด API๋Š” Node.js ์ „์šฉ์ž…๋‹ˆ๋‹ค. Deno๋‚˜ ์ตœ์‹  ์—ฃ์ง€ ๋Ÿฐํƒ€์ž„์ฒ˜๋Ÿผ Web Streams๋ฅผ ์ง€์›ํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” prerender๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๋ ˆํผ๋Ÿฐ์Šค

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: ํ”„๋ฆฌ๋ Œ๋”๋ง์„ ์ค‘๋‹จํ•˜๊ณ  ๋‚˜๋จธ์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ์ค‘๋‹จ ์‹ ํ˜ธ์ž…๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’

prerenderToNodeStream ๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ Œ๋”๋ง์ด ์„ฑ๊ณตํ•˜๋ฉด Promise๋Š” ๋‹ค์Œ์„ ํฌํ•จํ•˜๋Š” ๊ฐ์ฒด๋กœ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.
    • prelude: HTML์˜ Node.js Stream์ž…๋‹ˆ๋‹ค. ์ด ์ŠคํŠธ๋ฆผ์„ ์‚ฌ์šฉํ•ด ์‘๋‹ต์„ ์ฒญํฌ ๋‹จ์œ„๋กœ ์ „์†กํ•˜๊ฑฐ๋‚˜, ์ „์ฒด ์ŠคํŠธ๋ฆผ์„ ๋ฌธ์ž์—ด๋กœ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ Œ๋”๋ง์— ์‹คํŒจํ•˜๋ฉด, Promise๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์‚ฌ์šฉํ•ด ํด๋ฐฑ ์…ธFallback Shell์„ ์ถœ๋ ฅํ•˜์„ธ์š”.

์ฃผ์˜ ์‚ฌํ•ญ

ํ”„๋ฆฌ๋ Œ๋”๋ง ์‹œ nonce ์˜ต์…˜์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Nonce๋Š” ์š”์ฒญ๋งˆ๋‹ค ๊ณ ์œ ํ•ด์•ผ ํ•˜๋ฉฐ, CSP๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ณดํ˜ธํ•  ๋•Œ Nonce ๊ฐ’์„ ํ”„๋ฆฌ๋ Œ๋”๋ง ๊ฒฐ๊ณผ์— ํฌํ•จํ•˜๋Š” ๊ฒƒ์€ ๋ถ€์ ์ ˆํ•˜๊ณ  ์•ˆ์ „ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

prerenderToNodeStream์€ ์–ธ์ œ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋‚˜์š”?

์ •์  prerenderToNodeStream API๋Š” ์ •์  ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ์ƒ์„ฑ(SSG)์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. renderToString๊ณผ ๋‹ฌ๋ฆฌ, prerenderToNodeStream์€ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ํ›„์— ์„ฑ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” Suspense๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ€์ ธ์™€์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ด, ์ „์ฒด ํŽ˜์ด์ง€์˜ ์ •์  HTML์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋˜๋Š” ๋™์•ˆ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋ ค๋ฉด, renderToReadableStream๊ณผ ๊ฐ™์€ ์ŠคํŠธ๋ฆฌ๋ฐ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR) API๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.


์‚ฌ์šฉ๋ฒ•

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์— ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์—ฐ๊ฒฐํ•ด ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

์ž์„ธํžˆ ์‚ดํŽด๋ณด๊ธฐ

๋นŒ๋“œ ์ถœ๋ ฅ์—์„œ CSS ๋ฐ JS ์—์…‹ ๊ฒฝ๋กœ ์ฝ๊ธฐ

์ตœ์ข… ์—์…‹ 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๋กœ ๋ณ€ํ™˜์„ ์™„๋ฃŒํ•ฉ๋‹ˆ๋‹ค.

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

Suspense๋ฅผ ์ง€์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค๋งŒ์ด Suspense ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

  • Relay ํ˜น์€ Next.js ์ฒ˜๋Ÿผ Suspense๋ฅผ ์ง€์›ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ.
  • lazy๋ฅผ ์‚ฌ์šฉํ•œ ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ์˜ ์ง€์—ฐ๋กœ๋”ฉ.
  • use๋ฅผ ์‚ฌ์šฉํ•ด Promise์˜ ๊ฐ’์„ ์ฝ๊ธฐ.

Suspense๋Š” Effect๋‚˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋‚ด๋ถ€์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ํŒจ์นญ๋  ๋•Œ ์ด๋ฅผ ๊ฐ์ง€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์œ„ ์˜ˆ์‹œ์˜ Posts ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ๊ตฌ์ฒด์ ์ธ ๋ฐฉ๋ฒ•์€ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. Suspense๋ฅผ ์ง€์›ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, ํ•ด๋‹น ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ฌธ์„œ์—์„œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠน์ • ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” Suspense ์ง€์› ๋ฐ์ดํ„ฐ ํŒจ์นญ์€ ์•„์ง ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Suspense๋ฅผ ์ง€์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ์š”๊ตฌ ์‚ฌํ•ญ์€ ํ˜„์žฌ ๋ถˆ์•ˆ์ •ํ•˜๊ณ  ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ Suspense์™€ ํ†ตํ•ฉํ•˜๊ธฐ ์œ„ํ•œ ๊ณต์‹ API๋Š” React์˜ ํ–ฅํ›„ ๋ฒ„์ „์—์„œ ์ œ๊ณต๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.


์‚ฌ์ „ ๋ Œ๋”๋ง ์ค‘๋‹จํ•˜๊ธฐ

ํƒ€์ž„์•„์›ƒ ์ดํ›„ ์‚ฌ์ „ ๋ Œ๋”๋ง์„ โ€œํฌ๊ธฐโ€ํ•˜๋„๋ก ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

try {
// Prelude์—๋Š” ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ค‘๋‹จํ•˜๊ธฐ ์ „์—
// ์‚ฌ์ „๋ Œ๋”๋ง๋œ ๋ชจ๋“  HTML์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
const {prelude} = await prerenderToNodeStream(<App />, {
signal: controller.signal,
});
//...

๋ถˆ์™„์ „ํ•œ ์ž์‹์„ ๊ฐ€์ง„ ๋ชจ๋“  Suspense ๊ฒฝ๊ณ„๋Š” ํ’€๋ฐฑ ์ƒํƒœ๋กœ Prelude์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.


๋ฌธ์ œ ํ•ด๊ฒฐ

์ „์ฒด ์•ฑ์ด ๋ Œ๋”๋ง๋  ๋•Œ๊นŒ์ง€ ์ŠคํŠธ๋ฆผ์ด ์‹œ์ž‘๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

prerenderToNodeStream ์‘๋‹ต์€ ๋ชจ๋“  Suspense ๊ฒฝ๊ณ„๊ฐ€ ํ•ด๊ฒฐ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ฒƒ์„ ํฌํ•จํ•˜์—ฌ ์ „์ฒด ์•ฑ์ด ๋ Œ๋”๋ง์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ํ›„ ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค. ์ด API๋Š” ์ •์  ์‚ฌ์ดํŠธ ์ƒ์„ฑ(SSG)์„ ์œ„ํ•ด ์„ค๊ณ„๋˜์—ˆ์œผ๋ฉฐ ์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋˜๋ฉด์„œ ๋” ๋งŽ์€ ์ฝ˜ํ…์ธ ๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋Š” ๊ฒƒ์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ฝ˜ํ…์ธ ๊ฐ€ ๋กœ๋“œ๋˜๋ฉด์„œ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋ ค๋ฉด renderToPipeableStream๊ณผ ๊ฐ™์€ ์ŠคํŠธ๋ฆฌ๋ฐ ์„œ๋ฒ„ ๋ Œ๋”๋ง API๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.