1. State
$state()
반응형 변수일 경우 $state()를 사용하여 반응형 상태임을 명시한다.
🏃♀️➡️ Svelte4
<script>
let count = 0;
</script>
🏃♂️➡️ Svelte5
<script>
let count = $state(0);
</script>
Svelte 4 vs Svelte 5
| 비교 항목 | Svelte 4 (기존 방식) | Svelte 5 (새로운 방식) |
| 반응성 선언 방식 | let count = 0; (자동 감지) | let count = $state(0); |
| 변수의 명확성 | 반응형인지 불분명함 | $state()를 사용하여 명확히 반응형 변수임을 표시 |
| 렌더링 최적화 | 전체 블록이 다시 실행될 가능성 있음 | 변경된 부분만 업데이트 |
즉, Svelte 5에서는 $state()를 통해 더 직관적인 상태 관리를 제공하면서, 렌더링 성능도 최적화되었다.
2. Computed State
$derived()
반응형 계산일 경우 $derived()를 사용한다.
복잡한 연산은 $derived.by()를 사용한다.
🏃♀️➡️ Svelte4
<script>
let count = 10;
$: doubleCount = count * 2;
</script>
<div>{doubleCount}</div>
🏃♂️➡️ Svelte5
<script>
let count = $state(10);
const doubleCount = $derived(count * 2);
let total = $derived.by(() => {
// code...
});
</script>
<div>{doubleCount}</div>
Svelte 4 vs Svelte 5
| 비교 항목 | Svelte 4 (기존 방식) | Svelte 5 (새로운 방식) |
| 반응형 계산 방식 | $:를 사용한 반응형 선언 | $derived()를 사용하여 명확히 선언 |
| 실행 방식 | 모든 반응형 값 변경 시 실행됨 | 필요한 값이 변경될 때만 실행됨 |
| 렌더링 성능 | 전체 블록이 다시 실행될 가능성 있음 | 변경된 부분만 업데이트 |
Svelte 5에서는 $derived()를 통해 불필요한 재계산을 줄이고, 더 최적화된 반응형 상태 관리를 제공한다.
3. Life Cycle methods
$effect()
$effect()를 사용해 생명주기와 반응성을 직관적으로 관리한다.
🏃♀️➡️ Svelte4
<script lang="ts">
import { onMount } from "svelte"
onMount(() => {
// code...
});
</script>
🏃♂️➡️ Svelte5
<script lang="ts">
$effect(() => {
// code...
});
</script>
Rune은 Svelte5에서 반응성을 처리하는 새로운 방식 중 하나다.
$effect는 참조된 반응형 변수(State)가 변경될 때마다 실행된다. 이러한 특성으로 인해 API 호출, DOM 조작 등 사이드 이펙트에만 사용해야 하고, 반응형 변수를 기반으로 새로운 값을 계산하는 용도로 쓰면 안 된다.
🏃♀️➡️ $effect를 사용해 반응형 값을 계산하는 예시
<script lang='ts'>
let total = 100;
let spent = $state(0);
let left = $state(total);
$effect(() => {
left = total - spent;
});
$effect(() => {
spent = total - left;
});
</script>
이처럼 작성할 경우 두 $effect가 서로 값을 갱신하면서 무한 루프가 발생할 위험이 있다. Svelte5에서는 이를 방지하기 위해 $derived를 사용하거나 직접 함수로 변경하는 것이 더 적절하다고 말한다.
🏃♂️➡️ 함수 기반으로 값을 계산하는 예시
<script lang='ts'>
let total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(e) {
spent = +e.target.value;
left = total - spent;
}
function updateLeft(e) {
left = +e.target.value;
spent = total - left;
}
</script>
변수 spent과 left를 updateSpent()와 updateLeft() 함수에서 직접 갱신하도록 수정해 보았다. 불필요한 반응형 트리거를 없애고 더 직관적인 코드가 되었다.
$effect.pre()를 사용하면 상태 변경 전에 특정 작업을 수행할 수 있다.
<script lang="ts">
$effect.pre(() => {
// code...
});
</script>
$effect.pre()
$effect.pre()는 Pre-Effect로, 일반적인 $effect보다 앞서 실행되는 반응형 효과를 정의하는 기능이다. 기존 값을 보존해야 하거나 선제적인 처리가 필요한 경우 유용하게 쓸 수 있다.
$effect와 $effect.pre()의 차이점
| $effect | $effect.pre() | |
| 실행 시점 | 상태가 변경된 후 실행됨 | 상태가 변경되기 직전에 실행됨 |
| 용도 | 상태 변경 후 후속 작업 (예: API 호출, 로깅) | 상태 변경 전에 선제적으로 실행해야 하는 작업 (예: 기존 값 백업) |
🏃♀️➡️ 일반적인 $effect
<script lang="ts">
let count = $state(0);
$effect(() => {
console.log(`Count changed to: ${count}`);
});
function increment() {
count++;
}
</script>
이 경우 count가 변경된 이후에 $effect가 실행된다.
🏃♀️➡️ $effect.pre()를 활용한 예시
<script lang="ts">
let count = $state(0);
let previousCount = $state(0);
$effect.pre(() => {
previousCount = count; // count 변경 전에 기존 값 저장
});
$effect(() => {
console.log(`Count changed from ${previousCount} to ${count}`);
});
function increment() {
count++;
}
</script>
이번에는 count가 변경되기 전에 previousCount를 저장하도록 $effect.pre()를 사용해 보았다. 변경 후 실행되는 $effect에서 이전 값과 새로운 값을 비교 가능하다.
4. Props
$props()
$props()를 통해 export할 수 있다.
🏃♀️➡️ Svelte4
<script>
export let name = "";
export let age = null;
export let favouriteColors = [];
export let isAvailable = false;
</script>
<p>My name is {name}!</p>
<p>My age is {age}!</p>
<p>My favourite colors are {favouriteColors.join(", ")}!</p>
<p>I am {isAvailable ? "available" : "not available"}</p>
🏃♂️➡️ Svelte5
<script lang="ts">
const {
name = '',
age = null,
favouriteColors = [],
isAvailable = false,
} = $props();
</script>
<p>My name is {name}!</p>
<p>My age is {age}!</p>
<p>My favourite colors are {favouriteColors.join(", ")}!</p>
<p>I am {isAvailable ? "available" : "not available"}</p>
기존 export let으로 하나하나 가져오던 props를 $props()를 사용하여 한 번에 가져올 수 있다.
$bindable()
$bindable()을 사용해 양방향 데이터 바인딩을 할 수 있다.
🏃♀️➡️ Svelte4
<!-- Parent.svelte -->
<UserProfile bind:optionalProp={someValue} />
<!-- UserProfile.svelte -->
<script>
export let optionalProp;
</script>
<input bind:value={optionalProp} />
기존에는 양방향 바인딩을 하기 위해서는 부모 컴포넌트에서 bind:를 붙이고 자식 컴포넌트에서 export let을 붙여야 했다.
🏃♂️➡️ Svelte5
<script>
const { optionalProp = $bindable() } = $props();
</script>
$bindable()을 사용하면 bind: 없이도 양방향 바인딩이 가능하다. optionalProp은 부모로부터 값을 받을 수 있고, 자식에서도 변경할 수 있다.
5. Debugging
$inspect()
$inspect()는 지정한 변수나 상태가 변경될 때마다 해당 값을 콘솔에 출력한다.
🏃♀️➡️ Svelte4
<script>
let count = $state(0);
let message = $state('hello');
$: {
console.log(count);
console.log(message);
}
</script>
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
🏃♂️➡️ Svelte5
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // count 또는 message가 변경될 때마다 콘솔에 출력
</script>
<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
위 코드에서 count나 message가 변경될 때마다 $inspect가 자동으로 해당 값을 콘솔에 출력한다. 이는 개발 중에 상태 변화를 추적하는 데 유용하며, 프로덕션 빌드에서는 자동으로 제거되어 성능에 영향을 주지 않는다.
$inspect().with()
$inspect().with()를 사용해 콜백 동작(함수)를 지정해 줄 수 있다.
🏃♂️➡️ Svelte5
<script>
let count = $state(0);
$inspect(count).with((type, count) => {
if (type === 'update') {
console.trace('Count updated:', count); // 상태 변경 시 스택 트레이스 출력
}
});
</script>
<button onclick={() => count++}>Increment</button>
위 예시에서 count가 변경될 때마다 스택 트레이스를 포함한 커스텀 메시지가 콘솔에 출력된다. 이를 통해 상태 변경의 출처를 쉽게 추적할 수 있다.
$inspect.trace()
$inspect().trace()는 함수가 실행될 때마다 어떤 반응형 상태의 변경으로 인해 실행되었는지 콘솔에 출력한다.
🏃♂️➡️ Svelte5
<script>
let count = $state(0);
function increment() {
$inspect.trace();
count += 1;
}
</script>
<button onclick={increment}>Increment</button>
increment 함수 내에 $inspect.trace()를 추가하면, count의 변경으로 인해 함수가 실행될 때마다 콘솔에 어떤 상태 변경이 함수 실행을 트리거했는지에 대한 정보가 출력된다.
6. Parent Access
$host()
$host()를 사용해 부모 컴포넌트의 상태나 메서드에 접근해 직관적으로 데이터를 공유할 수 있다.
🏃♀️➡️ Svelte4
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let message = "Hello from Parent!";
</script>
<Child message={message} />
<!-- Child.svelte -->
<script>
export let message;
</script>
<p>부모 메시지: {message}</p>
🏃♂️➡️ Svelte5
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let message = "Hello from Parent!";
</script>
<Child />
<!-- Child.svelte -->
<script>
let { message } = $host; // 부모 컴포넌트의 message 가져오기
</script>
<p>부모 메시지: {message}</p>
Svelte 4 vs Svelte 5
| 비교 항목 | Svelte 4 (기존 방식) | Svelte 5 (새로운 방식) |
| 부모 상태 접근 | Props(export let) 사용 | $host() 사용 |
| 데이터 전달 방식 | 부모 → 자식으로만 가능 | 부모의 데이터를 직접 참조 가능 |
| 렌더링 최적화 | Props 변경 시 전체 업데이트 가능 | 변경된 부분만 반영 |
| 사용 편의성 | export let을 매번 선언해야 함 | $host()로 간편하게 접근 가능 |
즉, Svelte 5에서는 $host()를 통해 부모 상태 및 메서드 접근이 더 쉬워지고, Props 선언 없이도 데이터를 공유할 수 있다.
7. Slots
children과 @render children()
slot 태그를 통해 children을 구현하던 방식이 @render 함수를 사용하는 구현 방식으로 변경되었다.
슬롯이 children이라는 prop으로 전달된다.
@render children()을 사용해 이를 렌더링한다.
🏃♀️➡️ Svelte4
<button>
<slot></slot> <!-- 슬롯을 여기에 삽입 -->
</button>
Svelte 4에서는 태그를 사용해서 슬롯 콘텐츠를 렌더링했다.
🏃♂️➡️ Svelte5
<script>
let { children } = $props(); // 슬롯을 props로 가져옴
</script>
<button>
{@render children()} <!-- 슬롯 내용을 렌더링 -->
</button>
8. Snippets
#snippet @render snippetName()
#snippet은 반복적으로 사용될 수 있는 UI 조각을 정의하는 역할을 한다.
🏃♀️➡️ Svelte4
{#each images as image}
{#if image.href}
<a href={image.href}>
<img src={image.src} />
</a>
{:else}
<img src={image.src} />
{/if}
{/each}
이 코드에서는 images 배열을 순회하면서 각각의 image를 처리하고 있다. image.href가 존재하면 이미지를 a 태그 안에 넣고, 존재하지 않으면 그냥 img 태그만 렌더링된다. 이렇게 하면 중복되는 img 태그가 여러 번 반복된다.
🏃♂️➡️ Svelte5
{#snippet figure(image)}
<img src={image.src} />
{/snippet}
{#each images as image}
{#if image.href}
<a href={image.href}>
{@render figure(image)}
</a>
{:else}
{@render figure(image)}
{/if}
{/each}
figure(image)라는 스니펫을 정의하고, image.src를 사용해 img 태그를 렌더링하도록 했다. 이 스니펫을 사용하면, 반복적인 img 태그를 직접 작성할 필요 없이 @render figure(image)를 호출하여 재사용할 수 있다. 따라서 중복되는 img 태그를 스니펫으로 묶어 가독성이 좋아지고 유지보수가 쉬워진다.
특징
- 스니펫은 컴포넌트 내부 어디에서든 선언할 수 있다.
- 같은 lexical scope 내에서 접근 가능하다. 즉, 형제 요소(siblings)와 그 형제의 자식(children)에서도 접근할 수 있다.
- 스니펫은 컴포넌트의 props로 전달할 수 있다.
- 타입을 지정할 수 있다. 즉, TypeScript와 결합 가능
9. Event Handlers
onclick
on:click 구문이 onclick으로 대체되었다.
🏃♀️➡️ Svelte4
<script>
let count = 0;
function handleClick() {
count++;
}
</script>
<button on:click={handleClick}>
clicks: {count}
</button>
기존에는 on ":" click을 사용했다.
🏃♂️➡️ Svelte5
<script>
let count = $state(0);
function onclick() {
count++;
}
</script>
<button {onclick}>
clicks: {count}
</button>
Svelte 5에서는 on:click이 아니라 HTML의 기본 onclick 속성을 그대로 사용한다. 또한 이벤트 핸들러를 함수로 분리한 뒤 {}를 사용해 속성으로 바로 전달이 가능하다.
10. Fine-grained reactivity
"세밀한 반응성을 제공한다."
불필요한 렌더링을 최소화하면서 필요한 부분만 업데이트하는 방식으로 성능을 최적화한다.
Svelte 4 vs Svelte 5
| 비교 항목 | Svelte 4 (기존 방식) | Svelte 5 (새로운 방식) |
| 반응형 상태 선언 | $:를 사용한 반응형 선언 | $state() 를 사용한 세밀한 상태 관리 |
| 렌더링 범위 | 상태 변경 시 전체 블록 실행 | 변경된 부분만 업데이트 |
| 최적화 수준 | 비교적 전체적으로 반응성 적용 | 최소한의 변경만 감지하여 업데이트 |
| 렌더링 성능 | 컴포넌트 전체 리렌더링 가능 | 필요한 부분만 재렌더링 |
🏃♀️➡️ Svelte4
<script>
let todos = [];
function remaining(todos) {
console.log('recalculating');
return todos.filter((todo) => !todo.done).length;
}
function addTodo(event) {
if (event.key !== 'Enter') return;
todos = [
...todos,
{
done: false,
text: event.target.value
}
];
event.target.value = '';
}
</script>
Svelte 4의 코드를 살펴보자. remaining() 함수는 todos가 변경될 때마다 실행된다. todos 배열이 새로운 값으로 할당되면서 컴포넌트 전체가 다시 렌더링될 가능성이 높다. 할 일 목록에서 특정 항목만 변경되더라도 전체 todos 배열을 다시 검사해야 되기 때문에 성능 저하가 발생한다.
🏃♂️➡️ Svelte5
<script>
let todos = $state([]);
let remaining = $derived(() => {
console.log('recalculating');
return todos.filter((todo) => !todo.done).length;
});
function addTodo(event) {
if (event.key !== 'Enter') return;
todos = [
...todos,
{
done: false,
text: event.target.value
}
];
event.target.value = '';
}
</script>
todos를 $state([])로 변경하여 배열 전체가 아닌 개별 항목이 변경될 때만 반응형 업데이트가 발생한다. 또한 remaining을 $derived()로 변경하여, todos가 변경될 때마다 remaining이 전체를 다시 계산하지 않고 필요한 부분만 업데이트 한다. 전체 렌더링이 아닌 최소한의 업데이트만 수행하는 방식으로 성능이 개선된 것이다.
댓글