서버사이드 렌더링

서버사이드 렌더링 (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 파일로 떼어내고 싶다면 다음 방식을 시도해 보세요:

  1. 의존성 설치
npm install ts-node tslib cross-env --save-dev
  1. tsconfig.node.json 추가
{
  "compilerOptions": {
    "strictNullChecks": true,
    "module": "NodeNext",
    "jsx": "react",
    "esModuleInterop": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
  1. 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 파일 내보내기 데모

더 알아보기 (Learn more)