본문 바로가기

웹개발 - Front 관련

[ Svelte 5 ] - 2. 상태 관리 Rune 정리 & Rune store subscribe 흉내 내보기

Topic =  Svelte5 Rune API 정리 해보기.

 

 


 

Rune 이란?

 

Rune은 Svelte 5 부터 도입된 반응성 시스템을 제공하는 구문으로 상태, 상태 이벤트, 종속성 등의 기능을 함수 형태로 캡슐화 하여 제공하는 것 이다.

 

상태 - $state, $derived
  • 상태관리 Rune 으로 정의한 변수는 DOM 반응성을 갖게 되어 변경이 발생하면 자동으로 DOM 이 업데이트 된다.
  • $state(), $derived() 를 통해 각각 상태, 파생된 상태를 정의할 수 있다.

변수 값이 1 이었던 count 변수의 값이 2로 바뀌면 기존 DOM 에서 보이던 1 을 2 로 업데이트 해주어야 한다. 이때 변수를  $state() 를 통해 상태로 정의해두면, 해당 상태의 값이 바뀔 때 자동으로 DOM이 업데이트 되게 된다.

 

[ $state ]

$state 룬을 사용하면 해당 요소가 변경될 때, UI 반응성을 갖는 반응형 상태를 정의할 수 있다,

🌟$state 로 정의된 객체나 배열은 은 기본적으로 Deep Reactive State Proxy 로 관리된다.

  • Deep State
    • 상태의 중첩된 속성을 포함하여 모든 속성에 대해 반응성을 자동으로 제공한다.
  • Proxy
    • 내부적으로 JS의 Proxy 객체를 사용하여 속성 접근과 변경을 감지하고 반응성을 관리한다.
    • 객체의 중첩된 값이나 배열의 특정 항목이 변경되더라도, 해당 변경 사항을 자동으로 추적하여 반응형 업데이트를 트리거 한다.

[ $state.row() ]

만약, 배열이나 객체의 일부 요소에 대한 변경은 막지만, 다른 배열이나 객체를 재할당 하는 것을 허용하려면, $state.row() Rune을 사용할 수 있다. 

 

[ $derived ]

$derived 는 특정 상태의 종속성을 갖는 파생된 상태를 만드는 것이다. 이는 종속 상태가 변경되면, 파생 상태도 재계산 되어 변경된다.

복잡한 계산로직이 필요한 경우에는 $derived.by( () => {} ) 와 같이 사용할 수도 있다.

 

 


🌟 $derived 내부에 선언된 모든 상태는 동기적으로 상태의 종속성이 된다. ( 여러 상태도 ok )

 

 $derived 내부에서 종속 상태 값을 변경할 수 없다 => 종속 상태 값을 변경하면 다시 파생상태 로직이 실행되므로, 순환되는 문제가 발생하기 때문에 컴파일 시점에서 막아두었다. 아래에서 다룰 $effect() 에서는 참조하는 상태의 값을 변경해도 컴파일 시점에 오류가 발생하지 않기 때문에 적어둠.

 

 

$props(), $bindable()

 

$props() 는 컴포넌트의 properties 에 해당하는 속성으로, 상위 컴포넌트에서 하위 컴포넌트 사용 시 생성자로 넘겨주는 인수들을 하위 컴포넌트에서 사용하기 위해 사용하는 속성이다.

 

 

참고용... 

 

 

 

 

 

 

[ 프로퍼티의 디폴트 값 설정하기 ]

  • 상위 컴포넌트에서 프로퍼티를 넘겨주지 않았을 때 기본 값을 설정할 수 있다.
let { userName = "DEFAULT" } = $props()

 

 

[ Rest props ]

  • 상위 컴포넌트에서 넘겨준 속성들은 Rest props 를 통해 일괄적으로 사용할 수 있다.
let { a, b, c, ...styles } = $props()

 

 

 

[ $bindable() ]

