서버사이드 렌더링
서버사이드 렌더링 (Server Side Rendering)
SSR에서 스타일을 처리하는 방법은 인라인 모드와 전체 내보내기(Whole export) 두 가지가 있어요. 각각 장단점이 있으니 상황에 맞게 고르면 돼요.
출처: 문서
본문
서버사이드 렌더링 스타일에는 두 가지 옵션이 있고, 각각 장단점이 있어요:
- 인라인 모드 (Inline mode): 렌더링 중에 추가 스타일 파일을 요청할 필요가 없어요. 장점은 추가 네트워크 요청을 줄이는 거예요. 단점은 HTML 용량이 늘어나고 첫 화면 렌더링 속도에 영향을 줄 수 있다는 점이에요. 관련 논의: #39891
- 전체 내보내기 (Whole export): antd 컴포넌트를 미리 구워(bake) 스타일을 css 파일로 만들어 페이지에 넣는 방식이에요. 장점은 어떤 페이지를 열든 전통적인 css 방식처럼 같은 css 파일 묶음을 재사용해 캐시를 활용할 수 있다는 점이에요. 단점은 페이지에 여러 테마가 있으면 추가로 구워야 한다는 점이에요.
인라인 스타일
@ant-design/[email protected]로 스타일을 추출해요:
import React from 'react';
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import type Entity from '@ant-design/cssinjs/es/Cache';
import { renderToString } from 'react-dom/server';
const App = () => {
// SSR Render
const cache = React.useMemo<Entity>(() => createCache(), []);
const html = renderToString(
<StyleProvider cache={cache}>
<MyApp />
</StyleProvider>,
);
// Grab style from cache
const styleText = extractStyle(cache);
// Mix with style
return `
<!DOCTYPE html>
<html>
<head>
${styleText}
</head>
<body>
<div id="root">${html}</div>
</body>
</html>
`;
};
export default App;
전체 내보내기
스타일 파일을 css 파일로 떼어내고 싶다면 다음 방식을 시도해 보세요:
- 의존성 설치
npm install ts-node tslib cross-env --save-dev
tsconfig.node.json추가
{
"compilerOptions": {
"strictNullChecks": true,
"module": "NodeNext",
"jsx": "react",
"esModuleInterop": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
scripts/genAntdCss.tsx추가
// scripts/genAntdCss.tsx
import fs from 'fs';
import { extractStyle } from '@ant-design/static-style-extract';
const outputPath = './public/antd.min.css';
const css = extractStyle();
fs.writeFileSync(outputPath, css);
혼합 테마나 커스텀 테마를 사용하고 싶다면 다음 스크립트를 사용할 수 있어요:
import fs from 'fs';
import React from 'react';
import { extractStyle } from '@ant-design/static-style-extract';
import { ConfigProvider } from 'antd';
const outputPath = './public/antd.min.css';
const testGreenColor = '#008000';
const testRedColor = '#ff0000';
const css = extractStyle((node) => (
<>
<ConfigProvider
theme={{
token: {
colorBgBase: testGreenColor,
},
}}
>
{node}
</ConfigProvider>
<ConfigProvider
theme={{
token: {
colorPrimary: testGreenColor,
},
}}
>
<ConfigProvider
theme={{
token: {
colorBgBase: testRedColor,
},
}}
>
{node}
</ConfigProvider>
</ConfigProvider>
</>
));
fs.writeFileSync(outputPath, css);
개발 명령을 시작하기 전이나 컴파일 전에 이 스크립트를 실행할 수 있어요. 실행하면 현재 프로젝트의 지정된 디렉터리(예: public)에 완전한 antd.min.css 파일이 바로 생성돼요.
Next.js를 예로 들면 (예제):
// package.json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"predev": "ts-node --project ./tsconfig.node.json ./scripts/genAntdCss.tsx",
"prebuild": "cross-env NODE_ENV=production ts-node --project ./tsconfig.node.json ./scripts/genAntdCss.tsx"
}
}
그다음 pages/_app.tsx 파일에 이 파일을 임포트하기만 하면 돼요:
import { StyleProvider } from '@ant-design/cssinjs';
import type { AppProps } from 'next/app';
import '../public/antd.min.css'; // add this line
import '../styles/globals.css';
export default function App({ Component, pageProps }: AppProps) {
return (
<StyleProvider hashPriority="high">
<Component {...pageProps} />
</StyleProvider>
);
}
커스텀 테마
프로젝트에서 커스텀 테마를 사용한다면 다음 방식으로 구워 보세요:
import { extractStyle } from '@ant-design/static-style-extract';
import { ConfigProvider } from 'antd';
const cssText = extractStyle((node) => (
<ConfigProvider
theme={{
token: {
colorPrimary: 'red',
},
}}
>
{node}
</ConfigProvider>
));
혼합 테마
프로젝트에서 혼합 테마를 사용한다면 다음 방식으로 구워 보세요:
import { extractStyle } from '@ant-design/static-style-extract';
import { ConfigProvider } from 'antd';
const cssText = extractStyle((node) => (
<>
<ConfigProvider
theme={{
token: {
colorBgBase: 'green ',
},
}}
>
{node}
</ConfigProvider>
<ConfigProvider
theme={{
token: {
colorPrimary: 'blue',
},
}}
>
<ConfigProvider
theme={{
token: {
colorBgBase: 'red ',
},
}}
>
{node}
</ConfigProvider>
</ConfigProvider>
</>
));
static-style-extract에 대해 더 알아보려면 static-style-extract를 참고하세요.
주문형 추출 (Extract on demand)
// scripts/genAntdCss.tsx
import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';
import { extractStyle } from '@ant-design/cssinjs';
import type Entity from '@ant-design/cssinjs/lib/Cache';
export interface DoExtraStyleOptions {
cache: Entity;
dir?: string;
baseFileName?: string;
}
export const doExtraStyle = (opts: DoExtraStyleOptions) => {
const { cache, dir = 'antd-output', baseFileName = 'antd.min' } = opts;
const baseDir = path.resolve(__dirname, '../../static/css');
const outputCssPath = path.join(baseDir, dir);
if (!fs.existsSync(outputCssPath)) {
fs.mkdirSync(outputCssPath, { recursive: true });
}
const css = extractStyle(cache, true);
if (!css) {
return '';
}
const md5 = createHash('md5');
const hash = md5.update(css).digest('hex');
const fileName = `${baseFileName}.${hash.substring(0, 8)}.css`;
const fullpath = path.join(outputCssPath, fileName);
const res = `_next/static/css/${dir}/${fileName}`;
if (fs.existsSync(fullpath)) {
return res;
}
fs.writeFileSync(fullpath, css);
return res;
};
_document.tsx에서 위 도구를 사용해 주문형으로 내보내요:
// _document.tsx
import { createCache, StyleProvider } from '@ant-design/cssinjs';
import type { DocumentContext } from 'next/document';
import Document, { Head, Html, Main, NextScript } from 'next/document';
import { doExtraStyle } from '../scripts/genAntdCss';
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const cache = createCache();
let fileName = '';
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => (
<StyleProvider cache={cache}>
<App {...props} />
</StyleProvider>
),
});
const initialProps = await Document.getInitialProps(ctx);
// 1.1 extract style which had been used
fileName = doExtraStyle({
cache,
});
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{/* 1.2 inject css */}
{fileName && <link rel="stylesheet" href={`/${fileName}`} />}
</>
),
};
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
데모 보기: 주문형 css 파일 내보내기 데모