Skip to content

필사 모드: Svelte 5 Runes — 반응성 재설계, 그리고 Solid·Vue·MobX·React Compiler와의 정면 비교 (2026 심층)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프롤로그 — 컴파일러 마법이 끝나고, 룬이 시작되었다

Svelte 3은 약속이었다. "리액티브 코드는 컴파일이 처리해야 한다, 런타임은 가벼워야 한다." `let count = 0`이 반응성을 갖고, `$: doubled = count * 2`가 자동으로 추적되고, 우리는 React의 `useState` 의존성 배열 없이도 살 수 있었다.

그러나 그 약속에는 작은 글씨가 있었다.

- **컴포넌트 안에서만 반응성** — `.svelte` 파일 밖, 일반 `.ts`/`.js`에서는 `$:`이 동작하지 않는다.

- **`$:` 라벨의 두 얼굴** — 같은 문법이 "파생 값"이기도 하고 "사이드이펙트"이기도 했다. 컴파일러가 알아서 판별했지만, 사람은 종종 헷갈렸다.

- **`let`이 마법이었다** — 컴포넌트 최상위 `let`은 반응성이고, 함수 안의 `let`은 평범한 변수다. 둘이 같은 문법인 게 문제였다.

- **TypeScript와의 마찰** — 컴파일러가 의미를 바꾸는 식별자에 타입을 붙이기는 까다로웠다.

2024년 10월 22일, Svelte 5가 GA로 나왔다. 그리고 한 줄로 그 모든 작은 글씨를 지웠다.

> **"반응성은 더 이상 마법이 아니다. 룬이다."**

룬(rune)은 `$state`·`$derived`·`$effect`·`$props`로 시작하는 네 개의 **함수처럼 보이는 컴파일러 키워드**다. 컴포넌트 안에서도, 밖에서도, 클래스 안에서도, 모듈 어디에서도 똑같이 동작한다. 의미는 한 줄로 읽힌다.

// Svelte 5

let count = $state(0)

let doubled = $derived(count * 2)

$effect(() => console.log(count))

이 글은 그 변화를 처음부터 끝까지 — **왜·무엇을·어떻게·언제 마이그레이션할지** — 한 호흡으로 정리한다. 그리고 그 옆에 Solid의 시그널, Vue의 ref/computed, MobX의 observable, React Compiler의 자동 메모이제이션을 놓고, "2026년 우리는 어디쯤 와 있는가"를 묻는다.

1장 · 왜 룬인가 — 암묵에서 명시로

먼저 Svelte 4와 Svelte 5의 같은 카운터를 나란히 본다.

<!-- Svelte 4 -->

let count = 0

$: doubled = count * 2

$: if (count > 10) console.log('high')

function increment() {

count += 1

}

<!-- Svelte 5 -->

let count = $state(0)

let doubled = $derived(count * 2)

$effect(() => {

if (count > 10) console.log('high')

})

function increment() {

count += 1

}

차이는 두 가지다.

1. **반응성이 식별자에 붙어 있다, 위치에 붙어 있지 않다.** Svelte 4는 "최상위 `let`"이라는 위치 규칙이었다. Svelte 5는 `$state()`를 통과한 값만 반응적이다.

2. **`$:` 라벨의 두 얼굴이 분리되었다.** 파생 값은 `$derived`, 사이드이펙트는 `$effect`로 명시한다.

이게 왜 중요한가? 세 가지 시나리오를 보자.

1.1 컴포넌트 밖에서의 반응성

Svelte 4에서 카운터 로직을 별도 파일로 빼고 싶었다고 하자.

// counter.js (Svelte 4 시대 — 동작하지 않는다)

let count = 0

$: doubled = count * 2 // ← .svelte 파일이 아니면 컴파일러가 무시

`$:`는 `.svelte` 파일의 컴파일러 지시어였다. 일반 `.js`/`.ts`에서는 무의미한 JS 라벨 문법이었다. 그래서 우리는 **store**라는 별도의 추상을 썼다. `writable`·`readable`·`derived` 그리고 `$store` 접두 문법.

Svelte 5에서는?

// counter.svelte.js

export function createCounter() {

let count = $state(0)

let doubled = $derived(count * 2)

return {

get count() { return count },

get doubled() { return doubled },

increment: () => (count += 1),

}

}