하위 컴포넌트에서 $props 를 통해 받은 속성은 기본적으로 부모 컴포넌트에 영향을 미치지 못한다. 즉, 부모 컴포넌트에서 넘겨준 상태 값 부터는 하위 컴포넌트 개별적인 속성으로 처리된다. 부모 컴포넌트에서 하위 컴포넌트로 내려준 속성의 값이 변경되었을 때, 부모 컴포넌트의 해당 속성도 변경되기를 원한다면, bind: 와 $bindable() 을 통해 양방향 바인딩을 지정해주어야 한다.

 

 

$effect()

 

$effect() 는 상태, 파생 상태를 트래킹하며, 트래킹하는 상태가 액세스 되거나, 상태가 변경되는 등의 이벤트가 발생 시 지정해둔 로직을 실행시키는 기능이다.

 

기본적으로 $effect() 는 정의된 컴포넌트의 라이프 사이클에 종속된다. 컴포넌트가 Destroy 되면, effect rune 도 실행되지 않게 되는 것이다.

 

🌟 $effect() 내부에 선언된 모든 상태는 동기적으로 트래킹의 대상이 된다. ( 여러 상태도 ok )

  • effect는 컴포넌트가 초기화 될 때 최초 1번 실행된다.
  • effect() 내부 함수의 반환 함수는 effect() 가 실행되기 전, 실행 후 실행된다.

 

 

[ $effect.pre() ]

$effect.pre() 는 상태가 변동되어 DOM이 업데이트 되기 전에 실행되는 함수이다. 

 

 

[ $effect.traking() ]

$effect.traking Rune은 실행되는 위치의 현재 코드가 $effect(), $derived, HTML 템플릿과 같이 종속성을 추적이 활성화된 흐름(컨텍스트) 내부에서 실행되고 있는지 상태를 Boolean 형태로 반환한다.

 

 

❓HTML 템플릿은 왜 종속성을 추적하는 컨텍스트라고 보는 것일까?

➡️ HTML 템플릿은 상태가 변경될 때, 변경된 상태를 UI 에 업데이트 하기 위해서 DOM 간의 연계를 유지하고, 내부적으로 $effect() 처럼 상태를 추적하는 컨텍스트 이기 때문에 템플릿 내부에서의 $effect.tracking() 를 실행하면 true 값을 반환하게 된다.

 

 

전역 상태 관리 [ .svelte.js 모듈, Store ]

 

상위 컴포넌트 - 하위 컴포넌트 구조가 커지게 되면, 중간 컴포넌트 중에서는 필요하지 않은 속성이 있지만, 하위 컴포넌트에서 $props 로 받아 사용하기 위해 해당 구조 내의 모든 컴포넌트가 속성을 전달 - 전달 하는 것은 번거롭다. 공통적으로 사용하는 전역 상태를 만들 수 있는데. Store 방식과 Rune 을 사용하는 .svelte.js 모듈을 사용하는 방식이 있다.

 

[ .svelte.js 모듈 With Rune Store ]

 

  • JS 모듈로 부터 import 로 가져온 객체는 기본적으로 읽기 전용이기 때문에 값을 재할당 할 수 없으며, bind: 로 지정이 불가능하기 때문에 객체형태로 값을 정의해야한다.

 

 

[ .svelte.js 모듈로 전역 상태 관리하기 ]

  • $derived() Rune은 .svelte.js 모듈 내부에서 작성하기 위해서는 export function 내부에 변수로 선언해두고, 해당 변수를 반환하는 식으로 작성해주어야 한다.

 

 

🌟 .svelte.js 확장자 내부에서는 상태 Rune 을 사용할 수 있다 ( $state(), $derived() ) 반면에, 상태 이벤트 Rune $effect() 는 .svelte 확장자 파일에서만 사용할 수 있다. 이유는 $effect() 는 컴포넌트의 런타임 환경에서 동작하며, 상태 종속성을 추적하는 것이다. 즉, $effect() 가 정의된 컴포넌트의 라이프 사이클을 따르는 것인데 .svelte.js 파일의 경우 모듈로 인식되어지므로 Svelte 컴포넌트의 라이플 사이클이 존재하지 않게 되므로 내부에서 사용할 수 없다.

  • 🟥내부에서 effect() 를 사용할 수 없다는 것은, svelte/store 에서 제공하는 wriable, readble 등의 스토어와 같이 store.js 파일 내부에서 store.subscribe( value => { 로직 } ) 와 같이 작성할 수 없는 것을 의미한다. 아예 $effect() 를 .svelte.js 모듈에 작성할 수 없다는 의미가 아님!
  • https://svelte.dev/docs/svelte/$effect#$effect.tracking .svelte.ts 내부에서 $effect() 를 통해 svelte/store 의 readable 기능구현 예시인데 복잡하다.

 

