모던 리액트 딥다이브 Week3 - Chapter4

지코·2025년 9월 22일

FE STUDY

목록 보기
5/7
post-thumbnail
본 포스팅 시리즈는 📚'모던 리액트 Deep Dive'를 주간 별로 1장씩 공부하며
* 새롭게 알게 된 것들
* 평소 알고 있다고 생각했지만 이번에 제대로 알게 된 것들
* 궁금한 부분에 대해 딥다이브한 것들
등을 기재하기 위해 시작되었다.

📖 4장. 서버 사이드 렌더링


싱글 페이지 애플리케이션이란? (P. 253)

📄 Single Page Application; SPA
: 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트에 의존하는 방식.

  • 최초 렌더링에는 첫 페이지에서 필요한 데이터를 모두 불러온다. (HTML 포함)
  • 이후에는 페이지 전환을 위한 모든 작업이 자바스크립트와 history.pushStatehistory.replaceState를 통해 이뤄진다. (HTML 포함 ❌)

🔮 장점

  • 한번 로딩된 이후에는 서버를 거쳐 필요한 리소스를 받아올 일이 적어지기 때문에, 사용자에게 훌륭한 UI/UX를 제공한다.

🧟 단점

  • 최초에 로딩해야 할 자바스크립트 리소스가 커진다.

서버 사이드 렌더링이란? (p. 263)

📡 Server Side Rendering, SSR
: 최초에 사용자에게 보여줄 페이지를 서버에서 렌더링해 빠르게 사용자에게 화면을 제공하는 방식.

🔮 장점

  • 데이터를 서버에서 제공하기 때문에, 사용자 기기의 성능에 영향을 받지 않고 비교적 안정적인 렌더링이 가능하다.
  • FCP(First Contentful Paint), 사용자가 최초 페이지에 진입했을 때 페이지에 유의미한 정보가 그려지는 시간이 더 빨라질 수 있다.
  • 검색 엔진 최적화에 유용하다. 검색 엔진에 제공할 정보를 서버에서 가공해서 HTML 응답으로 제공할 수 있으므로 검색 엔진 최적화에 대응하기가 매우 용이하다.
  • 누적 레이아웃 이동(Cumulative Layout Shift)을 줄일 수 있다. ➡️ 완전히 자유로운 것은 아니다❗️
  • 인증 혹은 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에 제공해, 보안 위협을 피할 수 있다.

🧟 단점

  • 모든 요청이 완료되기 전까지 페이지가 렌더링되지 않을 것이므로, 최초 페이지 다운로드가 굉장히 느려질 수 있다.
    • 애플리케이션의 규모가 커지고 작업이 복잡해지고, 이에 따라 다양한 요청에 얽혀있어 병목 현상이 심해진다면, 때로는 SSR이 더 안 좋은 사용자 경험을 제공할 수도 있다.
  • 소스 코드를 작성할 때 항상 서버를 고려해야 한다.
    • 특히 브라우저에만 있는 전역 객체인 window, sessionStorage의 사용을 고려해야 한다.
    • 불가피하게 사용해야 하는 상황이라면 해당 코드가 서버 사이드에서 실행되지 않도록 해야 한다.
  • 적절한 서버가 구축되어 있어야 한다.

SSR을 실행할 때 사용되는 API들 (p. 270 - 276)

리액트는 리액트 애플리케이션을 브라우저 자바스크립트 환경에서 렌더링할 수 있는 방법 뿐만 아니라 서버에서 렌더링할 수 있는 API도 제공하고 있는데, 대표적인 것들을 한 번 살펴보자.

📨 renderToString

renderToString인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수이다. SSR을 구현하는 데 가장 기초적인 API이며, 최초의 페이지를 HTML로 먼저 렌더링하는 역할을 한다.

import ReactDOMServer from 'react-dom/server'

const result = ReactDOMServer.rednerToString(
  React.createElement('div', { id: 'root' }, <SampleComponent />)
)

위 코드에서 result는 다음과 같은 HTML 문자열을 반환한다.

<div id="root" data-reactroot="">
  <!-- ... -->
</div>

반환된 문자열은 다음과 같은 특징을 가진다.

  • useEffect 같은 훅과 handleClick 같은 이벤트 핸들러는 결과물에 포함되지 않는다. 이는 renderToString 이 인수로 주어진 리액트 컴포넌트를 기준으로 브라우저가 렌더링할 수 있는 HTML을 빠르게 제공하는 데 목적이 있는 함수이기 때문이다.
  • 실제로 웹페이지가 사용자와 인터랙션할 준비가 되기 위해서는 관련된 자바스크립트 코드를 모두 다운로드, 파싱, 실행하는 과정을 거쳐야 한다.
  • data-reactroot 속성은 리액트 컴포넌트에서 루트 엘리먼트가 무엇인지 식별하는 역할을 하며, 이후에 자바스크립트를 실행하기 위한 hydrate 함수에서 루트를 식별하는 기준점이 된다.