확장자가 `.svelte.js` 또는 `.svelte.ts`면 컴파일러가 룬을 처리한다. 결과적으로 **store가 따로 필요 없다.** 룬 하나로 컴포넌트 안과 밖이 통일된다.

1.2 `$:`의 두 얼굴

Svelte 4의 `$:`는 같은 문법이 두 가지 의미를 가졌다. 컴파일러가 우변을 분석해서 "할당이면 파생, 표현식이면 이펙트"로 갈랐다.

let a = 1

let b = 2

$: sum = a + b // ← 파생 (할당)

$: console.log(a, b) // ← 이펙트 (표현식)

$: if (sum > 5) alert() // ← 이펙트 (if 문)

읽는 사람은 매번 우변을 한 번 더 봐야 했다. Svelte 5는 둘을 분리한다.

let a = $state(1)

let b = $state(2)

let sum = $derived(a + b)

$effect(() => console.log(a, b))

$effect(() => { if (sum > 5) alert() })

1.3 의존성 추적

둘 다 정적 분석으로 의존성을 추적한다. React는 의존성 배열을 사람이 적는다 — `useMemo(() => a + b, [a, b])`. Svelte는 적지 않는다. 차이는 React Compiler가 자동화하려는 것과 같은 지점이다.

2장 · 네 개의 룬 — `$state`·`$derived`·`$effect`·`$props`

2.1 `$state` — 반응성의 출발점

let count = $state(0)

let user = $state({ name: '영주', age: 32 })

let tags = $state(['svelte', 'runes'])

function rename() {

user.name = '영주2' // ← 깊이 반응성 (deep reactivity), 동작한다

tags.push('signals') // ← 배열 변이도 추적된다

}

`$state`는 기본적으로 **깊은 반응성**이다. 객체·배열을 Proxy로 감싸 깊은 속성 변경까지 추적한다. 큰 객체에서 오버헤드가 부담스러우면 `$state.raw()`로 얕은 반응성만 가져갈 수 있다.

let snapshot = $state.raw({ huge: data })

// snapshot.huge.foo = 'bar' ← 추적 안 됨

snapshot = { ...snapshot, huge: { ...snapshot.huge, foo: 'bar' } } // ← 추적됨

2.2 `$derived` — 파생, 그리고 게으름

let count = $state(0)

let doubled = $derived(count * 2)

let expensive = $derived.by(() => {

// 복수 줄 계산이 필요할 때

let acc = 0

for (let i = 0; i < count; i++) acc += i

return acc

})

`$derived`는 **게으르고 캐시된다.** 읽지 않으면 계산되지 않고, 의존성이 바뀌지 않으면 재계산되지 않는다. 이건 Solid의 `createMemo`, Vue의 `computed`와 같은 의미다.

2.3 `$effect` — 사이드이펙트의 명시적 게이트

let url = $state('/api/me')

$effect(() => {

const ctrl = new AbortController()

fetch(url, { signal: ctrl.signal })

.then(r => r.json())

.then(console.log)

return () => ctrl.abort() // ← cleanup 함수

})

`$effect`는 DOM 마운트 후, 그리고 의존성이 바뀔 때마다 실행된다. cleanup도 React `useEffect`와 같은 모양으로 반환한다. `$effect.pre`는 DOM 업데이트 **이전**에 실행되고, `$effect.root`는 명시적으로 dispose할 수 있는 효과 트리를 만든다.

핵심 규칙: **`$effect` 안에서 `$state`를 쓰지 마라.** 무한 루프와 동기화 폭발의 주범이다. 그게 `$derived`가 따로 있는 이유다.

2.4 `$props` — 컴포넌트 입력의 새 문법

<!-- Svelte 4 -->

export let name

export let age = 0

export let onSave

<!-- Svelte 5 -->

let { name, age = 0, onSave, ...rest } = $props()

여러 `export let`을 한 줄의 구조분해로 묶었다. 기본값·rest props·이름 변경까지 표준 JS 문법으로 풀린다. TypeScript와도 자연스럽다.

interface Props {

name: string

age?: number

onSave: (id: string) => void

}

let { name, age = 0, onSave }: Props = $props()

추가로 `$bindable()`로 양방향 바인딩을 명시하고, `$host()`로 커스텀 엘리먼트 호스트에 접근한다.

3장 · 룬 vs 시그널 vs ref vs observable vs useState — 비교 매트릭스

여기서 한 페이지로 정리하자. 다섯 가지 반응성 모델을 같은 카운터로 본다.

