Communitygithub.com

agentmemoir/skills

Svelte 5 서버사이드 렌더링(SSR) 애플리케이션을 구축, 마이그레이션 또는 리뷰합니다. Svelte 5의 runes($state, $derived, $effect, $props, $bindable)를 사용하는 SvelteKit SSR(adapter-auto/node/cloudflare/vercel/netlify) 또는 일반 Vite SSR(svelte/server render() 및 hydrate())을 지원합니다.

skills란 무엇인가요?

이 스킬은 Svelte 5의 runes 시스템을 사용하여 서버사이드 렌더링(SSR) Svelte 애플리케이션을 구축, 마이그레이션 또는 리뷰하는 방법을 제공합니다. 전문가는 SvelteKit(adapter-auto, node, cloudflare, vercel, netlify 등 지원) 또는 일반 Vite + Svelte 5 환경에서 SSR을 구현하는 완전한 워크플로를 안내합니다. 특히 $effect가 서버에서 실행되지 않아 발생하는 빈 HTML, 모듈 수준 $state가 여러 요청 간 공유되는 문제, Date.now()나 Math.random()으로 인한 hydration 불일치 등 SSR 특유의 버그를 방지하는 패턴을 중점적으로 다룹니다. 또한 기존 앱의 SSR 안전성 감사(서버에서 실행되지 않는 effect, 유저 간 상태 누수, 비결정적 렌더링 값, 시크릿 노출 등)를 수행하는 리뷰 프로세스를 포함합니다. 참조 파일(runes-ssr.md, ssr-build.md, ssr-review.md, security.md)을 통해 단계별 가이드와 보안 체크리스트를 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agentmemoir/skills/tree/HEAD/skills/frontend/svelte5-ssr-runes

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Svelte 5 서버사이드 렌더링 (Runes)

이 스킬이 다루는 내용

"SSR"은 모든 요청이 브라우저에 도달하기 전에 서버(Node, Bun, 서버리스 또는 엣지)에서 HTML로 렌더링되고, 동일한 컴포넌트가 이후에 인터랙티브 앱으로 하이드레이션됨을 의미합니다. 정적 빌드와 달리 요청 시점에 실제 서버가 존재하므로 +page.server.js, +layout.server.js, +server.js, 폼 액션 및 hooks.server.js가 허용되며, 데이터와 시크릿은 여기에 있어야 합니다.

Runes 부분이 SSR을 까다롭게 만듭니다. 모든 컴포넌트는 서버에서 한 번, 브라우저에서 한 번씩 총 두 번 실행됩니다. 클라이언트 전용 앱에서 괜찮은 코드가 여기서는 문제가 됩니다:

  • $effect는 서버에서 절대 실행되지 않으므로, 이펙트가 설정하는 모든 상태는 서버 HTML에서 누락됩니다.
  • .svelte.ts 파일의 모듈 수준 $state는 서버 프로세스당 한 번 생성되어 모든 사용자의 요청 간에 공유됩니다.
  • 렌더링 중 Date.now(), Math.random() 또는 localStorage에서 계산된 값은 서버와 클라이언트 간에 달라 hydration 불일치를 발생시킵니다.

위 세 가지 실패가 runes 코드의 대부분의 SSR 버그를 차지하며, 각각에 대한 올바른 작은 패턴이 아래에 있습니다.

1단계 — 프로젝트 형태 선택

필요 사항사용할 것
전체 앱: 라우트, 서버 데이터 로딩, 폼, 인증, 스트리밍SvelteKit + adapter-auto (Vercel/Netlify/Cloudflare 자동 감지), adapter-node (자가 호스팅) 또는 플랫폼 어댑터. 기본값이며, 특별한 이유가 없다면 이것을 사용하세요.
기존 백엔드(Express/Hono/Fastify/Bun) 내부의 Svelte 컴포넌트 트리일반 Vite + Svelte 5: 서버에서는 svelte/server의 render(), 브라우저에서는 hydrate(). 라우팅, 헤더, 요청 처리를 직접 소유합니다.
대부분 정적 콘텐츠에 소수의 동적 라우트라우트당 export const prerender = true (또는 인기 페이지는 프리렌더링하고 긴 꼬리는 서버 렌더링하는 'auto')를 사용하는 SvelteKit. 동적 라우트도 여기의 모든 규칙을 따릅니다.

