SSR은 무엇이고 언제 써야 할까: CSR, 하이드레이션, Next.js까지 현재 기준으로 정리
빠른 답
- SSR은 첫 화면 HTML을 서버에서 먼저 만들어 보내는 방식이다. 다만 버튼과 입력이 실제로 반응하는 시점은 하이드레이션 이후일 수 있다.
- 브라우저는 SSR 페이지도 DOM, CSSOM, 렌더 트리, 레이아웃, 페인트, 컴포지팅을 그대로 거친다. SSR은 브라우저 렌더링을 없애는 기술이 아니라 시작 시점을 바꾸는 쪽에 가깝다.
- SEO와 초기 콘텐츠 노출이 중요하면 SSR이 도움이 되지만, 요청마다 HTML을 만들면 TTFB와 서버 비용이 늘 수 있다.
- 2026-04-07 기준 Next.js는
getServerSideProps만으로 SSR을 설명하기 어렵다. App Router의 서버 컴포넌트, 스트리밍, 캐시 모델까지 함께 봐야 현재 동작과 가깝다.
목차
시간 흐름으로 이해하기
흐름으로 보기
이 순서를 먼저 잡아두면 SSR이 바꾸는 구간과 바꾸지 않는 구간이 자연스럽게 나뉜다. SSR은 서버가 HTML을 먼저 준비해 준다는 점에서 시작이 다를 뿐이고, 브라우저가 화면을 픽셀로 그리는 파이프라인 자체는 그대로 남아 있다.
SSR이 다시 중요해진 이유와 흔한 오해
SSR은 흔히 “서버에서 완성된 정적 HTML을 내려주는 방식”으로 압축해서 설명되지만, 이 표현은 SSG와 섞여 보일 수 있다. SSR의 핵심은 정적 HTML이 아니라 요청 시점에 HTML을 만든다는 데 있다. 사용자별 데이터, 로그인 상태, 실시간 가격처럼 시점 의존성이 있는 페이지에서 이 차이가 드러난다.
SPA와 CSR이 널리 쓰이면서 한동안은 빈 문서 뼈대와 큰 자바스크립트 번들을 받아 브라우저가 화면을 채우는 방식이 익숙해졌다. 이 접근은 화면 전환 경험과 개발 생산성 면에서 장점이 있지만, 첫 진입에서 번들 다운로드와 실행 비용이 커질수록 첫 콘텐츠 노출이 늦어질 수 있다. 저사양 기기나 느린 네트워크에서는 이 차이가 더 크게 보인다.
다만 “SSR이면 항상 빠르다”거나 “CSR은 SEO가 안 된다”는 식의 설명은 현재 기준으로는 거칠다. Google Search는 자바스크립트를 실행해 렌더링할 수 있고, 공식 문서도 서버 렌더링이나 프리렌더링이 사용자와 크롤러 모두에게 여전히 도움이 된다고 설명한다. 결국 비교 대상은 기술 이름보다도, 언제 HTML이 보이는지와 언제 상호작용이 살아나는지에 가깝다.
SSR, CSR, SSG를 먼저 구분해 두기
- SSR: 요청이 들어올 때마다 서버가 HTML을 만든다. 요청 시점의 데이터가 중요한 화면에 어울린다.
- CSR: 서버는 최소한의 HTML과 자바스크립트를 보내고, 브라우저가 실행하면서 화면을 채운다. 첫 진입의 실행 비용은 있지만 이후 전환은 가볍게 느껴질 수 있다.
- SSG: 빌드 시점에 HTML을 미리 만들어 둔다. 자주 바뀌지 않는 공개 페이지에서 효율이 좋다.
예전에는 페이지 단위로 SSR, CSR, SSG를 깔끔하게 나눠 설명해도 큰 무리가 없었다. 지금은 같은 라우트 안에서도 정적인 셸, 요청 시점 데이터, 클라이언트 상호작용 영역이 함께 섞인다. 그래서 “이 페이지가 SSR인가”보다 “어디까지를 서버에서 먼저 보내고, 어디부터를 브라우저에서 붙일 것인가”가 더 설명력이 있다.
요청부터 화면 반응까지: HTML, DOM, CSSOM, 하이드레이션
서버가 먼저 하는 일은 HTML을 만드는 것이다. 이 시점이 길어지면 사용자는 아직 아무것도 받지 못했기 때문에 TTFB가 늘어난다. SSR이 첫 화면에 유리하다고 해도, 서버 계산이 길어지면 체감 속도 이점이 바로 줄어든다.
브라우저가 HTML을 받으면 곧바로 파싱을 시작한다. HTML은 DOM으로, CSS는 CSSOM으로 이어진다. DOM과 CSSOM이 준비되면 렌더 트리가 만들어지고, 각 요소의 크기와 위치를 계산하는 레이아웃이 진행된다. 그다음 픽셀을 칠하는 페인트와 레이어를 합치는 컴포지팅이 이어진다. MDN의 Critical Rendering Path 문서가 설명하는 흐름이 바로 이 구간이다.
여기서 중요한 점은 SSR이 이 파이프라인을 생략하지 않는다는 것이다. 서버가 HTML을 먼저 보냈더라도 CSS가 무겁거나 DOM이 지나치게 크면 레이아웃과 페인트 비용은 그대로 발생한다. 첫 화면이 빨리 “보이는 것”과 그 화면이 “가볍게 그려지는 것”은 서로 다른 문제다.
하이드레이션은 그다음 단계다. React는 이미 존재하는 DOM을 다시 만드는 대신, 서버가 만든 마크업에 클라이언트 로직과 이벤트 핸들러를 연결한다. 그래서 화면은 먼저 보여도 버튼이 바로 동작하지 않는 짧은 구간이 생길 수 있다. 사용자는 콘텐츠를 읽을 수 있지만, 상호작용은 아직 준비되지 않은 상태다.
이 흐름을 프레임워크 없이 아주 단순하게 쓰면 아래와 비슷하다. Node.js 서버에서는 renderToPipeableStream, 클라이언트에서는 hydrateRoot를 사용한다.
// server.ts
import express from 'express'
import { renderToPipeableStream } from 'react-dom/server'
import App from './App'
const app = express()
app.get('/', (req, res) => {
let didError = false
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
res.statusCode = didError ? 500 : 200
res.setHeader('content-type', 'text/html; charset=utf-8')
pipe(res)
},
onError(error) {
didError = true
console.error(error)
},
})
})
// client.tsx
import { hydrateRoot } from 'react-dom/client'
import App from './App'
hydrateRoot(document.getElementById('root')!, <App />)
이 코드에서 보이는 차이는 분명하다. 서버는 HTML을 먼저 흘려보내고, 브라우저는 그 결과를 즉시 그릴 수 있다. 하지만 상호작용이 살아나는 시점은 별도의 자바스크립트 로드와 하이드레이션에 달려 있다.
브라우저 렌더링 관점에서 보면 왜 SSR도 느릴 수 있을까
SSR 페이지가 느리게 느껴질 때는 렌더링 비용이 어디에 몰려 있는지 나눠서 보는 편이 좋다.
- DOM 크기: 노드 수가 많으면 스타일 계산과 레이아웃 비용이 커진다.
- CSSOM 비용: 큰 스타일시트와 복잡한 선택자는 첫 렌더를 늦출 수 있다.
- 레이아웃 변경:
top,left,width,height같은 속성 변화는 레이아웃과 페인트를 다시 부르기 쉽다. - 페인트 비용: 큰 그림자, 블러, 복잡한 배경은 픽셀을 다시 칠하는 비용이 커진다.
- 컴포지팅: 별도 레이어가 많아지면 합성 단계가 복잡해질 수 있다.
- 하이드레이션 자바스크립트: 화면은 보이지만, 실행해야 할 번들이 크면 입력 반응이 늦어진다.
특히 애니메이션과 스크롤 성능을 볼 때는 컴포지팅을 따로 볼 필요가 있다. transform과 opacity는 레이아웃을 건드리지 않고 컴포지팅 단계에 머무를 가능성이 크지만, 위치와 크기를 직접 바꾸는 방식은 레이아웃과 페인트를 자주 다시 일으킨다. SSR을 적용한 뒤에도 버벅임이 남는다면, 서버보다 브라우저 파이프라인 후반부가 병목인 경우가 적지 않다.
Next.js 현재 기준으로 다시 보기
2026-04-07 기준 React 공식 문서는 최신 버전을 19.2 기준으로 안내하고, hydrate는 제거된 API로 분류한다. Next.js는 2026-03-18에 16.2를 공개했고, 현재 문서는 16 계열 App Router 설명을 중심으로 이어진다. 이 차이 때문에 오래된 글을 그대로 읽으면 SSR의 기준선이 달라 보일 수 있다.
Pages Router에서는 SSR이 비교적 전통적인 모습이다. 요청마다 getServerSideProps가 실행되고, 페이지 HTML이 만들어진다.
// pages/products/[id].tsx
export async function getServerSideProps({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`).then((r) => r.json())
return {
props: { product },
}
}
export default function ProductPage({ product }) {
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
</section>
)
}
App Router에서는 생각하는 단위가 달라진다. page와 layout은 기본적으로 서버 컴포넌트이고, 상호작용이 필요한 부분만 클라이언트 컴포넌트로 경계를 그린다. 첫 로드에서는 HTML이 빠른 비상호작용 미리보기 역할을 하고, 이어서 RSC Payload 정합과 클라이언트 컴포넌트 하이드레이션이 진행된다.
// app/products/[id]/page.tsx
import BuyButton from './buy-button'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const product = await fetch(`https://api.example.com/products/${id}`, {
cache: 'no-store',
}).then((r) => r.json())
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<BuyButton productId={product.id} />
</section>
)
}
// app/products/[id]/buy-button.tsx
'use client'
export default function BuyButton({ productId }: { productId: string }) {
return <button onClick={() => console.log('buy', productId)}>구매하기</button>
}
같은 “SSR”이라는 말로 묶여도 두 모델의 감각은 다르다. Pages Router는 요청 시점 HTML 생성이라는 설명이 잘 맞고, App Router는 서버 컴포넌트와 클라이언트 경계, 스트리밍, 캐시 전략까지 봐야 실제 동작을 설명할 수 있다.
스트리밍과 캐시를 같이 봐야 하는 이유
느린 데이터 하나 때문에 전체 페이지가 늦어지는 상황이라면, 페이지 전체를 한 번에 완성하는 방식보다 스트리밍이 더 잘 맞는다. 핵심 콘텐츠가 먼저 보이고, 늦는 영역은 나중에 채워지는 편이 사용자가 느끼는 속도와 더 가깝기 때문이다.
Next.js 16 문서는 Cache Components를 opt-in 기능으로 설명한다. 정적인 셸을 먼저 보내고, 동적인 부분은 준비되는 대로 스트리밍하는 구성이 가능하다. 예전의 “이 페이지는 SSR, 저 페이지는 SSG” 같은 페이지 단위 구분보다 더 세밀한 선택이 가능해진 셈이다.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
// app/products/[id]/loading.tsx
export default function Loading() {
return <p>상품 정보를 불러오는 중...</p>
}
이런 구조는 문서, 커머스, 콘텐츠 상세 페이지처럼 본문은 빨리 보여주고 추천 영역이나 개인화 영역은 조금 늦어도 되는 화면에서 자주 쓰인다. 반대로 화면 대부분이 로그인 사용자별 실시간 데이터로 가득하다면, 요청 시점 렌더링 비중이 커지고 TTFB도 함께 살펴봐야 한다.
무엇을 측정해야 SSR의 효과가 보일까
오래된 자료는 TTI를 많이 언급하지만, 현재는 LCP와 INP를 중심으로 보고 TTFB와 FCP를 함께 해석하는 편이 이해에 도움이 된다. web.dev 문서도 FCP와 LCP가 현장 데이터에서는 TTFB 영향을 포함한다고 설명한다. 따라서 서버 렌더링을 붙였더라도 서버 응답이 길어지면 FCP와 LCP가 같이 늦어질 수 있다.
Next.js에서는 useReportWebVitals로 기본 지표를 수집할 수 있다. Pages Router에는 Next.js-hydration 같은 커스텀 메트릭도 있지만, 현재는 LCP와 INP를 먼저 보는 편이 전체 해석이 수월하다.
// app/_components/web-vitals.tsx
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
if (['TTFB', 'FCP', 'LCP', 'INP'].includes(metric.name)) {
console.log(metric.name, metric.value)
}
})
return null
}
브라우저에서는 Chrome DevTools가 병목 지점을 나누어 보는 데 유용하다.
- Network 트랙: TTFB, 렌더 블로킹 CSS, 큰 자바스크립트 요청을 먼저 확인할 수 있다.
- Performance 패널: FCP, LCP 마커와 긴 작업, 사용자 상호작용 지연을 함께 볼 수 있다.
- Rendering 탭: Paint Flashing, Layer Borders, Core Web Vitals 오버레이로 렌더링 문제를 시각적으로 확인할 수 있다.
- Layers 패널: 어떤 요소가 별도 레이어로 올라갔는지, 왜 컴포지팅되는지, 페인트 비용이 어디서 커지는지 확인할 수 있다.
SSR을 적용했는데도 느리다면 서버 시간이 긴지, CSSOM이 무거운지, 레이아웃과 페인트가 큰지, 하이드레이션 자바스크립트가 큰지부터 나눠 보는 편이 더 정확하다.
언제 SSR을 선택하는 편이 잘 맞을까
SSR은 첫 화면의 정보가 빨리 보여야 하는 페이지에서 의미가 크다. 검색 유입이 많고 공개 콘텐츠가 중심인 문서, 블로그, 상품 상세, 마케팅 페이지가 여기에 가깝다. 인증 상태나 위치, 가격처럼 요청 시점의 데이터가 첫 화면에 직접 반영되어야 하는 경우에도 자연스럽게 연결된다.
반면, 대부분의 가치가 로그인 이후 상호작용에 있고 초기 HTML보다 클라이언트 상태 변화가 더 중요한 화면이라면 CSR이나 서버/클라이언트 경계를 더 세밀하게 나누는 구성이 나을 수 있다. 대시보드, 편집기, 고빈도 상호작용 UI는 첫 HTML보다 입력 반응과 지속적인 업데이트 비용이 더 크게 보일 때가 많다.
결국 선택 기준은 단순하다. 첫 콘텐츠를 얼마나 빨리 보여야 하는지, 요청 시점 데이터가 얼마나 중요한지, 브라우저에서 실행해야 할 자바스크립트를 얼마나 줄일 수 있는지, 그리고 그 대가로 서버 응답 시간이 얼마나 늘어나는지를 함께 봐야 한다.
현재 기준에서 바꿔 읽어야 할 오래된 설명
- “SSR은 서버가 완성된 정적 HTML을 보낸다”: 요청마다 달라지는 HTML도 SSR이다. 빌드 시점 HTML은 SSG 쪽 설명에 가깝다.
ReactDOM.hydrate(): React 19에서는 제거된 API다. 현재 기준은hydrateRoot()다.- “Next.js SSR은
getServerSideProps”: Pages Router에서는 맞지만, App Router에서는 서버 컴포넌트와 클라이언트 경계로 설명하는 편이 더 정확하다. - “SSR이면 무조건 빠르다”: SSR은 브라우저의 DOM, CSSOM, 레이아웃, 페인트, 컴포지팅 비용을 없애지 않는다.
- “CSR은 SEO가 안 된다”: Googlebot은 자바스크립트를 실행할 수 있다. 다만 서버 렌더링이나 프리렌더링은 사용자와 크롤러 모두에게 여전히 이점이 있다.
- “TTI만 보면 된다”: 현재는 LCP와 INP를 중심으로, 보조적으로 TTFB와 FCP를 함께 보는 편이 해석이 자연스럽다.
공식 문서와 참고 링크
- React hydrateRoot
- React renderToPipeableStream
- React Versions
- React DOM Removed APIs
- Next.js Server and Client Components
- Next.js Pages Router SSR
- Next.js Cache Components
- Next.js useReportWebVitals
- MDN Critical Rendering Path
- Chrome DevTools Performance Reference
- Chrome DevTools Rendering Tab
- Chrome DevTools Layers Panel
- Google Search Central JavaScript SEO Basics
- web.dev FCP
- web.dev LCP
- web.dev INP
원문 참고
https://www.maeil-mail.kr/question/48
댓글
댓글 쓰기