<!-- Svelte 5 / Runes -->

let count = $state(0)

let doubled = $derived(count * 2)

$effect(() => console.log(count))

// Solid / Signals

function Counter() {

const [count, setCount] = createSignal(0)

const doubled = createMemo(() => count() * 2)

createEffect(() => console.log(count()))

return <button onClick={() => setCount(count() + 1)}>{count()}</button>

}

<!-- Vue 3 / Composition + ref -->

const count = ref(0)

const doubled = computed(() => count.value * 2)

watchEffect(() => console.log(count.value))

// MobX

class Store {

count = 0

constructor() { makeAutoObservable(this) }

get doubled() { return this.count * 2 }

inc() { this.count += 1 }

}

const store = new Store()

const Counter = observer(() => (

))

// React 19 + React Compiler

function Counter() {

const [count, setCount] = useState(0)

// Compiler가 알아서 메모이즈한다 — 수동 useMemo 없음

const doubled = count * 2

useEffect(() => console.log(count), [count])

return <button onClick={() => setCount(count + 1)}>{count}</button>

}

비교 매트릭스 — 같은 차원을 다섯 줄로.

| 항목 | Svelte 5 Runes | Solid Signals | Vue 3 ref/computed | MobX | React + Compiler |

|---|---|---|---|---|---|

| 읽기 문법 | `count` (식별자) | `count()` (호출) | `count.value` (속성) | `store.count` (속성) | `count` (구조분해) |

| 쓰기 문법 | `count++` | `setCount(c => c + 1)` | `count.value++` | `store.count++` | `setCount(c => c + 1)` |

| 파생 | `$derived(...)` | `createMemo(...)` | `computed(...)` | `get ...()` (게터) | 자동 (Compiler) |

| 효과 | `$effect(...)` | `createEffect(...)` | `watchEffect(...)` | `autorun(...)` | `useEffect(..., [deps])` |

| 컴포넌트 밖 | `.svelte.ts` | 어디서나 | 어디서나 | 어디서나 | 훅은 안 됨 |

| 깊은 반응성 | 기본 (Proxy) | 얕음 (스토어로 보강) | 기본 (Proxy) | 기본 (Proxy/getter) | 얕음 (참조 비교) |

| 컴파일 단계 | 컴파일타임 변환 | 컴파일타임 변환 | 런타임 Proxy | 런타임 Proxy | 컴파일러 + 런타임 훅 |

| 의존성 배열 | 없음 (자동) | 없음 (자동) | 없음 (자동) | 없음 (자동) | 사람 또는 Compiler |

| TypeScript | 자연 | 자연 | 자연 | 데코레이터 가능 | 자연 |

| 번들 무게 | 매우 작음 | 매우 작음 | 중간 | 중간 | 큼 |

한 줄로 — **Svelte 5는 "컴파일 시간에 변환되는 시그널"**이다. Solid와 가장 가깝다. 다만 호출 문법(`count()`)이 없어 보통의 JS 변수처럼 읽힌다.

4장 · `$:`에서 룬으로 — 실전 마이그레이션

좋은 소식: **legacy 모드가 계속 동작한다.** Svelte 5는 같은 컴파일러가 `$:`도 처리한다. `svelte.config.js`에서 `compilerOptions.runes = false`를 명시하거나, 컴포넌트 최상단에 `<svelte:options runes={false}/>`를 쓰면 4 시대 문법이 그대로 된다.

또 한 가지 좋은 소식: **컴포넌트 단위로 점진 마이그레이션**이 가능하다. 한 컴포넌트는 룬, 다른 컴포넌트는 `$:`, 한 프로젝트에 공존한다.

자주 만나는 변환 패턴 다섯 개를 살펴본다.

4.1 `let` → `$state`

<!-- before -->

let count = 0

<!-- after -->

let count = $state(0)

마이그레이션 스크립트(`npx sv migrate svelte-5`)가 이 변환을 거의 자동으로 해 준다. 단, "쓰여지지 않는 `let`"은 변환하지 않는다 — 반응성이 필요 없는 변수다.

4.2 `$: foo = ...` → `$derived`

<!-- before -->

let a = 1, b = 2

$: sum = a + b

<!-- after -->

let a = $state(1), b = $state(2)

let sum = $derived(a + b)

4.3 `$: { ... }` 또는 `$: if (...)` → `$effect`

