Runes
Rune (룬)문법은svelte5에 도입된 반응성 시스템을 근본적으로 개선하기 위한 새로운 기능이다. 기존의 암시적 반응성 대신, 더 명확하고 직관적인 코드 작성을 가능하게 한다.- 룬은
$로 시작하는 함수 형태를 가지며, 이 함수들을 호출하여 반응성 변수를 만들거나 상호작용을 제어한다.
Rune의 탄생
기존
Svelte의 반응성 시스템은 간단한 경우 매우 편리했지만 복잡한 상황에서는 몇가지 혼란을 야기하였다.let의 모호함- let 키워드 하나로 컴포넌트 state, props, 일반 변수를 모두 선언하여 역할이 불분명하다.
$:문법의 한계- 반응성을 위해 사용된
$:레이블은 JS 표준 문법이 아니라서 처음 배우는 사람들에게 혼란을 주었고, 언제 실행되는지 예측하기 어려운 경우가 많았다.
- 반응성을 위해 사용된
.svelte파일의 제약- svelte의 반응성은
.svelte파일 안에서만 동작했다..js,.ts파일에서 반응성 상태를 관리하려면store를 사용해야만 해서 불편함이 있었다.
- svelte의 반응성은
룬은 이러한 문제들을 해결하여
Svelte의 반응성을 더 명확하고, 강력하며, 일관성 있게 만들었다. (명확성, 단순성, 범용성)
$state
$state는 컴포넌트의 반응형 상태를 만드는 가장 기본적인 룬이다.- 과거 버전에서는 let 키워드가 맡았던 역할을 대체하며, 더 명확하고 강력한 반응성 모델을 제공한다.
$state
- 모든 변경 감지
- 할당, 또는 객체나 배열의 내부 변경 (push, pop, 속성 수정 등) 모두 자동으로 감지된다.
- 세밀한 반응성
$state로 만든 상태가 변경되면, Svelte는 컴포넌트 전체를 다시 그리는 것이 아니라 해당상태 (state)를 사용하는 DOM 요소만 정확히 업데이트 한다.
<script>
let count = $state(0)
let numbers = $state([1, 2])
const addNumber = () => {
numbers.push(3)
}
</script>
<button onclick={() => count++}>Count: {count}</button>
<button onclick={addNumber}>Numbers: {numbers.join(', ')}</button>$state.raw
- 일반적으로
$state에 객체를 할당하면, Svelte는 그 객체와 그 안의 모든 중첩된 객체들까지 전부 프록시로 감싸 객체 내부의 어떤 속성이 변경되더라도 Svelte가 즉시 감지하고 UI를 업데이트를 한다.- 이러한 깊은 반응성은 매우 크고 복잡한 객체의 모든 속성을 모두 감시하기에는 성능의 오버헤드를 유발할 수 있다. 또한 외부 라이브러리는 내부적으로 프록시 객체와 호환되지 않는 로직을 가질 수 있어, 이런 라이브러리의 인스턴스를
$state에 넣으면 오류가 발생할 수 있다.
- 이러한 깊은 반응성은 매우 크고 복잡한 객체의 모든 속성을 모두 감시하기에는 성능의 오버헤드를 유발할 수 있다. 또한 외부 라이브러리는 내부적으로 프록시 객체와 호환되지 않는 로직을 가질 수 있어, 이런 라이브러리의 인스턴스를
$state.raw는 깊은 반응성을 의도적으로 비활성화하는 유틸리티 함수로 객체와 배열이 깊이 반응하지 않기를 원하는 경우 사용한다.$state.raw로 선언된 값은 변경할 수 없으며, 재할당만 가능하다.
let person = $state.raw({
name: 'Ballboy',
age: 49
})
// 작동하지 않는다.
person.age += 1
// $state.raw로 할당된 상태값은 재할당을 통한 변경만 가능하다
person = {
name: 'Ballboy',
age: 50
}$state.snapshot
$state.snapshot은 반응형 상태($state)의 현재 값을 복사하여 불변의 스냅샷을 만드는 역할을 한다.- 사용 경우
- 불변 데이터 구조가 필요할 때
- 상태의 현재 버전을 저장해 두고 나중에 비교하고 싶을 때
- 외부 라이브러리에 데이터를 전달 할 때
- Svelte의 반응성 시스템을 모르는 외부 라이브러리에 데이터를 전달할 때 프록시 객체가 아닌 일반 객체를 넘겨주는 경우
- 원본 상태를 바꾸지 않고 값을 수정할때
- 상태를 복사해서 수정한 다음, 그 결과가 유효할 때만 원본 상태에 반영하고 싶을 경우
- 불변 데이터 구조가 필요할 때
let user = $state({name: 'Ballboy', age: 30})
const logSnapshot = () => {
const userSnapshot = $state.snapshot(user)
userSnapshot.age = 99
console.log('origin >>', user)
console.log('snapshot >>', userSnapshot)
}$state.eager
$state.eager는 비동기 작업의 결과가 완료되기를 기다리지 않고 현재 값을 즉시 얻을 수 있게 한다.- 일반적으로 svelte는 비동기 작업의 완료를 기다린 후에 상태를 업데이트 해서 UI를 갱신 하지만
$state.eager는 비동기 작업이 진행 중이더라도 즉시 UI를 업데이트 할 수 있다.
<script>
let loading = $state(false)
let data = $state(null)
const fetchData = $state.eager(async () => {
loading = true
data = null
await new Promise(...)
data = '데이터 로드 완료'
loading = false
})
</script>
<div>
<button onclick={fetchData} disabled={loading}>
{#if loading}
데이터 로드 중...
{:else}
Click
{/if}
</button>
{#if loading}
<p class='loading'>데이터를 불러 오는 중입니다.</p>
{:else}
<p>버튼을 눌러 데이터를 로드하세요.</p>
{/if}
</div>$derived
$derived는 하나 이상의 반응형 값에 의존하는 파생 상태를 선언적으로 정의하는 API
let count = $state(0)
// doubled는 직접 수정할 수 없는 Read-only 값이다.
let doubled = $derived(() => count * 2)$derived.by
$derived.by는 반응성 모델을 단방향에서 필요할 때는 양방향으로 확장해주는 API이다.$derived.by는 기본적으로 Read only(읽기 전용)이지만 setter를 통해 "역방향 업데이트"를 허용하는 파생 상태이다.$derived.by(getter, setter)- getter: 일반 $derived와 동일 (순수 게산)
- setter: derived에 값을 "대입 했을 때" 호출
let count = $state(0)
let doubled = $derived.by(
() => count * 2,
(value) => {
count = value / 2
}
)
doubled = 10; // count = 5$derived
의존성
$derived의 가장 핵심적인 기능은 자동 의존성 추적이다.$derived표현식 내부에서 동기적으로 읽히는 모든 상태는 해당 파생값의 의존성으로 간주된다.- 동작 순서
- 내부 식의 의존성이 변경되면 해당
$derived값은 즉시더티(dirty)상태로 표시된다. 더티로 표시된$derived값은 실제로 읽히거나 코드에서 사용될 때 재계산 된다. 이는 불필요한 계산을 방지하여 성능을 최적화 한다.
- 내부 식의 의존성이 변경되면 해당
- 만약 특정 상태가
$derived값의 계산에 사용되지만, 그 상태 변경이$derived값의 재계산을 막고 싶다면untrack함수를 사용하여 해당 상태를 의존성 추적에서 제외할 수 있다.
파생 값 재정의
$derived값은 일반적으로 의존성 변경에 따라 자동으로 재계산 되는 읽기 전용 값이다. 하지만 Svelte 5.25 부터는const로 선언 되지 않은$derived값을 일시적으로 재할당하여 재 정의 할 수 있다.$derived값을 재 할당하면, 해당 값은 더 이상 의존성을 변화에 따라 자동으로 재계산 되지 않아 파생된 특성을 읽게 된다.- 이 기능은 UI/UX를 개선하기 위한 특정 상황에서만 사용해야 한다.
파생과 반응성
Svelte의 상태 관리에서$state와$derived는 반응성 처리 방식에 차이가 있다.$state$state로 선언된 객체나 배열을 내부적으로 '깊은 반응형 프록시'로 변환되어 객체나 배열의 중첩된 속성이 변경되어도 값이 반응하도록 만든다.
$derived$derived는 객체나 배열을 반환하더라도,$derived자체는 그 객체나 배열의 중첩된 속성까지 깊게 반응형으로 만들지 않는다.
구조 분해
$derived선언과 함께 구조 분해를 사용하면, 구조 분해된 각각의 변수들이 모두 반응형이 된다.
업데이트 전파
svelte 5는 push-pull 반응성이라는 매커니즘을 사용하여 효율적인 업데이트 전파를 관리한다.
$effect
$effect룬은 리액티브 값의 변경에 반응하여 부수 효고를 실행하는 데 사용된다. 이는 주로 컴포넌트의 라이프 사이클 외부에서 발생하거나, DOM을 직접 조작하거나, 외부 API와 상호작용하는 등의 작업을 수행할 때 활용된다.
$effect
- 라이프 사이클
$effect는 컴포넌트의 마운트 시점에 처음 실행되며, 이후 의존성이 변경될 때마다 재실행된다.- 마운트 시점 (Mount)
- 컴포넌트가 처음 렌더링 될 때
$effect내부의 코드가 한 번 실행된다.
- 컴포넌트가 처음 렌더링 될 때
- 업데이트 시점 (Update)
$effect내부에서 참조하는 리액티브 상태가 변경되면 해당$effect는 즉시 재 실행된다.Svelte는 변경된 상태를 감지하고$effect를 효율적으로 다시 실행한다.
- 정리 (Cleanup)
$effect는 클린업 함수를 반환할 수 있다. 이 클린업 함수는 다음 번$effect가 재실행 되기 전 또는 컴포넌트가 언 마운트 될 때 실행된다.- 이는 타이머 해제, 이벤트 리스터 제거, 구독 해지 등 리소스 정리가 필요할 때 유용하다.
- 마운트 시점 (Mount)
<script>
let count = $state(0)
$effect(() => {
console.log('Count changed : ', count)
return () => {
console.log('Cleanup for count effect')
}
})
</script>
<button onclick={() => count++}>Increment</button>- 의존성
$effect블록 내부에서 참조되는 모든 리액티브 값은 자동으로 해당$effect의 의존성이 된다.- 자동 추적 (Automatic tracking)
Svelte컴파일러는$effect내부에서 사용되는 모든 리액티브 변수를 자동으로 감지하고 추적한다.- 리액트의
useEffect와 다르게 수동으로 의존성 배열을 지정할 필요가 없다.
- 자동 추적 (Automatic tracking)
$effect.pre
$effect.pre는 DOM이 업데이트되기 직전에 실행되는$effect이다. 일반$effect는 DOM 업데이트 이후에 실행되지만,$effect.pre는 렌더링 전에 상태를 읽고 준비 작업을 할 때 사용한다.- 주로 DOM 업데이트 전에 값을 캡처해야 할 때 사용한다.
$effect.tracking
- 현재 코드가 반응형 추적 컨텍스트 안에서 실행 중인지 알려주는 Boolean 값이다.
$effect.tracking()true- $effect 내부 함수, 템플릿 표현식, $derived 내부
false- 일반 함수 내부, 이벤트 핸들러 등
$effect.pending
- 현재 컴포넌트 트리에서 대기 중인 Promise 개수를 나타내는 반응형 값이다.
$effect.root
- effect의 생명 주기를 수동으로 관리하기 위한 API로
$effect.root는 명시적으로 destory 시점을 제어할 수 있다. destroy()호출 시- 내부의 모든 $effect cleanup 실행
- root 전체가 제거됨
const destroy = $effect.root(() => {
$effect(() => {
console.log('effect 실행 =')
})
return () => {
console.log("root cleanup")
}
})$props
$props는 컴포넌트가 받을 속성을 선언하고 사용하는 방식이다.- svelte4 :
export let foo - svelte5 :
let { foo } = $props();
<script>
let { data1, data2, data3 } = $props();
</script>
<p>Data1 : {data1}</p>
<p>Data2 : {data2}</p>
<p>Data3 : {data3}</p>$props
Default(기본값) 지정
let { count = 0 } = $props()- 부모가 해당 prop을 넘기지 않으면 기본값 0이 사용된다.
Props 이름 바꾸기 (Renaming)
let { name: nickname } = $props()nameprop을 받아와 지역 변수nickname으로 사용한다.
Rest Props
let { foo, ...rest } = $props()- 구조 분해에서
...rest를 사용하면 전달된 props 중 특정 props를 제외한 나머지 모든 props를 객체로 수집할 수 있다.
props의 업데이트
- props로 넘겨진 값들은 반응형이다. 부모에서 값이 바뀌면 자식 컴포넌트 내에서도 자동으로 업데이트 된다.
props의 특징
- 생성된 객체를 직접 변경하지 말것
let {obejct} = $props(); object.count += 1; (X)
- TypeScript에서의 props 타입 처리
let {foo, bar}: {foo: string; bar?: number} = $props()
- $props.id() : 고유 ID 생성
const uid = $props.id()
- 생성된 객체를 직접 변경하지 말것
$bindable
$bindable은svelte5의 런타임 시 컴포넌트 props를 바인딩 가능하게 표시하는 룬으로부모->자식단 방향 props 전달이 기본인 svelte에서,$bindable을 명시함으로서자식->부모로 값이 흐르는 양방향 바인딩을 허용한다.
<!-- 부모 컴포넌트 (App.svelte) -->
<script>
let message = $state('hello')
</script>
<!-- 자식에서 값을 바꾸면 부모의 message도 동시에 업데이트 된다. -->
<Input bind:value={message} />
<p>Message: {message}</p>
<!-- 자식 컴포넌트 (Input.svelte) -->
<script>
// $bindable() 자체는 기본값을 제공할 수 있다.
let { value = $bindable("hi~"), ...props } = $props();
</script>
<input bind:value={value} {...props} />$inspect
$inspect는 개발중에만 동작하는 디버깅용 룬으로 프로덕션 빌드에서는 아무 동작도 하지 않는다.- 기본적으로 console.log와 유사한 역할을 하지만 반응ㅇ형 값이 변경될 때마다 자동으로 다시 실행되고 깊은 추적까지 수행한다.
<script>
let count = $state(0)
let message = $state('hello')
// 처음 컴포넌트 렌더링시와 count나 message가 바뀔 때마다 console.log()가 자동으로 실행된다.
$inspect(count, message)
</script>$inspect(...).with(callback)
- 기본 console.log 대신 커스텀 콜백 함수를 지정할 수 있다.
<script>
let count = $state(0)
$inspect(count).with((type, count) => {
if(type === 'update'){
console.log('updated count: ', count)
}
})
</script>
<button onclick={() => count++}>Increment</button>$inspect.trace(...)
$inspect.trace는 함수 또는 이팩트가 재실행되는 이유를 추적해서 콘솔에 자세한 정보를 보여주는 디버깅 툴이다.- 이 룬은 함수가 다시 실행될 때 어떤 반응형 값이 원이이었는지 추적 정보를 출력한다.
<script>
$effect(() => {
// 반드시 effect, derived 함수의 가장 첫 줄이어야 한다.
$inspect.trace();
...
})
</script>$host
$host()는 Svelte5의 커스텀 엘리먼트 전용 룬으로, 해당 컴포넌트를 커스텀 엘리먼트로 컴파일한 경우에만 호스트 DOM 요소에 접근할 수 있게 해준다.Svelte자체가 아닌 순수 HTML 환경에서 쓰이는 Web Component의 최상위 DOM 노드를 얻는 용도이다.$host()의 반환 값은 현재 컴포넌트가 렌더링된 호스트 DOM의 엘리먼트 이다. 이 노드를 통해 DOM API 호출, 이벤트 dispatch, 속성 조작 등이 가능하다.
<!-- 부모 컴포넌트 -->
<script lang="ts">
import "./component/Stepper.svelte";
let count = $state(0);
</script>
<my-stepper onde={() => (count -= 1)} onincrement={() => (count += 1)}
></my-stepper>
<p>count: {count}</p>
<!-- 자식 컴포넌트 -->
<svelte:options customElement="my-stepper" />
<script lang="ts">
function dispatch(type: any) {
$host().dispatchEvent(new CustomEvent(type));
}
</script>
<button onclick={() => dispatch("de")}>decrement</button>
<button onclick={() => dispatch("in")}>increment</button>