1. Server Code
Next.js 서버에서 HTML 생성하는 법
export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: NextParsedUrlQuery,
renderOpts: RenderOpts
): Promise<RenderResult | null> {
// ...
const renderDocument = async () => {
// ...
async function loadDocumentInitialProps(
renderShell?: (
_App: AppType,
_Component: NextComponentType
) => Promise<ReactReadableStream>
) {
// ...
const renderPage: RenderPage = (
options: ComponentsEnhancer = {}
): RenderPageResult | Promise<RenderPageResult> => {
// ...
const html = ReactDOMServer.renderToString(
<Body>
<AppContainerWithIsomorphicFiberStructure>
{renderPageTree(EnhancedApp, EnhancedComponent, {
...props,
router,
})}
</AppContainerWithIsomorphicFiberStructure>
</Body>
);
return { html, head };
};
}
// ...
return {
bodyResult,
documentElement,
head,
headTags: [],
styles,
};
};
}
- 다음의 코드를 이용해서, 서버 단에서 ReactNode를 HTML string으로 만듬
- 그리고 이렇게 생성된 HTML은 htmlProps가 되어 document로 반환된다.
- Next.js로 만들어진 페이지의 Network 탭에서 서버에서 반환한 HTML을 볼 수 있음
// src/server/render.tsx
export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: NextParsedUrlQuery,
renderOpts: RenderOpts
): Promise<RenderResult | null> {
// ...
const documentResult = await renderDocument();
const htmlProps: HtmlProps = {
__NEXT_DATA__: {
// ...
},
};
const document = (
<AmpStateContext.Provider value={ampState}>
<HtmlContext.Provider value={htmlProps}>
{documentResult.documentElement(htmlProps)}
</HtmlContext.Provider>
</AmpStateContext.Provider>
);
const documentHTML = ReactDOMServer.renderToStaticMarkup(document);
// ...
// 운영환경 여부에 따라 prefix에 속성을 다르게 하고,
// prefix와 suffix 정보를 가진 streams를 선언한다
// 이때 '<!-- __NEXT_DATA__ -->'가 prefix에 입력된다
if (generateStaticHTML) {
// ...
return new RenderResult(optimizedHtml);
}
return new RenderResult(
chainStreams(streams).pipeThrough(
createBufferedTransformStream(postOptimize)
)
);
}
2. Client Code
// client/next.js
initialize({})
.then(() => hydrate())
.catch(console.error);
// client/index.tsx
export async function initialize(opts: { webpackHMR?: any } = {}): Promise<{
assetPrefix: string;
}> {
// initialize()
// __NEXT_DATA__fmf id로 갖는 요소의 컨텐츠를
// window객체에 __NEXT_DATA__키로 저장함
initialData = JSON.parse(
document.getElementById("__NEXT_DATA__")!.textContent!
);
window.__NEXT_DATA__ = initialData;
const prefix: string = initialData.assetPrefix || "";
appElement = document.getElementById("__next");
return { assetPrefix: prefix };
}
// 실행하려는 페이지의 에러가 있는지 확인 및 validation 체크
export async function hydrate(opts?: { beforeRender?: () => Promise<void> }) {
// ...
const renderCtx: RenderRouteInfo = {
App: CachedApp,
initial: true,
Component: CachedComponent,
props: initialData.props,
err: initialErr,
};
render(renderCtx);
}
async function render(renderingProps: RenderRouteInfo): Promise<void> {
// ...
await doRender(renderingProps);
}
function doRender(input: RenderRouteInfo): Promise<any> {
// ...
renderReactElement(appElement!, (callback) => (
<Root callbacks={[callback, onRootCommit]}>
{process.env.__NEXT_STRICT_MODE ? (
<React.StrictMode>{elem}</React.StrictMode>
) : (
elem
)}
</Root>
));
}
let shouldHydrate: boolean = true; // 첫 렌더에서는 항상 true이다
function renderReactElement(
domEl: HTMLElement,
fn: (cb: () => void) => JSX.Element
): void {
//...
const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete);
// ...
if (shouldHydrate) {
ReactDOM.hydrate(reactEl, domEl);
shouldHydrate = false;
} else {
ReactDOM.render(reactEl, domEl);
}
}
hydrate란?
- 서버에서 렌더링된 HTML에 기반해서, 클라이언트측에서 자바스크립트 이벤트, 상태를 연결하는 과정
- 초기 로딩 시, 즉시 상호작용 가능
- 이후에는 일반 리액트처럼 동작가능
- hydrate가 없으면 서버측에서 렌더링된 HTML을 무시하고
- 클라이언트 측에서 새로 렌더링해야함
- Next.js에서는
- hydrate는 서버에서 렌더링된 HTML을 가져와 기존 클라이언트에 이미 존재하는 마크업과 일치시키기 위해 사용됩니다.
- 기존 마크업은 서버에서 렌더링될 때 생성되고, 클라이언트에서는 기존 마크업과 일치하는 컴포넌트를 찾아 이벤트 처리 및 상태 업데이트를 연결합니다.
- 서버에서 생성된 HTML과 클라이언트에서 실행되는 리액트 코드 사이의 동기화
- 기존 마크업은 서버에서 렌더링될 때 생성되고, 클라이언트에서는 기존 마크업과 일치하는 컴포넌트를 찾아 이벤트 처리 및 상태 업데이트를 연결합니다.
- 이러한 과정을 순서대로 정리하면 이렇습니다.
- 서버에서는 Next.js를 사용하여 리액트 애플리케이션을 렌더링하고, 생성된 HTML을 클라이언트로 전송합니다.
- 클라이언트에서는 전송된 HTML을 가져와서 기존 마크업과 일치시킵니다.
- 일치하는 컴포넌트를 찾아 해당 컴포넌트의 이벤트 처리 및 상태 업데이트를 활성화합니다.
- 클라이언트에서는 이제 일반적인 리액트 애플리케이션처럼 동작하며, 상호작용과 상태 변화에 따라 UI를 업데이트할 수 있습니다.
- hydrate는 서버에서 렌더링된 HTML을 가져와 기존 클라이언트에 이미 존재하는 마크업과 일치시키기 위해 사용됩니다.
- React 에서는
- react의 hydration은 자동기능이 아니라 SSR을 할 때에 굳이 render트리를 만들지 않고 기존에 형성된 DOM에 이벤트만 붙이면 되므로 해당 과정을 실행하는 메서드이다.
- react 의 Root 개념이 업데이트되면서, 이제는 createRoot 메서드를 호출하여 만들어지는 클로져 환경에 root를 등록하고 리턴되는 객체의 메서드에 존재하는 hydrate관련 함수를 이용해 SSR로 전달받은 html에 이벤트 핸들러를 붙여줄 수 있게 되었다.
- createRoot와 hydrateRoot 비교하기
import React from "react";
import * as ReactDOM from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
// 클라이언트 사이드 렌더링 (Client-Side Rendering)
const root = ReactDOM.createRoot(container);
root.render(<App />);
// 서버 사이드 렌더링 후 클라이언트에서 하이드레이션 (Server-Side Rendering + Hydration)
const hydrateRoot = ReactDOM.hydrateRoot(container, <App />);
hydrateRoot.render(<App />);
참고