SSR 버그를 "수정"하기 위해 export const ssr = false를 설정하지 마세요. 이는 라우트를 클라이언트 렌더링 셸(SPA)로 전환하여 SEO와 첫 페인트를 잃게 됩니다. 브라우저 전역 없이는 진정으로 렌더링할 수 없는 페이지에만 정당합니다. csr = false는 반대 스위치입니다. 상호작용이 필요 없는 페이지를 위한 하이드레이션 없는 서버 HTML입니다.

두 경로 모두에 대한 설정 단계와 어댑터 및 페이지 옵션은 references/ssr-build.md에 있습니다. svelte.config.js / vite.config.js를 생성하기 전에 읽으세요. 옵션 이름과 어댑터 동작이 버전에 따라 다르며 미묘하게 잘못 설정되기 쉽습니다.

2단계 — 서버에서 실행되는 runes 파악

이 표는 이 스킬의 핵심입니다:

RuneSSR 중참고
$state(v)실행됨초기 값이 렌더링됩니다. 렌더링 중 변경해도 이미 내보낸 HTML은 다시 렌더링되지 않습니다. SSR은 단일 패스입니다.
$state.raw(v)실행됨프록시 없음. 변경 대신 통째로 교체하는 큰 data 페이로드에 좋습니다.
$derived(expr)실행됨렌더링의 일부로 평가되므로 출력이 HTML에 포함됩니다. 순수하게 유지하고 브라우저 전용 API를 사용하지 마세요.
$effect / $effect.pre절대 실행 안 됨브라우저 전용. 이펙트가 설정하는 상태는 SSR HTML에 없습니다(하이드레이션 후 나타남). 하이드레이션 오류는 아니지만 빈 HTML과 눈에 띄는 레이아웃 이동입니다.
$effect.root(fn)절대 실행 안 됨동일함. 내부에 서버에서 보이는 상태를 구축하지 마세요.
$props() / $bindable()실행됨load 데이터가 props로 도착하고 초기 값이 렌더링됩니다.
$props.id()실행됨SSR-안전 고유 ID, 서버와 클라이언트 간에 일관됨(Svelte ≥ 5.20). for/aria-labelledby 링크에 사용하세요.
$inspect(v)개발 전용SSR 출력 없음.
$state.snapshot(v)실행됨비-Svelte API나 서버 라이브러리에 상태를 전달하기 전에 사용하세요.
$effect.tracking()false서버 컴파일러가 리터럴 false로 대체합니다. SSR 중에는 추적 컨텍스트가 없습니다.

이 표가 방지하는 고전적인 SSR 버그:

<!-- ❌ 서버 HTML에서 비어 있고, 하이드레이션 후 나타남 -->
<script>
  let user = $state();
  $effect(() => { user = getUser(); });
</script>
<div>{user?.name}</div>

해결 방법은 거의 항상 동일합니다: SSR에서 보이는 값을 $derived에서 계산하거나, load에서 가져와 props로 전달하세요. $effect는 브라우저에만 존재하는 것(캔버스, 옵저버, 분석, 스토리지 동기화)을 위해 유지하세요.

전체 rune별 서버 동작, 하이드레이션 안전 값 패턴 및 실험적 비동기 SSR 상태는 references/runes-ssr.md를 읽으세요.

3단계 — 상태를 요청별로 유지, 모듈 전역으로 절대 금지

// ❌ 전체 서버 프로세스를 위한 하나의 공유 객체 — 사용자 A가 사용자 B의 데이터를 볼 수 있음
// src/lib/user.svelte.ts
export const user = $state({ name: '' });

표준 해결 방법은 load에서 요청별 데이터를 반환하고 props로 전달하는 것입니다. 여러 중첩 컴포넌트가 필요할 때는 getter와 함께 컨텍스트에 넣으세요:

