서버 렌더링
서버 렌더링 (Server rendering)
서버 사이드 렌더링의 가장 흔한 사용 사례는 사용자(또는 검색 엔진 크롤러)가 앱을 처음 요청할 때의 초기 렌더를 처리하는 것이에요.
출처: 문서
본문
서버 사이드 렌더링의 가장 흔한 사용 사례는 사용자(또는 검색 엔진 크롤러)가 처음으로 앱을 요청할 때의 초기 렌더를 처리하는 것이에요.
서버가 요청을 받으면, 필요한 컴포넌트를 HTML 문자열로 렌더링한 뒤 응답으로 클라이언트에 보내요. 그 시점부터는 클라이언트가 렌더링 작업을 이어받아요.
서버에서의 Material UI (Material UI on the server)
Material UI는 처음부터 서버에서 렌더링된다는 제약을 염두에 두고 설계됐어요. 다만 올바르게 통합하는 것은 여러분 몫이에요. 페이지에 필요한 CSS를 제공하는 것이 중요해요. 그렇지 않으면 페이지가 HTML만으로 렌더링된 뒤 클라이언트가 CSS를 주입할 때까지 기다리게 되어 깜빡임(FOUC)이 발생해요. 스타일을 클라이언트까지 주입하려면 다음을 해야 해요:
- 요청마다 새롭고 깨끗한
emotion cache인스턴스를 생성해요. - 서버 사이드 컬렉터로 React 트리를 렌더링해요.
- CSS를 추출해요.
- 그 CSS를 클라이언트에 전달해요.
클라이언트 쪽에서는, 서버 사이드에서 주입된 CSS를 제거하기 전에 CSS를 한 번 더 주입하게 돼요.
설정하기 (Setting up)
아래 레시피에서는 서버 사이드 렌더링을 설정하는 방법을 살펴볼 거예요.
테마 (The theme)
클라이언트와 서버 간에 공유할 테마를 만드세요:
import { createTheme } from '@mui/material/styles';
import { red } from '@mui/material/colors';
// Create a theme instance.
const theme = createTheme({
palette: {
primary: {
main: '#556cd6',
},
secondary: {
main: '#19857b',
},
error: {
main: red.A400,
},
},
});
export default theme;
서버 사이드 (The server-side)
다음은 서버 사이드가 어떤 모습일지에 대한 개요예요.
app.use를 사용해 Express 미들웨어를 설정해 서버로 들어오는 모든 요청을 처리할 거예요.
Express나 미들웨어에 익숙하지 않다면, handleRender 함수가 서버가 요청을 받을 때마다 호출된다는 점만 알아 두면 돼요.
import express from 'express';
// We are going to fill these out in the sections to follow.
function renderFullPage(html, css) {
/* ... */
}
function handleRender(req, res) {
/* ... */
}
const app = express();
// This is fired every time the server-side receives a request.
app.use(handleRender);
const port = 3000;
app.listen(port);
요청 처리하기 (Handling the request)
매 요청마다 가장 먼저 해야 할 일은 새 emotion cache를 만드는 것이에요.
렌더링할 때는 루트 컴포넌트인 App을 CacheProvider와 ThemeProvider 안에 감싸서, 스타일 설정과 theme이 컴포넌트 트리의 모든 컴포넌트에서 사용 가능하도록 만들 거예요.
서버 사이드 렌더링의 핵심 단계는 컴포넌트의 초기 HTML을 클라이언트 쪽으로 보내기 전에 렌더링하는 것이에요. 이를 위해 ReactDOMServer.renderToString()을 사용해요.
Material UI는 기본 styled 엔진으로 Emotion을 사용해요. Emotion 인스턴스에서 스타일을 추출해야 해요. 이를 위해 클라이언트와 서버가 동일한 캐시 구성을 공유해야 해요:
import createCache from '@emotion/cache';
export default function createEmotionCache() {
return createCache({ key: 'css' });
}
이렇게 해서 새 Emotion 캐시 인스턴스를 만들고, HTML에 필요한 핵심 스타일을 추출하는 데도 이 캐시를 사용해요.
이것이 renderFullPage 함수에서 어떻게 전달되는지는 곧 살펴볼 거예요.
import express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@mui/material/styles';
import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import App from './App';
import theme from './theme';
import createEmotionCache from './createEmotionCache';
function handleRender(req, res) {
const cache = createEmotionCache();
const { extractCriticalToChunks, constructStyleTagsFromChunks } =
createEmotionServer(cache);
// Render the component to a string.
const html = ReactDOMServer.renderToString(
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline
to build upon. */}
<CssBaseline />
<App />
</ThemeProvider>
</CacheProvider>,
);
// Grab the CSS from emotion
const emotionChunks = extractCriticalToChunks(html);
const emotionCss = constructStyleTagsFromChunks(emotionChunks);
// Send the rendered page back to the client.
res.send(renderFullPage(html, emotionCss));
}
const app = express();
app.use('/build', express.static('build'));
// This is fired every time the server-side receives a request.
app.use(handleRender);
const port = 3000;
app.listen(port);
초기 컴포넌트 HTML과 CSS 주입하기 (Inject initial component HTML and CSS)
서버 사이드의 마지막 단계는 초기 컴포넌트 HTML과 CSS를 템플릿에 주입해 클라이언트 쪽에서 렌더링되도록 하는 것이에요.
function renderFullPage(html, css) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>My page</title>
${css}
<meta name="viewport" content="initial-scale=1, width=device-width" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
/>
</head>
<body>
<div id="root">${html}</div>
</body>
</html>
`;
}
클라이언트 사이드 (The client-side)
클라이언트 사이드는 간단해요. 서버 사이드와 동일한 캐시 구성을 사용하기만 하면 돼요. 클라이언트 파일을 살펴볼게요:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@mui/material/styles';
import { CacheProvider } from '@emotion/react';
import App from './App';
import theme from './theme';
import createEmotionCache from './createEmotionCache';
const cache = createEmotionCache();
function Main() {
return (
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline
to build upon. */}
<CssBaseline />
<App />
</ThemeProvider>
</CacheProvider>
);
}
ReactDOM.hydrateRoot(document.querySelector('#root'), <Main />);
참조 구현 (Reference implementations)
다음은 이 튜토리얼의 참조 구현이에요.
GitHub 저장소의 /examples 폴더에서 더 많은 SSR 구현을 볼 수 있어요. 다른 예제들을 참고하세요.