SSR이 뭔가요?
SEO가 뭔가요?
TTV, TTI 뭔가요?
서버사이드 렌더링을 지원하기 위한 리액트 API를 알고 있나요 ?
하이드레이션에 대해 알고 있나요?
질문에 대해 답하기 위해 공부해보겠습니다.
최근 프론트엔드 개발에서 사실상의 표준으로 자리매김 하는 기술이 있다고 합니다.
바로 SSR, Server Side Rendering인데요. Next.js를 비롯한 다양한 프레임워크에서 SSR을 손쉽게 구축할 수 있는 솔루션을 제공하고 있습니다.
SSR(Server Side Rendering)은 브라우저가 아닌 서버에서 HTML을 렌더링한 뒤, 완성된 HTML을 클라이언트에게 전달하는 방식입니다.
현대의 SSR은 “첫 HTML 렌더링을 서버에서 처리하고, 이후의 렌더링 사이클은 클라이언트에서 처리”하는 하이브리드 형태의 SSR을 가리킵니다.
Next.js, Astro 등의 현대적인 웹 프레임워크는 기본적으로 제공하는 아키텍처입니다. Static Site Generation이나 Dynamic SSR처럼 다양한 방식이 있습니다.
SSR은 “첫 HTML 렌더링을 서버에서 처리”하기 때문에, 사용자의 화면에 컨텐츠가 그려지는데 걸리는 시간(FCP, First Contentful Paint)가 더 짧습니다.
출처 : https://toss.tech/article/ssr-server

사용자의 화면에 JavaScript 번들이 모두 다운로드된 다음 첫 렌더링을 실행하면서 인증, 데이터 요청 등의 과정을 거치다보니 화면이 렌더링되는 시간이 상대적으로 길었습니다.

