본문으로 건너뛰기 Svelte 5 Complete Guide | Runes· Snippets

Svelte 5 Complete Guide | Runes· Snippets

Svelte 5 Complete Guide | Runes· Snippets

이 글의 핵심

Svelte 5 replaces the compiler-magic reactivity of Svelte 4 with explicit Runes ??$state, $derived, $effect, $props. The result is more predictable reactivity, better TypeScript support, and a simpler mental model for complex state.

What Changed in Svelte 5

Svelte 5 replaces implicit compiler magic with explicit Runes ??special function calls the compiler recognizes.

<!-- Svelte 4 -->
<script>
  let count = 0;           // Reactive by default (compiler magic)
  $: doubled = count * 2;  // Reactive declaration ($ label)
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);              // Explicit reactive state
  const doubled = $derived(count * 2); // Explicit derived value
</script>

Why Runes?

  • Work outside .svelte files (in .js/.ts for shared logic)
  • Explicit ??no “which variables are reactive?” confusion
  • Better TypeScript inference
  • Consistent with how frameworks like Solid and Angular handle reactivity

$state ??Reactive State

<script lang="ts">
  // Primitive state
  let count = $state(0);
  let name = $state('Alice');
  let visible = $state(true);

  // Object state ??deep reactive (nested properties are also reactive)
  let user = $state({
    name: 'Alice',
    settings: { theme: 'dark', lang: 'en' }
  });

  function updateTheme() {
    user.settings.theme = 'light'; // ??Reactive ??UI updates automatically
  }

  // Array state
  let items = $state<string[]>([]);

  function addItem(item: string) {
    items.push(item);  // ??Reactive push
  }
</script>

<button onclick={() => count++}>Count: {count}</button>
<p>Doubled: {count * 2}</p>

$state.raw ??Shallow Reactive

<script>
  // Only the reference is reactive, not the contents
  let items = $state.raw(['a', 'b', 'c']);

  function replace() {
    items = [...items, 'd']; // Must reassign to trigger update
  }
</script>

$derived ??Computed Values

<script lang="ts">
  let price = $state(100);
  let quantity = $state(3);
  let taxRate = $state(0.1);

  // Recomputes automatically when dependencies change
  const subtotal = $derived(price * quantity);
  const tax = $derived(subtotal * taxRate);
  const total = $derived(subtotal + tax);

  // Complex derived with $derived.by
  const sortedItems = $derived.by(() => {
    // Use .by for multi-line derived logic
    return items
      .filter(item => item.active)
      .sort((a, b) => a.name.localeCompare(b.name));
  });
</script>

<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>

$effect ??Side Effects

<script lang="ts">
  let query = $state('');
  let results = $state<string[]>([]);

  // Runs after DOM updates, re-runs when dependencies change
  $effect(() => {
    if (query.length < 2) {
      results = [];
      return;
    }

    // Fetch results when query changes
    fetchResults(query).then(data => results = data);

    // Return cleanup function
    return () => {
      // Cancels previous fetch if query changes before it completes
    };
  });

  // $effect.pre ??runs before DOM updates (like useLayoutEffect)
  $effect.pre(() => {
    // Measure DOM before render
  });
</script>

<input bind:value={query} placeholder="Search..." />

$props ??Component Props

<!-- Button.svelte -->
<script lang="ts">
  interface Props {
    label: string;
    variant?: 'primary' | 'secondary';
    disabled?: boolean;
    onclick?: () => void;
  }

  // $props() destructures all props at once
  let { label, variant = 'primary', disabled = false, onclick }: Props = $props();
</script>

<button
  class={`btn btn-${variant}`}
  {disabled}
  {onclick}
>
  {label}
</button>
<!-- Usage -->
<Button label="Submit" variant="primary" onclick={() => handleSubmit()} />

Spread Props

<script lang="ts">
  let { class: className, ...rest } = $props();
</script>

<!-- Pass remaining props to underlying element -->
<button class={`base-btn ${className}`} {...rest} />

Bindable Props

<!-- Input.svelte -->
<script lang="ts">
  let { value = $bindable('') } = $props();
</script>

