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>
시간 업데이트 예제
현재 시간: 2:20:03 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: 완료
마지막 업데이트: 4/20/2025, 2:20:03 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 활용
- 부모-자식 컴포넌트 간 통신 패턴
- 양방향 데이터 바인딩
- 컴포넌트 생명주기 이해하기