지난 시간에는 리액트의 렌더링 원리에 대해 알아보았습니다.
Single Page Application(SPA)
리액트는 Single Page Application(SPA)를 만들기 위한 라이브러리로써,
초기 페이지 요청시 빈 형태의 HTML 문서를 받아 렌더링하고,
브라우저 돔의 업데이트를 가상 돔 형태로 관리하며 변경 요소를 한번에 모아 대부분의 상황에서 성능이 좋은 UI 업데이트를 보장합니다.
이러한 리액트의 동작은 클라이언트 사이드에서 이뤄지는데요, 따라서 클라이언트 사이드 렌더링 방식을 취하고 있다고 할 수 있습니다.
그렇다면 클라이언트 사이드 렌더링(CSR)이란 무엇일까요?
클라이언트 사이드 렌더링 (CSR) 이란
Javascript를 이용하여 브라우저 DOM을 수정, 조작하는 형태의 렌더링 방식을 말합니다.
예를들어 document.getElementById()와 같은 DOM API로 돔 노드를 직접 취해 변경하거나, JQuery를 활용하는 방법도 이러한 CSR의 방식으로 돔을 업데이트 하는 것이라고 할 수 있죠.
때문에 렌더링 주체가 클라이언트 사이드라는 것을 이해할 수 있습니다.
하지만 이러한 DOM의 직접 수정 방식은 성능상 치명적인 문제를 야기합니다.
바로 브라우저는 돔이 변경되면 이를 UI에 반영하기 위해 렌더트리를 다시 계산하고 Layout, Paint 과정을 재수행해야하기 때문이죠.
첫 번째 포스트인 브라우저 렌더링 원리에서 알아보았듯, 이 Layout과 Paint 과정은 브라우저 렌더링에서 리소스가 가장 많이 소모되는 작업으로, 이 작업이 반복는 것을 별도로 Reflow, Repaint 라고 부른다고도 했는데요.
반면 리액트는 이러한 단점을 극복하기 위해 브라우저 DOM의 변경을 최소한으로 수행합니다.
메모리상에 순수 자바스크립트 객체 형태로 관리되는 Virtual DOM을 활용해서 말이죠.
하지만 이러한 리액트를 활요한 SPA도 단점이 있습니다.
HTML,CSS,JS가 모두 로드된 이후 동작한다는 점, 초기 빈 화면을 렌더한 뒤 동적으로 DOM이 수정되는 과정으로 렌더링 됨으로써 초기 페이지 로드가 느려질 수 있다는 점, 등으로 인해
사용자 경험이 떨어질 수 있다는 문제가 생깁니다.
또한 하나의 빈 HTML 문서를 받아 초기 렌더링을 수행한다는 점에서도 페이지 별 meta태그를 설정하기 어려워 SEO에도 좋지 못하다는 단점이 있는데요,
이러한 단점을 극복하기 위한 렌더링 방식으로 SSR을 활용합니다.
서버 사이드 렌더링 (SSR) 이란
페이지 렌더링 주체가 서버에 있는 것을 말합니다.
따라서 서버 환경에서 HTML 페이지를 생성하여 문자열 형태로 클라이언트에 내려줍니다.
서버에서 완성된 형태의 HTML 페이지를 렌더링 한다고 볼 수 있죠.
따라서 클라이언트는 이러한 완성된 HTML을 받아 바로 렌더함으로써 더욱 빠른 초기 페이지 로드를 보장할 수 있게됩니다.
그렇다면 이러한 서버 사이드 렌더링은 내부적으로 어떠한 원리에 의해 수행되는 것일까요?
우선 리액트의 서버 사이드 렌더링 메서드에 대해 알아봅시다.
서버 사이드 렌더링 메서드
리액트도 서버 사이드 렌더링이 가능하다는 사실 알고 계셨나요?
리액트도 사실 서버 사이드 렌더링을 수행하기 위한 메서드들이 존재합니다.
Nextjs가 Reactjs 기반의 서버사이드렌더링 프레임워크인 이유가 이러한 리액트의 SSR 메서드를 내부적으로 사용하기 때문인데요.
리액트를 활용해 SSR 설정을 하는 것은 매우 복잡하고 생산성이 떨어지는 일이므로 Nextjs라는 프레임워크를 사용하는 것입니다.
그렇다면 서버 사이드 렌더링은 어떻게 HTML문자열을 클라이언트로 내려줄 수 있을까?
RenderToString
인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수입니다.
서버 사이드 렌더링 구현을 위한 가장 기초적인 API이빈다.
서버가 리액트 컴포넌트를 HTML 문자열로 바꿀 수 있게 해줍니다.
RenderToNodeStream
RenderToString과 유사한 역할이지만 결과물의 형태만 다릅니다.
서버의 Node.js 환경에서 실행가능한 함수로써
렌더링 결과물을 Node.js의 ReadableStream 형태로 반환하는데요,
ReadableStream은 HTML문자열을 uft-8 형태로 인코딩한 바이트 스트림으로써
서버 사이드 렌더링 결과물을 작은 단위의 Chunk형태로 나누어 브라우저에 완성되는 순으로 조금씩 내려줄 수 있게합니다.
이러한 방식을 사용하면 HTML의 크기가 매우 큰 경우에도 서버의 메모리를 최소한으로 사용해 렌더링 함으로써
서버의 렌더링 부담을 덜 수 있게죠?
Nextjs는 내부적으로 RenderToNodeStream을 사용해 서버 사이드 렌더링을 지원하고 있습니다.
hydrate() vs render()
hydrate함수는 renderTostring,renderToNodeStream으로 생성한 HTML 콘텐츠에 이벤트 핸들러나 이벤트를 붙이는 역할을 합니다.
import * as ReactDOM from 'react-dom'
import App from './App'
const element = document.getElementById(containerId)
//* continerId : 서버에서 렌더링 된 HTML의 특정 위치
ReactDOM.hydrate(<App />, element)
반면,
render 함수는 컴포넌트와 HTML요소를 인수로 받아 HTML요소 내부에 해당 컴포넌트를 렌더링하고,
이에 이벤트핸들러를 붙이는 작업까지 한번에 수행합니다.
(* 함수형 컴포넌트 호출은 클래스 컴포넌트의 render 메서드의 기능: <React Element 반환>을 동일하게 수행합니다. )
import * as ReactDOM from 'react-dom'
import App from './App'
const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)
이렇듯 render메서드와 hydrate메서드는 'react-dom'에서 import하는 메서드로써 각 메서드의 활용 형태 비슷한 유사점이 있습니다.
render와의 차이점은
hydrate는 기본적으로 이미 렌더링된 HTML이 있다는 가정하에 작업이 수행되고,
이 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 실행한다는 것입니다.
즉, hydrate는 서버에서 만들어진 HTML이 클라이언트에 이미 동일한 형태로 전달됐음을 기대하고
이에 하이드레이션을 적용하는 함수임을 뜻합니다.
Reference