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