prerenderToNodeStream
prerenderToNodeStream
์ Node.js Stream์ ์ฌ์ฉํ์ฌ React ํธ๋ฆฌ๋ฅผ ์ ์ HTML ๋ฌธ์์ด๋ก ๋ ๋๋งํฉ๋๋ค.
const {prelude} = await prerenderToNodeStream(reactNode, options?)
๋ ํผ๋ฐ์ค
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
: ํ๋ฆฌ๋ ๋๋ง์ ์ค๋จํ๊ณ ๋๋จธ์ง๋ฅผ ํด๋ผ์ด์ธํธ์์ ๋ ๋๋งํ ์ ์๊ฒ ํ๋ ์ค๋จ ์ ํธ์ ๋๋ค.
- optional
๋ฐํ๊ฐ
prerenderToNodeStream
๋ Promise๋ฅผ ๋ฐํํฉ๋๋ค.
- ๋ ๋๋ง์ด ์ฑ๊ณตํ๋ฉด Promise๋ ๋ค์์ ํฌํจํ๋ ๊ฐ์ฒด๋ก ํด๊ฒฐ๋ฉ๋๋ค.
prelude
: HTML์ Node.js Stream์ ๋๋ค. ์ด ์คํธ๋ฆผ์ ์ฌ์ฉํด ์๋ต์ ์ฒญํฌ ๋จ์๋ก ์ ์กํ๊ฑฐ๋, ์ ์ฒด ์คํธ๋ฆผ์ ๋ฌธ์์ด๋ก ์ฝ์ ์ ์์ต๋๋ค.
- ๋ ๋๋ง์ ์คํจํ๋ฉด, Promise๊ฐ ์คํจํฉ๋๋ค. ์ด๋ฅผ ์ฌ์ฉํด ํด๋ฐฑ ์ ธFallback Shell์ ์ถ๋ ฅํ์ธ์.
์ฃผ์ ์ฌํญ
ํ๋ฆฌ๋ ๋๋ง ์ nonce
์ต์
์ ์ฌ์ฉํ ์ ์์ต๋๋ค. Nonce๋ ์์ฒญ๋ง๋ค ๊ณ ์ ํด์ผ ํ๋ฉฐ, CSP๋ก ์ ํ๋ฆฌ์ผ์ด์
์ ๋ณดํธํ ๋ Nonce ๊ฐ์ ํ๋ฆฌ๋ ๋๋ง ๊ฒฐ๊ณผ์ ํฌํจํ๋ ๊ฒ์ ๋ถ์ ์ ํ๊ณ ์์ ํ์ง ์์ต๋๋ค.
์ฌ์ฉ๋ฒ
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์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ฐ๊ฒฐํด ์ธํฐ๋ํฐ๋ธํ๊ฒ ๋ง๋ญ๋๋ค.
์์ธํ ์ดํด๋ณด๊ธฐ
์ต์ข
์์
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๋ก ๋ณํ์ ์๋ฃํฉ๋๋ค.
์ฌ์ ๋ ๋๋ง ์ค๋จํ๊ธฐ
ํ์์์ ์ดํ ์ฌ์ ๋ ๋๋ง์ โํฌ๊ธฐโํ๋๋ก ๊ฐ์ ํ ์ ์์ต๋๋ค.
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๋ฅผ ์ฌ์ฉํ์ธ์.