포스트

Svelte 5 Runes: 새로운 반응성 패러다임

Svelte 5의 혁신적인 Runes 시스템을 심층 분석하고, 기존 Svelte 4와의 차이점, 주요 Runes 사용법, 마이그레이션 전략까지 종합적으로 다룹니다.

Svelte 5 Runes: 새로운 반응성 패러다임

들어가며

Svelte 5가 가져온 가장 큰 변화는 바로 Runes입니다. 기존의 let 기반 반응성에서 벗어나 명시적이고 예측 가능한 반응성 시스템을 도입했습니다. 이번 포스트에서는 Svelte 5 Runes의 핵심 개념부터 실무 적용까지 종합적으로 살펴보겠습니다.

Runes란 무엇인가?

Runes는 Svelte 5의 새로운 반응성 원시 타입(reactivity primitives)입니다. $state, $derived, $effect 등의 특별한 함수를 통해 반응성을 명시적으로 관리할 수 있습니다.

Runes는 기존 Svelte의 “마법적인” 반응성을 더 명확하고 예측 가능하게 만듭니다.

주요 Runes 살펴보기

1. $state - 반응형 상태 관리

기존 Svelte 4의 let 변수를 대체하는 명시적 상태 선언입니다.

1
2
3
4
5
// Svelte 4
let count = 0;

// Svelte 5
let count = $state(0);

객체와 배열도 반응형으로

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
	let user = $state({
		name: 'John',
		age: 30
	});
	
	let items = $state([1, 2, 3]);
</script>

<button onclick={() => user.age++}>
	나이 증가: {user.age}
</button>

<button onclick={() => items.push(items.length + 1)}>
	아이템 추가: {items.length}</button>

2. $derived - 파생 상태

기존의 $: 반응형 구문을 대체하는 더 명확한 파생 상태입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
	let count = $state(0);
	
	// Svelte 4
	$: doubled = count * 2;
	
	// Svelte 5 - 간단한 표현식
	const doubled = $derived(count * 2);
	const isEven = $derived(count % 2 === 0);
	
	// 복잡한 로직은 $derived.by 사용
	const status = $derived.by(() => {
		if (count === 0) return '시작하지 않음';
		if (count < 5) return '낮음';
		if (count < 10) return '보통';
		return '높음';
	});
</script>

<p>{count}의 두 배: {doubled}</p>
<p>짝수인가요? {isEven ? '' : '아니오'}</p>
<p>상태: {status}</p>

중요: $derived는 단순 표현식에, $derived.by는 복잡한 로직에 사용합니다. 둘 다 순수 함수여야 하며 부작용이 없어야 합니다.

3. $effect - 부작용 처리

기존의 $: 부작용 처리를 전담하는 새로운 시스템입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
	let count = $state(0);
	
	// 카운트가 변경될 때마다 실행
	$effect(() => {
		console.log(`카운트가 ${count}으로 변경되었습니다`);
		
		if (count > 10) {
			alert('카운트가 너무 높습니다!');
		}
	});
	
	// 정리(cleanup) 함수 반환 가능
	$effect(() => {
		const timer = setInterval(() => {
			console.log('1초마다 실행');
		}, 1000);
		
		return () => clearInterval(timer);
	});
</script>

$effect.pre - DOM 업데이트 전 실행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
	let messages = $state([]);
	let viewport;
	
	$effect.pre(() => {
		// DOM 업데이트 전에 스크롤 위치 확인
		const autoscroll = viewport && 
			viewport.offsetHeight + viewport.scrollTop > 
			viewport.scrollHeight - 50;
		
		if (autoscroll) {
			tick().then(() => {
				viewport.scrollTo(0, viewport.scrollHeight);
			});
		}
	});
</script>

4. $props - 컴포넌트 속성

기존의 export let 문법을 대체하는 더 직관적인 props 처리입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
	// Svelte 4
	export let title;
	export let subtitle = '기본값';
	export let items = [];
	
	// Svelte 5
	let { title, subtitle = '기본값', items = [] } = $props();
	
	// 나머지 props 처리
	let { class: className, ...rest } = $props();
</script>

<div class={className} {...rest}>
	<h1>{title}</h1>
	<h2>{subtitle}</h2>
</div>

TypeScript 타입 지원

1
2
3
4
5
6
7
8
9
<script lang="ts">
	interface Props {
		title: string;
		count?: number;
		items: string[];
	}
	
	let { title, count = 0, items }: Props = $props();
</script>

5. $bindable - 양방향 바인딩

부모-자식 간 양방향 데이터 바인딩을 위한 새로운 시스템입니다.

1
2
3
4
5
6
<!-- FancyInput.svelte -->
<script>
	let { value = $bindable(), ...props } = $props();
</script>

<input bind:value={value} {...props} />
1
2
3
4
5
6
7
8
9
<!-- App.svelte -->
<script>
	import FancyInput from './FancyInput.svelte';
	
	let message = $state('hello');
</script>

<FancyInput bind:value={message} />
<p>현재 값: {message}</p>

이벤트 처리의 변화

Svelte 5는 이벤트 처리도 더 직관적으로 변경되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
	let count = $state(0);
	
	function handleClick() {
		count++;
	}
</script>

<!-- Svelte 4 -->
<button on:click={handleClick}>클릭</button>

<!-- Svelte 5 -->
<button onclick={handleClick}>클릭</button>
<button {onclick}>클릭 (단축 문법)</button>

마이그레이션 가이드

자동 마이그레이션 도구

Svelte 5는 자동 마이그레이션 스크립트를 제공합니다:

1
npx sv migrate svelte-5

주요 변경사항 요약

