렌더링 & 프레임워크
SSR과 SPA는 어떻게 바통을 주고받는가 (SvelteKit 기준)
한 줄 요약
SvelteKit 기본은 "첫 응답은 서버가 완성된 HTML을 그려주고(SSR), 그 이후 페이지 전환은 브라우저 JS가 처리하는(SPA)" 하이브리드다. 둘은 충돌하는 모드가 아니라 한 페이지의 생애에서 시점별로 바통을 주고받는 릴레이다.
왜 알아야 했나
- SvelteKit을 쓰면서 "이거 항상 SPA 아닌가?"라는 의문에서 출발. 라우팅 전환 시
<html>이 안 깜빡이고 네트워크 탭에 HTML 요청도 없어서. - 실제로는 전환 동작만 SPA와 같았던 것이고, 첫 응답이 SSR이냐 빈 껍데기냐가 핵심 구분점.
- streaming SSR 페이지에서 JS를 끄니 데이터가 화면에 안 나오는 현상을 만나면서 progressive enhancement의 동작 원리까지 파고들게 됨.
- adapter 선택(node / static / vercel 등)이 결국 "SSR을 어디서 돌릴지 / SPA로 뺄지"의 문제라 함께 이해 필요.
핵심 개념
1. SPA vs MPA
- MPA (전통적): 링크 클릭마다 서버에 새 HTML 요청 → 전체 페이지 새로고침(깜빡임).
- SPA: 처음에 JS 앱 전체를 받고, 이후 전환은 서버에 HTML 재요청 없이 JS가 화면 일부만 교체(필요한 데이터는 JSON 등으로). 깜빡임 없음.
2. SvelteKit은 하이브리드 (SSR + SPA)
| 첫 진입 / 새로고침 시 서버 응답 | 이후 페이지 전환 (JS 켜짐) | |
|---|---|---|
| SvelteKit 기본 | 완성된 HTML (SSR) | SPA처럼 JS 라우팅 |
| 순수 SPA (adapter-static fallback, ssr=false) | 빈 HTML 껍데기 | SPA처럼 JS 라우팅 |
- 전환 동작만 보면 둘 다 같아서 구분 안 됨. 차이는 오직 "첫 응답에 콘텐츠가 들어있느냐".
3. SSR과 SPA가 공존하는 원리 (시점의 릴레이)
같은 페이지를 두 가지 방법으로 만들 수 있다:
- 방법 A (서버): 서버가 완성된 HTML 생성 → 브라우저는 받아서 표시만 → SSR
- 방법 B (브라우저): 브라우저가 JS로 컴포넌트 실행 + 데이터로 화면 직접 생성 → SPA
시간 순서:
[/about 직접 진입/새로고침]
서버가 완성된 HTML 전송 ← SSR (방법 A)
↓
JS 번들 로드 + hydration ← 다리 놓기 (바통 터치)
↓
[/contact 링크 클릭]
브라우저가 직접 화면 생성 ← SPA (방법 B)
- "동시에"가 아니라 "순서대로". SSR은 첫 화면 한 번, SPA는 그 이후 전환. 모순이 아니다.
- 전환 시 HTML은 안 받아오지만 데이터(JSON)는 받아옴. (
/contact/__data.json같은 요청)
4. Progressive Enhancement (점진적 향상)
SvelteKit의 설계 철학. JS는 필수가 아니라 경험을 향상시키는 레이어.
- SvelteKit이 만든 링크는 결국 평범한
<a href="...">. - JS 켜짐: 클라이언트 라우터가 클릭을 가로채(
preventDefault) JS로 전환 → SPA 방식, 깜빡임 없음. - JS 꺼짐: 가로챌 JS가 없으니 브라우저가
<a>기본 동작 실행 → 서버에 새 HTML 요청, 전체 새로고침 → MPA 방식, 깜빡임 있음. - 즉 JS 꺼도 SSR 덕분에 콘텐츠 표시 + 링크 이동은 그대로 동작한다 (단, 전환은 SPA가 아닌 브라우저 기본 방식).
5. Streaming SSR (중요한 예외)
- load 함수가 promise를
await안 하고 그대로 반환하면 streaming. - 서버는 HTML 뼈대를 먼저 보내고, promise가 resolve되는 대로 HTML 맨 아래에
<script>__sveltekit_dev.resolve(...)</script>형태로 데이터를 흘려보냄. - 이 데이터를 화면에 꽂는 건 클라이언트 JS. JS 끄면 데이터는 HTML 하단에 텍스트로 도착해 있지만 화면엔 안 그려짐.
- progressive enhancement가 적용되되, streaming한 부분만 JS(향상 레이어)에 의존.
| await (non-streaming) | streaming (promise 반환) | |
|---|---|---|
| 첫 HTML 응답 | 데이터까지 완성된 HTML | 뼈대만, 데이터는 빈 채로 |
| 데이터 도착 방식 | HTML 안에 이미 박혀있음 | 맨 아래 script로 나중에 흘려보냄 |
| JS 끄면 | 콘텐츠 보임 | 빈 화면 |
6. SSR의 장점 (SEO 말고도)
- 빠른 초기 렌더링: FCP/LCP 개선. JS 로드 전에 콘텐츠가 보임. 저사양 기기·느린 네트워크에서 특히 유리(무거운 JS 파싱/실행을 서버가 대신).
- 링크 미리보기 / 소셜 공유: 카톡·트위터 OG 카드는 JS 실행 없이 첫 HTML만 봄. SSR이면 제대로 뜸(SEO 불필요한 사내 링크에서도 의미).
- 견고함: JS 로딩 실패·지연 시에도 콘텐츠는 이미 보임. JS 꺼진 환경, 구형 브라우저에서도 최소 기능 동작.
- 접근성: 초기 DOM에 콘텐츠가 존재 → 스크린 리더 등 보조기술에 안정적.
- 보안:
+page.server.js/server load는 서버에서만 실행. API 키·DB 정보가 클라이언트 번들에 노출 안 됨. - 네트워크 효율: 서버가 DB·API 가까이서 데이터를 모아 한 번에 HTML로 말아 보냄 → 클라이언트 워터폴 감소.
7. SSR의 트레이드오프
- 서버 부하: 요청마다 서버 렌더링 → 트래픽 많으면 비용·자원 부담(순수 SPA는 정적 호스팅으로 끝). 캐싱으로 완화.
- TTFB(첫 바이트까지 시간): 서버 렌더링 시간만큼 첫 응답 지연. 특히 streaming 안 쓰고 느린 데이터를 await하면 체감.
- 복잡성: 서버/클라 양쪽에서 도는 코드라
window접근 등이 서버에서 터질 수 있어 신경 쓸 게 늘어남.
8. Adapter와의 연결
| 환경 | adapter | SSR/SPA |
|---|---|---|
| 자체 Node 서버 / Docker | adapter-node | 상시 SSR 서버 |
| Vercel / Netlify / Cloudflare | 각 adapter | 서버리스 SSR |
| 순수 정적 사이트 | adapter-static | 빌드타임 프리렌더 |
| SPA만 | adapter-static (fallback) | 순수 SPA |
- "Node 서버" = adapter-node로 빌드 후
node build로 띄우는, 포트를 계속 listen하는 상시 프로세스. VPS·Docker 등 내가 관리하는 환경 필요. 서버리스(요청 시에만 함수 실행)와 대비됨.
예시 / 코드
순수 SPA 모드로 만들기 (adapter-static)
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
fallback: 'index.html' // SPA의 핵심: 어떤 경로든 index.html 하나 반환
})
}
};
// src/routes/+layout.js
export const ssr = false; // 서버 렌더링 끔
export const prerender = false;
fallback: 'index.html': 서버는 모든 경로 요청에 빈 index.html을 돌려주고, 그 안의 JS가 현재 URL을 보고 화면을 그림. 서버는 라우팅에 관여 안 함.
Streaming (JS 꺼지면 데이터 안 보임)
// +page.server.js
export function load() {
return {
posts: fetchPosts() // await 안 함 → streaming
};
}
Streaming 끄기 (JS 없어도 데이터 보이게)
export async function load() {
return {
posts: await fetchPosts() // await → HTML에 박혀서 나감 (대신 첫 응답 느려짐)
};
}
헷갈렸던 점
- "라우팅 시 HTML 안 깜빡이고 네트워크에 HTML 요청도 없으니 항상 SPA 아닌가?" → 전환 동작만 SPA와 같을 뿐, 첫 응답은 SSR이라 하이브리드.
- "JS를 꺼도 라우팅이 되네?" → 그건 SPA 라우팅이 아니라 브라우저의
<a>기본 동작(새 HTML 요청, 전체 새로고침). SSR이 각 경로를 제대로 렌더링해주기 때문에 가능. - "SSR은 HTML 받아오고 SPA는 안 받아오는데 어떻게 공존?" → 동시가 아니라 순서. 첫 번째만 받아오고 두 번째부터 안 받아옴.
- "streaming 페이지에서 JS 끄니 데이터가 화면 맨 아래 텍스트로만 보임" → 버그 아님. streaming 데이터를 화면에 꽂는
resolve함수가 JS라서 JS 끄면 안 그려지는 정상 동작.
실제 테스트 결과 ✅
- JS 끈 상태에서 라우팅 → HTML을 새로 불러옴(network 탭에 새 document 요청), 화면 깜빡임. → 브라우저 기본 동작(MPA 방식) 확인.
- JS 켠 상태 → 첫 진입/새로고침에서만 HTML 로드, 이후 전환은 HTML 요청 없이 JS 처리. → SPA 방식 확인.
- streaming SSR + JS 끄고 새로고침 →
__sveltekit_dev.resolve(...)데이터가 HTML 맨 아래 붙어 오지만 화면엔 안 나옴. → streaming 데이터는 JS 의존임 확인.
구분 체크리스트
| 확인 항목 | SPA 전환 (JS 켜짐) | 브라우저 전환 (JS 꺼짐) |
|---|---|---|
| HTML document 요청 | 없음 (데이터만) | 있음 (새 HTML) |
| 화면 깜빡임 | 없음 | 있음 |
<html> 태그 | 안 깜빡임 | 깜빡임(문서 교체) |
SSR vs 순수SPA 구분법: 크롬 개발자도구 → ⚙️ → Disable JavaScript 체크 후 새로고침. SSR이면 콘텐츠 보임, 순수 SPA면 빈 화면.
실무 적용
- 순수 SPA가 적합: 로그인 후 쓰는 관리자 대시보드/내부 툴(SEO 불필요), 백엔드 API 따로 있고 프론트만 정적 호스팅.
- SSR(하이브리드) 권장: SEO·공유 미리보기가 중요한 블로그/마케팅/커머스.
- streaming 사용 시 주의: 느린 데이터로 첫 화면을 막고 싶지 않을 때 유용하지만, 크롤러·JS 비활성 환경에서 그 데이터가 꼭 보여야 하면
await로 전환. - 관련 작업: [[Hydration & 컴파일 & 런타임]]
더 공부할 것
-
ssr/csr/prerender페이지 옵션 각각의 동작 차이 - universal load(
+page.js) vs server load(+page.server.js) — 시점에 따라 서버/클라 어디서 도는지 -
data-sveltekit-preload— 링크 hover 시 다음 페이지 미리 받아오기 - adapter-static fallback SPA에서 SEO 보완(프리렌더 가능한 페이지만 미리 렌더)
- streaming SSR에서
{#await}로딩 UI 패턴 - form actions +
use:enhance(progressive enhancement의 form 버전)
🔗 참고
- SvelteKit 공식 문서: Page options (ssr / csr / prerender)
- SvelteKit 공식 문서: Loading data (streaming with promises)
- SvelteKit 공식 문서: Adapters (adapter-static, adapter-node)
- SvelteKit 공식 문서: Link options (preloading)