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

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

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

์ด API๋Š” Web Stream์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค. Node.js์—์„œ๋Š” prerenderToNodeStream์„ ๋Œ€์‹  ์‚ฌ์šฉํ•˜์„ธ์š”.


๋ ˆํผ๋Ÿฐ์Šค

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๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’

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

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

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

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

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

prerender๋ฅผ ์–ธ์ œ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋‚˜์š”?

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


์‚ฌ์šฉ๋ฒ•

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์— ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด ์ƒํ˜ธ์ž‘์šฉํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

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

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

์ตœ์ข… ์—์…‹ 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๋กœ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

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

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 prerender(<App />, {
signal: controller.signal,
});
//...

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


๋ฌธ์ œ ํ•ด๊ฒฐ

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

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

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