📨 renderToStaticMarkup

renderToStaticMarkup 은 리액트 컴포넌트를 기준으로 HTML 문자열을 만든다는 점에서 renderToString 함수와 동일하다.

한 가지 차이점은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다는 점이다. 이것은 결과물인 HTML의 크기를 아주 약간이라도 줄일 수 있다는 장점이 있다.

renderToString 대신 renderToStaticMarkup 함수를 사용해서 렌더링하면, 클라이언트에서는 리액트에서 제공하는 브라우저 API를 절대로 실행할 수 없다. renderToStaticMarkup 의 결과물은 hydrate 함수를 수행하지 않는다는 가정 하에 순수한 HTML만 반환하기 때문이다.

📨 renderToNodeStream

renderToNodeStreamrenderToString 함수의 결과물과 완전히 동일한 결과물을 만들지만 두 가지 차이점이 있다.

  • renderToNodeStream 은 브라우저에서 사용하는 것이 완전히 불가능하다.
  • renderToNodeStream 의 결과물은 Node.js의 ReadableStream이다. ReadableStream은 utf-8로 인코딩된 바이트 스트림으로, Node.js, Deno, Bun 같은 서버 환경에서만 사용할 수 있다.

🤔 그럼 renderToNodeStream 은 언제 사용하는 걸까?

renderToString으로 생성해야 하는 HTML의 크기가 매우 크다면❓ 크기가 큰 문자열을 한 번에 메모리에 올려두고 응답을 수행해야 하기 때문에 Node.js가 실행되는 서버에 큰 부담이 될 수 있다.

따라서 데이터를 청크(chunk) 로 분할해 조금씩 가져오는 방식인 스트림 방식을 활용해, 큰 크기의 데이터를 순차적으로 처리하는 renderToNodeStream 을 사용한다.

📨 renderToStaticNodeStream

renderToStaticNodeStream 또한 renderToNodeStream과 결과물은 동일하나, 리액트 자바스크립트에 필요한 리액트 속성이 제공되지 않는다. 마찬가지로 hydrate를 할 필요가 없는 순수 HTML 결과물이 필요할 때 사용하는 메서드이다.

hydrate 함수와 render 함수의 차이 (p. 276-278)

hydrate 함수는 renderToString과 renderToNodeStream으로 생성된 HTML 컨텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할을 한다.

주로 CRA(Create React App)으로 생성된 프로젝트에서 사용되는 render 함수는 hydrate 함수와 비슷하게 브라우저에서만 사용되는 메서드이다.

render 는 다음과 같이 사용된다.

import * as ReactDOM from 'react-dom'
import App from './App'

const rootElement = document.getElementById('root')

ReactDOM.render(<App />, rootElement)

render 함수는 렌더링할 컴포넌트와 렌더링 위치인 HTML 요소를 인수로 받는다. HTML 요소에 해당 컴포넌트를 렌더링하며, 여기에 이벤트 핸들러를 붙이는 작업까지 수행한다. 따라서 render 함수를 통해서는 리액트를 기반으로 한 온전한 웹페이지를 만드는 데 필요한 모든 작업 수행이 가능하다.

import * as ReactDOM from 'react-dom'
import App from './App'
// containerID를 가리키는 element는 서버에서 렌더링된 HTML의 특정 위치를 의미.
const element = document.getElementById(containerId)
// 해당 element를 기준으로 리액트 이벤트 핸들러를 붙인다.
ReactDOM.hydrate(<App />, element)

그렇다면 hydrate를 사용한 예제를 살펴보자.
render와의 가장 큰 차이점은 hydrate기본적으로 이미 렌더링된 HTML이 있다는 가정 하에 작업이 수행되고, 이벤트를 붙이는 작업만 실행한다는 것이다.

따라서 이 예제 코드에서는 element 내부에는 App 컴포넌트를 렌더링한 정보가 사전에 포함되어있었어야 hydrate 함수를 실행할 수 있다. 이에 비해 render 함수는 아무것도 없는 빈 HTML에 정보를 렌더링한다.

Reference

📚 모던 리액트 Deep Dive

profile
꾸준함이 무기

0개의 댓글