심심한 개발자의 취미생활

Runes

  • Rune (룬) 문법은 svelte5에 도입된 반응성 시스템을 근본적으로 개선하기 위한 새로운 기능이다. 기존의 암시적 반응성 대신, 더 명확하고 직관적인 코드 작성을 가능하게 한다.
  • 룬은 $로 시작하는 함수 형태를 가지며, 이 함수들을 호출하여 반응성 변수를 만들거나 상호작용을 제어한다.

Rune의 탄생

  • 기존 Svelte의 반응성 시스템은 간단한 경우 매우 편리했지만 복잡한 상황에서는 몇가지 혼란을 야기하였다.

    1. let의 모호함
      • let 키워드 하나로 컴포넌트 state, props, 일반 변수를 모두 선언하여 역할이 불분명하다.
    2. $: 문법의 한계
      • 반응성을 위해 사용된 $: 레이블은 JS 표준 문법이 아니라서 처음 배우는 사람들에게 혼란을 주었고, 언제 실행되는지 예측하기 어려운 경우가 많았다.
    3. .svelte파일의 제약
      • svelte의 반응성은 .svelte 파일 안에서만 동작했다. .js, .ts 파일에서 반응성 상태를 관리하려면 store를 사용해야만 해서 불편함이 있었다.
  • 룬은 이러한 문제들을 해결하여 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)의 현재 값을 복사하여 불변의 스냅샷을 만드는 역할을 한다.
  • 사용 경우
    1. 불변 데이터 구조가 필요할 때
      • 상태의 현재 버전을 저장해 두고 나중에 비교하고 싶을 때
    2. 외부 라이브러리에 데이터를 전달 할 때
      • Svelte의 반응성 시스템을 모르는 외부 라이브러리에 데이터를 전달할 때 프록시 객체가 아닌 일반 객체를 넘겨주는 경우
    3. 원본 상태를 바꾸지 않고 값을 수정할때
      • 상태를 복사해서 수정한 다음, 그 결과가 유효할 때만 원본 상태에 반영하고 싶을 경우
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

  1. 의존성

    • $derived의 가장 핵심적인 기능은 자동 의존성 추적이다. $derived 표현식 내부에서 동기적으로 읽히는 모든 상태는 해당 파생값의 의존성으로 간주된다.
    • 동작 순서
      1. 내부 식의 의존성이 변경되면 해당 $derived 값은 즉시 더티(dirty) 상태로 표시된다.
      2. 더티로 표시된 $derived 값은 실제로 읽히거나 코드에서 사용될 때 재계산 된다. 이는 불필요한 계산을 방지하여 성능을 최적화 한다.
    • 만약 특정 상태가 $derived 값의 계산에 사용되지만, 그 상태 변경이 $derived값의 재계산을 막고 싶다면 untrack 함수를 사용하여 해당 상태를 의존성 추적에서 제외할 수 있다.
  2. 파생 값 재정의

    • $derived값은 일반적으로 의존성 변경에 따라 자동으로 재계산 되는 읽기 전용 값이다. 하지만 Svelte 5.25 부터는 const로 선언 되지 않은 $derived값을 일시적으로 재할당하여 재 정의 할 수 있다.
    • $derived값을 재 할당하면, 해당 값은 더 이상 의존성을 변화에 따라 자동으로 재계산 되지 않아 파생된 특성을 읽게 된다.
    • 이 기능은 UI/UX를 개선하기 위한 특정 상황에서만 사용해야 한다.
  3. 파생과 반응성

    • Svelte의 상태 관리에서 $state$derived는 반응성 처리 방식에 차이가 있다.
      • $state
        • $state로 선언된 객체나 배열을 내부적으로 '깊은 반응형 프록시'로 변환되어 객체나 배열의 중첩된 속성이 변경되어도 값이 반응하도록 만든다.
      • $derived
        • $derived는 객체나 배열을 반환하더라도, $derived 자체는 그 객체나 배열의 중첩된 속성까지 깊게 반응형으로 만들지 않는다.
  4. 구조 분해

    • $derived 선언과 함께 구조 분해를 사용하면, 구조 분해된 각각의 변수들이 모두 반응형이 된다.
  5. 업데이트 전파

    • svelte 5는 push-pull 반응성이라는 매커니즘을 사용하여 효율적인 업데이트 전파를 관리한다.

$effect

  • $effect룬은 리액티브 값의 변경에 반응하여 부수 효고를 실행하는 데 사용된다. 이는 주로 컴포넌트의 라이프 사이클 외부에서 발생하거나, DOM을 직접 조작하거나, 외부 API와 상호작용하는 등의 작업을 수행할 때 활용된다.

$effect

  • 라이프 사이클
    • $effect는 컴포넌트의 마운트 시점에 처음 실행되며, 이후 의존성이 변경될 때마다 재실행된다.
      • 마운트 시점 (Mount)
        • 컴포넌트가 처음 렌더링 될 때 $effect 내부의 코드가 한 번 실행된다.
      • 업데이트 시점 (Update)
        • $effect 내부에서 참조하는 리액티브 상태가 변경되면 해당 $effect는 즉시 재 실행된다. Svelte는 변경된 상태를 감지하고 $effect를 효율적으로 다시 실행한다.
      • 정리 (Cleanup)
        • $effect는 클린업 함수를 반환할 수 있다. 이 클린업 함수는 다음 번 $effect가 재실행 되기 전 또는 컴포넌트가 언 마운트 될 때 실행된다.
        • 이는 타이머 해제, 이벤트 리스터 제거, 구독 해지 등 리소스 정리가 필요할 때 유용하다.
<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와 다르게 수동으로 의존성 배열을 지정할 필요가 없다.

$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()
    • name prop을 받아와 지역 변수 nickname으로 사용한다.
  • Rest Props

    • let { foo, ...rest } = $props()
    • 구조 분해에서 ...rest를 사용하면 전달된 props 중 특정 props를 제외한 나머지 모든 props를 객체로 수집할 수 있다.
  • props의 업데이트

    • props로 넘겨진 값들은 반응형이다. 부모에서 값이 바뀌면 자식 컴포넌트 내에서도 자동으로 업데이트 된다.
  • props의 특징

    1. 생성된 객체를 직접 변경하지 말것
      • let {obejct} = $props(); object.count += 1; (X)
    2. TypeScript에서의 props 타입 처리
      • let {foo, bar}: {foo: string; bar?: number} = $props()
    3. $props.id() : 고유 ID 생성
      • const uid = $props.id()

$bindable

  • $bindablesvelte5의 런타임 시 컴포넌트 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>