리액트 애플리케이션을 서버에서 렌더링할 수 있는 API도 제공한다. 이 API는 Node.js와 같은 서버 환경에서만 실행할 수 있으며 window 환경에서 실행 시 에러가 발생할 수 있다.
관련된 API를 확인하고 싶다면 리액트 저장소의 react-dom/server.js를 확인하면 된다. react-dom이 서버에서 렌더링하기 위한 다양한 메소드를 제공하고 있으며, 어떤 함수를 export하는지 알 수 있다.
인수로 넘겨 받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 변환하는 함수다. SSR을 구현하는 가장 기초적인 API로 최초의 페이지를 HTML로 먼저 렌더링 하는 역할을 하는 함수이다.
import { useEffect } from "react";
import ReactDOMServer from "react-dom/server";
function ChildrenComponent({ fruits }: { fruits: Array<string> }) {
useEffect(() => {
console.log(fruits);
}, [fruits]);
function handleClick() {
console.log("clicked");
}
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit} onClick={handleClick}>
{fruit}
</li>
))}
</ul>
);
}
function SampleComponent() {
return (
<>
<div>hello</div>
<ChildrenComponent fruits={["apple", "banana", "orange"]} />
</>
);
}
const result = ReactDOMServer.renderToString(
React.createElement("div", { id: "root" }, <SampleCompoent />)
);
위의 result는 다음과 같은 문자열을 반환한다.
<div id="root" data-reactroot="">
<div>hello</div>
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
</div>
renderToString을 사용해 실제 브라우저가 그려야 할 HTML 결과로 만들어 낸 모습이다.
여기서 주목할 점은 useEffect와 같은 훅과 handleClick과 같은 이벤트 핸들러는 결과물에 포함되지 않았다는 것이다. renderToString의 목적은 빠르게 브라우저가 렌더링할 수있는 HTML을 제공하는 데 있다. 따라서 클라이언트에서 실행되는 자바스크립트 코드를 포함시키거난 렌더링하는 역할을 해주지 않는다.
renderToString는 완성된 HTML을 서버에서 제공할 수 있으므로 초기 렌더링에 뛰어난 성능을 보인다.
리액트의 서버 사이드 렌더링은 단순히 최초 HTML 페이지를 빠르게 그리는데 목적이 있다. 따라서 사용자와 인터랙션하려면 이와 관련된 별도의 자바스크립트 코드를 모두 다운로드, 파싱, 실행하는 과정을 거쳐야 한다.
data-reactroot는 리액트 컴포넌트의 루트 엘리먼트가 무엇인지 식별하는 역할을 한다. 이후에 자바스크립트를 실행하기 위한 hydrate 함수에서 루트를 식별하는 기준점이 된다.
리액트 컴포넌트를 기준으로 HTML 문자열을 만든다는 점에서 renderToString과 유사하다.
차이점은 data-reactroot 와 같은 리액트에서만 사용하는 추가적인 DOM속성을 만들지 않는다는 점이다. 따라서 HTML의 크기를 아주 약간이라도 줄일 수 있다는 장점이 있다.
<div id="root">
<div>hello</div>
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
</div>
해당 결과로 렌더링을 수행하면 useEffect와 같은 브라우저 API를 절대 실행할 수 없다. 따라서 리액트의 이벤트 리스너가 필요 없는 완전 순수한 HTML을 만들 때만 사용된다.
renderToString과 결과물이 동일하지만 차이점이 있다.
renderToString, renderToStaticMarkup 모두 브라우저에서도 실행할 수 있지만 renderToNodeStream는 브라우저에서 사용하는 것이 완전 불가능하다.
renderToNodeStream는 완전히 Node.js 환경에 의존하고 있으며, 결과물도 Node.js의 ReadableStream이다.
그렇다면 renderToNodeStream가 필요할까?
HTML 결과물의 크기가 작다면 한번에 생성하든 스트림으로 하든 문제가 없다. 하지만 크기가 클 경우 큰 문자열을 한번에 메모리에 올려두고 응답을 수행해야 해서 Node.js가 실행되는 서버에 큰 부담이 될 수 있다. 이때 스트림을 활용해 큰 크기의 데이터를 청크 다위로 분리해 순차적으로 처리해야 한다. 이렇게 함으로써 Node.js 서버의 부담을 덜 수 있다. 대부분 알려진 리액트 서버 사이드 렌더링 프레임워크는 모두 renderToNodeStream을 채택하고 있다.
renderToNodeStream과 제공하는 결과물은 동일하나, renderToStaticMarkup과 마찬가지로 리액트 자바스트립에 필요한 리액트 속성이 제공되지 않는다. 마찬가지로 hydrate를 할 필요가 없는 순수 HTML 결과물이 필요할 때 사용하는 메서드다.
hydrate 함수는 앞서 살펴본 두개의 함수 renderToString, renderToNodeStream으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할을 한다.
정적으로 생성된 HTML에 이벤트와 핸들러를 붙여 완전한 웹페이지 결과물을 만든다.
hydrate와 비슷한 브라우저에서만 사용되는 메서드인 render에 대해 먼저 살펴보자.
import * aas ReactDOM from 'react-dom'
import App from './App'
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement)
render함수는 컴포넌트와 HTML의 요소를 인수로 받는다. 이렇게 받은 두 정보를 바탕으로 HTML의 요소에 해당 컴포넌트를 렌더링하며, 여기에 이벤트 핸들러를 붙이는 작업까지 모두 한번에 수행한다.
render는 클라이언트에서만 실행되는, 렌더링과 이벤트 핸들러 추가 등 리액트를 기반으로 한 온전한 웹페이지를 만드는데 필요한 모든 작업을 수행한다.
hydrate도 render와 인수를 넘기는 것은 유사하다.
import * aas ReactDOM from 'react-dom'
import App from './App'
const element = document.getElementById(containerId);
ReactDOM.hydrate(<App />, element)
render와 차이점은 hydrate는 기본적으로 이미 렌더링된 HTML이 있다는 가정하에 작업이 수행되고, 이 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 실행한다는 것이다.
만약 hydrate의 두번째 인수로 renderToStaticMarkup등으로 생성된 리액트와 관련 정보가 없는 순수한 HTML 정보를 넘겨주면 어떻게 될까?
<!DOCTYPE html>
<head>
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
function App() {
return (
<span>안녕하세요.</span>
);
}
import * as React from 'react';
import App from './App';
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
// Waring : Expected server HTML to contain a matching <span> in <div>.
서버에서 제공받은 HTML에 App 컴포넌트에 있는 것과 마찬가지로 이 있기를 기대했지만 요소가 없다는 경고 문구가 출력된다.이는 hydrate가 서버에서 제공해 준 HTML이 클라이언트의 결과물과 같을 것이라는 가정하에 실행된다는 것을 의미한다.
rootElement 내부에 을 렌더링한 정보가 이미 포함돼 있어야만 hydrate를 실행할 수 있다.
hydrate 작업이 단순 이벤트나 핸들러를 추가하는 것 이외에도 렌더링을 한 번 수행하면서 hydrate가 수행한 렌더링 결과물 HTML과 인수로 넘겨받은 HTML을 비교하는 작업도 수행하기 때문이다.
여기서 발생한 불일치가 바로 에러의 원인이며, 불일치가 발생하면 hydrate가 렌더링한 기준으로 웹페이지를 그리게 된다.