이 글은 Next.js Pages Router에서 SSR과 SSG가 각각 어떤 과정을 거쳐 화면을 그리는지, timeline 단위로 분해합니다. 이는 Page Router 에 대한 글이니, 최신 Next.js 버전과는 맞지 않음을 먼저 알립니다!
왜 rendering timeline을 알아야 하는가
SSR(
getServerSideProps)과 SSG(getStaticProps)는 최종적으로 동일한 HTML을 브라우저에 전달합니다. 그런데도 두 방식의 성능 특성은 크게 다릅니다. 그 차이가 어디서 발생하는지 정확히 짚으려면, "요청이 발생한 순간부터 페이지가 interactive 해지는 순간까지" 전체 과정을 단계별로 이해해야 합니다. 이 timeline을 알면 TTFB(Time To First Byte)가 왜 느린지, hydration 비용이 어디서 발생하는지 구체적으로 진단할 수 있습니다.SSR (getServerSideProps) Timeline
1. Browser -> Server 요청
사용자가 URL에 접근하면 브라우저가 Next.js 서버에 HTTP 요청을 보냅니다. 이 시점에서 사용자 화면에는 아무것도 표시되지 않습니다.
2. Server:
getServerSideProps 실행서버가 요청을 수신하면 해당 page의
getServerSideProps를 실행합니다. DB query, 외부 API 호출 등이 이 단계에서 수행됩니다. 이 함수가 완료될 때까지 서버는 어떤 응답도 반환하지 않습니다. 즉, getServerSideProps의 실행 시간이 곧 TTFB 지연으로 직결됩니다.TTFB(Time To First Byte)는 브라우저가 서버에 요청을 보낸 시점부터 첫 번째 byte를 수신하기까지의 시간입니다. server-side 처리가 길어질수록 TTFB가 증가하고, 사용자는 빈 화면을 더 오래 보게 됩니다.
3. Server: React component tree rendering (
renderToString)getServerSideProps가 반환한 props를 page component에 주입하고, React의 server-side rendering이 수행됩니다. component tree 전체가 HTML string으로 변환되는 과정입니다.4. Server -> Browser: HTML 응답
서버가 완성된 HTML을 브라우저에 전송합니다. 이 HTML에는 두 가지 핵심 요소가 포함되어 있습니다. 첫째, rendering된 HTML DOM. 둘째,
<script id="__NEXT_DATA__"> tag 안에 JSON으로 serialize된 pageProps data입니다. 이 __NEXT_DATA__는 이후 hydration 단계에서 핵심 역할을 합니다.5. Browser: HTML parsing & FCP (First Contentful Paint)
브라우저가 HTML을 수신하여 parsing하고, DOM을 구성하고, CSS를 적용합니다. 이 시점에서 사용자는 콘텐츠를 시각적으로 확인할 수 있습니다. 하지만 버튼을 클릭해도 아무 반응이 없습니다.
onClick 같은 event handler가 아직 연결되지 않은 상태, 즉 "정적 HTML"입니다.6. Browser: JS bundle download
HTML 내의
<script> tag를 기반으로 브라우저가 JS chunk를 download합니다. 주요 파일은 _app.js, framework.js(React runtime), 해당 page chunk, webpack runtime 등입니다.7. Browser: JS parsing & 실행
download된 JS를 parsing하고 실행합니다. Next.js의 client entrypoint가 시작됩니다.
8. Browser: Hydration
전체 timeline에서 가장 중요한 단계입니다.
먼저 Next.js router가 초기화되고,
__NEXT_DATA__에서 pageProps를 추출합니다. 이어서 React가 ReactDOM.hydrateRoot() (React 18) 또는 ReactDOM.hydrate() (React 17)를 호출합니다. 이때 React는 서버에서 전송된 HTML DOM을 새로 생성하는 것이 아니라, 기존 DOM node를 그대로 재사용(adopt)합니다.React는 component tree를 상위에서 하위로 순회하면서, 각 component의 render 결과와 실제 DOM을 비교합니다. 불일치가 발견되면 console에 hydration mismatch 경고가 출력됩니다. 순회 과정에서 동시에 event handler(
onClick, onChange 등)를 해당 DOM node에 부착하고, useEffect, useLayoutEffect 같은 side effect가 실행됩니다.9. TTI (Time To Interactive)
hydration이 완료되면 페이지가 완전히 interactive한 상태가 됩니다. 사용자의 click, input 등이 실제로 동작하기 시작하는 시점입니다.
SSG (getStaticProps) Timeline
Build time (배포 전)
next build 시점에 getStaticProps가 실행되고, HTML 파일과 JSON 파일이 미리 생성됩니다. HTML 파일 하나와 pageProps가 담긴 .json 파일 하나가 각각 disk에 저장됩니다.1. Browser -> CDN 요청
사용자가 URL에 접근합니다. 이때 요청은 server-side 함수를 실행하는 것이 아니라, 이미 생성된 static file을 CDN에서 가져옵니다.
2. CDN -> Browser: HTML 응답 (즉시)
CDN에서 미리 생성된 HTML을 즉시 반환합니다. server-side 함수 실행(API 호출, React component rendering 등)이 없으므로 TTFB가 극적으로 빠릅니다.
__NEXT_DATA__ 역시 동일하게 포함되어 있습니다.3~9: 이후 과정은 SSR과 동일
HTML parsing, FCP, JS download, hydration 과정은 SSR과 완전히 동일합니다.
핵심 차이: HTML이 도착하기까지의 과정
SSR과 SSG의 차이는 timeline의 앞부분, 즉 "HTML이 브라우저에 도착하기까지의 과정"에 집중되어 있습니다. SSR은 매 요청마다 서버가 data fetching과 rendering을 수행하므로,
getServerSideProps가 느릴수록 TTFB가 증가합니다. 반면 SSG는 build time에 이 작업이 완료되어 있으므로, runtime에는 static file serving만 수행합니다.HTML이 브라우저에 도착한 이후의 hydration 과정은 양쪽 모두 완전히 동일합니다. React 입장에서는 server에서 생성한 HTML이든 build time에 생성한 HTML이든 구분하지 않습니다. DOM과
__NEXT_DATA__를 받아 hydration을 수행할 뿐입니다. 따라서 서버단에서의 Fetching이 생략되어서 SSR 보다 SSG가 일반적으로 훨씬 빠릅니다.SSG 전환 시 고려할 점
SSR에서
getServerSideProps로 요청 시점의 최신 data를 주입했다면, SSG로 전환하는 순간 해당 data는 build 시점의 snapshot이 됩니다. 자주 변경되는 data가 있다면 두 가지 보완 전략을 고려해야 합니다.첫째, ISR(Incremental Static Regeneration)을 사용하여
revalidate 옵션으로 일정 주기마다 page를 재생성하는 방법입니다. 둘째, client에서
useEffect와 SWR 또는 React Query를 활용하여 data를 다시 fetching하는 pattern입니다.client fetching은 hydration 이후에 발생하므로, 사용자 경험 관점에서 보면 초기 HTML에는 build 시점 data가 표시되고, hydration 완료 후 client fetching이 수행되어 최신 data로 교체되는 흐름입니다. 즉, SSR 대비 한 단계가 추가되는 구조입니다. 그치만 앞단에서 받아오는 시간이 너무 길다면(TTFB가 너무 길다면) 흰 화면을 보고 있는 것보다, Fallback Loading 이라도 보여줄 수 있습니다.
kyu-log