<!-- before -->

let count = 0

$: if (count > 10) document.title = `high: ${count}`

<!-- after -->

let count = $state(0)

$effect(() => {

if (count > 10) document.title = `high: ${count}`

})

4.4 stores → 룬 in 클래스

Svelte 4의 store 패턴을 클래스 + 룬으로 옮기는 것이 2026년 권장 패턴이다.

// before — store

function createCart() {

const items = writable([])

const count = derived(items, ($i) => $i.length)

return {

items,

count,

add: (item) => items.update((arr) => [...arr, item]),

}

}

// after — runes in class (.svelte.ts)

export class Cart {

items = $state([])

count = $derived(this.items.length)

add(item) { this.items.push(item) }

}

export const cart = new Cart()

호출부도 더 짧다. `$cart.count` 같은 store 접두사가 없어진다. `cart.count`다.

4.5 `on:click` → `onclick`

이건 룬과 직접 관계는 없지만 Svelte 5의 변화다.

<!-- before -->

<!-- after -->

`on:click` 같은 콜론 디렉티브 대신 표준 HTML 속성처럼 적는다. 이는 props로 핸들러를 전달하는 패턴과 자연스럽게 맞물린다.

5장 · SvelteKit 2 — 데이터 흐름이 명시적이 되었다

룬과 같은 방향의 변화가 SvelteKit에도 일어났다. 2024년 초의 SvelteKit 2.0이 도입한 두 변화가 2026년 표준이 되었다.

5.1 `await` 명시 — `load`가 더 이상 마법이 아니다

// before (SvelteKit 1)

export const load = async ({ fetch, params }) => {

const post = await fetch(`/api/posts/${params.id}`).then(r => r.json())

return { post }

}

// after (SvelteKit 2)

export const load = async ({ fetch, params, depends }) => {

depends('app:post')

const post = await fetch(`/api/posts/${params.id}`).then(r => r.json())

return { post }

}

`depends`로 무효화 키를 명시하고, `invalidate('app:post')`로 다시 부른다. 이전엔 URL 패턴 매칭으로 암묵적으로 됐다 — 이젠 명시적이다.

5.2 폼 액션과 `enhance` — JS 없이도, JS와 함께도

SvelteKit의 폼 액션은 룬과 무관하게 잘 살아 있다. 점진적 향상의 정수를 보여준다.

let { form } = $props()

