๋ค์ด๊ฐ๋ฉฐ
์ต๊ทผ Allini ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋ช ๊ฐ์ง ๊ณ ๋ฏผ์ด ์๊ฒผ์ต๋๋ค.
over-fetching, waterfall ํ์์ผ๋ก ์ธํ ์ฑ๋ฅ ์ ํ, ๊ทธ๋ฆฌ๊ณ ๋ฌด์๋ณด๋ค ๋น ๋ฅธ ์์ฅ ๊ฒ์ฆ์ ์ํด ๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด์ผ ํ๋ ์ํฉ์์ ๋ฐ์ํ๋ ๋ฒ๋ค ์ฌ์ด์ฆ ์ฆ๊ฐ ๋ฌธ์ ์์ฃ .
์ข์ ๊ฐ๋ฐ์๋ ๋จ์ํ ๋ชจ๋ ๊ฒ์ ์์ ์ฒ์๋ถํฐ ๊ตฌํํ๋ ๊ฒ์ด ์๋๋ผ, ์ ์ ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ํํ๊ณ ์กฐ์กํด ๋น ๋ฅด๊ฒ ์ ํ์ ๋ง๋ค์ด๋ด๋ ๋ฅ๋ ฅ์ด ์ค์ํ๋ค๊ณ ์๊ฐํฉ๋๋ค.
ํ์ง๋ง ์ด๋ฌํ ์ ๊ทผ์ ํ์ฐ์ ์ผ๋ก ๋ฒ๋ค ์ฌ์ด์ฆ ์ฆ๊ฐ๋ผ๋ ๋ฌธ์ ๋ฅผ ๋๋ฐํ๊ฒ ๋์๊ณ , ์ด๋ ์ ๊ฐ React ์๋ฒ ์ปดํฌ๋ํธ์ ๊ด์ฌ์ ๊ฐ๊ฒ ๋ ์ฃผ๋ ๊ณ๊ธฐ๊ฐ ๋์์ต๋๋ค!
1. React ์๋ฒ ์ปดํฌ๋ํธ์ ํ์ฌ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ
ํ์ฌ ์ํ
- React 18 ๋ฒ์ : ์๋ฒ ์ปดํฌ๋ํธ ๋ฏธ์ง์
- React 19 RC(Release Candidate): ์๋ฒ ์ปดํฌ๋ํธ ์ง์
ํ์ง๋ง ํ์ฌ React 18.2.0์ ์ฌ์ฉ์ค์ธ ํ๋ก์ ํธ์์ ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ์ง์ ์ฌ์ฉํ๊ธฐ๋ ์ด๋ ต์ต๋๋ค.
React 19๋ก์ ์ ๊ทธ๋ ์ด๋๋ฅผ ๊ณ ๋ คํด๋ณผ ์ ์์ง๋ง, RC ๋ฒ์ ์ด๊ธฐ ๋๋ฌธ์ ํ๋ก๋์ ํ๊ฒฝ์์์ ์ฌ์ฉ์ ์ ์คํ ๊ฒฐ์ ํด์ผ ํฉ๋๋ค.
2. SSR๊ณผ ์๋ฒ ์ปดํฌ๋ํธ์ ๊ด๊ณ
ํ์ต ์ด๊ธฐ์ ๊ฐ์ฅ ํท๊ฐ๋ ธ๋ ๊ฐ๋ ์ ๋๋ค!
SSR (Server-Side Rendering)
- ๋ชฉ์ : ์ด๊ธฐ ํ์ด์ง ๋ก๋ ์ฑ๋ฅ ๊ฐ์ ๋ฐ SEO ์ต์ ํ
- ๋์ ๋ฐฉ์:
- ์๋ฒ์์ ์ ์ฒด ํ์ด์ง์ ์ด๊ธฐ HTML์ ์์ฑ
- ํด๋ผ์ด์ธํธ๋ก HTML๊ณผ JS ๋ฒ๋ค์ ์ ์ก
- ํด๋ผ์ด์ธํธ์์ hydration ๊ณผ์ ์ ํตํด ์ธํฐ๋ํฐ๋ธํ ์ฑ์ผ๋ก ์ ํ
์ด์ React ๊ณต์ ๋ฌธ์์ ๋ฐ๋ฅด๋ฉด, React์ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง ๋ฉ์๋๋ค์ ํฌ๊ฒ ์ธ ๊ฐ์ง ํ๊ฒฝ์ผ๋ก ๊ตฌ๋ถ๋ฉ๋๋ค:
Node.js Streams ํ๊ฒฝ์ฉ:
- renderToPipeableStream()
- renderToNodeStream() (Deprecated)
- renderToStaticNodeStream()
Web Streams ํ๊ฒฝ์ฉ (๋ธ๋ผ์ฐ์ , Deno, modern edge runtimes):
- renderToReadableStream()
์คํธ๋ฆผ์ ์ง์ํ์ง ์๋ ํ๊ฒฝ์ฉ:
- renderToString()
- renderToStaticMarkup()
๊ธฐ์กด SSR์ ๋์ ๋ฐฉ์์ ์๋์ ๊ฐ์ต๋๋ค.
// ๊ธฐ์กด SSR์ ๋์ ๋ฐฉ์
// 1. ์๋ฒ์์ ์ด๊ธฐ ๋ ๋๋ง
const app = ReactDOMServer.renderToPipeableStream(<App />);
// ํ๋ฒ์ ์ ์ฒด HTML์ ์์ฑํ์ฌ ํด๋ผ์ด์ธํธ๋ก ์ ์ก
// 2. HTML ๋ฌธ์์ ์ฝ์
const html = `
<!doctype html>
<html>
<body>
<div id="root">${app}</div>
<script src="/bundle.js"></script>
</body>
</html>
`;
๊ณต์ ๋ฌธ์์ ๋ฐ๋ฅด๋ฉด, renderToPipeableStream์ ํตํด ์ด๊ธฐ ์ ธ(shell)์ ๋ ๋๋งํ๊ณ ์ ์กํฉ๋๋ค.
renderToPipeableStream์ ๊ธฐ๋ณธ ๊ฐ๋ ์ ์๋์ ๊ฐ์ต๋๋ค.
- Node.js ํ๊ฒฝ์์ React ํธ๋ฆฌ๋ฅผ HTML ์คํธ๋ฆผ์ผ๋ก ๋ณํํ๋ API์ ๋๋ค.
- ์ ์ง์ ์ธ ๋ก๋ฉ์ด ๊ฐ๋ฅํ ์คํธ๋ฆฌ๋ฐ SSR์ ๊ตฌํํ ์ ์์ต๋๋ค.
์์ ์ฝ๋
// ๋ฐ์ ๋ SSR (์คํธ๋ฆฌ๋ฐ ๋ฐฉ์)
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
// 1. ๋จผ์ ๊ธฐ๋ณธ ๋ผ๋(shell)๋ฅผ ๋ณด๋
// ์ด๊ธฐ shell(Suspense ๊ฒฝ๊ณ ์์ ์ฝํ
์ธ )์ด ์ค๋น๋๋ฉด ์คํธ๋ฆฌ๋ฐ ์์
pipe(response);
},
onAllReady() {
// ๋ชจ๋ ์ฝํ
์ธ ๊ฐ ์ค๋น๋๋ฉด ํธ์ถ (ํฌ๋กค๋ฌ๋ ์ ์ ์์ฑ์ฉ)
}
});
๊ธฐ์กด์ renderToString์ ์ ํ์ ์ธ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๋ฐ๋ฉด, renderToPipeableStream์ Suspense๋ฅผ ์์ ํ ์ง์ํ๊ณ HTML ์คํธ๋ฆฌ๋ฐ์ด ๊ฐ๋ฅํ ๋ ๋ฐ์ ๋ ํํ์ SSR ๋ฐฉ์์ ๋๋ค.
์ฃผ์ ์ฐจ์ด์ :
- ๊ธฐ์กด SSR: ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ค ๋ชจ์์ ํ๋ฒ์ ๋ณด๋ (๋๋ฆผ)
- ์๋ก์ด SSR:
- ๋จผ์ ํ์ด์ง์ ๊ธฐ๋ณธ ๋ผ๋๋ฅผ ๋ณด๋ด๊ณ
- ๋๋จธ์ง ๋ด์ฉ์ ์ค๋น๋๋ ๋๋ก ์กฐ๊ธ์ฉ ๋ณด๋ (๋ ๋น ๋ฆ)
์ฝ๊ฒ ๋งํ๋ฉด, ๊ธฐ์กด SSR์ "์๋น์์ ๋ชจ๋ ์๋ฆฌ๊ฐ ์์ฑ๋ ๋๊น์ง ๊ธฐ๋ค๋ ธ๋ค๊ฐ ํ๋ฒ์ ์๋น"ํ๋ ๊ฒ์ด๊ณ , ์๋ก์ด ๋ฐฉ์์ "์ค๋น๋ ๋ฉ๋ด๋ถํฐ ๋จผ์ ์๋นํ๊ณ , ๋๋จธ์ง๋ ์์ฑ๋๋ ๋๋ก ๊ฐ์ ธ๋ค์ฃผ๋" ๊ฒ๊ณผ ๋น์ทํฉ๋๋ค.
React ์๋ฒ ์ปดํฌ๋ํธ (RSC)
- ๋ชฉ์ : ๋ฒ๋ค ์ฌ์ด์ฆ ์ต์ ํ ๋ฐ ์๋ฒ ๋ฆฌ์์ค ์ง์ ์ ๊ทผ
- ๋์ ๋ฐฉ์:
- ํน์ ์ปดํฌ๋ํธ๋ฅผ ์๋ฒ์์๋ง ์คํ๋๋๋ก ์ง์
- ์๋ฒ์์ ๋ ๋๋ง๋ ๊ฒฐ๊ณผ๋ง ํด๋ผ์ด์ธํธ๋ก ์ ์ก
- JS ๋ฒ๋ค์ ์๋ฒ ์ปดํฌ๋ํธ ์ฝ๋๋ ๋ฏธํฌํจ
๋ ๊ธฐ์ ์ ์ํธ๋ณด์์ ๊ด๊ณ
SSR๊ณผ ์๋ฒ ์ปดํฌ๋ํธ๋ ์๋ก๋ฅผ ๋์ฒดํ๋ ๊ธฐ์ ์ด ์๋, ๋ณด์ํ๋ ๊ธฐ์ ์ ๋๋ค:
- ์ญํ ๋ถ๋ด
- SSR: ์ด๊ธฐ ํ์ด์ง ๋ก๋ ์ ์ ์ฒด HTML ์์ฑ
- ์๋ฒ ์ปดํฌ๋ํธ: ํน์ ์ปดํฌ๋ํธ๋ค์ ์๋ฒ ์คํ ๋ฐ ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ
3. ํ๋ ์์ํฌ๋ค์ ์๋ฒ ์ปดํฌ๋ํธ ๊ตฌํ
Next.js์ ์ ๊ทผ
Next.js๋ ์์ฒด์ ์ธ ์๋ฒ ์ปดํฌ๋ํธ ๊ตฌํ์ ํตํด React์ ์คํ์ ๊ธฐ๋ฅ์ ์์ ์ ์ผ๋ก ์ ๊ณตํฉ๋๋ค.
- React ์ฝ์ด ํ๊ณผ Next.js ํ์ ํ๋ ฅ
- React ์ฝ์ด ํ์ ์ฃผ์ ๋ฉค๋ฒ๋ค(Joe Savona, Sebastian Markbåge ๋ฑ)์ด Vercel(Next.js)์ ํฉ๋ฅ
- ์ด๋ฅผ ํตํด React์ ์คํ์ ๊ธฐ๋ฅ์ Next.js์์ ๋จผ์ ์์ ์ ์ผ๋ก ๊ตฌํ ๊ฐ๋ฅ
- ์๋ฒ ์ปดํฌ๋ํธ์ ๋ฐ์ ๊ณผ์
- 2020๋ : React ํ์ด ์๋ฒ ์ปดํฌ๋ํธ ๊ฐ๋ ์ฒซ ๋ฐํ
- React 18: ์๋ฒ ์ปดํฌ๋ํธ ๊ธฐ์ด ์ธํ๋ผ ํฌํจ, but ์์ ํ ๊ตฌํ์ ์๋
- Next.js 13: App Router์ ํจ๊ป ์๋ฒ ์ปดํฌ๋ํธ ์ ๋ฉด ๋์
๊ธฐ์ ์ ๊ตฌํ ๋ฐฉ์
React์ Server Components์ ๋ํ ๊ณต์๋ฌธ์๋ฅผ ๋ณด๋ฉด,
๋ฌธ์์์๋ Server Components ๊ตฌํ์ ๋ํด ๋ค์๊ณผ ๊ฐ์ด ์ธ๊ธํ๊ณ ์์ต๋๋ค:
"To support React Server Components as a bundler or framework, we recommend pinning to a specific React version, or using the Canary release. We will continue working with bundlers and frameworks to stabilize the APIs used to implement React Server Components in the future."
์ด๋ Server Components ๊ตฌํ์ ์ํ ๋ฒ๋ค๋ฌ API๊ฐ ์์ง ์์ ํ ์์ ํ๋์ง ์์์์ ์์ฌํฉ๋๋ค.
Next.js๋ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ์ต๋๋ค:
// next.config.js
{
webpack: (config, { isServer }) => {
if (isServer) {
// ์๋ฒ ์ปดํฌ๋ํธ ์ฒ๋ฆฌ
config.plugins.push(new webpack.DefinePlugin({
// marked, sanitize-html ๊ฐ์ ์๋ฒ ์ ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด
// ํด๋ผ์ด์ธํธ ๋ฒ๋ค์ ํฌํจ๋์ง ์๋๋ก ์ฒ๋ฆฌ
'process.env.SERVER_ONLY': JSON.stringify(true)
}));
config.module.rules.push({
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
plugins: [
// async ์ปดํฌ๋ํธ ์ง์์ ์ํ ๋ณํ
'@babel/plugin-syntax-top-level-await',
// Server Component์ import ๊ตฌ๋ฌธ ์ฒ๋ฆฌ
['@babel/plugin-transform-modules-commonjs', {
importInterop: 'node'
}]
]
}
}
});
} else {
// ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ์ฒ๋ฆฌ
config.module.rules.push({
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
plugins: [
// "use client" ์ง์์ด๊ฐ ์๋ ํ์ผ๋ง ํด๋ผ์ด์ธํธ ๋ฒ๋ค์ ํฌํจ
['./plugins/client-directive', {}],
// Server Component์ ์ถ๋ ฅ์ ํด๋ผ์ด์ธํธ์์ hydrateํ๊ธฐ ์ํ ๋ณํ
['./plugins/server-reference', {}]
]
}
}
});
}
return config;
}
}
// Server Component ๋ ๋๋ง ํจ์
async function renderServerComponent(Component, props) {
// ๋ฌธ์์ ์ธ๊ธ๋ ๊ฒ์ฒ๋ผ build time ๋๋ request time์ ์คํ ๊ฐ๋ฅ
const stream = await ReactServerDOM.renderToReadableStream(
<Component {...props} />,
// ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ ๋ฒ๋ค ์ ๋ณด๋ฅผ ๋ด์ ๋งคํ
webpackMap
);
// React์ Flight ํฌ๋งท์ผ๋ก ์ง๋ ฌํ
return encodeRSCPayload(stream);
}
์ด ๊ตฌํ์ ๊ณต์ ๋ฌธ์์ ๋ค์ ๊ฐ๋ ๋ค์ ๋ฐ์ํฉ๋๋ค:
- "Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server."
- webpack ์ค์ ์์ isServer ํ๋๊ทธ๋ก ํ๊ฒฝ์ ๋ถ๋ฆฌ
- "Server Components can run once at build time on your CI server, or they can be run for each request using a web server."
- renderServerComponent ํจ์๊ฐ ์ด ๋ ๊ฐ์ง ์ผ์ด์ค๋ฅผ ๋ชจ๋ ์ง์
- "Async Components are a new feature of Server Components that allow you to await in render."
- ์๋ฒ ์ธก babel ์ค์ ์ @babel/plugin-syntax-top-level-await ํฌํจ
ํต์ฌ ๊ตฌํ ์์
- RSC ํฌ๋งท
- ์๋ฒ ์ปดํฌ๋ํธ์ ๋ ๋๋ง ๊ฒฐ๊ณผ๋ฅผ ํน๋ณํ ํฌ๋งท์ผ๋ก ์ง๋ ฌํ
- JSON์ด ์๋ ํน๋ณํ ๋ฐ์ด๋๋ฆฌ ํ์ ์ฌ์ฉ
- React Flight ์ํคํ ์ฒ ํ์ฉ
- ๋ฒ๋ค๋ง ์ต์ ํ
- ์๋ฒ ์ ์ฉ ์ฝ๋๋ฅผ ํด๋ผ์ด์ธํธ ๋ฒ๋ค์์ ์ ์ธ
- ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ง ์ ํ์ ์ผ๋ก ๋ฒ๋ค๋ง
- ์คํธ๋ฆฌ๋ฐ ์ง์
- React Suspense์ ํตํฉ
- ์ ์ง์ ์ธ ํ์ด์ง ๋ก๋ฉ ๊ตฌํ
์ดํด๋ฅผ ์ํด ๊ธฐ๋ณธ์ ์ธ ํ์ ๋ง์ถฐ ๊ฐ์ํํด ๊ตฌํํด ๋ดค์ต๋๋ค!
ํน์ ํ๋ฆฐ ๋ถ๋ถ์ด ์๋ค๋ฉด ํธํ๊ฒ ๋๊ธ ์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค!
Remix์ ์ ๊ทผ
Remix๋ ๋ผ์ฐํฐ ๊ธฐ๋ฐ์ ์๋ฒ ์ค์ฌ ์ํคํ ์ฒ๋ฅผ ์ฑํํ์ฌ ์ ์ฌํ ์ด์ ์ ์ ๊ณตํฉ๋๋ค.
// Remix์ ์๋ฒ ์ฌ์ด๋ ๋ก์ง ์์
export async function loader({ request }) {
const data = await getProducts();
return json(data);
}
export default function Products() {
const products = useLoaderData();
return (
<div>
<h1>Products</h1>
<ProductList products={products} />
</div>
);
}
4. Next.js ์ฑ ๋ผ์ฐํฐ์ ์๋ฒ ์ปดํฌ๋ํธ์ ๊ด๊ณ
Next.js์ ์ฑ ๋ผ์ฐํฐ๋ ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ฑํํ์ต๋๋ค. ๋งค์ฐ ์ค์ํ ์ํคํ ์ฒ์ ๊ฒฐ์ ์ด์ฃ ?!
๊ธฐ๋ณธ ๊ตฌ์กฐ
// app/layout.jsx (์๋ฒ ์ปดํฌ๋ํธ)
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
// app/page.jsx (์๋ฒ ์ปดํฌ๋ํธ)
async function Home() {
const data = await getData(); // ์๋ฒ์์ ์ง์ ๋ฐ์ดํฐ fetch
return (
<main>
<h1>Welcome</h1>
<ClientComponent data={data} />
</main>
);
}
// components/client-component.jsx
'use client';
function ClientComponent({ data }) {
// ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์ธํฐ๋์
์ด ํ์ํ ์ปดํฌ๋ํธ
return <div>{/* ์ธํฐ๋ํฐ๋ธ UI */}</div>;
}
์ฃผ์ ํน์ง
์๋ ์ฝ๋ ๋ถํ
// app/products/page.jsx
import { ProductList } from './components/product-list';
async function ProductsPage() {
const products = await fetchProducts(); // ์๋ฒ์์๋ง ์คํ
return <ProductList products={products} />;
}
// ์ด ์ปดํฌ๋ํธ์ ๊ด๋ จ๋ ์๋ฒ ๋ก์ง์ ํด๋ผ์ด์ธํธ ๋ฒ๋ค์ ํฌํจ๋์ง ์์
์คํธ๋ฆฌ๋ฐ๊ณผ Suspense ํตํฉ
// app/dashboard/page.jsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<Suspense fallback={<LoadingUI />}>
<SlowComponent />
</Suspense>
</div>
);
}
๋ง์น๋ฉฐ
ํ์ฌ ์ ๊ฐ ์งํ ์ค์ธ ํ๋ก์ ํธ์์๋ React 18.2.0์ ์ฌ์ฉํ๊ณ ์์ด ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ์ง์ ์ฌ์ฉํ๊ธฐ๋ ์ด๋ ค์ด ์ํฉ์ ๋๋ค.
ํ์ง๋ง ๊ณ ๊ฐ ํผ๋๋ฐฑ์ ๋ฐ์ ํ Next.js๋ก์ ๋ง์ด๊ทธ๋ ์ด์ ์ ๊ณ ๋ คํ๊ณ ์์ผ๋ฉฐ, ์๋ฒ ์ปดํฌ๋ํธ์ ์ด์ ์ ์์ฐ์ค๋ฝ๊ฒ ํ์ฉํ ์ ์๋ ์ข์ ๊ธฐํ๊ฐ ๋ ๊ฒ ๊ฐ์ต๋๋ค.
ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉฐ, ๋ฒ๋ค ์ฌ์ด์ฆ ์ต์ ํ์ ์๋ฒ ๋ฆฌ์์ค ํ์ฉ ์ธก๋ฉด์์ ํฐ ์ด์ ์ ์ ๊ณตํ ๊ฒ์ผ๋ก ๊ธฐ๋๋ฉ๋๋ค!