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은 다음 순서로 동작한다.
- 빈 HTML 다운로드
- JS 번들 다운로드
- React 실행
- 데이터 요청
- 렌더링
- DOM 업데이트
- 화면 표시
문제는 사용자가 아무것도 보지 못하는 시간이 길다는 것
CSR에서 “빈 화면”이 길어지는 이유
- 워터폴: HTML → JS 로드 → React 실행 → (그다음에야) API 호출 → 응답 후 렌더링. 단계가 직렬이라 끝날 때까지 다음 단계가 기다린다.
- TTI(Time to Interactive): 화면이 채워져도 이벤트가 붙기 전까지는 클릭·입력이 안 되므로, 사용자 입장에선 “아직 안 떴다”와 같다.
- 큰 번들: JS가 무거우면 다운로드·파싱·실행까지 시간이 길어져 FCP와 TTI 모두 나빠진다.
SSR은 “JS가 올 때까지 아무것도 안 보인다”를 “HTML이라도 먼저 보여준다”로 바꾸는 것이다.
전체 작업량이 줄어드는 것은 아니다.
SSR의 핵심 아이디어
SSR은 이 흐름을 이렇게 바꾼다.
- 서버에서 React 실행
- HTML 생성
- HTML 전송
- 브라우저에 즉시 표시
- JS 다운로드
- Hydration
- 이벤트 연결
중요한 포인트는 이것이다.
HTML은 먼저 보이지만, React는 아직 붙지 않았다
서버에서 실제로 일어나는 일
SSR 시 서버는 대략 다음을 한다.
- 요청별로 React 앱을 한 번 실행한다 (요청이 많으면 서버 CPU·메모리 부담 증가).
- 필요한 데이터를 서버에서 fetch한 뒤, 그 결과를 반영해 HTML을 만든다.
- 생성된 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이 하는 일 (단순화)
- 서버에서 받은 HTML의 DOM 노드를 그대로 둔 채
- 같은 트리 구조로 React 컴포넌트를 다시 실행하고
- 서버 HTML과 클라이언트 트리를 일치시킨다고 가정한 뒤
- 그 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 전략이다