본문으로 건너뛰기

SSR은 렌더링을 빠르게 하지 않는다

SSR(Server Side Rendering)에 대해 가장 흔하게 듣는 설명은 이것이다.

“SSR을 쓰면 렌더링이 빨라진다”

하지만 이 말은 절반만 맞고, 절반은 틀리다.
정확히 말하면 SSR은 렌더링을 빠르게 하지 않는다.
대신, 렌더링이 시작되는 시점을 앞당긴다.

이 차이를 이해하지 못하면
SSR, Hydration, Client Component를 전부 헷갈리게 된다.


먼저, React에서 렌더링이란 무엇인가

React에서 렌더링(render)은 DOM을 그리는 행위가 아니다.

  • 컴포넌트 함수를 실행하고
  • React Element 트리를 만들고
  • 이전 트리와 비교하는 과정

즉, 렌더링은 계산 단계다.

DOM에 실제로 반영되는 것은
Commit Phase에서 일어난다.

  • Render Phase: 컴포넌트 실행 → Element 트리 생성 → diff. 순수한 계산만 하고 DOM을 건드리지 않는다.
  • Commit Phase: 계산 결과를 실제 DOM에 반영. 이때만 브라우저가 레이아웃·페인트를 한다.

그래서 “렌더링이 느리다”는 말은 보통 Render Phase의 계산 비용이 크거나, Commit 전까지 걸리는 시간이 길다는 뜻이다.
SSR은 이 계산의 일부를 서버로 옮길 뿐, 계산량 자체를 없애지는 않는다.

이 정의는 SSR에서도 그대로 유지된다.


CSR(Client Side Rendering)의 실제 흐름

일반적인 CSR은 다음 순서로 동작한다.

  1. 빈 HTML 다운로드
  2. JS 번들 다운로드
  3. React 실행
  4. 데이터 요청
  5. 렌더링
  6. DOM 업데이트
  7. 화면 표시

문제는 사용자가 아무것도 보지 못하는 시간이 길다는 것

CSR에서 “빈 화면”이 길어지는 이유

  • 워터폴: HTML → JS 로드 → React 실행 → (그다음에야) API 호출 → 응답 후 렌더링. 단계가 직렬이라 끝날 때까지 다음 단계가 기다린다.
  • TTI(Time to Interactive): 화면이 채워져도 이벤트가 붙기 전까지는 클릭·입력이 안 되므로, 사용자 입장에선 “아직 안 떴다”와 같다.
  • 큰 번들: JS가 무거우면 다운로드·파싱·실행까지 시간이 길어져 FCP와 TTI 모두 나빠진다.

SSR은 “JS가 올 때까지 아무것도 안 보인다”를 “HTML이라도 먼저 보여준다”로 바꾸는 것이다.
전체 작업량이 줄어드는 것은 아니다.


SSR의 핵심 아이디어

SSR은 이 흐름을 이렇게 바꾼다.

  1. 서버에서 React 실행
  2. HTML 생성
  3. HTML 전송
  4. 브라우저에 즉시 표시
  5. JS 다운로드
  6. Hydration
  7. 이벤트 연결

중요한 포인트는 이것이다.

HTML은 먼저 보이지만, React는 아직 붙지 않았다

서버에서 실제로 일어나는 일

SSR 시 서버는 대략 다음을 한다.

  1. 요청별로 React 앱을 한 번 실행한다 (요청이 많으면 서버 CPU·메모리 부담 증가).
  2. 필요한 데이터를 서버에서 fetch한 뒤, 그 결과를 반영해 HTML을 만든다.
  3. 생성된 HTML 문자열을 응답으로 보낸다.

즉, 클라이언트가 하던 "첫 렌더 + 데이터 요청"을 서버가 대신 하는 것이다.
그래서 서버 응답 시간은 "데이터 조회 + React 실행 + HTML 생성"을 모두 포함한다.
데이터가 느리거나 컴포넌트가 무거우면 서버에서의 첫 바이트(TTFB)도 느려진다.


