003: Svelte 5 Runes 시스템
Runes 소개
Svelte 5는 Runes라는 새로운 반응성 시스템을 도입했습니다. Runes는 $state, $derived, $effect와 같은 특별한 함수로, 반응형 상태와 계산을 더 명시적으로 선언할 수 있게 해줍니다.
Runes는 $ 접두사를 가진 함수처럼 보이지만, 실제로는 Svelte 컴파일러에게 지시를 내리는 특별한 키워드입니다. 일반 JavaScript 함수와 달리 임포트할 필요가 없으며, 변수에 할당하거나 함수 인자로 전달할 수 없습니다.
Svelte 4 vs Svelte 5 실시간 비교:
Svelte 4 스타일
안녕하세요, Svelte 4!
카운트: 0, 두 배: 0
컴포넌트 코드:
<script lang="ts">
// Svelte 4 스타일 컴포넌트
export let name: string = 'World';
let count: number = 0;
$: doubled = count * 2;
function increment(): void {
count += 1;
}
</script>
<div class="bg-slate-700 p-4 rounded">
<h3 class="text-lg text-sky-400 mb-2">Svelte 4 스타일</h3>
<p class="text-gray-200">안녕하세요, {name}!</p>
<p class="text-gray-200">카운트: {count}, 두 배: {doubled}</p>
<button
on:click={increment}
class="bg-sky-600 text-white py-1 px-3 rounded mt-2 hover:bg-sky-500"
>
증가
</button>
</div>Svelte 5 스타일
안녕하세요, Svelte 5!
카운트: 0, 두 배: 0
컴포넌트 코드:
<script lang="ts">
// Svelte 5 스타일 컴포넌트
let { name = "World" } = $props<{name?: string}>();
let count = $state(0);
let doubled = $derived(count * 2);
function increment(): void {
count += 1;
}
</script>
<div class="bg-slate-700 p-4 rounded">
<h3 class="text-lg text-sky-400 mb-2">Svelte 5 스타일</h3>
<p class="text-gray-200">안녕하세요, {name}!</p>
<p class="text-gray-200">카운트: {count}, 두 배: {doubled}</p>
<button
onclick={increment}
class="bg-sky-600 text-white py-1 px-3 rounded mt-2 hover:bg-sky-500"
>
증가
</button>
</div>두 컴포넌트는 동일한 기능을 하지만, 구현 방식이 다릅니다. Svelte 4는 $: 반응형 선언을 사용하고, Svelte 5는 $state와 $derived 룬을 사용합니다. 또한 이벤트 핸들링 방식(on:click vs onclick)도 다릅니다.
$state 룬
$state 룬은 반응형 상태를 생성합니다. 이 상태가 변경되면 UI가 자동으로 업데이트됩니다. 기본 타입(숫자, 문자열, 불리언)뿐만 아니라 객체와 배열도 반응형으로 만들 수 있습니다.
$state 기본 예제
카운트: 0
이름: Svelte
활성화 상태: 활성
사용자: 홍길동, 30세
항목: 사과, 바나나, 오렌지
$state 코드 예제:
<script lang="ts">
// $state 룬 예제 컴포넌트
let count = $state(0);
let name = $state("Svelte");
let isActive = $state(true);
let user = $state({
name: "홍길동",
age: 30,
});
let items = $state(["사과", "바나나", "오렌지"]);
function updateState() {
// 기본 타입 업데이트
count += 1;
name = "Svelte 5";
isActive = !isActive;
// 객체 업데이트
user.age += 1;
// 배열 업데이트
items = [...items, "포도"];
}
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<p class="text-gray-200">카운트: <span class="text-sky-400">{count}</span></p>
<p class="text-gray-200">이름: <span class="text-sky-400">{name}</span></p>
<p class="text-gray-200">활성화 상태: <span class="text-sky-400">{isActive ? '활성' : '비활성'}</span></p>
<p class="text-gray-200">사용자: <span class="text-sky-400">{user.name}, {user.age}세</span></p>
<p class="text-gray-200">항목: <span class="text-sky-400">{items.join(', ')}</span></p>
</div>
<button
onclick={updateState}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
상태 업데이트
</button>객체 반응성 예제
이름: 홍길동
나이: 30
취미: 독서, 영화 감상
객체와 배열 반응성 코드 예제:
<script lang="ts">
// 객체와 배열의 반응성 예제
let person = $state({
name: "홍길동",
age: 30,
hobbies: ["독서", "영화 감상"],
});
function incrementAge() {
person.age += 1;
}
function changeName() {
person.name = person.name === "홍길동" ? "김철수" : "홍길동";
}
function addHobby() {
person.hobbies = [...person.hobbies, "여행"];
}
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<p class="text-gray-200">이름: <span class="text-sky-400">{person.name}</span></p>
<p class="text-gray-200">나이: <span class="text-sky-400">{person.age}</span></p>
<p class="text-gray-200">취미: <span class="text-sky-400">{person.hobbies.join(", ")}</span></p>
</div>
<div class="flex flex-wrap gap-2">
<button
onclick={incrementAge}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
나이 증가
</button>
<button
onclick={changeName}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
이름 변경
</button>
<button
onclick={addHobby}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
취미 추가
</button>
</div>할일 목록 예제
- 스벨트 배우기
- 룬 시스템 이해하기
할일 목록 코드 예제:
<script lang="ts">
// 할일 목록 예제
let todoList = $state([
{ id: 1, text: "스벨트 배우기", done: false },
{ id: 2, text: "룬 시스템 이해하기", done: true },
]);
function addTodo() {
const newId =
todoList.length > 0 ? Math.max(...todoList.map((t) => t.id)) + 1 : 1;
todoList = [...todoList, { id: newId, text: "새 할일", done: false }];
}
function toggleTodo(id: number) {
todoList = todoList.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo,
);
}
function removeTodo(id: number) {
todoList = todoList.filter((todo) => todo.id !== id);
}
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<ul class="list-none p-0">
{#each todoList as todo (todo.id)}
<li class="flex items-center mb-2">
<input
type="checkbox"
checked={todo.done}
onchange={() => toggleTodo(todo.id)}
class="mr-2"
/>
<span class={todo.done ? "line-through text-gray-400" : "text-gray-200"}>
{todo.text}
</span>
<button
onclick={() => removeTodo(todo.id)}
class="ml-auto bg-red-600 text-white text-xs py-1 px-2 rounded hover:bg-red-500"
>
삭제
</button>
</li>
{/each}
</ul>
</div>
<button
onclick={addTodo}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
할일 추가
</button>$derived 룬
$derived 룬은 다른 상태에서 파생된 값을 계산합니다. 원본 상태가 변경되면 파생된 값도 자동으로 업데이트됩니다. 이는 이전 버전의 $: 반응형 선언을 대체합니다.
$derived 예제
Count: 0
Doubled: 0
Tripled: 0
Message: 0의 2배는 0, 3배는 0입니다
할일 진행률: 50% (1/2)
$derived 코드 예제:
<script lang="ts">
// $derived 룬 예제
let count = $state(0);
let doubled = $derived(count * 2);
let tripled = $derived(count * 3);
let message = $derived(`${count}의 2배는 ${doubled}, 3배는 ${tripled}입니다`);
// 할일 목록 예제를 위한 상태
let todos = $state([
{ id: 1, done: false, text: "스벨트 배우기" },
{ id: 2, done: true, text: "룬 시스템 이해하기" },
]);
let completedCount = $derived(todos.filter((todo) => todo.done).length);
let totalCount = $derived(todos.length);
let progress = $derived(
totalCount === 0 ? 0 : Math.round((completedCount / totalCount) * 100)
);
function incrementCount() {
count += 1;
}
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<p class="text-gray-200">Count: <span class="text-sky-400">{count}</span></p>
<p class="text-gray-200">Doubled: <span class="text-sky-400">{doubled}</span></p>
<p class="text-gray-200">Tripled: <span class="text-sky-400">{tripled}</span></p>
<p class="text-gray-200">Message: <span class="text-sky-400">{message}</span></p>
<hr class="border-slate-700 my-2" />
<p class="text-gray-200">할일 진행률: <span class="text-sky-400">{progress}% ({completedCount}/{totalCount})</span></p>
</div>
<button
onclick={incrementCount}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
카운트 증가
</button>$effect 룬
$effect 룬은 상태 변경에 반응하여 부수 효과를 실행합니다. 이는 DOM 조작, 타이머 설정, API 호출 등 반응형 상태에 따라 실행해야 하는 작업에 유용합니다.
조건부 effect 예제
상태: 활성
(콘솔에 "활성화되었습니다" 메시지가 표시됩니다)
$effect 코드 예제:
<script lang="ts">
// 조건부 effect 예제
let isActive = $state(true);
$effect(() => {
if (isActive) {
console.log("활성화되었습니다");
}
});
function toggleActive() {
isActive = !isActive;
}
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<p class="text-gray-200">상태: <span class="text-sky-400">{isActive ? "활성" : "비활성"}</span></p>
<p class="text-sm text-gray-400">
(콘솔에 "활성화되었습니다" 메시지가 표시됩니다)
</p>
</div>
<button
onclick={toggleActive}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
상태 토글
</button>Effect 카운트 예제
Effect 카운트: 0
메시지:
Effect 카운트 코드 예제:
<script lang="ts">
// Effect 카운트 증가 예제
let effectCount = $state(0);
let effectMessage = $state("");
// 상태 변화에 반응하는 effect
$effect(() => {
effectMessage = `effectCount가 ${effectCount}로 변경되었습니다`;
console.log(effectMessage);
});
function incrementEffectCount() {
effectCount += 1;
}
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<p class="text-gray-200">Effect 카운트: <span class="text-sky-400">{effectCount}</span></p>
<p class="text-gray-200">메시지: <span class="text-sky-400">{effectMessage}</span></p>
</div>
<button
onclick={incrementEffectCount}
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
Effect 카운트 증가
</button>시간 업데이트 예제
현재 시간: 12:00:39 AM
(1초마다 자동으로 업데이트됩니다)
시간 업데이트 코드 예제:
<script lang="ts">
// 시간 업데이트 예제
let currentTime = $state(new Date());
// 시간 업데이트를 위한 effect
$effect(() => {
const interval = setInterval(() => {
currentTime = new Date();
}, 1000);
// 정리 함수 반환 (컴포넌트가 제거될 때 실행)
return () => {
clearInterval(interval);
};
});
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<p class="text-gray-200">현재 시간: <span class="text-sky-400">{currentTime.toLocaleTimeString()}</span></p>
<p class="text-sm text-gray-400">
(1초마다 자동으로 업데이트됩니다)
</p>
</div>$state.raw와 $state.snapshot
$state.raw는 얕은 반응성을 제공하는 변형된 $state 룬입니다. 최상위 속성만 반응형으로 만들고 중첩된 객체나 배열은 반응형으로 만들지 않습니다. 성능 최적화가 필요한 경우 유용합니다.
참고: Svelte 5의 최신 버전에서는 $state.raw의 동작이 변경되어 중첩 속성 변경도 UI에 반영될 수 있습니다. 그러나 공식 문서에 따르면 중첩 속성의 반응성은 보장되지 않으므로 전체 객체를 재할당하는 것이 안전합니다.
$state.snapshot은 반응형 상태의 일반 JavaScript 객체 스냅샷을 생성합니다. API 요청이나 로컬 스토리지 저장 등에 유용합니다.
$state vs $state.raw 비교
일반 $state 객체:
이름: 홍길동
나이: 30
직업: 개발자
$state.raw 객체:
이름: 이영희
나이: 25
직업: 디자이너
참고: Svelte 5 최신 버전에서는 $state.raw의 중첩 속성 변경도 UI에 반영될 수 있지만, 공식적으로는 보장되지 않으므로 전체 객체 재할당 방식을 권장합니다.
$state vs $state.raw 코드 예제:
<script lang="ts">
// $state vs $state.raw 비교 예제
let normalPerson = $state({
name: "홍길동",
profile: {
age: 30,
job: "개발자",
},
});
let rawPerson = $state.raw({
name: "이영희",
profile: {
age: 25,
job: "디자이너",
},
});
function updateNormalPerson() {
normalPerson.profile.age += 1;
}
function updateRawPersonWrong() {
// 작동하지 않는 코드
rawPerson.profile.age += 1;
}
function updateRawPersonCorrect() {
rawPerson = {
...rawPerson,
profile: {
...rawPerson.profile,
age: rawPerson.profile.age + 1,
},
};
}
</script>$state.snapshot 예제
복잡한 객체:
- 아이템 1: 미완료
- 아이템 2: 완료
마지막 업데이트: 10/22/2025, 12:00:39 AM
항목 수: 2
$state.snapshot 코드 예제:
<script lang="ts">
// $state.snapshot 예제
let complexObject = $state({
items: [
{ id: 1, name: "아이템 1", completed: false },
{ id: 2, name: "아이템 2", completed: true },
],
meta: {
lastUpdated: new Date(),
count: 2,
},
});
function takeSnapshot() {
// 프록시가 아닌 일반 객체 스냅샷 생성
const snapshot = $state.snapshot(complexObject);
alert(JSON.stringify(snapshot, null, 2));
}
</script>실용적인 예제
카운터 앱
카운터 앱 코드:
<script lang="ts">
let counterValue = $state(0);
let counterHistory = $state<number[]>([]);
let counterAverage = $derived(
counterHistory.length > 0
? counterHistory.reduce((sum, val) => sum + val, 0) / counterHistory.length
: 0
);
function updateCounter(amount: number) {
counterValue += amount;
counterHistory = [...counterHistory, counterValue];
}
function resetCounter() {
counterValue = 0;
counterHistory = [];
}
</script>
<div class="bg-slate-800 p-4 rounded mb-4">
<p class="text-2xl mb-2">{counterValue}</p>
<p>기록: {counterHistory.join(", ")}</p>
<p>평균: {counterAverage.toFixed(2)}</p>
</div>
<div class="flex gap-2">
<button
onclick={() => updateCounter(-1)}
class="bg-red-600 text-white py-2 px-4 rounded hover:bg-red-500"
>
감소
</button>
<button
onclick={() => updateCounter(1)}
class="bg-green-600 text-white py-2 px-4 rounded hover:bg-green-500"
>
증가
</button>
<button
onclick={resetCounter}
class="bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-500"
>
초기화
</button>
</div>0
기록:
평균: 0.00
테마 전환
테마 전환 코드:
<script lang="ts">
let theme = $state("dark");
$effect(() => {
if (theme === "dark") {
document.documentElement.classList.add("dark-theme");
document.documentElement.classList.remove("light-theme");
} else {
document.documentElement.classList.add("light-theme");
document.documentElement.classList.remove("dark-theme");
}
console.log(`테마가 ${theme}로 변경되었습니다`);
});
function toggleTheme() {
theme = theme === "dark" ? "light" : "dark";
}
</script>
<div
class={`p-4 rounded mb-4 transition-colors duration-300 ${
theme === "dark" ? "bg-slate-800 text-sky-400" : "bg-slate-100 text-sky-600"
}`}
>
<p>
현재 테마: <span class="font-bold">{theme}</span>
</p>
<p class="mt-2">
{theme === "dark"
? "어두운 테마가 적용되었습니다."
: "밝은 테마가 적용되었습니다."}
</p>
</div>
<button
onclick={toggleTheme}
class={`py-2 px-4 rounded cursor-pointer text-base ${
theme === "dark"
? "bg-sky-600 text-white hover:bg-sky-500"
: "bg-sky-600 text-white hover:bg-sky-500"
}`}
>
{theme === "dark" ? "밝은 테마로 전환" : "어두운 테마로 전환"}
</button>현재 테마: dark
어두운 테마가 적용되었습니다.
폼 처리
폼 처리 코드:
<script lang="ts">
let formName = $state("");
let formEmail = $state("");
let formSubmitted = $state(false);
let formData = $state({ name: "", email: "" });
function handleSubmit(event: Event) {
event.preventDefault();
formData = { name: formName, email: formEmail };
formSubmitted = true;
setTimeout(() => {
formSubmitted = false;
}, 5000);
}
</script>
{#if formSubmitted}
<div class="bg-green-800 text-white p-4 rounded mb-4">
<p>폼이 제출되었습니다!</p>
<p>이름: {formData.name}</p>
<p>이메일: {formData.email}</p>
</div>
{/if}
<form onsubmit={handleSubmit} class="bg-slate-800 p-4 rounded mb-4">
<div class="mb-4">
<label for="name" class="block mb-2">이름:</label>
<input
id="name"
type="text"
bind:value={formName}
class="w-full p-2 bg-slate-700 border border-slate-600 rounded"
/>
</div>
<div class="mb-4">
<label for="email" class="block mb-2">이메일:</label>
<input
id="email"
type="email"
bind:value={formEmail}
class="w-full p-2 bg-slate-700 border border-slate-600 rounded"
/>
</div>
<button
type="submit"
class="bg-sky-600 text-white py-2 px-4 rounded hover:bg-sky-500"
>
제출
</button>
</form>결론
Svelte 5의 Runes 시스템은 반응성을 더 명시적이고 강력하게 만들어 줍니다. $state, $derived, $effect와 같은 룬을 사용하면 상태 관리가 더 직관적이고 예측 가능해집니다.
이 새로운 접근 방식은 Svelte의 간결함을 유지하면서도 더 복잡한 애플리케이션을 구축할 수 있는 강력한 도구를 제공합니다. 룬 시스템을 마스터하면 더 효율적이고 유지보수하기 쉬운 Svelte 애플리케이션을 개발할 수 있습니다.
실습 과제
Todo 앱 만들기
지금까지 배운 Svelte 5의 Runes 시스템을 활용하여 다음 기능이 있는 Todo 앱을 만들어보세요:
- $state를 사용한 할 일 목록 관리
- $derived를 사용한 완료된 할 일 개수 계산
- $effect를 사용한 로컬 스토리지 저장
- $state.snapshot을 사용한 데이터 백업 기능
다음 강의 미리보기
4강: 컴포넌트 통신
다음 강의에서는 Svelte 컴포넌트 간의 통신 방법에 대해 배웁니다:
- $props를 사용한 데이터 전달
- 이벤트 디스패치와 처리 방법
- 컴포넌트 구성 (슬롯과 스니펫)
- 컨텍스트 API 활용
- 부모-자식 컴포넌트 간 통신 패턴
- 양방향 데이터 바인딩
- 컴포넌트 생명주기 이해하기