블로그를 시작하며 — SSG, SSR, CSR에 대해 그리고 SSG를 선택한 이유
2025-07-11#ssg#ssr#csr#nextjs
렌더링 전략 세 가지에 대해서. 그리고 나의 블로그는 왜 SSG로 만들었는지를 담은 글이다.
블로그를 만들기로 했다. 그리고 첫 글은 이 블로그 자체의 이야기로 시작하고 싶었다.
만들면서 가장 먼저 마주친 결정이 렌더링 전략이었는데, CSR, SSR, SSG라는 약어들은 실무에서 매일 스치면서도 막상 "왜 이걸 골랐어요?"라는 질문에 한 문장으로 답하려니 나부터 정리가 필요했다.
그전에 — SPA와 MPA부터#
렌더링 얘기 앞에 페이지 구성 방식을 먼저 놓아야 그림이 맞는다.
MPA(Multi-Page Application) 는 페이지를 이동할 때마다 서버에서 새 HTML 문서를 받아오는 전통적인 방식이고,
SPA(Single-Page Application) 는 문서 하나를 받은 뒤 JS가 화면을 갈아끼우는 방식이다.
React로 앱을 만들면 기본값이 SPA고, SPA의 기본 렌더링이 CSR이다.
그런데 SPA라고 첫 HTML까지 브라우저에서 만들 필요는 없다
— 첫 HTML을 누가, 언제 만들어주느냐 가 바로 렌더링 전략의 문제다.
세 약어는 결국 한 가지 질문이었다#
정리하고 나니 셋을 가르는 질문은 하나였다. "사용자가 보는 HTML은 언제, 어디서 만들어지는가."
CSR — 브라우저에서, 방문한 뒤에
서버는 빈 HTML과 자바스크립트 번들만 준다. 브라우저가 번들을 내려받아 실행하고, 데이터를 또 요청해서, 그제서야 화면이 그려진다. 그때까지 사용자가 보는 건 흰 화면이나 스피너다.
- 장점 — 한번 로드되면 페이지 전환이 빠르다.
서버는 정적 파일만 주면 되니 렌더링 부담이 없다. 상호작용 많은 "앱"에 자연스럽다. - 단점 — 첫 화면이 늦다(번들 크기만큼).
검색엔진과 링크 미리보기가 빈 문서를 만날 수 있다. 저사양 기기일수록 불리하다. - 첫 로드 문제는 code splitting(라우트별로 번들 쪼개기), lazy loading으로 줄일 수는 있다 — 없앨 수는 없다.
SSR — 서버에서, 요청이 올 때마다
요청이 오면 서버가 그 순간 데이터를 조회해 완성된 HTML을 만들어 응답한다.
방문자마다 다른 HTML 을 만들 수 있다는 게 본질적인 힘이다.
로그인한 사용자의 이름, 실시간 재고 같은 것들.
- 장점 — 첫 화면이 빠르고 SEO가 완전하다. 개인화·실시간 데이터가 된다.
- 단점 — 요청마다 서버가 일한다. 서버 비용과 스케일링 고민이 따라오고, 서버 장애가 곧 사이트 장애다. DB가 느려지면 첫 응답도 같이 느려진다.
여기서 짚고 갈 개념이 하이드레이션(hydration) 이다.
서버가 보낸 HTML은 완성된 그림일 뿐 아직 동작하지 않는다.
뒤이어 JS 번들이 로드되면 프레임워크가 이미 그려진 HTML 위에 이벤트 핸들러를 연결하고, 그제서야 버튼이 눌린다.
"화면은 보이는데 클릭이 안 되는" 찰나가 바로 하이드레이션이 끝나기 전의 시간이다.
SSG — 빌드에서, 배포 전에
배포 전 빌드 시점에 모든 페이지를 미리 렌더링해 HTML 파일로 만들어 둔다. 요청이 오면 이미 완성된 파일을 CDN이 던져줄 뿐, 요청 시점에 실행되는 서버 코드가 없다.
- 장점 — 응답이 가장 빠르다(가까운 CDN 노드에서 나간다).
서버 비용이 없고 장애 지점도 없다. 트래픽이 몰려도 CDN이 감당한다. SEO 완전. - 단점 — 모두에게 같은 화면만 가능하다.
내용 수정은 재빌드·재배포를 뜻하고, 페이지가 수만 장이면 빌드가 길어진다. 실시간 데이터는 못 담는다.
하이드레이션은 SSG에서도 똑같이 일어난다 — 미리 구운 HTML이라도 JS가 붙으면 상호작용이 살아난다.
정적이라는 건 "죽은 페이지"라는 뜻이 아니라 "HTML을 미리 만들었다"는 뜻일 뿐이다.
| HTML 생성 시점 | 방문자별 화면 | 서버 비용 | |
|---|---|---|---|
| CSR | 방문 후, 브라우저에서 | 가능 | 정적 서빙만 |
| SSR | 요청마다, 서버에서 | 가능 | 요청마다 발생 |
| SSG | 배포 전, 빌드에서 | 불가 | 없음 |
그래서 무엇을 골라야 하나#
내 나름의 판별식은 이렇다. 위에서부터 순서대로 물으면 된다.
- 방문자마다 화면이 다르거나 실시간이어야 하나? → SSR
- 로그인 뒤에 쓰는, 상호작용 위주의 도구인가? (어드민, 대시보드, 에디터) → CSR
- 누가 와도 같은 화면이고 가끔만 바뀌나? → SSG
- 거의 정적인데 주기적으로는 갱신돼야 하나? → ISR
그리고 이 질문은 사이트 단위가 아니라 페이지 단위로 던지는 것이다. 실무에서 다루는 서비스도 정적인 안내 페이지, 방문자마다 다른 예약 화면, 상호작용 위주의 어드민이 각자 다른 전략 위에 서 있다.
내 블로그의 성질을 대보니#
글은 발행하면 거의 바뀌지 않는다. 로그인도 개인화도 없다.
— 누가 와도 같은 화면을 본다.
그리고 내용이 바뀌는 시점은 내가 배포하는 순간뿐이다.
SSG의 단점이라던 것들("모두에게 같은 화면", "바뀌면 재빌드")이 내 블로그에서는 제약이 아니라 원래 기본적으로 가지고있는 성질이다. 반대로 SSR을 쓰면 요청마다 서버가 같은 HTML을 다시 조립하게 된다. CSR은 글 하나 읽으러 온 사람에게 JS 번들부터 내려받게 하고, 검색엔진에는 빈 문서를 보여준다. 포트폴리오를 겸하는 블로그에서 검색에 안 잡히는 건 치명적이다.
![]()
그래서 결론은 싱거울 만큼 빨리 났다. 기술 선택이 화려할 필요는 없다고 생각한다. 문제의 성질과 도구의 성질이 맞아떨어지면 그게 정답이다. 내게는 블로그 + SSG가 그 조합이었다.
중간 지대, ISR도 살펴봤다#
넷째 선택지도 있었다. ISR(Incremental Static Regeneration) — SSG처럼 미리 굽되, 설정한 주기로 백그라운드에서 페이지를 다시 굽는다. "거의 정적인데 가끔 바뀌는" 페이지, 이를테면 커머스 상품 목록 같은 곳에서 SSG의 속도와 SSR의 신선함 사이 타협점이 된다.
내 블로그에는 넣지 않았다. 글이 바뀌는 유일한 시점이 배포할 때이다 — 주기적 재생성이 갱신할 내용 자체가 없다. 선택지를 안다는 것과 써야 한다는 건 다른 문제인 것 같다.
// app/posts/[slug]/page.tsx
export function generateStaticParams() {
return getPosts().map((post) => ({ slug: post.slug }));
}나의 블로그 글은 마크다운 문서로 되어있다. DB도 CMS도 안 뒀다 — 파일시스템이 데이터베이스고 git이 어드민인 셈이다. 참고로 Next.js에는 더 극단적인 정적화도 있다 — output: "export"를 켜면 빌드 결과가 순수 HTML 파일 뭉치만 남아서 아무 웹서버에나 올릴 수 있다. 대신 배포 플랫폼이 해주던 이미지 최적화(원본을 기기에 맞게 줄여서 내려주는 것)와 응답 헤더 설정을 잃는다. 내 블로그는 거기까지는 하지 않았다. 글 페이지가 전부 미리 빌드한 HTML이라는 이득은 이미 챙겼고, 1MB짜리 썸네일 원본을 모바일에 그대로 내려보내면서까지 "더 정적"일 이유가 없어서다.
알수없이 SSR로 바뀜을 방지하기#
만들면서 알게 된 이 구조의 함정은 하나다. 페이지 어딘가에 동적 API가 스며들면 빌드는 성공하고 사이트도 멀쩡히 도는데, 정적이라는 성질만 조용히 사라진다. 에러 한 줄 없이 SSR로 바뀌는 것이다.
Next.js는 페이지를 미리 빌드할지 말지를 나의 선언이 아니라
코드가 뭘 쓰는지 보고 스스로 판단한다.
요청이 와야만 알 수 있는 값(cookies(), headers(), searchParams, no-store fetch)을 쓰는 순간 "이건 미리 빌드못하네"가 되는 것이다.
// 어느 날 글 페이지에 이런 컴포넌트 하나를 얹으면 —
function DeviceNotice() {
const ua = headers().get("user-agent"); // 요청이 와야 아는 값
// ...
}
// 이 페이지 전체가 SSG에서 SSR로 조용히 전환된다내가 직접 쓴 코드가 아니라 설치한 라이브러리가 내부에서 쿠키를 읽어도 같은 일이 벌어진다. 에러도 없고 화면도 똑같다 — CDN에서 나가던 응답이 매 요청 서버 렌더링으로 바뀌었을 뿐이다.
그래서 빌드 결과에서 프리렌더되지 않은 라우트를 찾아 빌드를 실패시키는 검사를 CI에 넣어뒀다.
npm run build && npm run check:static글을 마치며#
서버가 없으니 장애도 스케일링도 없다. 글을 쓰고 배포하게되면 빌드가 돌고, 몇 분 뒤 CDN에 HTML이 깔린다. 블로그 운영에 필요한 건 그게 전부다.
앞으로 여기 쌓을 글들도 같은 태도로 쓰려고 한다 — 선택에는 이유가 있어야 하고, 그 이유는 문제의 성질에서 나와야 한다. 첫 글의 결론이 이 블로그의 방향인 셈이다.