<!-- +layout.svelte -->
<script>
  import { setContext } from 'svelte';
  let { data } = $props();
  setContext('user', () => data.user);
</script>
<!-- 모든 자식 -->
<script>
  import { getContext } from 'svelte';
  const user = getContext('user');
</script>
<p>Hi {user().name}</p>

컨텍스트는 컴포넌트 트리에 첨부되며, 각 SSR 요청은 자체 트리를 가지므로 사용자 간 누출이 불가능합니다. 값 자체가 아니라 함수(또는 getter가 있는 객체)를 setContext에 전달해야 반응성을 잃지 않습니다. 많은 서버 함수가 필요로 하는 요청별 데이터는 hooks.server.ts에서 채워진 event.locals에 있을 수 있습니다.

모듈 수준 상태는 사용자 데이터와 관련 없는 클라이언트 전용 앱 상태(테마, 기능 플래그)에는 여전히 괜찮습니다. 단, 서버에서 개인화된 값을 렌더링하지 않는 한입니다.

4단계 — 하이드레이션을 계약으로 취급

하이드레이션은 서버 HTML을 재생합니다. 클라이언트의 첫 번째 렌더링은 그것과 일치해야 합니다. 값이 다르면 hydration_mismatch가 발생하며, 이는 의도적으로 억제할 수 없습니다. 경고가 아니라 코드를 수정하세요.

  • 결정적으로 렌더링하세요: 렌더링 경로에서 Date.now(), new Date() 포맷팅, Math.random(), crypto.randomUUID(), localStorage/window/navigator 읽기를 사용하지 마세요.
  • ID는 무작위성이나 모듈 카운터 대신 $props.id()로 생성하세요.
  • 날짜는 서버의 load에서 포맷팅하여 문자열로 전달하거나, 항상 명시적 timeZone을 전달하세요. 서버와 클라이언트의 시간대가 다릅니다.
  • 클라이언트 전용 값(스토리지, 미디어 쿼리, 지리적 위치): 서버 안전 기본값에서 시작하고 $effect/onMount 내부에서 채우거나, 안정적인 서버 폴백과 함께 mounted 플래그로 마크업을 제어하세요.
<script>
  import { browser } from '$app/environment';

  let theme = $state('light');
  $effect(() => {
    if (!browser) return;
    theme = localStorage.getItem('theme') ?? 'light'; // 마운트 후이므로 하이드레이션이 일치함
  });
</script>

일반 Svelte SSR(SvelteKit 없음)에서는 $app/environment를 사용할 수 없습니다. 이펙트 내부에서 typeof window !== 'undefined'를 사용하고, 렌더링 경로에서는 절대 사용하지 마세요.

