$state 및 $derived: Svelte 5 룬을 사용한 범용 반응성
응답성 시스템은 모든 최신 UI 프레임워크의 핵심입니다. Svelte 5가 완전히 새롭게 디자인되었습니다.
Svelte 4의 "마법 할당"에서 다음으로 이동하여 반응성이 작동하는 방식 Runes: 기능
접두사가 있는 특수 $ 컴파일러와 직접 통신합니다. 두 가지 기본 룬
나는 $state e $derived, 이는 응답성 사용 사례의 90%를 포괄합니다.
모든 Svelte 애플리케이션.
이 가이드에서는 이 두 Rune이 내부적으로 어떻게 작동하는지, 왜 ES6 프록시를 사용하는지 자세히 설명합니다.
깊은 반응성 $derived 자동 메모 기능을 구현하고 — 그 이상
중요 — 일반 TypeScript 모듈의 .svelte 파일 외부에서 사용하는 방법
완전히 새로운 상태 관리 패턴.
전제 조건
- Svelte 5에 대한 기본 지식(문서 1: 컴파일러 기반 접근 방식 참조)
- 중급 TypeScript(제네릭, 프록시, getter/setter)
- 메모 및 종속성 추적 개념
$state: ES6 프록시의 반응 상태
Svelte 4에서는 블록에 선언된 모든 변수 <script> 자동으로 그랬어
반응성: 컴파일러가 각 할당을 추적하고 업데이트 코드를 생성했습니다. 이것은 효과가 있었습니다
기본 유형이지만 객체와 배열의 경우 제한이 있었습니다.
array.push() o obj.nested.prop = value 그들은 추적되지 않았습니다.
Svelte 5는 이 문제를 다음과 같이 해결합니다. $state, 그가 사용하는 ES6 프록시 모든 것을 가로채기 위해
중첩된 객체를 포함한 객체 액세스 및 수정. 그 결과는 진정한 심층 반응성입니다.
<script lang="ts">
// Tipi primitivi: reattivita diretta
let count = $state(0);
let name = $state('Federico');
let isVisible = $state(true);
// Oggetti: reattivita profonda tramite Proxy
let user = $state({
name: 'Federico',
address: {
city: 'Bari',
country: 'IT'
},
tags: ['developer', 'writer']
});
function updateCity() {
// Questo funziona! La modifica profonda e tracciata
user.address.city = 'Milano';
}
function addTag(tag: string) {
// Anche push() e tracciato grazie al Proxy
user.tags.push(tag);
}
</script>
<p>Citta: {user.address.city}</p>
<p>Tags: {user.tags.join(', ')}</p>
스벨트 4를 사용하면, user.address.city = 'Milano' UI 업데이트가 실행되지 않았을 것입니다.
왜냐하면 컴파일러는 최상위 변수(user = ...).
Svelte 5 및 Proxy를 사용하면 모든 깊이 수준의 모든 액세스가 차단됩니다.
$state 프록시가 내부적으로 작동하는 방식
깊은 응답성을 이해하려면 ES6 프록시의 작동 방식과 Svelte가 이를 사용하는 방식을 이해해야 합니다.
// Implementazione concettuale del Proxy usato da $state
// (semplificata rispetto al codice reale di Svelte 5)
function createReactiveProxy<T extends object>(
target: T,
onChange: () => void
): T {
return new Proxy(target, {
get(obj, prop) {
const value = Reflect.get(obj, prop);
// Se il valore e un oggetto/array, crea un Proxy annidato
if (value !== null && typeof value === 'object') {
return createReactiveProxy(value, onChange);
}
return value;
},
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
// Notifica gli osservatori che qualcosa e cambiato
onChange();
return result;
}
});
}
기본적으로 글을 쓰다보면 user.address.city = 'Milano', 이런 일이 발생합니다:
- 외부 프록시가 가로채기
.address다음에 대해 중첩된 프록시를 반환합니다.address - 중첩된 프록시 가로채기
.city = 'Milano' - 값을 업데이트하고 Svelte의 응답 시스템에 알립니다.
- Svelte는 다음에 의존하는 바인딩에 대해서만 업데이트를 예약합니다.
user.address.city
프록시가 작동하지 않는 경우
프록시를 통한 응답이 예상대로 작동하지 않는 경우가 있습니다.
-
구조 분해는 응답성을 잃습니다.
const { city } = user.address;—city이제 기본 문자열입니다. 프록시가 아닙니다. 항상 사용user.address.city템플릿에서 직접 -
스프레드가 반응성을 잃습니다.
const copy = { ...user };비반응형 복사본을 만듭니다. -
Map 및 Set에는 사용자 정의 배열을 위한 $state.raw가 필요합니다.
모든 이국적인 컬렉션 방법이 패치되는 것은 아닙니다. 맵/세트의 경우 다음을 사용합니다.
$state(new Map())— Quick 5 패치도 적용됩니다.
TypeScript를 사용한 $state: 전체 입력
$state TypeScript와 완전히 통합되었습니다. 유형이 자동으로 추론됩니다.
초기 값에서 시작하지만 제네릭을 사용하여 명시적으로 지정할 수 있습니다.
<script lang="ts">
// Inferenza automatica del tipo
let count = $state(0); // number
let name = $state(''); // string
let flag = $state(false); // boolean
// Tipo esplicito per casi complessi
interface User {
id: number;
name: string;
role: 'admin' | 'user';
metadata: Record<string, unknown>;
}
let user = $state<User>({
id: 1,
name: 'Federico',
role: 'admin',
metadata: {}
});
// Array con tipo esplicito
let items = $state<string[]>([]);
// Stato nullable (inizializzato a null)
let selectedItem = $state<User | null>(null);
// $state.raw: reattivita superficiale (solo l'assegnamento, non le mutazioni)
// Utile per grandi array dove la performance conta
let bigList = $state.raw<number[]>([]);
function addToList(n: number) {
// Con $state.raw, devi riassegnare (non push)
bigList = [...bigList, n];
}
</script>
$derived: 자동 저장소
$derived 종속성이 변경될 때만 다시 계산되는 계산된 값을 생성합니다.
그리고 useMemo React의 종속성을 명시적으로 선언할 필요 없이 —
컴파일러는 함수 코드를 분석하여 이를 자동으로 추적합니다.
<script lang="ts">
let items = $state([
{ id: 1, name: 'Laptop', price: 1200, category: 'tech' },
{ id: 2, name: 'Mouse', price: 25, category: 'tech' },
{ id: 3, name: 'Desk', price: 450, category: 'furniture' }
]);
let filterCategory = $state('');
let sortBy = $state<'name' | 'price'>('name');
let sortAsc = $state(true);
// $derived traccia automaticamente: items, filterCategory, sortBy, sortAsc
const filteredAndSorted = $derived(() => {
let result = filterCategory
? items.filter(i => i.category === filterCategory)
: [...items];
result.sort((a, b) => {
const mult = sortAsc ? 1 : -1;
if (sortBy === 'name') return a.name.localeCompare(b.name) * mult;
return (a.price - b.price) * mult;
});
return result;
});
// Dipende solo da filteredAndSorted
const totalValue = $derived(
filteredAndSorted.reduce((sum, item) => sum + item.price, 0)
);
// Dipende da items direttamente
const categories = $derived([...new Set(items.map(i => i.category))]);
</script>
Svelte 5는 푸시-풀 시스템 메모용: 종속성이 변경되면
$derived "더티"(푸시)로 표시되지만 값이 즉시 다시 계산되지는 않습니다.
누군가가 읽을 때만(풀) 다시 계산됩니다. 아무도 파생된 내용을 읽지 않으면 계산이 발생하지 않습니다.
$derived.by: 함수를 사용한 복잡한 계산
둘 이상의 표현식이 필요한 계산의 경우 다음을 사용하십시오. $derived.by() 그가 받아들이는 것
본문이 있는 함수:
<script lang="ts">
let transactions = $state([
{ amount: 100, type: 'income', date: new Date('2026-01-15') },
{ amount: -50, type: 'expense', date: new Date('2026-01-16') },
{ amount: 200, type: 'income', date: new Date('2026-02-01') },
{ amount: -75, type: 'expense', date: new Date('2026-02-15') }
]);
// Per computazioni multi-step, usa $derived.by
const stats = $derived.by(() => {
const income = transactions
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0);
const expenses = transactions
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
const balance = income - expenses;
const byMonth = transactions.reduce((acc, t) => {
const key = t.date.toISOString().slice(0, 7); // YYYY-MM
acc[key] = (acc[key] ?? 0) + t.amount;
return acc;
}, {} as Record<string, number>);
return { income, expenses, balance, byMonth };
});
</script>
<p>Saldo: {stats.balance}</p>
<p>Entrate: {stats.income} — Uscite: {stats.expenses}</p>
TypeScript 파일의 룬: 혁명적인 뉴스
Svelte 4와 Svelte 5의 가장 중요한 차이점은 Runes의 구문이 아니라 룬은 .svelte.ts 또는 .svelte.js 확장자를 가진 모든 파일에서 작동합니다., 그뿐만 아니라 구성 요소에서. 이를 통해 완전히 새로운 상태 관리 패턴이 가능해졌습니다.
// src/lib/stores/cart.svelte.ts
// Un "store" costruito con i Runes, senza dipendenze da Svelte stores
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function createCart() {
let items = $state<CartItem[]>([]);
// Computati derivati dallo stato del carrello
const total = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
const itemCount = $derived(
items.reduce((sum, item) => sum + item.quantity, 0)
);
const isEmpty = $derived(items.length === 0);
function addItem(item: Omit<CartItem, 'quantity'>) {
const existing = items.find(i => i.id === item.id);
if (existing) {
existing.quantity++; // Mutazione diretta: il Proxy la traccia
} else {
items.push({ ...item, quantity: 1 });
}
}
function removeItem(id: number) {
const index = items.findIndex(i => i.id === id);
if (index !== -1) items.splice(index, 1);
}
function updateQuantity(id: number, quantity: number) {
const item = items.find(i => i.id === id);
if (item) {
if (quantity <= 0) removeItem(id);
else item.quantity = quantity;
}
}
function clear() {
items = [];
}
return {
// Esponi come getter per proteggere l'accesso diretto
get items() { return items; },
get total() { return total; },
get itemCount() { return itemCount; },
get isEmpty() { return isEmpty; },
addItem,
removeItem,
updateQuantity,
clear
};
}
// Singleton: un'istanza condivisa tra tutti i componenti che importano questo modulo
export const cart = createCart();
이 저장소는 모든 .svelte 구성 요소로 가져와 전체 기능과 함께 직접 사용할 수 있습니다. 반응성:
<!-- CartSummary.svelte -->
<script lang="ts">
import { cart } from '$lib/stores/cart.svelte';
</script>
{#if cart.isEmpty}
<p>Il carrello e vuoto</p>
{:else}
<p>{cart.itemCount} prodotti — Totale: €{cart.total.toFixed(2)}</p>
{#each cart.items as item (item.id)}
<div>
<span>{item.name}</span>
<button onclick={() => cart.updateQuantity(item.id, item.quantity - 1)}>-</button>
<span>{item.quantity}</span>
<button onclick={() => cart.updateQuantity(item.id, item.quantity + 1)}>+</button>
<button onclick={() => cart.removeItem(item.id)}>Rimuovi</button>
</div>
{/each}
<button onclick={cart.clear}>Svuota carrello</button>
{/if}
TypeScript 클래스의 $state
룬은 TypeScript 클래스에서도 작동하여 상태 관리를 위한 OOP 스타일 패턴을 활성화합니다.
// src/lib/models/todo-list.svelte.ts
export class TodoList {
// I campi della classe usano $state come inizializzatore
items = $state<{ id: number; text: string; done: boolean }[]>([]);
filter = $state<'all' | 'active' | 'done'>('all');
// $derived come getter calcolato
readonly filtered = $derived.by(() => {
switch (this.filter) {
case 'active': return this.items.filter(i => !i.done);
case 'done': return this.items.filter(i => i.done);
default: return this.items;
}
});
readonly stats = $derived.by(() => ({
total: this.items.length,
done: this.items.filter(i => i.done).length,
active: this.items.filter(i => !i.done).length
}));
private nextId = 1;
add(text: string) {
this.items.push({ id: this.nextId++, text, done: false });
}
toggle(id: number) {
const item = this.items.find(i => i.id === id);
if (item) item.done = !item.done;
}
delete(id: number) {
this.items = this.items.filter(i => i.id !== id);
}
clearDone() {
this.items = this.items.filter(i => !i.done);
}
}
// Puo essere usata come singleton o istanziata per componente
export const todoList = new TodoList();
React useMemo 및 Vue 계산 비교
자동 종속성 추적 $derived 버그의 주요 원인을 제거합니다.
동등한 React 및 Vue 솔루션에서:
// React: dipendenze manuali, fonte di bug
const filteredItems = useMemo(() => {
return items.filter(i => i.category === filter);
}, [items, filter]); // Se dimentichi una dipendenza: bug silenzioso
// Vue 3: tracking automatico, ma sintassi verbosa
const filteredItems = computed(() => {
return items.value.filter(i => i.category === filter.value);
});
// Svelte 5: tracking automatico, sintassi diretta
const filteredItems = $derived(
items.filter(i => i.category === filter)
);
실질적인 차이점은 React를 사용하면 배열의 종속성을 잊어버린다는 것입니다. useMemo 생산하다
자동으로 사용되지 않는 값, 디버그하기 매우 어려운 버그입니다. Svelte 5에는 이 문제가 없습니다.
추적은 프록시를 통해 런타임에 발생하기 때문입니다.
$state와 $derived를 사용해야 하는 경우
- $state 사용 사용자 작업이나 이벤트에 따라 변경되는 값 (입력 필드, 토글 스위치, API에서 로드된 데이터)
- $파생 사용 기존 $state에서 계산할 수 있는 모든 것에 대해 (합계, 필터, 서식 지정, 변환)
-
계산에 $ effect를 사용하지 마십시오 — 당신이 글을 쓰고 있다면
$effect(() => { derived = compute(state); }), 미국$derived대신에
고급 패턴: 공유 반응성 컨텍스트
복잡한 애플리케이션과 공유되는 반응형 "컨텍스트" 생성에 매우 유용한 패턴입니다. 계층 구조의 구성 요소 간:
// src/lib/context/theme.svelte.ts
import { getContext, setContext } from 'svelte';
const THEME_KEY = Symbol('theme');
export function createThemeContext() {
let isDark = $state(
typeof window !== 'undefined'
? localStorage.getItem('theme') === 'dark'
: false
);
const theme = $derived(isDark ? 'dark' : 'light');
const cssClass = $derived(`theme-${theme}`);
$effect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('theme', theme);
document.documentElement.className = cssClass;
}
});
function toggle() { isDark = !isDark; }
function setDark(value: boolean) { isDark = value; }
const context = { get isDark() { return isDark; }, get theme() { return theme; }, toggle, setDark };
setContext(THEME_KEY, context);
return context;
}
export function useTheme() {
return getContext<ReturnType<typeof createThemeContext>>(THEME_KEY);
}
결론 및 다음 단계
$state e $derived 이들은 Svelte 5의 반응성의 두 기둥입니다.
.svelte 구성 요소 외부에서 작동하는 기능은 Svelte 4의 아키텍처 가능성을 열어줍니다.
허용되지 않음: 모듈식 상태 관리, 격리된 논리 테스트 가능, 반응성이 있는 OOP 패턴
명시적인 Context API 없이 구성 요소 간 상태를 통합하고 공유합니다.
다음 기사에서 검토해 보겠습니다. $효과 — 부작용에 대한 룬 — 그리고 언제 설명되는지
올바르게 사용하고 대신 사용해야 하는 경우 $derived. 중요한 주제이기 때문에
남용 $effect Svelte 5의 주요 안티 패턴입니다.
시리즈: Svelte 5 및 프런트엔드 컴파일러 기반
- 기사 1: 컴파일러 기반 접근 방식 및 정신 모델
- 제2조(본): $state 및 $derived — 룬과의 범용 반응성
- 3조: $효과 및 수명 주기 — 사용 시기(사용하지 않을 경우)
- 기사 4: SvelteKit SSR, 스트리밍 및 로드 기능
- 기사 5: Svelte 5의 전환 및 애니메이션
- 기사 6: Svelte의 접근성: 컴파일러 경고 및 모범 사례
- 7장: 전역 상태 관리: 컨텍스트, 룬 및 저장소
- 기사 8: Svelte 4에서 Svelte 5로 마이그레이션 — 실용 가이드
- 조항 9: Svelte 5: Vitest, 테스트 라이브러리 및 극작가에서의 테스트







