Svelte 4 vs Svelte 5

·2025년 2월 19일
1

Svelte

목록 보기
8/8

💡 들어가며

2024년 10월 20일 Svelte 5가 출시되었다. 기존 회사에서 Svelte 4를 사용하여 프로젝트를 진행 중이었는데 이번에 Svelte 5로 마이그레이션을 하게 되었다. 이에 따라 공부를 하면서 한글로 정리된 정보는 많이 없는 것 같아 벨로그에 Svelte5가 되며 달라진 점을 정리해 보려고 한다.


🔥 Svelte5는 뭐가 다를까?

"Svelte 5의 핵심 변화"
Svelte 5에서는 보다 직관적인 API와 성능 최적화를 목표로 큰 변화를 가져왔다.

  • 반응성 모델 변경: $state(), $derived(), $effect() 도입
  • Props & 이벤트 개선: $props(), $bindable() 지원
  • 컴포넌트 구조 최적화: slot 대신 children props 사용
  • 렌더링 최적화: Fine-Grained Reactivity 도입

이제 하나씩 상세히 알아보도록 하자.


🌿 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()를 통해 더 직관적인 상태 관리를 제공하면서, 렌더링 성능도 최적화되었다.


🌿 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()를 통해 불필요한 재계산을 줄이고, 더 최적화된 반응형 상태 관리를 제공하게 되었다.


🌿 Life Cycle methods

$effect()

$effect()를 활용해 라이프사이클과 반응성을 더욱 직관적으로 관리할 수 있다.

🏃‍♀️‍➡️ Svelte4

<script lang="ts">
	import { onMount } from "svelte"

	onMount(() => {
    	// code...
    });
</script>

🏃‍♂️‍➡️ Svelte5

<script lang="ts">
	$effect(() => {
    	// code...
    });
</script>

Effect Rune은 Svelte5에서 반응성을 처리하는 새로운 방식 중 하나다.
$effect는 참조된 반응형 변수(State)가 변경될 때마다 실행된다. (Untrack을 사용하지 않는 이상 😥) 이러한 특성으로 인해 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에서 이전 값과 새로운 값을 비교 가능하다.


🌿 Props

$props()

$props()를 사용해 기존 export let으로 가져오던 props를 한 번에 쉽게 가져올 수 있다.

🏃‍♀️‍➡️ 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은 부모로부터 값을 받을 수 있고, 자식에서도 변경할 수 있다.


🌿 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는 기본적으로 console.log를 사용하여 값을 출력하지만, .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의 변경으로 인해 함수가 실행될 때마다 콘솔에 어떤 상태 변경이 함수 실행을 트리거했는지에 대한 정보가 출력된다.


🌿 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 선언 없이도 데이터를 공유할 수 있다.


🌿 Slots

children과 @render children()

슬롯이 children이라는 prop으로 전달된다.
@render children()을 사용해 이를 렌더링한다.

🏃‍♀️‍➡️ Svelte4

<button>
  <slot></slot> <!-- 슬롯을 여기에 삽입 -->
</button>

Svelte 4에서는 태그를 사용해서 슬롯 콘텐츠를 렌더링했다.

🏃‍♂️‍➡️ Svelte5

<script>
    let { children } = $props(); // 슬롯을 props로 가져옴
</script>

<button>
    {@render children()}  <!-- 슬롯 내용을 렌더링 -->
</button>

Svelte 5에서는 슬롯이 children이라는 prop으로 전달된다. 그리고 @render children()을 사용해 렌더링한다. 기존 방식보다 더 명확하고 함수형 컴포넌트 스타일에 가까운 구조다.


🌿 Snippets

@render snippetName()

#snippet은 Svelte 5에서 기존 slot을 대체하는 기능으로, 반복적으로 사용될 수 있는 UI 조각을 정의하는 역할을 한다.
@render snippetName()을 사용하면 스니펫을 원하는 위치에서 쉽게 렌더링 가능하다.

🏃‍♀️‍➡️ 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와 결합 가능!

🌿 Event Handlers

onclick

on:click이 아니라 HTML의 기본 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 속성을 그대로 사용한다. 또한 이벤트 핸들러를 함수로 분리한 뒤 {}를 사용해 속성으로 바로 전달이 가능하다.


🌿 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이 전체를 다시 계산하지 않고 필요한 부분만 업데이트 한다. 전체 렌더링이 아닌 최소한의 업데이트만 수행하는 방식으로 성능이 개선된 것이다.


💡 마치며

지금까지 Svelte 5가 되면서 Svelte 4와 달라진 점들에 대해 살펴보았다. 여러 변화들이 있지만 이전보다 세밀하게 상태 관리를 할 수 있다는 것이 장점이지 않을까 싶다. 애증의 스벨트.. 기는 하지만 파면 팔수록 재미있는 건 사실이다. 앞으로도 화이팅해보자고. 😎

profile
풀스택 개발자 기록집 📁

1개의 댓글

comment-user-thumbnail
2025년 2월 22일

정리가 잘되어 있네요 잘보고 갑니다

답글 달기

관련 채용 정보