Skip to main content

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)
)
);
}

스크린샷 2023-08-16 오후 1.07.51.png

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과 클라이언트에서 실행되는 리액트 코드 사이의 동기화
      • 이러한 과정을 순서대로 정리하면 이렇습니다.
        1. 서버에서는 Next.js를 사용하여 리액트 애플리케이션을 렌더링하고, 생성된 HTML을 클라이언트로 전송합니다.
        2. 클라이언트에서는 전송된 HTML을 가져와서 기존 마크업과 일치시킵니다.
        3. 일치하는 컴포넌트를 찾아 해당 컴포넌트의 이벤트 처리 및 상태 업데이트를 활성화합니다.
        4. 클라이언트에서는 이제 일반적인 리액트 애플리케이션처럼 동작하며, 상호작용과 상태 변화에 따라 UI를 업데이트할 수 있습니다.
    • 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 />);

참고

Next.js의 렌더링 과정(Hydrate) 알아보기

[React] hydration이란? (+ root란?)