위에 🟥 항목에서 다룬 것 처럼, svelte/store 의 writable, readable 은 subscribe() 메서드를 통해 store.js 모듈 내부에서 상태 변경에 따른 콜백 이벤트를 내부에서 실행할 수 있었다. 반면, Rune의 $state 는 subscribe 와 같이 변경에 따른 콜백 이벤트를 실행하는 기능은 없는 것 같다. 때문에 다음과 같은 방법으로 시도 해보았다.

 

1. .svelte.js 모듈 내부에 함수 선언하고 $state() 내부 상태 관리 메서드 안에서 실행 시킨다.

❌ 정상작동은 되지만, 상태 변화를 감지해서 자동으로 콜백을 실행해주는 subscribe 의 형태가 아니여서 실패...

function callbackA(count) {
    console.log(count)
}

export const counter = $state(
    {
        count: 0,
        addCount() {
            this.count++
            callbackA(this.count)
        },
        deleteCount() {
            this.count--
            callbackA(this.count)
        }
    }
);

 

2. derived()

상태가 변경되면 콜백 이벤트를 실행할 상태에 파생 상태를 만들어 export 한다. $derived.by() 함수 내부에서 콜백 이벤트를 정의하고 실행한다.

❌심각한 문제 ❌  ==> 여러 컴포넌트에서 사용하게 되면, .derived 가 여러번 실행되는 문제가 발생한다.

 

3. $effect.root 활용

  • HTML 템플릿이 빈 svelte 컴포넌트를 만들어서 .svete.js 모듈 내부의 상태를 추적하도록 하고, 변경이 발생하면 실행될 로직을 작성한다.

[ counter.svelte.js ]

export const counter = $state(
    {
        count: 0
    }
);

 

[ CounterSubscribe.svelte ]

<script>
    import {counter} from "./list.svelte.js";

    $effect.root( () => {
        $effect( () => {
            // observe 객체가 변경이 되면 특정 로직이 실행
            console.log("ROOT",counter.count)
        })

        return () => {
            console.log("EFFECT END")
        }
    })
</script>
  • 관련 로직이 시작되는 최상위 컴포넌트에 해당 컴포넌트를 등록해둔다...
<script>
    import LoginPage from "./components/login/LoginPage.svelte";
    import CounterSubscribe from "./store/CounterSubscribe.svelte";
</script>

<CounterSubscribe/>
<LoginPage/>

<style lang="scss">
    
</style>

하위 컴포넌트에서 count에 다른 값을 할당해도 정상적으로 작동한다.

 

❌ 이 방법도 상태 관리 변경에 따른 콜백 함수가 많아지면 상위 컴포넌트에 필요없는 컴포넌트들이 덕지덕지 붙게 되는 문제가 발생한다

 

.svlete.js 모듈 내부의 Rune 활용해서 store.subscribe 처럼 만들기에는 어려운 것 같다... 복잡한 로직이 필요한 경우에는svelte/store 를 사용하도록 하자.

 

 

 

 

 

 

 

 

 

 

 

 


[Reference]

 

How to make global effects in svelte 5? · sveltejs svelte · Discussion #13970

Follow this documents https://svelte.dev/docs/svelte/context I create global state in state.svelte.js, how to observe them changed and do somethings?

github.com

 

 

Overview • Docs • Svelte

Svelte is a framework for building user interfaces on the web. It uses a compiler to turn declarative components written in HTML, CSS and JavaScript... App function greet() { alert('Welcome to Svelte!'); } click me button { font-size: 2em; } ...into lean,

svelte.dev