인증과 데이터 처리의 첫 과정이 서버에서 먼저 모두 이루어진 다음 사용자는 완성된 HTML을 받아보면서 로딩 속도를 상당히 감축할 수 있습니다.
SEO는 Search Engine Optimization 의 약자로 검색 엔진 최적화를 말합니다.
-> 검색 엔진(예: Google)은 웹사이트를 크롤링(crawling) 하고
그 내용을 인덱싱(indexing) 해서 검색 결과에 노출시켜줍니다.
그런데 CSR은 자바스크립트로 HTML을 동적으로 만들기 때문에, 즉
초기 로딩 시 브라우저에 빈 HTML 틀만 먼저 전달하고
내용은 자바스크립트가 실행된 후에야 채워집니다.
<!-- 사용자가 처음 받는 CSR HTML -->
<body>
<div id="root"></div>
<script src="/main.js"></script>
</body>
이 시점에서 #root 안에는 아무 내용도 없습니다.
브라우저가 JS 파일을 실행하고 나서야 내용이 들어가기 때문에 일부 똑똑한 검색엔진(Google)은 JS 실행까지 기다려주지만,모든 크롤러가 그렇진 않습니다.
그러나 SSR의 경우에는 완성된 HTML을 브라우저에 주니까 크롤러가 바로 콘텐츠를 읽고 인덱싱이 가능하고 결과적으로 검색 노출(SEO) 이 잘됩니다.
SSR과 Hydration을 이야기할 때 중요한 성능 지표가 있습니다.
바로 TTV(Time To Visually Ready), TTI(Time To Interactive)입니다.
서버가 HTML을 미리 만들어 주기 때문에 콘텐츠가 빠르게 눈에 보입니다.
SSR의 장점 중 하나입니다.
콘텐츠는 보이지만, 자바스크립트를 통해 이벤트 리스너나 상태 등을 연결하는 Hydration 과정이 끝나야 실제로 동작합니다.
이 때문에 사용자가 클릭했을 때 반응이 없는 것처럼 느껴질 수 있습니다.
특히 저사양 기기에서는 Hydration 비용이 커져 TTI가 더욱 늦어집니다.
서버에서 미리 렌더링한 HTML을 클라이언트에서 자바스크립트를 통해 동적으로 활성화하는 과정을 말합니다.
SSR(서버 사이드 렌더링) 또는 SSG(정적 사이트 생성)를 통해 서버에서 생성된 HTML은 브라우저에서 시각적으로 완성된 페이지로 보이지만, 이 HTML은 단순히 보이는 것만 가능하며 동작하지는 않습니다.
이후 React 같은 프론트엔드 프레임워크가 자바스크립트를 실행하여, 서버에서 생성된 HTML 구조와 연결하고 이벤트 리스너 등을 부여해서 살아있는 React앱을 만드는 과정이 필요한데, 이를 수분을 충전시켜서 살아나게 만드는 것처럼 보인다해서 Hydration(하이드레이션)이라고 합니다.
서버에서 응답한 HTML (SSR 결과):
<div id="root">
<button>클릭</button>
</div>
이 시점에서는 버튼이 보일 뿐이며, 실제로 클릭해도 아무 동작도 하지 않습니다.
클라이언트 측에서 JS가 실행되어 하이드레이션 수행:
ReactDOM.hydrate(<App />, document.getElementById('root'));
이후 버튼 클릭 시 이벤트가 작동하고, 상태 변화가 반영되는 완전한 React 앱으로 동작하게 됩니다.
SSR을 할 때 React에서 HTML을 문자열로 미리 렌더링해서 서버에서 응답할 수 있도록 도와주는 API들이 있다. 이는 React 18 전후로 크게 바뀌었다.
renderToStringuseEffect, onClick 등의 클라이언트 이벤트는 이 결과물에 포함되지 않습니다.React-specific 속성이란?
React가 클라이언트에서 Hydration(하이드레이션) 을 정확히 수행하기 위해
HTML 태그에 추가로 삽입하는 속성들을 말합니다.
이 속성들은 브라우저에 직접 보여지는 콘텐츠에는 영향을 주지 않지만,
React 내부에서 DOM과 가상 DOM을 연결하고 이벤트를 붙이기 위한 중요한 힌트 역할을 합니다.
data-reactroot 등의 속성을 통해 가상 DOM과 실제 DOM을 연결합니다.| 속성 | 설명 |
|---|---|
data-reactroot | 컴포넌트의 루트 엘리먼트를 식별하며, Hydration 시 기준이 되는 DOM입니다. |
data-reactid | (React 15 이하) DOM 요소 식별용 내부 ID입니다. React 16 이후 제거됨. |
__reactProps$ | (React 18) 이벤트 핸들러 등 Hydration 정보가 임시로 저장됨. Hydration 후 제거됩니다. |
renderToStaticMarkuprenderToString과 매우 유사하지만, React-specific 속성 없이 HTML을 반환합니다.renderToNodeStream (React 17 이하)renderToStaticNodeStreamrenderToNodeStream과 동일한 스트리밍 기반이지만,renderToStaticMarkup처럼 React-specific 속성이 제거된 HTML 스트림을 반환합니다.hydrate (브라우저에서 실행)renderToString 등의 결과물 위에 이벤트 핸들러와 상태를 연결합니다.import { hydrate } from "react-dom";
hydrate(<App />, document.getElementById('root'));
브라우저에서만 사용되며, 기존 HTML과 React 상태를 연결해 완전한 SPA로 동작하게 만듭니다.
import { renderToPipeableStream } from "react-dom/server";
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
pipe(response); // 스트리밍 전송 시작
},
onError(err) {
console.error("SSR Error:", err);
}
});
type RenderToPipeableStreamOptions = {
onShellReady?: () => void;
onAllReady?: () => void;
onError?: (error: unknown) => void;
bootstrapScripts?: string[];
};
onShellReady?: () => void
onAllReady?: () => void
onError?: (error: unknown) => void
bootstrapScripts?: string[]
<script src="..."> 형식으로 클라이언트에 삽입됨bootstrapScripts: ["/static/client.bundle.js"]
// 이는 서버에서 HTML을 보낼 때 아래처럼 자동으로 삽입됨:
<script src="/static/client.bundle.js" async></script>
RenderToPipeableStreamOptions는 React 18의 Streaming SSR에서
스트리밍 시작 시점, 전체 완료 시점, 에러 처리, 초기 스크립트 삽입을 제어할 수 있도록 도와주는 옵션 객체
import { renderToReadableStream } from "react-dom/server";
async function render() {
const stream = await renderToReadableStream(<div>Hello</div>);
const reader = stream.getReader();
const result = await reader.read();
console.log(new TextDecoder().decode(result.value));
}
render();
type RenderToReadableStreamOptions = {
bootstrapScripts?: string[];
bootstrapModules?: string[];
onError?: (error: unknown) => void;
signal?: AbortSignal;
nonce?: string;
};
bootstrapScripts : 클라이언트에서 사용할 JS 파일 경로 (기본 - <script src="..."> 형태 삽입됨) bootstrapModules : ESModules 형태로 삽입할 JS (type="module") onError : 렌더링 중 오류 발생 시 실행될 콜백 signal : AbortController와 연결하여 렌더링도중 중단(abort)할 수 있음 const controller = new AbortController();
const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
});
// 일정 조건에서 취소하고 싶을 때
controller.abort(); // SSR 중단!nonce : CSP(Content Security Policy) 대응을 위한 nonce 속성 추가용 CSP (Content Security Policy) 라는 웹 보안 기능과 관련된 옵션이야.
서버에서 <script> 태그를 만들 때 nonce="abc123"처럼 값을 붙이면,
브라우저는 CSP 정책에 따라 nonce가 일치하는 스크립트만 실행해.=
React SSR이 <script> 태그를 자동 삽입할 때 이 값을 추가해서 보안 정책을 통과할 수 있게 도와주는 거야.
nonce는 특정 <script> 태그가 신뢰된 코드인지 확인하는 값으로 사용돼.
```renderToReadableStream()에서는
스트리밍 시작 시점을 직접 제어하지 않기 때문에, onShellReady, onAllReady 같은 콜백은 존재하지 않습니다.
이 API는 Promise 기반이고, .then() 또는 await으로 스트림을 받을 수 있습니다.
| API | 특징 | 사용 환경 |
|---|---|---|
renderToString | React-specific 속성이 포함된 HTML 문자열 반환 | 모든 SSR |
renderToStaticMarkup | React 속성이 없는 순수 HTML 반환 | 정적 콘텐츠 |
renderToNodeStream | HTML을 스트리밍 형태로 반환 (Node.js 전용) | React 17 이하 |
renderToStaticNodeStream | React 속성 없는 스트리밍 HTML 반환 | 정적 스트리밍 |
hydrate | 클라이언트에서 HTML에 React 기능을 연결 | 브라우저 |
renderToPipeableStream | React 18의 SSR 스트리밍 API | Node.js |
renderToReadableStream | Web Streams API 기반 SSR | Edge / Web 환경 |
React에서 SSR을 구현할 때는 renderToString, renderToPipeableStream 등의 API로 HTML을 만들고,
클라이언트에서 hydrate를 통해 이를 인터랙티브한 React 앱으로 연결합니다.
React 18 이후에는 스트리밍 기반 SSR이 핵심 트렌드로 자리잡고 있습니다.