메인 컨텐츠로 이동
Version: 2.0.0-beta.17

Static site generation (SSG)

In architecture, we mentioned that the theme is run in Webpack. But beware: that doesn't mean it always has access to browser globals! The theme is built twice:

  • During server-side rendering, the theme is compiled in a sandbox called React DOM Server. You can see this as a "headless browser", where there is no window or document, only React. SSR produces static HTML pages.
  • During client-side rendering, the theme is compiled with standard React DOM, and has access to browser variables. CSR produces dynamic JavaScript.
SSR or SSG?

Server-side rendering and static site generation can be different concepts, but we use them interchangeably.

Therefore, while you probably know not to access Node globals like process (or can we?) or the 'fs' module, you can't freely access browser globals either.

import React from 'react';

export default function WhereAmI() {
return <span>{window.location.href}</span>;
}

This looks like idiomatic React, but if you run docusaurus build, you will get an error:

ReferenceError: window is not defined

This is because during server-side rendering, the Docusaurus app isn't actually run in browser, and it doesn't know what window is.

What about process.env.NODE_ENV?

One exception to the "no Node globals" rule is process.env.NODE_ENV. In fact, you can use it in React, because Webpack injects this variable as a global:

import React from 'react';

export default function expensiveComp() {
if (process.env.NODE_ENV === 'development') {
return <>This component is not shown in development</>;
}
const res = someExpensiveOperationThatLastsALongTime();
return <>{res}</>;
}

During Webpack build, the process.env.NODE_ENV will be replaced with the value, either 'development' or 'production'. You will then get different build results after dead code elimination:

import React from 'react';

export default function expensiveComp() {
if ('development' === 'development') {
+ return <>This component is not shown in development</>;
}
- const res = someExpensiveOperationThatLastsALongTime();
- return <>{res}</>;
}

SSR 이해하기

React is not just a dynamic UI runtime—it's also a templating engine. Because Docusaurus sites mostly contain static contents, it should be able to work without any JavaScript (which React runs in), but only plain HTML/CSS. And that's what server-side rendering offers: statically rendering your React code into HTML, without any dynamic content. An HTML file has no concept of client state (it's purely markup), hence it shouldn't rely on browser APIs.

These HTML files are the first to arrive at the user's browser screen when a URL is visited (see routing). Afterwards, the browser fetches and runs other JS code to provide the "dynamic" parts of your site—anything implemented with JavaScript. However, before that, the main content of your page is already visible, allowing faster loading.

In CSR-only apps, all DOM elements are generated on client side with React, and the HTML file only ever contains one root element for React to mount DOM to; in SSR, React is already facing a fully built HTML page, and it only needs to correlate the DOM elements with the virtual DOM in its model. This step is called "hydration". After React has hydrated the static markup, the app starts to work as any normal React app.

탈출구

브라우저 API가 작동하도록 하는 동적 콘텐츠를 화면에 렌더링하려면 다음과 같이 합니다.

  • Our live codeblock, which runs in the browser's JS runtime
  • Our themed image that detects the user's color scheme to display different images
  • The JSON viewer of our debug panel which uses the window global for styling

정적 HTML은 클라이언트 상태를 알지 못하면 어떤 유용한 것도 표시할 수 없으므로 SSR에서 빠져나와야 할 수도 있습니다.

caution

첫 번째 클라이언트 측 렌더링이 서버 측 렌더링과 정확하게 같은 DOM 구조를 생성하는 것이 중요합니다. 그렇지 않으면 리액트는 가상 DOM을 잘못된 DOM 요소와 연결합니다.

따라서 if (typeof window !== 'undefined) {/* render something */} 같은 순진한 시도는 첫 번째 클라이언트 렌더링에서 서버에서 생성된 마크업과 다른 마크업을 즉시 생성하기 때문에 브라우저인지 서버인지 감지하기 위한 용도로 적절하게 작동하지 않습니다.

리하이드레이션의 위험에서 어떤 문제가 있는지 자세하게 살펴볼 수 있습니다.

도큐사우루스는 SSR에서 빠져나오는 몇 가지 더 안정적인 방법을 지원합니다.

<BrowserOnly>

브라우저에서 일부 컴포넌트만 렌더링해야 하는 경우(예를 들어 컴포넌트가 작동하기 위해 브라우저 특정 사항에 의존하는 경우) 일반적인 접근 방식 중 하나는 SSR에서는 처리되지 않고 CSR에서만 렌더링되도록 컴포넌트를 <BrowserOnly>로 감싸는 것입니다.

import BrowserOnly from '@docusaurus/BrowserOnly';

function MyComponent(props) {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
const LibComponent =
require('some-lib-that-accesses-window').LibComponent;
return <LibComponent {...props} />;
}}
</BrowserOnly>
);
}

<BrowserOnly>의 자식은 JSX 요소가 아니라 요소를 반환하는 함수라는 사실을 인식하는 것이 중요합니다. 이것은 설계 상의 결정입니다. 다음 코드를 참고하세요.

import BrowserOnly from '@docusaurus/BrowserOnly';

function MyComponent() {
return (
<BrowserOnly>
{/* DON'T DO THIS - doesn't actually work */}
<span>page url = {window.location.href}</span>
</BrowserOnly>
);
}

BrowserOnly가 서버 측 렌더링 중에 자식을 숨길 것처럼 보이지만 실제 그렇지 않습니다. 리액트 렌더러가 JSX 트리를 렌더링할 때 {window.location.href} 변수를 해당 트리의 노드로 보고 렌더링을 시도하지만 실제로는 사용되지 않습니다! 함수를 사용하면 필요할 때만 렌더러가 browser-only 컴포넌트를 볼 수 있도록 합니다.

useIsBrowser

useIsBrowser() 후크를 사용해 컴포넌트가 현재 어떤 브라우저 환경에 있는지 테스트할 수 있습니다. 첫 번째 클라이언트 렌더링 후에 false를 반환하면 SSR, true를 반환하면 CSR 환경입니다. 클라이언트 측에서 특정 조건부 작업만 수행하고 완전히 다른 UI를 렌더링할 필요가 없는 경우 이 후크를 사용합니다.

import useIsBrowser from '@docusaurus/useIsBrowser';

function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
}

useEffect

마지막으로 useEffect() 안에 코드를 추가해 첫 번째 CSR 이후 실행을 지연시킬 수 있습니다. 클라이언트에서 데이터를 가져오지 않고 사이드 효과만 원하는 경우 적절한 방식입니다.

function MyComponent() {
useEffect(() => {
// Only logged in the browser console; nothing is logged during server-side rendering
console.log("I'm now in the browser");
}, []);
return <span>Some content...</span>;
}

ExecutionEnvironment

ExecutionEnvironment 네임스페이스에는 여러 값이 포함되어 있으며 canUseDOM은 브라우저 환경을 감지하는 효과적인 방법입니다.

내부적으로 typeof window !== 'undefined' 코드로 확인했다는 점에 유의하세요. 때문에 렌더링 관련 로직으로 사용해서는 안되고 웹에서 요청을 보내 사용자 입력에 반응하거나 DOM을 업데이트하지 않고 동적으로 라이브러리를 가져오는 것과 같은 명령형 코드로 사용할 수 있습니다.

a-client-module.js
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

if (ExecutionEnvironment.canUseDOM) {
document.title = "I'm loaded!";
}