Svelte 4Svelte 5설명
let count = 0let count = $state(0)반응형 상태
$: doubled = count * 2const doubled = $derived(count * 2)파생 상태
$: console.log(count)$effect(() => console.log(count))부작용
export let proplet { prop } = $props()컴포넌트 속성
on:clickonclick이벤트 처리

점진적 마이그레이션

기존 Svelte 4 코드는 Svelte 5에서도 정상 작동합니다. 점진적으로 마이그레이션할 수 있습니다.

실전 예제: Todo 앱

Svelte 5 Runes를 활용한 간단한 Todo 앱 예제입니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<script>
	let newTodo = $state('');
	let todos = $state([]);
	let filter = $state('all'); // 'all', 'active', 'completed'
	
	// 필터링된 todos (복잡한 로직이므로 $derived.by 사용)
	const filteredTodos = $derived.by(() => {
		switch (filter) {
			case 'active':
				return todos.filter(todo => !todo.completed);
			case 'completed':
				return todos.filter(todo => todo.completed);
			default:
				return todos;
		}
	});
	
	// 통계
	const activeCount = $derived(todos.filter(todo => !todo.completed).length);
	const completedCount = $derived(todos.length - activeCount);
	
	// 로컬 스토리지 동기화
	$effect(() => {
		localStorage.setItem('todos', JSON.stringify(todos));
	});
	
	function addTodo() {
		if (newTodo.trim()) {
			todos.push({
				id: Date.now(),
				text: newTodo.trim(),
				completed: false
			});
			newTodo = '';
		}
	}
	
	function toggleTodo(id) {
		const todo = todos.find(t => t.id === id);
		if (todo) {
			todo.completed = !todo.completed;
		}
	}
	
	function removeTodo(id) {
		todos = todos.filter(t => t.id !== id);
	}
</script>

<div class="todo-app">
	<h1>Todo App (Svelte 5)</h1>
	
	<form onsubmit={(e) => { e.preventDefault(); addTodo(); }}>
		<input 
			bind:value={newTodo} 
			placeholder="할 일을 입력하세요..." 
		/>
		<button type="submit">추가</button>
	</form>
	
	<div class="filters">
		<button 
			class:active={filter === 'all'} 
			onclick={() => filter = 'all'}
		>
			전체 ({todos.length})
		</button>
		<button 
			class:active={filter === 'active'} 
			onclick={() => filter = 'active'}
		>
			진행중 ({activeCount})
		</button>
		<button 
			class:active={filter === 'completed'} 
			onclick={() => filter = 'completed'}
		>
			완료 ({completedCount})
		</button>
	</div>
	
	<ul class="todo-list">
		{#each filteredTodos as todo (todo.id)}
			<li class:completed={todo.completed}>
				<input 
					type="checkbox" 
					checked={todo.completed}
					onchange={() => toggleTodo(todo.id)}
				/>
				<span>{todo.text}</span>
				<button onclick={() => removeTodo(todo.id)}>
					삭제
				</button>
			</li>
		{/each}
	</ul>
</div>

<style>
	.todo-app {
		max-width: 400px;
		margin: 0 auto;
		padding: 2rem;
	}
	
	.filters button.active {
		background: #007acc;
		color: white;
	}
	
	.todo-list li.completed {
		opacity: 0.6;
		text-decoration: line-through;
	}
</style>

성능 및 개발자 경험 개선

1. 더 나은 TypeScript 지원

1
2
3
4
5
6
7
8
// .svelte.ts 파일에서 반응성 활용
export const appState = $state({
	user: null as User | null,
	theme: 'dark' as 'light' | 'dark',
	notifications: [] as Notification[]
});

export const isLoggedIn = $derived(appState.user !== null);

2. 향상된 디버깅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
	let count = $state(0);
	let message = $state('hello');
	
	// 디버깅을 위한 $inspect
	$inspect(count, message);
	
	// 커스텀 디버깅
	$inspect(count).with((type, value) => {
		if (type === 'update') {
			console.log(`Count updated to: ${value}`);
		}
	});
</script>

3. 더 작은 번들 크기

Svelte 5는 Runes를 통해 더 효율적인 코드를 생성하여 번들 크기를 줄였습니다.

주의사항과 베스트 프랙티스

❌ 피해야 할 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
	let count = $state(0);
	
	// ❌ $derived에서 부작용 금지
	const bad = $derived(() => {
		console.log(count); // 부작용!
		return count * 2;
	});
	
	// ✅ $effect 사용
	$effect(() => {
		console.log(`Count: ${count}`);
	});
	
	const good = $derived(count * 2);
</script>

✅ 권장 패턴

  1. 상태는 최소한으로: 필요한 경우에만 $state 사용
  2. 파생 상태 활용: 계산 가능한 값은 $derived 사용
  3. 부작용 분리: $effect에서만 부작용 처리
  4. 타입 안전성: TypeScript와 함께 사용하여 타입 안전성 확보

결론

Svelte 5 Runes는 다음과 같은 이점을 제공합니다:

  • 명시적 반응성: 코드의 의도가 더 명확해졌습니다
  • 🔧 향상된 도구 지원: TypeScript, 디버깅, 린팅이 개선되었습니다
  • 🚀 더 나은 성능: 더 효율적인 업데이트와 작은 번들 크기
  • 🔄 점진적 마이그레이션: 기존 코드와 호환되어 안전하게 전환 가능

Svelte 5는 기존의 간결함을 유지하면서도 더 강력하고 예측 가능한 개발 경험을 제공합니다. 여러분의 다음 프로젝트에서 Svelte 5 Runes를 활용해 보세요!


이 포스트는 Svelte 5 공식 문서와 마이그레이션 가이드를 바탕으로 작성되었습니다. 더 자세한 내용은 Svelte 공식 문서를 참고하세요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.