반응 SSR아키텍처 - 렌더링 흐름
다음.js는 거대 프로젝트인 만큼 모두의 니즈에 맞출 수는 없으며, 대체로 엔터프라이즈용 기능이 즉시 사용 가능탑재되어있다.
소프트웨어의 복잡도와 성능(컴퓨팅 속도의)은 상충관계이므로 가벼운 용도의 SSR서버를 직접 만들어 보는 것도 좋은 접근일 것이다.
그렇다면 반응 SSR은 어떻게 설계해야 할까? 먼저 리액션의 작동 원리부터 생각해보자.
반응 가상 DOM
Virtual DOM 은 의미 그대로 가상으로 도모을 관리하겠다는 말이다. 리액트 앱은 가상 DOM을 메모리에서 관리하면서 상태 변화(차이)를 감지하여 브라우저 사용자 인터페이스에 반영시킨다.
그런데 이 가상 도모이 메모리에서 관리된다면, 브라우저가 아닌 서버 메모리에서 생성해도 될 것이다.
즉, 요청으로 받은 상태를 기반으로 서버 메모리에서 가상 도모을 만들고, 이를 기반으로 한 HTML를 응답으로 보낸다면, 사용자는 SSR된 리액트 앱을 사용할 수 있다.
이것이 반응 SSR의 기본 개념이다.
사실 이 방식은 흔히 쓰이는 템플릿엔진을 이용한 SSR과 같고, 리액트를 템플릿으로 하여 만들어진 도모을 HTML에 주입시키는 것으로 보면 된다.
<리액트를 뷰템플릿으로 한 SSR>
이 과정을 그럼 브라우저와 서버의 통신 과정에서 정리해보자.
먼저, 브라우저가 서버에 요청을 보내면, 서버는 브라우저가 제공하는 정보(헤더, 상태 등)을 기반으로 가상도모을 만든다.
이 도모은 다음과 같이 서버에서 렌더된 후 그대로 HTML템플릿에 주입되어 보내진다.
// Express.js 서버에서 React SSR을 만드는 과정
const App = <h1>Hello World!</h1>;
const content = renderToString(App); // 가상 DOM을 렌더링 후 string 반환
// 렌더링이 완료된 리액트 요소를 템플릿에 주입
const template = (content) => `
<html>
<body>
<div id="app">
${content}
</div>
<script src="bundle.js"></script>
</body>
</html>
`;
res.send(content); // Express.js response 사용을 가정한다
리액트는 SSR을 염두해
ReactDOMServer.renderToString
등의 메서드를 제공하며, 이는 렌더링된 HTML string을 반환한다. (클라이언트의ReactDOM.render
을 대체한다)
그 후 실제 브라우저가 받는 응답은 다음과 같다.
<html>
<body>
<div id="app">
<h1>Hello World!</h1>
<div>
</body>
<script src="bundle.js"></script>
</html>
보다시피 리액트가 성공적으로 렌더링 된 것을 볼 수 있다!그런데
<script>
번들은 어떻게 만들까?아무리 서버에서 렌더링이 완료된 HTML를 가져왔어도, 상호작용한 사용자 인터페이스를 사용하려면 당연히 JavaScript가 필요하다.
필수 패키지들을 효율적으로 번들링하여 가져올 수는 있지만, 이 번들에 리액트를 어떻게 포함시켜야 하는지가 관건이다.
즉, 리액트가 제대로 작동하기 위해 리액트 의존성을 번들링하는 것은 문제가 없지만 리액트 컴포넌트를 어떻게 관리할 것이냐에 대한 고민이 생긴다.
동구 응용 프로그램
반응 SSR을 개발할 때 같은 구성의구조로 컴포넌트를 관리하는 것은 필수적이다.
동색의 사전적 의미는 '동일'이며 같은 구성의리액트 앱이라 함은 서버와 클라이언트의 컴포넌트 구조를 동일하게 관리하는 형태를 일컫는다.
이렇게 동일 구조를 유지하면 클라이언트에서
<script>
번들의 리액트앱을 렌더링 할 경우 ReactDOM
은 이미 페인트가 완료된 SSR의 HTML과 번들의 가상도모을 비교하여 리액트JS를 바인딩(또는 hydration )한다.hydration 용도로 클라이언트는
ReactDOM.render
대신ReactDOM.hydrate
를 사용한다.
이 때, 같은 구성의하게 앱을 관리하지 않는다면 리액트는 진상을 부릴 것이고, 우리의 뜻대로 리액트가 바인딩되지 않을 것이다.
// 클라이언트의 리액트
const App = () => {
// handler와 같은 JS 요소들이 hydration을 통해 corresponding component에 바인딩된다.
const handler = () => {
console.log('hydration success!');
};
return (
<>
<div>
<h1>Misplaced Component</h1>
<button onClick={handler}>Click Me!</button>
</div>
</>
);
};
ReactDOM.hydrate(App, document.getElementById('app'));
// 서버의 리액트
// 클라이언트와 구조가 다르다
const App = (
<>
<h1>Misplaced Component</h1>
<div>
<button>Click Me!</button>
</div>
</>
);
const content = renderToString(App);
res.send(content);
위의 예시와 같이 클라이언트와 서버의 리액트 구조가 다르면 경우 컴포넌트(가상 DOM)포함)가 다시 생성되거나 구조를 멋대로 해석해 기능이 제대로 작동하지 않는다.미스매치에 대한
ReactDOM.hydrate
의 대응은 보호로써 존재하지만, 이런 버그를 그냥 놔둔다면 퍼포먼스는 더 악화되며 SSR을 사용하는 의미가 없다.SSR의 목표는 JS요청을 기다리기 전에 빠르게 HTML을 Paint하겠다는 것이다.
응답받은 HTML이 다시 렌더링 된다면 의미가 퇴색된다.
<기본적인 렌더 스트림 >
위 그림에서 브라우저는 첫 번째 요청↔응답에서 바로 마크업을 받아볼 수 있다.
그 다음 요청인
<script>
번들(bundle.js)은 대체로 HTML보다 사이즈가 훨씬 크다. 느린 네트워크 환경에선 먼저 렌더링된 사용자 인터페이스부터 보는 것이 사용자경험에 이로울 것이다.최종적으로 서버는 알맞은 번들을 보내고, 클라이언트는 반응 응용을 바인딩하여 상호작용한 기능을 사용할 수 있다.
Reference
이 문제에 관하여(반응 SSR아키텍처 - 렌더링 흐름), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/yomandawg/react-ssr-779텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)