← Blog
렌더링 & 프레임워크

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와의 연결

환경adapterSSR/SPA
자체 Node 서버 / Dockeradapter-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)