4. ServerSide Rendering

서버가 React로 HTML을 먼저 만들고, 브라우저는 그 HTML을 받아서 hydrate로 React 앱으로 되살린다


1. 왜 다시 서버 사이드 렌더링(SSR)인가?

1.1 SPA의 성공과 동시에 드러난 한계

SPA(Single Page Application)는 렌더링과 라우팅을 브라우저의 자바스크립트에 맡겨 페이지 전환이 빠르고 부드러운 사용자 경험을 제공하며 웹 개발의 표준이 되었습니다.

하지만 서비스 규모가 커질수록, SPA는 다음과 같은 구조적인 한계를 드러냅니다.

  • 초기 로딩 시 JS 번들 다운로드·실행 비용 증가
  • 콘텐츠가 JS 실행 이후에 생성되어 SEO·SNS 미리보기 대응의 어려움
  • 저사양 디바이스에서 렌더링 성능 저하

즉, SPA는 “인터랙션”에는 강했지만 첫 화면과 접근성 측면에서는 약점을 가지게 되었습니다.


1.2 SSR이 다시 주목받은 이유

이러한 문제를 해결하기 위해 다시 주목받은 방식이 서버 사이드 렌더링(Server Side Rendering, SSR) 입니다. SSR은 리액트 컴포넌트를 서버에서 실행해 HTML을 미리 완성한 뒤 브라우저로 전달합니다.

그 결과, 사용자는 자바스크립트가 모두 로드되기 전에도 즉시 화면을 확인할 수 있습니다.

  • 빠른 초기 화면 표시
  • 검색 엔진 및 SNS 미리보기 대응
  • 디바이스 성능 차이에 따른 UX 편차 완화


1.3 SSR과 SPA는 왜 함께 사용되는가

하지만 중요한 점은, SSR이 SPA를 대체하지는 않는다는 것입니다.

  1. 첫 요청 → 서버에서 HTML을 생성해 빠르게 화면 제공 (SSR)
  2. 자바스크립트 로드 이후hydrate를 통해 이벤트와 상태 연결
  3. 이후 페이지 전환 → 다시 SPA 방식으로 클라이언트에서 처리

즉, 실제 서비스 환경에서

SSR과 SPA는 경쟁 관계가 아니라, 역할을 나눠 맡는 관계입니다.

SSR → 빠른 첫 화면, SEO, 접근성 / SPA → 부드러운 인터랙션, 빠른 페이지 전환, 풍부한 UX



2. 리액트의 SSR 핵심 API 살펴보기

2.1 renderToString

리액트 컴포넌트를 HTML 문자열로 변환하는 가장 기본적인 SSR 방식입니다. 생성된 HTML은 클라이언트에서 Hydration이 가능합니다.

import { renderToString } from 'react-dom/server';

function App() {
  return <h1>Hello SSR</h1>;
}

const html = renderToString(<App />);


2.2 renderToStaticMarkup

리액트 관련 속성을 제거한 완전한 정적 HTML을 생성합니다. 클라이언트에서 다시 리액트로 이어지지 않는 콘텐츠에 적합합니다.

import { renderToStaticMarkup } from 'react-dom/server';

function App() {
  return <h1>Hello Static</h1>;
}

const html = renderToStaticMarkup(<App />);


2.3 renderToNodeStream

HTML을 한 번에 생성하지 않고 스트림 방식으로 나누어 전송합니다. 페이지가 클수록 사용자 체감 속도가 크게 개선됩니다.

import { renderToNodeStream } from 'react-dom/server';
import express from 'express';

const app = express();

app.get('/', (req, res) => {
  res.write('<html><body>');
  const stream = renderToNodeStream(<App />);
  stream.pipe(res, { end: false });

  stream.on('end', () => {
    res.write('</body></html>');
    res.end();
  });
});


2.4 hydrate

서버에서 내려온 정적인 HTML에 이벤트와 상태를 연결하여 완전한 리액트 애플리케이션으로 만드는 단계입니다.

import { hydrate } from 'react-dom';
import App from './App';

hydrate(<App />, document.getElementById('root'));



3. 서버 사이드 렌더링 예제 프로젝트

3.1 브라우저 세계 (클라이언트에서 실행되는 영역)

브라우저는 화면을 새로 그리지 않고, 이미 있는 HTML에 이벤트만 연결한다. 서버가 만든 HTML을 화면에 보여주고, JavaScript로 인터랙션을 활성화합니다.

  • index.html 👉 “빈 무대”
    • 기본 HTML 틀 역할
    • 서버가 만든 HTML을 끼워 넣을 자리(__placeholder__)와 React 및 클라이언트 스크립트를 불러오는 역할
  • index.tsx 👉 “화면에 생명을 불어넣는 버튼”
    • 브라우저에서 실행되는 진입점
    • hydrate()를 호출해 서버 HTML에 이벤트와 상태를 연결
  • App.tsx, Todo.tsx 👉 “화면 설계도”
    • 실제 화면을 구성하는 React 컴포넌트
    • 서버와 브라우저 양쪽에서 동일하게 실행


3.2 서버 세계 (Node.js에서 실행되는 영역)

서버는 HTML만 만든다 (이벤트 ❌, 클릭 ❌, useEffect ❌)

  • server.ts 👉 “공연 총괄 매니저”
    • 서버의 중심 파일
    • HTTP 서버 실행 + 라우팅 + SSR 수행
  • createServer 👉 “공연장 입구”
    • Node.js 기본 HTTP 서버 생성
    • “요청을 받을 준비”를 하는 역할
  • serverHandler 👉 “안내 데스크”
    • 요청 URL에 따라 처리 분기
    • /, /stream, /browser.js 등을 구분

서버의 두 가지 SSR 방식

  • / 라우터 (renderToString) 👉 “완성된 화면 한 번에 전달”
    • React 컴포넌트를 한 번에 HTML 문자열로 생성
    • 완성된 화면을 한 번에 전달
  • /stream 라우터 (renderToNodeStream) 👉 “화면을 조금씩 보여줌”
    • HTML을 조각(chunk) 단위로 생성해서 즉시 전송
    • 사용자는 더 빨리 화면을 보기 시작


3.3 빌드 세계 (webpack)

실행 환경이 다르기 때문에 서버용 코드와 브라우저용 코드는 절대 한 번들일 수 없다.

SSR 프로젝트에서는 번들이 반드시 2개 필요합니다.

  • webpack.config.js 👉 “짐 정리 담당자”

    • 브라우저 번들 → browser.js (hydrate 담당)

    • 서버 번들 → server.ts 실행용



reference: 모던리액트 Deep Dive 4장. 서버 사이드 렌더링