<input bind:value />
<!-- Usage ??two-way binding -->
<Input bind:value={myValue} />

Snippets ??Replace Slots

Snippets replace the <slot> system with explicit, composable render functions.

<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    title: string;
    children: Snippet;             // Default snippet (replaces default slot)
    footer?: Snippet;              // Optional named snippet
    header?: Snippet<[string]>;    // Snippet with parameters
  }

  let { title, children, footer, header } = $props();
</script>

<div class="card">
  <div class="card-header">
    {#if header}
      {@render header(title)}
    {:else}
      <h2>{title}</h2>
    {/if}
  </div>

  <div class="card-body">
    {@render children()}
  </div>

  {#if footer}
    <div class="card-footer">
      {@render footer()}
    </div>
  {/if}
</div>
<!-- Usage -->
<Card title="My Card">
  <!-- Default children snippet -->
  <p>Card content goes here.</p>

  {#snippet footer()}
    <button>Close</button>
  {/snippet}

  {#snippet header(title)}
    <h2 class="custom-title">{title}</h2>
    <span class="badge">New</span>
  {/snippet}
</Card>

Event Handling

Svelte 5 uses standard DOM event attributes (no more on: directive):

<!-- Svelte 4 -->
<button on:click={handleClick}>Click</button>
<button on:click|preventDefault={handleSubmit}>Submit</button>

<!-- Svelte 5 -->
<button onclick={handleClick}>Click</button>

<!-- Modifiers via inline handler -->
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
  <button type="submit">Submit</button>
</form>

<!-- Inline handlers -->
<button onclick={() => count++}>+1</button>
<input oninput={(e) => (value = e.currentTarget.value)} />

Shared Reactive State (Runes in .svelte.ts)

One of the biggest Svelte 5 improvements ??reactive logic in plain .svelte.ts files:

// stores/counter.svelte.ts
// This is a .svelte.ts file ??runes work here!

function createCounter(initial = 0) {
  let count = $state(initial);
  const doubled = $derived(count * 2);

  return {
    get count() { return count; },
    get doubled() { return doubled; },
    increment() { count++; },
    reset() { count = initial; },
  };
}

export const counter = createCounter(0);
<!-- Any component can import and use it -->
<script>
  import { counter } from '../stores/counter.svelte.ts';
</script>

<button onclick={counter.increment}>
  Count: {counter.count} (doubled: {counter.doubled})
</button>

Lifecycle

<script>
  import { onMount, onDestroy } from 'svelte';

  let element: HTMLElement;

  onMount(() => {
    // Runs after component mounts to DOM
    const observer = new ResizeObserver(() => { /* ... */ });
    observer.observe(element);

    return () => observer.disconnect();  // Cleanup on destroy
  });

  // $effect replaces most onMount use cases in Svelte 5
  $effect(() => {
    const timer = setInterval(() => tick(), 1000);
    return () => clearInterval(timer);
  });
</script>

Migration: Svelte 4 ??5

# Automated migration tool
npx sv migrate svelte-5

Key changes:

Svelte 4Svelte 5
let count = 0let count = $state(0)
$: doubled = count * 2const doubled = $derived(count * 2)
$: { sideEffect() }$effect(() => { sideEffect(); })
export let proplet { prop } = $props()
<slot />{@render children()}
<slot name="x" />{@render x()}
on:click={handler}onclick={handler}
Svelte stores (writable).svelte.ts files with $state

SvelteKit Integration

Svelte 5 works seamlessly with SvelteKit ??the routing and SSR layer:

// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const posts = await fetch('/api/posts').then(r => r.json());
  return { posts };
};
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();
  // data.posts is fully typed from the load function
</script>

{#each data.posts as post}
  <article>
    <h2>{post.title}</h2>
  </article>
{/each}

Related posts:


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Svelte 5 replaces the compiler-magic reactivity of Svelte 4 with explicit Runes ??$state, $derived, $effect, $props. The result is more predictable reactivity, better TypeScript support, and a simpler mental model for complex state.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Svelte, Svelte 5, SvelteKit, JavaScript, TypeScript, Frontend, Runes.