5단계 — 서버에서 데이터 로드 및 스트리밍

  • 데이터베이스, 파일 및 비공개 환경 변수는 +page.server.ts / +layout.server.ts를 사용하고, 공용 API는 어디서든 실행될 수 있는 범용 +page.ts를 사용하세요.
  • 서버 load는 devalue 직렬화를 견딜 수 있는 데이터를 반환해야 합니다: JSON + Date, Map, Set, BigInt, RegExp 및 순환 참조. 함수나 클래스 인스턴스는 안 됩니다.
  • 느리고 필수가 아닌 데이터: 프로미스를 반환하고 페이지에서 {#await}하세요. 중첩된 프로미스는 해결될 때 스트리밍됩니다. 필수 데이터는 반환하기 전에 await하여 첫 페인트에 포함되도록 하세요.
  • load 내부에서 event.fetch를 사용하세요: 요청 쿠키를 상속받고 하이드레이션을 위해 응답을 인라인 처리하여 다시 가져오지 않습니다.
  • 네비게이션 후 data는 컴포넌트를 다시 생성하지 않고 업데이트됩니다: 비반응형 로컬 변수로 복사하지 말고 $derived를 사용하세요.

Svelte 5.57 / SvelteKit 2.7x(2026년 9월) 기준으로 컴포넌트 내 await 및 원격 함수는 여전히 실험적입니다. 명시적으로 요청하지 않는 한 프로덕션 코드에서 compilerOptions.experimental.async를 활성화하지 마세요. 정확한 상태와 옵트인은 references/runes-ssr.md에 있습니다.

6단계 — SSR 보안 규칙 적용

  1. 시크릿은 비공개로 유지. $env/static/private / $env/dynamic/private는 서버 전용입니다. PUBLIC_ 접두사가 있는 변수만 브라우저에 도달합니다. $lib/server/ 및 *.server.js의 가져오기는 컴포넌트가 가져오면 오류를 발생시킵니다.
  2. load에서 반환된 모든 것은 HTML 페이로드에 포함됩니다. 렌더링하는 필드만이 아니라 전부입니다. 클라이언트에 필요하지 않은 비밀번호 해시, 토큰 또는 내부 필드를 절대 반환하지 마세요.
  3. CSP와 nonces. kit.csp를 구성하세요. SvelteKit은 자체 인라인 스크립트에 nonce를 주입하고, %sveltekit.nonce%는 app.html의 수동 태그를 다룹니다. 실제 서버는 실제 CSP 헤더를 보낼 수 있습니다. 약한 <meta> 태그 대신 이것을 사용하세요.
  4. {@html}은 여전히 XSS입니다. 서버에서 렌더링되었다고 해서 살균된 것은 아닙니다. 신뢰할 수 없는 HTML은 렌더링 직전 DOMPurify로 살균하세요.
  5. 인증은 hooks.server.ts(locals 채움)와 라우트 수준 검사에 속합니다. 레이아웃 load 함수는 모든 클라이언트 측 네비게이션에서 실행되지 않습니다.
  6. CSRF 보호는 폼 제출에 대해 기본적으로 켜져 있습니다. 그대로 두고, 사용자 정의 +server.js 변경 엔드포인트에서 직접 출처를 확인하세요.
  7. 세션 쿠키: httpOnly, 프로덕션에서 secure, 최소 sameSite: 'lax'.
  8. 사용자 제공 URL의 서버 측 fetch는 SSRF입니다. 호스트를 허용 목록으로 관리하고, 개인 IP와 클라우드 메타데이터 엔드포인트를 차단하세요.

전체 체크리스트 및 코드: references/security.md.

기존 앱 리뷰

작업이 빌드보다 감사나 디버깅일 때는 references/ssr-review.md를 따라 진행하세요: 렌더링 모드 확인, 서버에서 보이지 않는 runes, 하이드레이션 위험, 요청 범위 상태, 페이로드 노출, 보안 grep, 실패 훈련. 각각이 발생시키는 프로덕션 실패와 함께 file:line 형식으로 결과를 보고하세요.

참조 파일

  • references/runes-ssr.md — rune별 서버 동작, 하이드레이션 안전 패턴, 요청 범위 상태, 일반 svelte/server render + hydrate, 실험적 비동기 SSR / 원격 함수 상태.
  • references/ssr-build.md — SvelteKit 설정 및 어댑터, 페이지 옵션(ssr/csr/prerender), 훅, 폼 액션, 스트리밍, 일반 Vite SSR, 배포 참고 사항.
  • references/ssr-review.md — 기존 SSR 앱 감사 체크리스트: 렌더링 모드, 서버에서 보이지 않는 runes, 하이드레이션 위험, 상태 범위, 페이로드 노출, 보안 grep, 실패 훈련.
  • references/security.md — 시크릿 및 환경 변수, CSP nonces, XSS, CSRF, 인증/세션 처리, SSRF, 오류 누출, 출시 전 체크리스트.
  • assets/ — 복사 가능한 시작 파일: svelte.config.js (CSP가 있는 SvelteKit), hooks.server.ts (인증 + 보안 헤더), 일반 Vite SSR용 index.html / entry-server.js / entry-client.js (이 세트는 tests/plain-ssr에서 CI로 빌드됨).

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

관련 스킬