renderToPipeableStream
renderToPipeableStream
์ React ํธ๋ฆฌ๋ฅผ ํ์ดํ ๊ฐ๋ฅํ Node.js ์คํธ๋ฆผ์ผ๋ก ๋ ๋๋งํฉ๋๋ค.
const { pipe, abort } = renderToPipeableStream(reactNode, options?)
- ๋ ํผ๋ฐ์ค
- ์ฌ์ฉ๋ฒ
- React ํธ๋ฆฌ๋ฅผ HTML๋ก Node.js ์คํธ๋ฆผ์ ๋ ๋๋งํ๊ธฐ
- ์ฝํ ์ธ ๊ฐ ๋ก๋๋๋ ๋์ ๋ ๋ง์ ์ฝํ ์ธ ์คํธ๋ฆฌ๋ฐํ๊ธฐ
- ์ ธ์ ๋ค์ด๊ฐ ๋ด์ฉ ์ง์ ํ๊ธฐ
- ์๋ฒ์์ ํฌ๋์ ๋ก๊น ํ๊ธฐ
- ์ ธ ๋ด๋ถ์ ์ค๋ฅ๋ก๋ถํฐ ๋ณต๊ตฌํ๊ธฐ
- ์ ธ ์ธ๋ถ์ ์ค๋ฅ๋ก๋ถํฐ ๋ณต๊ตฌํ๊ธฐ
- ์ํ ์ฝ๋ ์ค์ ํ๊ธฐ
- ๋ค์ํ ์ค๋ฅ๋ฅผ ์๋ก ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ
- ํฌ๋กค๋ฌ ๋ฐ ์ ์ ์์ฑ์ ์ํด ๋ชจ๋ ์ฝํ ์ธ ๊ฐ ๋ก๋๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๊ธฐ
- ์๋ฒ ๋ ๋๋ง ์ค๋จํ๊ธฐ
๋ ํผ๋ฐ์ค
renderToPipeableStream(reactNode, options?)
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์ ์ํธ์์ฉํ ์ ์๋๋ก ๋ง๋ญ๋๋ค.
์๋์์ ๋ ๋ง์ ์์๋ฅผ ํ์ธํ์ธ์.
๋งค๊ฐ๋ณ์
-
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
: ์ฒญํฌ์ ๋ฐ์ดํธ ์์ ๋๋ค. ๊ธฐ๋ณธ ํด๋ฆฌ์คํฑ์ ๋ํด ์์ธํ ์์๋ณด์ธ์.
- ์ ํ ์ฌํญ
๋ฐํ๊ฐ
renderToPipeableStream
์ ๋ ๊ฐ์ ๋ฉ์๋๊ฐ ์๋ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
pipe
๋ HTML์ ์ ๊ณต๋ ์ฐ๊ธฐ ๊ฐ๋ฅํ Node.js ์คํธ๋ฆผ์ผ๋ก ์ถ๋ ฅํฉ๋๋ค. ์คํธ๋ฆฌ๋ฐ์ ํ์ฑํํ๋ ค๋ฉดonShellReady
์์, ํฌ๋กค๋ฌ์ ์ ์ ์์ฑ์ ์ฌ์ฉํ๋ ค๋ฉดonAllReady
์์pipe
๋ฅผ ํธ์ถํ์ธ์.abort
๋ฅผ ์ฌ์ฉํ๋ฉด ์๋ฒ ๋ ๋๋ง์ ์ค๋จํ๊ณ ๋๋จธ์ง๋ ํด๋ผ์ด์ธํธ์์ ๋ ๋๋งํ ์ ์์ต๋๋ค.
์ฌ์ฉ๋ฒ
React ํธ๋ฆฌ๋ฅผ HTML๋ก Node.js ์คํธ๋ฆผ์ ๋ ๋๋งํ๊ธฐ
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์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ์ฒจ๋ถ๋์ด ์ํธ์์ฉ์ด ๊ฐ๋ฅํด์ง๋๋ค.
Deep Dive
์ต์ข
์์
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
์ ๋ ๋๋งํ๋ฏ๋ก ํ์ด๋๋ ์ด์
์ค๋ฅ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
์ฝํ ์ธ ๊ฐ ๋ก๋๋๋ ๋์ ๋ ๋ง์ ์ฝํ ์ธ ์คํธ๋ฆฌ๋ฐํ๊ธฐ
์คํธ๋ฆฌ๋ฐ์ ์ฌ์ฉํ๋ฉด ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์๋ฒ์ ๋ก๋๋๊ธฐ ์ ์๋ ์ฌ์ฉ์๊ฐ ์ฝํ ์ธ ๋ฅผ ๋ณผ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด ํ์ง์ ์น๊ตฌ ๋ฐ ์ฌ์ง์ด ์๋ ์ฌ์ด๋๋ฐ, ๊ธ ๋ชฉ๋ก์ด ํ์๋๋ ํ๋กํ ํ์ด์ง๋ฅผ ์๊ฐํด ๋ณด์ธ์.
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>
๊ฒฝ๊ณ ๋ฐ์ ์๋ ๋ถ๋ถ์ ์
ธ์ด๋ผ๊ณ ํฉ๋๋ค.
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>
๊ฒฝ๊ณ์ ์๋ ์ปดํฌ๋ํธ๋ ์ฌ์ ํ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๊ณ ์์ ์ ์์ต๋๋ค.
์๋ฒ์์ ํฌ๋์ ๋ก๊น ํ๊ธฐ
๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฒ์ ๋ชจ๋ ์ค๋ฅ๋ ์ฝ์์ ๊ธฐ๋ก๋ฉ๋๋ค. ์ด ๋์์ ์ฌ์ ์ํ์ฌ ํฌ๋์ ๋ณด๊ณ ์๋ฅผ ๊ธฐ๋กํ ์ ์์ต๋๋ค.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
์ฌ์ฉ์ ์ ์ onError
๊ตฌํ์ ์ ๊ณตํ๋ ๊ฒฝ์ฐ ์์ ๊ฐ์ด ์ฝ์์ ์ค๋ฅ๋ฅผ ๊ธฐ๋กํ๋ ๊ฒ๋ ์์ง ๋ง์ธ์.
์ ธ ๋ด๋ถ์ ์ค๋ฅ๋ก๋ถํฐ ๋ณต๊ตฌํ๊ธฐ
์ด ์์ ์์๋ ์
ธ์ 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์ด ์ค๋ฅ ํ์ด์ง์ผ ํ์๋ ์์ต๋๋ค. ๋์ ํด๋ผ์ด์ธํธ์์๋ง ์ฑ์ ๋ ๋๋งํ๋ ๋์ฒด ์
ธ์ ํฌํจํ ์ ์์ต๋๋ค.
์ ธ ์ธ๋ถ์ ์ค๋ฅ๋ก๋ถํฐ ๋ณต๊ตฌํ๊ธฐ
์ด ์์ ์์๋ <Posts />
์ปดํฌ๋ํธ๊ฐ <Suspense>
๋ก ๋ํ๋์ด ์์ผ๋ฏ๋ก ์
ธ์ ์ผ๋ถ๊ฐ ์๋๋๋ค.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Posts
์ปดํฌ๋ํธ ๋๋ ๊ทธ ๋ด๋ถ ์ด๋๊ฐ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด React๋ ์ด๋ฅผ ๋ณต๊ตฌํ๋ ค๊ณ ์๋ํฉ๋๋ค.
- ๊ฐ์ฅ ๊ฐ๊น์ด
<Suspense>
๊ฒฝ๊ณ(PostsGlimmer
)์ ๋ํ ๋ก๋ฉ ํด๋ฐฑ์ HTML๋ก ๋ฐฉ์ถํฉ๋๋ค. - ๋ ์ด์ ์๋ฒ์์
Posts
์ฝํ ์ธ ๋ฅผ ๋ ๋๋งํ๋ ๊ฒ์ โํฌ๊ธฐโํฉ๋๋ค. - ์๋ฐ์คํฌ๋ฆฝํธ ์ฝ๋๊ฐ ํด๋ผ์ด์ธํธ์์ ๋ก๋๋๋ฉด React๋ ํด๋ผ์ด์ธํธ์์
Posts
๋ ๋๋ง์ ์ฌ์๋ํฉ๋๋ค.
ํด๋ผ์ด์ธํธ์์ Posts
๋ ๋๋ง์ ๋ค์ ์๋ํด๋ ์คํจํ๋ฉด React๋ ํด๋ผ์ด์ธํธ์์ ์๋ฌ๋ฅผ ๋์ง๋๋ค. ๋ ๋๋ง ์ค์ ๋ฐ์ํ๋ ๋ชจ๋ ์๋ฌ์ ๋ง์ฐฌ๊ฐ์ง๋ก, ๊ฐ์ฅ ๊ฐ๊น์ด ๋ถ๋ชจ ์๋ฌ ๊ฒฝ๊ณ์ ๋ฐ๋ผ ์ฌ์ฉ์์๊ฒ ์๋ฌ๋ฅผ ํ์ํ๋ ๋ฐฉ๋ฒ์ด ๊ฒฐ์ ๋ฉ๋๋ค. ์ค์ ๋ก๋ ์ค๋ฅ๋ฅผ ๋ณต๊ตฌํ ์ ์๋ค๋ ๊ฒ์ด ํ์คํด์ง ๋๊น์ง ์ฌ์ฉ์์๊ฒ ๋ก๋ฉ ํ์๊ธฐ๊ฐ ํ์๋๋ค๋ ์๋ฏธ์
๋๋ค.
ํด๋ผ์ด์ธํธ์์ Posts
๋ ๋๋ง์ ๋ค์ ์๋ํ์ฌ ์ฑ๊ณตํ๋ฉด ์๋ฒ์ ๋ก๋ฉ ํด๋ฐฑ์ด ํด๋ผ์ด์ธํธ ๋ ๋๋ง ์ถ๋ ฅ์ผ๋ก ๋์ฒด๋ฉ๋๋ค. ์ฌ์ฉ์๋ ์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค๋ ์ฌ์ค์ ์ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์๋ฒ onError
์ฝ๋ฐฑ ๋ฐ ํด๋ผ์ด์ธํธ onRecoverableError
์ฝ๋ฐฑ์ด ์คํ๋์ด ์ค๋ฅ์ ๋ํ ์๋ฆผ์ ๋ฐ์ ์ ์์ต๋๋ค.
์ํ ์ฝ๋ ์ค์ ํ๊ธฐ
์คํธ๋ฆฌ๋ฐ์๋ ์ฅ๋จ์ ์ด ์์ต๋๋ค. ์ฌ์ฉ์๊ฐ ์ฝํ ์ธ ๋ฅผ ๋ ๋นจ๋ฆฌ ๋ณผ ์ ์๋๋ก ๊ฐ๋ฅํ ํ ๋นจ๋ฆฌ ํ์ด์ง ์คํธ๋ฆฌ๋ฐ์ ์์ํ๊ณ ์ถ์ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์คํธ๋ฆฌ๋ฐ์ ์์ํ๋ฉด ๋ ์ด์ ์๋ต ์ํ ์ฝ๋๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
์ฑ์ ์
ธ(ํนํ <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);
}
});
์ด๋ ์ด๊ธฐ ์ ธ ์ฝํ ์ธ ๋ฅผ ์์ฑํ๋ ๋์ ๋ฐ์ํ ์ ธ ์ธ๋ถ์ ์ค๋ฅ๋ง ํฌ์ฐฉํ๋ฏ๋ก ์์ ํ ๊ฒ์ ์๋๋๋ค. ์ผ๋ถ ์ฝํ ์ธ ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋์ง ์ฌ๋ถ๋ฅผ ํ์ ํ๋ ๊ฒ์ด ์ค์ํ ๊ฒฝ์ฐ ํด๋น ์ฝํ ์ธ ๋ฅผ ์ ธ๋ก ์ด๋ํ๋ฉด ๋ฉ๋๋ค.
๋ค์ํ ์ค๋ฅ๋ฅผ ์๋ก ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ
์์ ๋ง์ 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);
}
});
์ ธ์ ๋ด๋ณด๋ด๊ณ ์คํธ๋ฆฌ๋ฐ์ ์์ํ๋ฉด ์ํ ์ฝ๋๋ฅผ ๋ณ๊ฒฝํ ์ ์๋ค๋ ์ ์ ์ ์ํ์ธ์.
ํฌ๋กค๋ฌ ๋ฐ ์ ์ ์์ฑ์ ์ํด ๋ชจ๋ ์ฝํ ์ธ ๊ฐ ๋ก๋๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๊ธฐ
์คํธ๋ฆฌ๋ฐ์ ์ฝํ ์ธ ๊ฐ ์ ๊ณต๋ ๋ ๋ฐ๋ก ๋ณผ ์ ์๊ธฐ ๋๋ฌธ์ ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
๊ทธ๋ฌ๋ ํฌ๋กค๋ฌ๊ฐ ํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํ๊ฑฐ๋ ๋น๋ ์์ ์ ํ์ด์ง๋ฅผ ์์ฑํ๋ ๊ฒฝ์ฐ ๋ชจ๋ ์ฝํ ์ธ ๋ฅผ ์ ์ง์ ์ผ๋ก ํ์ํ๋ ๋์ ๋ชจ๋ ์ฝํ ์ธ ๋ฅผ ๋จผ์ ๋ก๋ํ ๋ค์ ์ต์ข 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 ์ถ๋ ฅ์ ๋ฐ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ ํฌ๋กค๋ฌ๊ฐ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ค๋ ค์ผ ํ๋ค๋ ๊ฒ์ ์๋ฏธํ๋ฉฐ, ๊ทธ์ค ์ผ๋ถ๋ ๋ก๋ ์๋๊ฐ ๋๋ฆฌ๊ฑฐ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ฑ์ ๋ฐ๋ผ ํฌ๋กค๋ฌ์๋ ์ ธ์ ๋ณด๋ด๋๋ก ์ ํํ ์ ์์ต๋๋ค.
์๋ฒ ๋ ๋๋ง ์ค๋จํ๊ธฐ
์๊ฐ ์ด๊ณผ ํ ์๋ฒ ๋ ๋๋ง์ ๊ฐ์ ๋ก โํฌ๊ธฐโํ ์ ์์ต๋๋ค.
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);
React๋ ๋๋จธ์ง ๋ก๋ฉ ํด๋ฐฑ์ HTML๋ก ํ๋ฌ์ํ๊ณ ๋๋จธ์ง๋ ํด๋ผ์ด์ธํธ์์ ๋ ๋๋ง์ ์๋ํฉ๋๋ค.