리액트 세상에서 우리는 서버 사이드 렌더링(SSR), 클라이언트 사이드 렌더링(CSR), ReactDOM, ReactDOMServer와 같은 이해하기 어려운 다양한 용어들을 마주친다.
이 용어들을 빠르게 이해해보자.
ReactDOM은 DOM 관련 메소드들을 제공하는 패키지이며, 웹앱 최상위 레벨에서 이용할 수 있다. 웹 페이지의 DOM 컴포넌트들을 효율적으로 관리할 수 있게 도와준다.
render()
와 hydrate()
함수들은 react-dom 패키지의 모듈들이다.
ReactDOM.render(element, container[, callback])
render() 함수는 ReactDOM의 가장 유용한 함수 중 하나이다. 이 함수는 container
안의 DOM에 리액트 엘리먼트가 렌더링 된 후, 해당 컴포넌트에 대한 참조값을 반환한다. (stateless 컴포넌트의 경우 null을 반환한다.)
ReactDOM.hydrate(element, container[, callback])
hydrate()
는 render()와 같지만, HTML 컨텐츠가 ReactDOMServer에서 렌더링될 때 container
를 hydrate하기 위해 사용된다. 리액트는 이벤트 리스너를 기존 마크업에 연결하려고 할 것이다.
ReactDOMServer 객체는 정적인 마크업에 컴포넌트들을 렌더링할 수 있게 해준다.
ReactDOMServer의 renderToString(), renderToStaticMarkup() 함수들은 서버와 브라우저에서 모두 사용할 수 있다.
클라이언트 사이드 렌더링(CSR)은 자바스크립트를 이용해 브라우저에서 페이지를 직접 렌더링하는 것이다. 모든 로직과 데이터 페칭, 템플리팅, 라우팅은 클라이언트에서 처리된다.
서버는 페이지에 대한 요청을 받으면 브라우저에서 실행될 수 있도록 HTML, CSS, JS 코드를 보내준다. script 태그에는 리액트 코드에 대한 모든 지침이 포함된다. 이는 브라우저에서 로드되어 인터랙티브한 앱이 만들어지게 된다.
인터넷이 느리거나 번들 사이즈가 큰 경우 CSR에서는 사용자가 사이트를 볼 수 있기까지 시간이 걸릴 수 있다. 이럴 때 사용자는 보통 빈 화면을 보게 된다. 이는 나쁜 사용자 경험으로 이어진다. 웹 크롤러 또한 빈 사이트를 인덱싱할 수 없으므로 SEO에도 영향을 미친다.
SSR을 이해하기 위해 레스토랑에서 서빙되는 음식 비유를 들어보자. 전채, 차파티, 달, 커리 등을 주문한다. 음식들이 오기까지 시간이 오래 걸린다. 우리는 불만스러울 것이다. 배고픈 손님은 웨이터에게 소리를 지르기까지 한다.
그런데 우리가 앉자마자 몇가지 빠빠드, 샐러드, 웰컴 드링크가 서빙되면 어떨까?
훌륭하다. 적어도 무언가 시작을 할 수 있다.
SSR에서도 비슷한 접근법이 사용된다.
서버사이드 렌더링에서는 사용자가 웹페이지를 요청하면 서버는 사용자별 데이터를 가져와 HTML 페이지를 준비하고, 사용자의 컴퓨터에 보낸다. 그런 다음 브라우저는 컨텐츠를 구성하고 페이지를 보여준다. DB에서 데이터를 가져오고, HTML 페이지를 만들고, 클라이언트에 보내는 이 모든 프로세스는 몇 밀리초 안에 벌어진다.
이 과정에서 사용자는 빈 화면 대신 브라우저의 컨텐츠를 볼 수 있어 사용자를 만족시키고 사용자 경험을 향상 시킨다.
이제 리액트 어플리케이션의 Hydration을 알아보자.
리액트 하이드레이션은 렌더링과 유사하게 사용되는 기술이다. 하지만 리액트 컴포넌트를 렌더링하기 위해 빈 DOM을 사용하는 대신, 모든 컴포넌트가 HTML로 렌더링 된, 이미 빌드된 DOM을 사용한다.
기본 리액트 앱:
const root = document.querySelector("#root");
ReactDOM.render(<App name="Saeloun" />, root);
클라이언트 사이드에서 스크립트가 로딩되기 전의 어플리케이션:
<html>
<head></head>
<body>
<div id="root"></div>
</body>
</html>
SSR 어플리케이션:
// index.js
ReactDOM.hydrate(<App name="Saeloun"/>, document.getElementById('root'));
//server.js
import React from "react";
import ReactDOMServer from "react-dom/server";
app.use("/", (req, res, next) => {
fs.readFile(path.resolve("./build/index.html"), "utf-8", (err, data) => {
if (err) {
console.log(err);
return res.status(500).send("Some error happened");
}
return res.send(ReactDOMServer.renderToString(<App name="Saeloun" />)
)
});
});
//App.js
import React from "react";
function App(props) {
return (
<div>
Hello {props.name}!
</div>
)
}
export default App;
클라이언트 사이드에서 스크립트가 로딩되기 전의 어플리케이션:
<html>
<head></head>
<body>
<div id="root">
<h1>Hello Saeloun!</h1>
</div>
</body>
</html>
이제 SSR discussion에서 언급된 예제를 보자.
앱이 준비되기 전에 완전히 인터랙티브한 앱을 보여주려고 한다.
초록색은 페이지에서 인터랙티브한 부분을 보여준다. 즉, 모든 이벤트 핸들러가 연결 되었다.(JS가 완전히 로드됨)
CSR에서는 자바스크립트가 로딩되는 동안 사용자가 볼 수 있는건 빈 화면뿐이므로 사용자 경험이 감소된다.
SSR에서는 서버에서 리액트 컴포넌트를 HTML로 렌더링하여 사용자에게 보낸다. HTML은 별로 익터랙티브하지 않지만, 자바스크립트가 로딩되는 동안 사용자가 무언가 볼 수 있게 해준다.
회색은 인터랙티브 하지 않은 부분을 나타낸다. 자바스크립트 코드가 아직 로드되지 않았으므로 버튼을 클릭해도 아무것도 실행되지 않는다.
리액트를 통해 HTML에 이벤트 핸들러를 연결시켜 앱을 인터랙티브하게 만들 수 있다. 컴포넌트들을 렌더링하고 이벤트 핸들러를 연결하는 이 과정을 'hydration'이라 한다. 이는 '마른' HTML에 상호작용과 이벤트 핸들러로 '물'을 주는 것과 같다. 하이드레이션 후, 앱은 인터랙티브해져 클릭에 응답하게 된다.
리액트는 서버와 클라이언트에서 렌더링되는 컨텐츠가 동일할 것이라 예상한다. 리액트는 텍스트 컨텐츠의 차이는 수정할 수 있다. 불일치는 버그로 간주되어 수정되어야 한다. 개발 모드에서 리액트는 하이드레이션 중 불일치에 대해 경고한다.
만약 단일 엘리먼트나 텍스트 컨텐츠가 서버와 클라이언트에서 불가피하게 다르다면, 엘리먼트에 suppressHydrationWarning={true}
를 추가해 경고를 끌 수 있다. 하지만 이는 남용되면 안된다!
만약 의도적으로 서버와 클라이언트에서 다른 것을 렌더링해야 하는 경우 2단계 렌더링을 수행할 수 있다. 클라이언트에서 다른 내용을 렌더링하는 컴포넌트는 componentDidMount()
에서 true로 설정되는 this.state.isClient
같은 상태 변수를 읽을 수 있다. 불일치를 피하면서 서버에서 다른 컨텐츠를 렌더링할 수 있다. 그러나 하이드레이션 직후 추가 단계가 동일하게 발생한다. 이 방법은 어플리케이션을 느리게 만드므로 주의해서 사용해야 한다.
원문
Understanding Hydration in React applications(SSR)
의역 포함