그래서 SSR이 “빠르다”는 말의 정체

SSR이 빠르다고 느껴지는 이유는 하나다.

  • First Contentful Paint(FCP) 가 빠르다

사용자는:

  • 텍스트를 보고
  • 레이아웃을 인지하고
  • “페이지가 떴다”고 느낀다

하지만 이 시점에서:

  • 클릭 ❌
  • input ❌
  • state ❌

체감 속도와 실제 메트릭

  • FCP(First Contentful Paint): 처음으로 콘텐츠(텍스트·이미지)가 그려진 시점. SSR이 개선하는 대표 지표다.
  • LCP(Largest Contentful Paint): 가장 큰 콘텐츠가 표시되는 시점. 메인 이미지·헤더 등이 서버 HTML에 포함되면 개선될 수 있다.
  • TTI(Time to Interactive): 사용자가 실제로 클릭·입력할 수 있게 되는 시점. Hydration이 끝나야 TTI가 된다. SSR만으로는 TTI가 빨라지지 않는다.
  • INP(Interaction to Next Paint): 사용자 상호작용에 앱이 얼마나 빨리 반응하는지. 역시 Hydration 이후의 클라이언트 성능에 좌우된다.

정리하면, SSR은 FCP·LCP를 앞당기지만, TTI·INP는 JS 실행과 Hydration 품질에 달려 있다.


Hydration 이전에는 React가 아니다

Hydration은:

  • 서버에서 만든 HTML에
  • React를 다시 실행해서
  • 이벤트와 상태를 연결하는 과정이다.

즉:

SSR은 렌더링을 두 번 한다

  • 서버에서 한 번
  • 클라이언트에서 한 번

클라이언트 렌더링이 끝나야
비로소 "React 앱"이 된다.

Hydration이 하는 일 (단순화)

  1. 서버에서 받은 HTML의 DOM 노드를 그대로 둔 채
  2. 같은 트리 구조로 React 컴포넌트를 다시 실행하고
  3. 서버 HTML과 클라이언트 트리를 일치시킨다고 가정한 뒤
  4. 그 DOM에 이벤트 리스너와 state를 붙인다

DOM을 새로 만들지 않고 기존 HTML에 React를 "붙이는" 과정이라, 서버와 클라이언트 결과가 하나도 달라지면 안 된다.

Hydration Mismatch

서버에서 만든 HTML과 클라이언트에서 만든 트리가 다르면 React는 mismatch 오류를 낸다.

  • Date.now(), Math.random()처럼 서버/클라이언트에서 값이 다른 것 사용
  • window, localStorage 등 브라우저 전용 API를 렌더링에 사용
  • 서버와 클라이언트 번들/코드 경로가 달라 같은 컴포넌트가 다른 결과를 만드는 경우

이럴 때는 해당 부분을 클라이언트 전용으로 두거나, useEffect 안에서만 브라우저 API를 쓰는 식으로 맞춰야 한다.


input과 state는 언제부터 동작할까?

아주 중요한 포인트다.

<input value={value} onChange={handleChange} />

이 input은:

  • SSR 시점: 그냥 HTML
  • Hydration 이후: React controlled input

즉, 모든 상태 기반 UI는 hydration 이후에야 의미를 가진다.

그래서:

  • Controlled / Uncontrolled
  • setState
  • 렌더링 규칙

이 전부 클라이언트 영역의 이야기다.


SSR이 해결하는 문제, 해결하지 못하는 문제

SSR이 해결하는 것

  • 초기 화면 노출
  • SEO
  • 소셜 미리보기
  • 체감 속도

SSR이 해결하지 못하는 것

  • JS 실행 속도
  • hydration 비용
  • 복잡한 client state
  • 무거운 렌더링 로직

구체적으로:

  • 번들이 크면 다운로드·파싱이 길어져 Hydration이 늦어지고, 그동안 화면은 보이지만 클릭·입력이 안 된다.
  • Hydration은 보통 루트부터 한 번에 이루어지므로, 트리가 크면 TTI가 그만큼 밀린다.
  • 서버 HTML과 다른 결과를 만들면 mismatch로 Hydration이 실패하거나, React가 클라이언트에서 다시 그리느라 이득이 반감된다.

