SvelteKit 5 Runes란 무엇인가?

SvelteKit 5는 2024년 말 정식 출시된 메이저 버전으로, 반응성 시스템을 근본적으로 재설계한 Runes를 도입했습니다. 기존 SvelteKit 4의 $: 문법과 스토어 기반 반응성에 익숙하다면 처음에는 낯설 수 있지만, 마이그레이션을 마친 뒤에는 훨씬 명확하고 예측 가능한 코드를 작성할 수 있게 됩니다. 이 글에서는 실제 프로젝트에서 SvelteKit 4를 5로 마이그레이션하면서 겪은 주요 변화와 실전 패턴을 정리합니다.
핵심 Runes 개요 — $state, $derived, $effect
SvelteKit 5 Runes는 크게 세 가지 핵심 rune으로 구성됩니다. $state는 반응형 상태를 선언하고, $derived는 다른 상태에서 파생된 값을 정의하며, $effect는 사이드 이펙트를 처리합니다. 기존의 암묵적 반응성 대신 명시적 선언 방식을 채택해 코드의 의도가 명확해졌습니다.
Before: SvelteKit 4 문법
<script>
// SvelteKit 4 — 암묵적 반응성
let count = 0;
let doubled;
$: doubled = count * 2; // 반응형 선언
$: { // 반응형 블록
console.log('count changed:', count);
if (count > 10) {
alert('카운트가 10을 넘었습니다!');
}
}
function increment() {
count += 1;
}
</script>
<button on:click={increment}>{count} (doubled: {doubled})</button>
After: SvelteKit 5 Runes 문법
<script>
// SvelteKit 5 — 명시적 Runes
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log('count changed:', count);
if (count > 10) {
alert('카운트가 10을 넘었습니다!');
}
});
function increment() {
count += 1;
}
</script>
<button onclick={increment}>{count} (doubled: {doubled})</button>
눈에 띄는 변화가 몇 가지 있습니다. $: 반응형 선언이 $derived()로, 반응형 블록이 $effect()로 바뀌었고, 이벤트 핸들러 문법도 on:click에서 onclick으로 변경되었습니다. 또한 let count = 0이 let count = $state(0)로 바뀌어 어떤 변수가 반응형인지 코드만 보고도 즉시 알 수 있게 되었습니다.
$props — 컴포넌트 Props 마이그레이션
SvelteKit 5에서는 컴포넌트 props 선언 방식도 크게 바뀌었습니다. 기존의 export let 패턴 대신 $props() rune을 사용합니다. 특히 TypeScript와의 통합이 더욱 자연스러워졌습니다.
<script lang="ts">
// SvelteKit 4 방식
// export let name: string;
// export let age: number = 0;
// export let onClose: () => void;
// SvelteKit 5 Runes 방식
interface Props {
name: string;
age?: number;
onClose: () => void;
children?: import('svelte').Snippet;
}
let { name, age = 0, onClose, children }: Props = $props();
</script>
<div>
<h2>{name} ({age})</h2>
{@render children?.()}
<button onclick={onClose}>닫기</button>
</div>
한 가지 중요한 변화는 slot이 Snippet으로 대체된 것입니다. 기존의 <slot />과 <slot name="header" /> 패턴은 각각 {@render children?.()}와 named snippet으로 바뀝니다. 처음에는 다소 verbose하게 느껴질 수 있지만, TypeScript 자동완성이 정확하게 동작하는 장점이 있습니다.
스토어(Store) → $state 마이그레이션
SvelteKit 4에서 전역 상태 관리를 위해 자주 쓰던 Svelte 스토어(writable, readable, derived)는 SvelteKit 5에서도 여전히 사용 가능합니다. 하지만 $state를 활용한 클래스 기반 상태 관리 패턴이 더 간결하고 테스트하기 쉬운 경우가 많습니다.
// src/lib/stores/user.svelte.ts
// SvelteKit 5 — 클래스 기반 반응형 상태 관리
class UserStore {
name = $state('');
email = $state('');
isLoggedIn = $derived(this.name !== '' && this.email !== '');
login(name: string, email: string) {
this.name = name;
this.email = email;
}
logout() {
this.name = '';
this.email = '';
}
}
// 싱글톤으로 export
export const userStore = new UserStore();
// 컴포넌트에서 사용
// import { userStore } from '$lib/stores/user.svelte';
// <p>{userStore.name}</p> ← 자동 반응형!
파일 확장자를 .svelte.ts로 지정해야 Runes가 활성화됩니다. 일반 .ts 파일에서는 $state를 사용할 수 없으니 주의하세요.
마이그레이션 체크리스트 및 자동화 도구
SvelteKit 팀은 공식 마이그레이션 스크립트를 제공합니다. 대부분의 기계적 변환은 자동으로 처리되지만, 복잡한 반응형 로직은 수동 검토가 필요합니다.
# 공식 마이그레이션 CLI 실행
npx sv migrate svelte-5
# 주요 자동 변환 항목:
# - on:click → onclick
# - export let → $props()
# - $: expr → $derived()
# - $: { } → $effect()
# - <slot> → {@render children()}
# 수동 확인 필요 항목:
# - createEventDispatcher → $props() 콜백 방식으로 변경
# - 스토어 구독 ($store) → 대부분 자동 처리되나 복잡한 경우 수동 검토
# - beforeUpdate / afterUpdate → $effect.pre() / $effect()
# 마이그레이션 후 타입 체크
npx tsc --noEmit
npm run check
자주 겪는 문제와 해결법
1. $effect 무한 루프: $effect 내부에서 해당 effect가 의존하는 상태를 변경하면 무한 루프가 발생합니다. $effect.pre()를 사용하거나 untrack()으로 특정 의존성을 추적에서 제외하세요.
2. 배열/객체 깊은 반응성: $state로 선언된 배열이나 객체는 기본적으로 deep reactive입니다. 기존 SvelteKit 4에서 arr = [...arr]처럼 재할당해야 했던 패턴이 이제는 arr.push(item)으로 직접 변이해도 UI가 업데이트됩니다.
3. SSR 호환성: 서버사이드 렌더링 중에는 $effect가 실행되지 않습니다. 브라우저 전용 초기화 코드는 반드시 $effect나 onMount 안에 작성하세요.
코드벤터는 글로벌 협력 네트워크를 기반으로 다양한 프론트엔드 프로젝트에서 SvelteKit 5 Runes를 실전 적용해왔습니다. 마이그레이션 과정에서의 노하우와 패턴은 앞으로도 이 블로그를 통해 계속 공유할 예정입니다. 새로운 기술 스택 도입이나 프로젝트 개발에 관심 있으시다면 언제든지 코드벤터에 문의해 주세요.



