SSR vs SSG vs ISR — 우리 사이트의 분류표
SSR·SSG·ISR 렌더링 선택을 결제·정산 운영 관점에서 재정리. 서버 비용·데이터 신선도·보안 세 축의 실전 판단 기준표와 구체 적용 사례를 공개한다.
렌더링 선택이 결제·정산 팀에게 성능 문제가 아닌 운영 문제인 이유
우리가 다루는 페이지들은 단순한 콘텐츠 사이트가 아니다. 결제 상태를 보여주는 실시간 대시보드, 월별 정산 요약 리포트, 자동화 작업의 실행 로그, 파트너사에 공개하는 API 문서, 그리고 랜딩 페이지가 같은 프로젝트 안에 공존한다. 이 페이지들은 각각 완전히 다른 데이터 신선도(freshness) 요구사항을 가진다.
문제는 이것이 단순한 성능 튜닝 이야기가 아니라는 점이다. 결제 대시보드에서 사용자가 잘못된 잔액 정보를 보거나, 정산 리포트가 stale 데이터를 캐싱한 채 오전 내내 틀린 집계 숫자를 보여준다면, 그건 기술 버그가 아니라 운영 신뢰도 문제다. 반대로 월 1회 업데이트되는 약관 페이지를 SSR로 구성해 매 요청마다 DB를 찌르는 건 돈을 태우는 일이다. 우리 같은 작은 팀이 서버 비용과 운영 복잡도를 통제하려면, 렌더링 방식 선택 기준이 개발자 개인의 "감"이 아니라 명문화된 룰로 존재해야 한다.
실제로 이 문제를 정면으로 겪은 건, 관리자 대시보드와 파트너 정산 페이지를 같은 Next.js 프로젝트 안에서 개발하던 시기였다. 초기엔 개발자마다 다른 기준으로 페이지를 만들었고, 일부 정산 요약 페이지는 SSR로, 일부는 SSG로 구현되어 있었다. 어떤 페이지는 배포 때마다 최신화되고, 어떤 페이지는 실시간으로 DB를 쿼리했다. 일관성이 없으니 캐싱 전략도, 에러 핸들링도, 모니터링 설정도 각각 달랐다. 이 글은 그 혼란을 정리하면서 만든 기준이다.
세 방식의 핵심 차이: 비용과 신선도의 스펙트럼
기술적 정의는 이미 알 것이다. 여기서는 우리 운영 관점에서 중요한 두 축인 서버 비용과 데이터 신선도로 재정리한다.
SSG(Static Site Generation) 는 빌드 시점에 HTML을 완성한다. 이후 모든 요청은 CDN에서 파일을 내려준다. TTFB(Time to First Byte)는 전 세계 어디서 접근하든 보통 20~50ms 수준이다. 서버 연산 비용은 거의 0에 가깝고, Vercel 같은 플랫폼에서는 추가 비용 없이 처리된다. 단점은 데이터 신선도다. 빌드하지 않으면 업데이트되지 않는다.
SSR(Server-Side Rendering) 은 요청마다 서버에서 HTML을 생성한다. DB 조회나 외부 API 호출이 포함되면 TTFB는 100~500ms 이상 쉽게 올라간다. Vercel Functions 기준으로 서버리스 실행 시간과 메모리 사용량이 비용으로 환산된다. 트래픽이 적을 때는 무시해도 되지만, 정산 일괄 조회처럼 한 번에 수백 건의 요청이 쏟아지는 상황에선 비용 스파이크가 생긴다.
ISR(Incremental Static Regeneration) 은 두 극단 사이에 위치한다. 정적 파일을 만들되, revalidate 시간 또는 on-demand 트리거에 따라 특정 경로를 재생성한다. Next.js App Router에서는 revalidatePath() / revalidateTag()로 특정 캐시를 무효화할 수 있다. 비용은 SSG에 가깝고, 신선도는 설정한 주기만큼 보장된다. 단, 재생성 중 사용자에게 stale 데이터가 노출되는 순간이 존재한다(stale-while-revalidate 패턴).
HEDVION이 쓰는 3단 판단 기준
새 페이지를 만들 때마다 아래 세 질문을 순서대로 거친다. 첫 번째에서 걸리면 바로 결정, 아니면 다음으로 넘긴다.
Q1. 요청마다 사용자별로 다른 데이터를 서버에서 렌더링해야 하는가? Yes라면 SSR (또는 SSG shell + client-side fetch). 결제 상태 대시보드, 사용자별 정산 내역, 관리자 권한에 따라 다른 화면이 대표적이다. 이 경우 SSG나 ISR은 보안 문제를 일으킬 수 있다. 빌드된 HTML에 특정 사용자의 데이터가 박히거나, 잘못된 캐시를 다른 사용자가 받는 리스크가 생기기 때문이다.
Q2. 배포 주기보다 데이터가 더 자주 바뀌는가?
Yes라면 ISR + on-demand revalidation 고려. 정산 요약 페이지는 하루에도 여러 번 집계 결과가 바뀐다. 하지만 초 단위 실시간이 필요한 건 아니고, 결제 이벤트가 발생할 때 해당 집계 캐시만 무효화하면 충분하다. revalidateTag('settlement-summary')를 결제 webhook 수신 시 호출하는 방식이 우리가 채택한 구현이다.
Q3. 빌드 타임에 모든 경로(path)를 알 수 있는가?
Yes라면 SSG가 기본값이다. No라면, 즉 경로가 런타임에 생성되거나 수가 매우 많다면 ISR + dynamicParams: true로 fallback 처리한다.
이 기준에서 빠지는 패턴이 하나 있다: SSG shell + client fetch. 페이지 구조(레이아웃, 메뉴, 설명 텍스트)는 SSG로 빌드하고, 실시간 데이터 영역만 클라이언트에서 fetch한다. 우리 관리자 대시보드 상단의 통계 위젯이 이 방식이다. 서버 비용을 아끼면서도 실시간성을 확보하는 타협점으로, SSR 전환 전에 반드시 먼저 검토해볼 만하다.
실전 적용: 우리 사이트 렌더링 분류표
아래는 HEDVION이 현재 운영하는 주요 페이지의 렌더링 방식과 그 이유다.
| 페이지 | 방식 | 이유 |
|---|---|---|
| 랜딩 페이지 / 서비스 소개 | SSG | 배포 주기 안에서 변경 없음, CDN 직서빙 |
| 기술 블로그 | SSG | 발행 시 빌드, 초 단위 실시간 불필요 |
| API 문서 | SSG + on-demand ISR | 코드 변경 배포 시 revalidation 트리거 |
| 결제 대시보드 | SSR | 사용자별 실시간 데이터 필수 |
| 정산 요약 리포트 | ISR (revalidateTag) | 결제 webhook 발생 시 캐시 무효화 |
| 자동화 실행 로그 | SSR | 실행 상태 실시간 반영 필요 |
| 약관 / 정책 페이지 | SSG | 수동 배포 시에만 변경 |
정산 요약 리포트의 ISR 구현을 조금 더 풀면: 결제 완료 webhook을 받는 엔드포인트에서 revalidateTag('settlement-daily')를 호출한다. Next.js는 해당 태그로 캐시된 페이지를 무효화하고, 다음 요청 시 새 HTML을 생성·캐싱한다. 이 방식으로 결제 발생 후 수 초 내에 정산 페이지가 갱신된다. 반면 SSR로 구현했을 때는 리포트 페이지 로드마다 복잡한 집계 쿼리가 실행되어 DB 부하가 눈에 띄게 올라갔다. 내부 테스트에서 동시 10명 접속 시 집계 쿼리 평균 응답 시간이 340ms에서 1,200ms로 증가했다. ISR 전환 후 같은 조건에서 서버리스 함수 호출 횟수가 약 85% 감소했다.
우리가 실제로 틀렸던 것, 그리고 ISR의 숨은 함정
처음에 관리자 대시보드 전체를 SSR로 구현했다. 이유는 단순했다: "실시간이어야 한다"는 막연한 요구사항. 그런데 실제로 분석해보니, 대시보드 안에서 실시간이 필요한 부분은 전체의 30% 미만이었다. 상단의 오늘 결제 건수/금액 위젯은 실시간이 맞지만, 최근 공지사항, 운영 가이드 링크, 메뉴 구조는 절대 실시간일 이유가 없었다. SSG shell + 부분 client fetch로 전환한 뒤, 서버리스 함수 호출 횟수가 약 60% 줄었다. 작은 숫자 같지만, 월 단위로 보면 Vercel 청구서에서 유의미한 차이가 난다.
ISR의 함정도 있다. revalidate: 60(60초 주기 재생성)을 설정해놓으면, 재생성 중에 요청이 들어온 사용자는 최대 60초 오래된 데이터를 볼 수 있다. 정산 금액이 표시되는 페이지에서 이 차이가 얼마나 허용 가능한지는 비즈니스 판단이다. 우리는 시간 기반 revalidate를 on-demand revalidation으로 전환했다. 결제 이벤트가 없으면 캐시는 그대로 살아 있고, 이벤트 발생 시에만 정확히 무효화한다. 시간 기반보다 캐시 상태를 훨씬 예측하기 쉽고, 불필요한 재생성도 없다.
보안 관점에서 반드시 짚어야 할 것도 있다. ISR 페이지에 사용자 인증 정보를 섞으면 안 된다. CDN에 캐시된 HTML이 다른 사용자에게 서빙될 수 있기 때문이다. 인증이 필요한 데이터는 반드시 client-side fetch 또는 SSR로 처리하고, ISR 페이지는 "로그인 여부에 무관하게 동일한 내용"인 경우에만 적용한다. 정산 요약이라도 사용자별 필터가 걸린다면, 그 부분은 반드시 client fetch로 분리해야 한다.
바로 써먹는 실행 체크리스트
새 페이지를 만들기 전 아래를 확인한다. 각 항목에 정직하게 답하면 렌더링 방식이 결정된다. PR 템플릿에 이 체크리스트를 넣어두면 코드 리뷰 시 선택 이유를 명시적으로 확인할 수 있다. 우리 팀은 실제로 그렇게 하고 있다.
SSR을 선택해야 하는 경우:
- 로그인한 사용자에 따라 다른 데이터를 서버에서 렌더링해야 한다
- 페이지 로드 시점의 최신 상태가 비즈니스적으로 중요하다 (결제 상태, 실시간 잔액 등)
- 개인정보·민감 데이터를 서버에서 HTML에 직접 삽입해야 한다
ISR을 선택해야 하는 경우:
- 데이터가 하루 수 회 이상 바뀌지만 초 단위 실시간은 불필요하다
- 특정 이벤트(결제 완료, 데이터 업데이트) 시 on-demand revalidation을 연결할 수 있다
- 캐시 중 stale 데이터 노출 가능성을 수용할 수 있다 (정산 요약, 통계 집계 등)
SSG를 선택해야 하는 경우:
- 위 두 조건 중 어느 것도 해당하지 않는다
- 모든 경로를 빌드 타임에 알 수 있다
- 배포 주기 안에서 콘텐츠가 바뀌지 않는다
선택 시 반드시 거쳐야 할 추가 검토:
- SSR 전체 대신 "SSG shell + client fetch" 패턴을 먼저 검토한다. 실시간이 필요한 부분이 페이지의 일부라면 이 패턴이 비용·성능 면에서 더 유리하다.
- ISR 사용 시 시간 기반(
revalidate: N) 대신 on-demand(revalidateTag) 방식을 기본으로 검토한다. 예측 가능성과 비용 효율이 모두 낫다. - ISR 페이지에 사용자별 데이터가 섞이는지 반드시 확인한다. 섞인다면 해당 부분은 분리해야 한다.
렌더링 방식은 한 번 정하면 바꾸기 번거롭다. 기준 없이 "일단 SSR"로 가는 건 나중에 비용과 복잡도로 돌아온다. 반대로 "일단 SSG"를 기본값으로 두되, 위 체크리스트에서 걸릴 때만 ISR이나 SSR로 올라가는 방향이 우리 팀이 도달한 결론이다.
— by slecs
* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.