{#if form?.error}

{/if}

// +page.server.js

export const actions = {

default: async ({ request }) => {

const data = await request.formData()

const title = data.get('title')

if (!title) return { error: 'title required' }

await db.posts.create({ title })

return { success: true }

},

}

JS가 꺼져도 동작한다. JS가 켜져 있으면 `enhance`가 fetch로 가로채 SPA처럼 처리한다.

5.3 Universal vs Server `load`

`+page.js`(universal)와 `+page.server.js`(server-only)의 구분이 룬으로 더 명확해졌다. 클라이언트에서 다시 도는 universal `load`에서 `$state`를 만들면 hydration 후에도 반응성이 살아 있다. 서버 전용 `load`는 직렬화 가능한 JSON만 반환한다.

6장 · 실 사례 — 누가 룬을 쓰고 있는가

룬은 GA 1년 반 만에 Svelte 생태계 거의 전체에 퍼졌다.

- **SvelteKit 자체**가 룬 기반의 데모와 템플릿을 기본으로 제공한다.

- **Skeleton UI v3** (Tailwind 4 기반 컴포넌트 라이브러리)가 룬 기반으로 다시 쓰였다.

- **shadcn-svelte**가 룬으로 마이그레이션됐다. 더 이상 store 접두사가 없다.

- **TanStack Query for Svelte v5**가 룬 기반 API를 채택했다.

- **HeyApi**·**Bits UI**·**Melt UI** — 모두 룬으로 이주했거나 이주 중이다.

그리고 큰 사용자들 — Spotify·Apple·1Password 일부 팀이 Svelte를 쓰고 있고, Apple Music의 일부 웹 표면이 Svelte로 알려져 있다. 다만 2026년 시점의 **공식 통계**로는 State of JS 2024 기준으로 Svelte의 retention(쓴 사람이 다시 쓸 의향)이 React·Vue와 비교해 가장 높은 축에 든다.

7장 · 무엇을 포기했나 — 정직한 거래

룬은 거의 모든 면에서 4를 이긴다. 그러나 "전부 좋은가"는 아니다.

7.1 어휘 수가 늘어났다

`let count = 0`에서 `let count = $state(0)`으로. 키 입력이 늘어났다. Svelte의 매력 중 하나였던 "JS 같은 간결함"이 살짝 흐려졌다. 다만 그 대가로 **명시성·일관성·확장성**을 얻었다.

7.2 Proxy 비용

`$state`는 객체·배열을 Proxy로 감싼다. 대규모 데이터 구조에서는 오버헤드가 있다. 권장: 핫 패스에는 `$state.raw()`를 쓰거나, 불변 자료구조를 한 덩어리로 갈아끼우는 패턴.

7.3 컴포넌트 밖 룬 = `.svelte.ts` 확장자

룬을 쓰는 일반 모듈은 `.svelte.js` 또는 `.svelte.ts`여야 한다. 그렇지 않으면 컴파일러가 처리하지 않는다. 작은 거지만 빌드 설정이 익숙하지 않으면 함정이다.

7.4 `$effect`의 함정

`$effect` 안에서 `$state`를 쓰면 무한 루프가 도는 게 가능하다. ESLint 룰(`svelte/no-reactive-reassign`)과 컴파일러 경고가 잡아 주지만, 사람의 주의가 여전히 필요하다.

7.5 학습 곡선

Svelte 4를 알던 사람에겐 두 버전을 동시에 머리에 둬야 하는 기간이 있다. 룬이 다 좋아도, 기존 자료의 절반은 `$:` 문법이다.

7.6 React 생태계만큼은 아니다

이건 룬 자체의 문제가 아니라 생태계의 크기 문제다. 2026년에도 npm 다운로드, 라이브러리 수, 채용 시장 모두 React가 더 크다. 다만 **개발자 만족도·러닝 곡선·번들 크기·런타임 성능**에서는 Svelte가 우위다.

8장 · "React-but-better"는 2026년에도 참인가

오래된 명제가 있다. "Svelte = React-but-better." 이걸 2026년에 다시 묻자.

8.1 React 19와 React Compiler

React 19가 `use()`·`Actions`·서버 컴포넌트의 시대를 굳혔다. React Compiler가 GA에 들어가면서 수동 `useMemo`·`useCallback`이 줄었다. 메모이제이션의 자동화는 사실 Solid·Svelte·Vue가 이미 갖고 있던 것 — React가 마침내 따라왔다.

그래도 React가 이기는 곳:

- **생태계 크기** — 라이브러리·툴·채용·자료 다 더 많다.

- **서버 컴포넌트(RSC)** — Svelte도 비슷한 방향(서버 데이터 + 클라이언트 인터랙션)이지만, React가 추상의 깊이에서 앞선다.

- **호환성 기반** — React Native·Expo·매우 큰 회사들의 표준.

8.2 Svelte 5가 이기는 곳

- **번들 크기** — 비교할 수 없을 만큼 작다. 라우터·반응성·DOM 처리 모두 합쳐서 React + ReactDOM의 1/5 수준.

- **명시적 반응성** — 룬이 의도를 코드에 박는다. React Compiler가 "자동"이라면, 룬은 "선언"이다.

- **컴포넌트 안과 밖의 일관성** — 룬은 어디서나 같다. React 훅은 컴포넌트(또는 다른 훅) 안에서만 된다.

- **DX의 단순함** — `bind:value`·`transition:fade`·`use:enhance` 같은 디렉티브가 그대로 쓰인다.

8.3 결론은 결국 "맥락"

"Svelte = React-but-better"는 **개발 경험**에서는 여전히 참이다. **생태계의 폭과 깊이**에서는 React가 여전히 크다. 채용·기존 코드·팀 익숙함을 빼면 Svelte 5의 매력은 GA 시점보다 더 커졌다. 룬이 그 마지막 약점("컴포넌트 밖에서의 반응성")을 메웠기 때문이다.

9장 · Solid·Vue와 더 깊게

룬을 시그널과 ref와 같은 자리에 놓는다면, 디테일에서 어디가 다른가.

9.1 vs Solid Signals

// Solid

const [count, setCount] = createSignal(0)

console.log(count()) // ← 함수 호출

setCount(1)

setCount(c => c + 1)

<!-- Svelte 5 -->

let count = $state(0)

console.log(count) // ← 변수처럼

count = 1

count += 1

Solid는 시그널을 함수로 만든다. 호출이 추적 단위가 된다. 장점: 어디서 추적되는지 코드에서 바로 보인다. 단점: 키 입력이 늘고, 객체 안에서 시그널을 다루기가 살짝 번거롭다(`createStore`로 풀긴 한다).

Svelte 5는 컴파일러가 같은 일을 한다. `count` 식별자 접근을 자동으로 추적 호출로 변환한다. 장점: 보통 JS처럼 읽힌다. 단점: "지금 추적되고 있는가"를 보려면 컴파일 결과를 봐야 한다.

9.2 vs Vue 3 ref/computed

const count = ref(0)

const doubled = computed(() => count.value * 2)

Vue는 `.value`를 통해 ref 접근을 명시한다. 템플릿 안에서는 `.value`가 자동 unwrap된다. 룬과 가장 가까운 모델이지만, **컴파일 시점이 아니라 런타임 Proxy**라는 차이가 있다.

Vue 3.4+는 **Vapor 모드**(컴파일타임 반응성, Solid·Svelte 스타일)를 실험 중이다. 2026년 시점에 Vapor가 일반화되면, ref와 룬의 거리는 더 좁혀진다.

9.3 vs MobX

MobX는 OOP 친화적이다. 클래스에 `makeAutoObservable`을 부르면 모든 필드가 자동 observable, 모든 getter가 자동 computed가 된다.

Svelte 5의 "runes in classes" 패턴은 의도적으로 MobX와 닮았다.

// Svelte 5

class Cart {

items = $state([])

count = $derived(this.items.length)

add(item) { this.items.push(item) }

}

// MobX

class Cart {

items = []

constructor() { makeAutoObservable(this) }

get count() { return this.items.length }

add(item) { this.items.push(item) }

}

MobX는 데코레이터 또는 호출로 변환한다. Svelte 5는 룬으로 명시한다. 의미는 거의 같다.

10장 · 컴파일 결과를 들여다보기 — 무엇이 진짜로 일어나나

룬을 쓴 코드는 컴파일러가 뭐로 바꿀까. 단순한 예를 추적해 보자.

<!-- 소스 -->

let count = $state(0)

let doubled = $derived(count * 2)

$effect(() => console.log(count))

대략적으로 컴파일 결과는 이렇게 된다(개념적, 실제 출력은 더 복잡하다).

function Component($$anchor) {

let count = $.state(0)

let doubled = $.derived(() => $.get(count) * 2)

$.user_effect(() => console.log($.get(count)))

const button = $.template('<button> </button>')()

$.event('click', button, () => $.set(count, $.get(count) + 1))

$.text(button.firstChild, () => $.get(count))

$.append($$anchor, button)

}

핵심:

- 식별자 `count`가 **시그널 셀**로 변환된다.

- `count` 읽기는 `$.get(count)` 호출로, 쓰기는 `$.set(count, ...)` 호출로 변환된다.

- `$derived`는 `$.derived(thunk)`로 — Solid의 `createMemo`와 사실상 같은 구현이다.

- `$effect`는 `$.user_effect(thunk)`로 — 마운트 이후, 의존성 변경 시마다 재실행된다.

즉 룬은 **사람이 시그널을 호출 없이 쓰게 만든 컴파일러 마법**이다. 4의 마법과 다른 점은, 마법의 트리거가 위치가 아니라 **명시적 토큰**이라는 것.

11장 · 큰 앱에서의 룬 — 패턴과 안티패턴

11.1 권장 패턴

1. **도메인 로직은 `.svelte.ts` 모듈에 두기** — 컴포넌트는 표현에 집중.

2. **클래스 + 룬으로 상태 컨테이너** — store보다 타입이 자연스럽다.

3. **`$derived`로 파생, `$effect`로만 부수 효과** — 둘을 섞지 않는다.

4. **`$effect`의 cleanup 함수**를 항상 챙긴다. 메모리 누수의 80%가 여기서 난다.

5. **얕은 반응성이 필요하면 `$state.raw`** — 큰 트리에서 Proxy 비용을 피한다.

6. **prop 타입은 인터페이스로** — `let { ... }: Props = $props()`.

7. **`$bindable()`은 정말 양방향이 필요한 곳만** — 단방향 데이터 흐름이 디폴트.

11.2 안티패턴

1. **`$effect` 안에서 `$state` 쓰기** — 무한 루프. 파생이 필요하면 `$derived`.

2. **module-level `$state`를 직접 export** — 여러 SSR 요청이 공유한다. 함수로 감싸야 한다.

3. **`$:` 잔재를 룬과 섞기** — legacy 모드 아니면 컴파일러가 거부한다. 한 컴포넌트는 한쪽으로 통일.

4. **거대한 객체를 `$state`** — Proxy가 깊이 들어가 비싸다. 평탄화하거나 `$state.raw`.

5. **store를 룬과 같이 운영** — 가능하지만 두 모델을 머리에 같이 둬야 한다. 가능하면 일관화.

6. **`$effect`로 데이터 페치** — 가능하지만 SvelteKit `load`가 더 좋다. `$effect`는 정말 클라이언트 사이드 부수 효과용.

7. **`$inspect`를 프로덕션에 두기** — 개발용 디버깅 룬이다.

12장 · 도구·생태계 — 룬 시대의 주변부

12.1 IDE·언어 서버

VS Code의 Svelte for VS Code 확장이 룬을 완벽히 인식한다. JetBrains WebStorm 2024.3+도 같다. 룬이 컴파일러 키워드라서, 의미는 LSP가 알려준다.

12.2 ESLint·Prettier

`eslint-plugin-svelte`가 룬용 규칙 묶음을 제공한다. `svelte/no-reactive-reassign`, `svelte/require-state-with-init` 같은 게 핵심이다.

12.3 테스트

`vitest` + `@testing-library/svelte`가 표준. `.svelte.ts` 안의 룬을 테스트할 때는 `flushSync()`로 동기 상태 업데이트를 강제하는 패턴이 자주 쓰인다.

12.4 빌드

Vite 5가 권장 번들러, Vite 6도 호환. SvelteKit이 그 위에 라우팅·SSR·어댑터를 얹는다. Bun도 Svelte 5를 정식 지원하지만, SvelteKit 어댑터는 Node·Vercel·Cloudflare·Netlify가 더 성숙하다.

12.5 컴포넌트 라이브러리

- **shadcn-svelte** — 룬 기반 무스타일 컴포넌트, 복사-붙여넣기 모델.

- **Skeleton v3** — 룬 + Tailwind 4.

- **Bits UI** — 헤드리스 컴포넌트, 룬 기반.

- **Melt UI** — 빌더 패턴 헤드리스, 룬 마이그레이션 완료.

13장 · 미래 — 룬 다음은 무엇인가

Svelte 코어팀이 공개적으로 이야기하는 다음 방향은 세 가지다.

1. **Svelte 6** — 룬 기반 API를 안정화하고, legacy 모드를 점진적으로 제거. 6에서 legacy는 deprecation, 7에서 제거 후보.

2. **컴파일러 최적화** — Vapor mode 같은 발상이 Svelte 5 위에 더 깊이 들어간다. fine-grained 업데이트는 이미 하고 있고, 그 위에 텍스트·속성·부분 트리 단위 최적화가 더해진다.

3. **SvelteKit과 RSC의 경계** — React 서버 컴포넌트와 같은 종류의 분리(서버 부품 vs 클라이언트 부품)를 룬·`load`·폼 액션 위에서 어떻게 더 자연스럽게 풀지가 과제.

그리고 더 큰 그림: **시그널의 표준화.** TC39 시그널 제안이 진행 중이다. Svelte·Solid·Vue·Angular가 비슷한 기본형을 공유하면, 룬은 시그널 호출 위의 표면 문법이 된다.

에필로그 — 명시성을 향한 한 걸음

Svelte 4의 `let`은 아름다웠다. "그냥 JS"였다. 그러나 "그냥 JS"가 아니었다. 컴파일러가 의미를 바꿨다. 그 비밀이 새로운 사람의 발목을 잡았고, TypeScript와 마찰했고, 컴포넌트 밖으로 못 나갔다.

Svelte 5의 룬은 그 비밀을 코드에 박았다. **`$state`가 있는 곳에 반응성이 있다.** 더 길어졌지만, 더 정직해졌다. Solid·Vue·MobX가 다 자기 식으로 도달한 같은 결론을 — Svelte는 자기 식으로 도달했다.

그리고 그 결론은 한 문장으로 요약된다.

> **"마법은 좋다. 다만 마법을 부르는 단어는 사람에게 보여야 한다."**

12개 항목 체크리스트

1. `.svelte` 컴포넌트가 룬 모드에 있는가(`runes: true` 또는 자동)?

2. `let` 변수가 진짜 반응성이 필요한가 — 필요한 경우만 `$state`?

3. 파생 값은 `$derived`로, 효과는 `$effect`로 명확히 분리됐는가?

4. `$effect` 안에서 `$state`에 쓰지 않는가?

5. `$effect`의 cleanup 함수를 챙겼는가?

6. 큰 객체는 `$state.raw`로 얕은 반응성만 가져갔는가?

7. 컴포넌트 밖 룬 모듈이 `.svelte.ts` 확장자인가?

8. props는 `let { ... }: Props = $props()` 스타일로 적었는가?

9. 양방향 바인딩이 필요한 prop만 `$bindable()`인가?

10. store에서 룬으로 마이그레이션할 때 SSR 안전한가(함수로 감싸기)?

11. SvelteKit `load`의 무효화를 `depends`/`invalidate`로 명시했는가?

12. legacy 모드의 잔재(`$:`·`on:click`·`export let`)가 새 코드에 섞이지 않았는가?

안티패턴 10가지

1. `$effect` 안에서 `$state` 쓰기 — 무한 루프.

2. module-level `$state`를 그대로 export — SSR 공유 버그.

3. 거대한 객체를 `$state` — Proxy 비용.

4. `$:`와 룬을 같은 컴포넌트에 섞기.

5. store와 룬을 같은 도메인에 공존시키기.

6. `$effect`로 데이터 페치 — `load`가 더 맞다.

7. `$inspect`·`$state.snapshot` 디버깅 코드를 프로덕션에 남기기.

8. `$bindable()`을 단방향 데이터에 남용.

9. `.svelte` 파일이 아닌 곳에 룬 쓰기.

10. 룬을 도입하면서 컴파일러 경고를 끄기.

다음 글 예고

다음 글 후보: **SvelteKit 2 데이터 흐름 심층 — `load`·actions·`enhance`의 진짜 의미**, **시그널 표준화(TC39) 추적 — Svelte·Solid·Vue·Angular가 공유할 미래**, **Svelte vs Astro vs SolidStart — 콘텐츠 사이트의 2026년 선택지**.

> "반응성은 마법이 아니라 도구다. 도구의 이름은 코드에 적혀 있어야 한다."

— Svelte 5 Runes, 끝.

참고 / References

- [Svelte 공식 사이트](https://svelte.dev/)

- [Svelte 5 발표 블로그](https://svelte.dev/blog/svelte-5-is-alive)

- [Svelte 5 마이그레이션 가이드](https://svelte.dev/docs/svelte/v5-migration-guide)

- [Runes 문서](https://svelte.dev/docs/svelte/what-are-runes)

- [`$state` 레퍼런스](https://svelte.dev/docs/svelte/$state)

- [`$derived` 레퍼런스](https://svelte.dev/docs/svelte/$derived)

- [`$effect` 레퍼런스](https://svelte.dev/docs/svelte/$effect)

- [`$props` 레퍼런스](https://svelte.dev/docs/svelte/$props)

- [SvelteKit 공식 문서](https://svelte.dev/docs/kit/introduction)

- [SvelteKit 2 마이그레이션](https://svelte.dev/docs/kit/migrating-to-sveltekit-2)

- [Solid 공식 — Reactivity](https://www.solidjs.com/guides/reactivity)

- [Vue 3 Reactivity API](https://vuejs.org/api/reactivity-core.html)

- [Vue Vapor mode RFC](https://github.com/vuejs/rfcs)

- [MobX 공식](https://mobx.js.org/)

- [React Compiler 문서](https://react.dev/learn/react-compiler)

- [TC39 Signals 제안](https://github.com/tc39/proposal-signals)

- [State of JS 2024](https://2024.stateofjs.com/)

- [shadcn-svelte](https://www.shadcn-svelte.com/)

- [Skeleton UI](https://www.skeleton.dev/)

- [Bits UI](https://www.bits-ui.com/)

- [Melt UI](https://melt-ui.com/)

현재 단락 (1/384)

Svelte 3은 약속이었다. "리액티브 코드는 컴파일이 처리해야 한다, 런타임은 가벼워야 한다." `let count = 0`이 반응성을 갖고, `$: doubled = coun...

작성 글자: 0원문 글자: 16,162작성 단락: 0/384