SSR은 성능 최적화의 만능키가 아니다.


Next.js에서 SSR이 더 복잡해진 이유

App Router 이후 Next.js의 SSR은
단순한 “HTML 생성”이 아니다.

  • Server Component
  • Streaming
  • Suspense
  • Partial Hydration

Server Component(RSC)와 기존 SSR의 차이

  • 기존 SSR: 서버에서 React를 실행해 HTML 문자열을 만들고, 클라이언트는 그 HTML을 받아 Hydration한다.
  • RSC: 서버에서 직렬화된 컴포넌트 결과를 보내고, 클라이언트는 그걸 받아 그리며, 해당 부분은 Hydration하지 않는다. 즉, 서버 전용 컴포넌트는 번들에 안 들어가고 이벤트도 붙지 않는다.
  • 그래서 "어디까지 서버에서 할지"를 컴포넌트 단위로 나누는 게 App Router의 핵심 질문이 된다.

이제 질문은 이것으로 바뀐다.

“어디까지를 서버에서 렌더링할 것인가?”


Streaming이 중요한 이유

Streaming은:

  • HTML을 한 번에 보내지 않고
  • 준비된 것부터 흘려보내는 방식이다.

이로 인해:

  • 전체 렌더링 완료를 기다리지 않아도 됨
  • 느린 데이터가 전체 화면을 막지 않음

Suspense와의 관계

  • <Suspense fallback={...}>로 감싼 부분은 “준비될 때까지” fallback HTML을 먼저 보내고, 나중에 실제 내용을 스트리밍으로 채운다.
  • 그래서 한 블록이 느려도 다른 블록은 먼저 보여줄 수 있어, 보여주는 시점을 세밀하게 나눌 수 있다.
  • 다만 여전히 “렌더링 자체를 빠르게” 하는 게 아니라, 무엇을 언제 보여줄지를 조절하는 전략이다.

하지만 이것도 여전히:

렌더링을 빠르게 하는 게 아니라
보여주는 시점을 조절하는 전략
이다.


SSR과 렌더링 규칙은 충돌하지 않는다

중요한 오해 하나를 정리하자.

  • SSR은 React의 렌더링 규칙을 바꾸지 않는다
  • 불변성, 참조 비교, Reconciliation은 그대로다

달라진 건 오직 이것이다.

렌더링이 실행되는 위치와 시점


그래서 언제 SSR을 써야 할까?

SSR은 다음 상황에서 의미가 있다.

  • 첫 화면이 중요한 페이지
  • SEO가 필요한 서비스
  • 정보 탐색 위주의 UI

반대로:

  • 인터랙션 중심
  • 내부 어드민
  • 복잡한 상태 관리

이런 경우엔 SSR의 이점이 크지 않다.


실무에서 염두할 점

  • Hydration 비용 줄이기: 번들 크기·의존성 줄이기, 코드 스플리팅으로 초기 로드 JS를 줄이면 TTI가 당겨진다.
  • mismatch 방지: 서버/클라이언트에서 다른 값을 쓰는 부분(날짜, 랜덤, 브라우저 API)은 클라이언트 전용 렌더링(useEffect 또는 Client Component)으로 빼두는 게 안전하다.
  • 측정: FCP·LCP는 “빨리 보이기”, TTI·INP는 “빨리 쓸 수 있기”를 본다. SSR을 쓸 때는 둘 다 보면서, Hydration 이후 지표가 나빠지지 않도록 확인하는 게 좋다.

정리

  • SSR은 렌더링을 빠르게 하지 않는다
  • 대신, 보여주는 시점을 앞당긴다
  • React는 hydration 이후에야 React가 된다
  • 모든 상태와 입력은 클라이언트의 책임이다
  • SSR은 성능 기술이 아니라 UX 전략이다