[svelte store][02] svelte store의 writable()

svelte store에서 제공하는 writable, subscribe, unsubscribe 에 대해서 정리합니다.

1. Before using svelte store

svlete에서는 상태 관리를 위해서 writable() 함수를 제공합니다.

TS
import { writable } from 'svelte/store';

여기서는 서버에서 날씨 정보를 받아와서 화면에 렌더링하는 간단한 예제로 시작합니다.

다음과 같은 날씨 정보를 서버에서 제공합니다.

JS
{ temperature: "35C", humidity:"65%" time: new Date() }

서버 데이터를 가져오는 기능을 다음과 같이 흉내냅니다(api.js).

api.js
export const api = { weather:{ load(lat, lng) { return new Promise((resolve) => { setTimeout(() => { resolve({ temperature: "20C", humidity: "65%", time: new Date() }) }, 2000) }) } } }
  • 2초 정도의 지연을 흉내내고 있습니다.

Weather.svelte 컴포넌트는 다음과 같이 간단하게 구현합니다.

Weather.svelte#01
<script> import {writable} from 'svelte/store' import {api} from './api' const weather = writable(undefined); api.weather.load().then(data => { $weather = data; }) </script>
Weather.svelte#01
<div> <h5>weather</h5> {#if $weather} <span>{$weather.temperature}</span> <span>{$weather.humidity}</span> {:else} <span>...loading</span> {/if} </div>

2. What is "$weather"

Weather.svelte 에서 함수 writable을 호출해서 반환받은 객체를 store 객체라고 부릅니다.

store객체는 데이터가 변경되면 자동으로 화면을 갱신하는 reactive 기능을 제공합니다.

A store is an object that allows reactive access to a value via a simple store contract. The svelte/store module contains minimal store implementations which fulfil this contract.

이러한 store 객체는 *.svelte 파일 안에서만 dollar 기호($)를 앞에 붙여서 간편하게 데이터를 읽거나 쓸 수 있습니다.

이 문서에서는 달러 기호를 store prefix 라고 하겠습니다.

데이터를 업데이트하는 다음과 같은 코드를 store prefix 없이 구현하면 아래와 같습니다.

without store prefix
// using store prefix api.weather.load().then((data) => { $weather = data; }); // without store prefix api.weather.load().then((data) => { weather.set(data); });
  • store.set(...) - $weather = data 가 작동하려면 store객체는 메소드 set을 구현해야합니다.
  • 뷰 영역에서(html) $weather가 참조하는 영역이 업데이트 됩니다.

상태 변경을 감지하는 Weather.svelte#01 코드를 store prefix 없이 구현하려면 더 많은 코드가 필요합니다.

Weather.svelte#01
<script> import {writable} from 'svelte/store' import {api} from './api' const weather = writable(undefined); api.weather.load().then(data => { $weather = data; }) </script> <div> {#if $weather} <span>{$weather.temperature}</span> <span>{$weather.humidity}</span> {:else} <span>...loading</span> {/if} </div>

Weather.svelte#02store prefix 를 사용하지 않을 때의 구현입니다.

Weather.svelte#02
<script> import {writable} from 'svelte/store' import {api} from './api' const weather = writable(undefined); api.weather.load().then(data => { weather.set(data); }) let _wdata = undefined; weather.subscribe((weatherData) => { _wdata = weatherData }) </script>
Weather.svelte#02
<div> {#if _wdata} <span>{_wdata.temperature}</span> <span>{_wdata.humidity}</span> {:else} <span>...loading</span> {/if} </div>
  • subscribe(callback) - store 객체는 메소드 subscribe 를 구현해야 뷰에서 reactive하게 업데이트 됩니다.

store prefix 를 사용하지 않으면 Weather.svelte#02와 같이 직접 subscribe 메소드에 callback 함수를 등록해서 상태 변경을 받아내야 합니다.

상태를 저장할 변수(_wdata)도 따로 필요합니다.

SVELTE
let _wdata; // required to get state of store object weather.subscribe((weatherData) => { _wdata = weatherData })
  • svelte 에서는 위와 같이 변수에 값을 할당하면 reactive 하게 작동합니다. _wdata = weatherData 링크
  • 하지만 객체 내부의 property 가 변경된 경우에는 reactive 하게 작동하지 않습니다. _wdata.temperature = "40C" 로 변경해도 화면은 갱신되지 않습니다.

Weather.svelte#02 코드에는 버그가 있습니다.

3. subscribe, unsubscribe

날씨 정보가 여러 컴포넌트들이 공유하는 전역 상태라고 가정합시다.

Weather.svelte
import {writable} from 'svelte/store' import {api} from './api' // 전역 상태를 컴포넌트 안에서 생성하지 않음. const weather = writable(undefined); // 직접 api를 호출하지 않음 api.weather.load().then(data => { $weather = data; })
  • 설명을 위해서 store 객체를 컴포넌트 안에서 생성했었습니다.

애플리케이션에서는 여러 개의 컴포넌트들이 하나의 상태를 공유할 때가 많습니다.

다음과 같이 전역 상태를 관리하는 파일을 생성합니다.

weather.store.js
import { writable } from 'svelte/store' import { api } from './api' const weather = writable(undefined); export default { weather, loadWeather: async () => { const data = await api.weather.load() weather.set(data) // makes view to be updated } }

상태를 관리하는 weather.store.js는 다음과 같은 형태입니다.

  • 상태 관리는 컴포넌트에서 분리됩니다.

Weather.svelte#03 는 아래와 같이 전역 상태를 임포트합니다.

Weather.svelte#03
import {writable} from 'svelte/store' import {weather, loadWeather} from './weather.store' let _wdata; const unsub = weather.subscribe((weatherData) => { _wdata = weatherData }) // api, weather를 컴포넌트 밖으로 빼냅니다. loadWeather()

아래와 같이 다른 컴포넌트(Slide.svelte) 내에 사용된다고 합시다.

SlideMenu.svelte
<menu> <Weather/> </menu>
  • Slide menu 는 버튼을 누를 때만(toggling) 화면에 나타납니다.

Slide menu가 화면에 나타날때 마다 <Weahter/> 컴포넌트가 생성됩니다.

Weather.svelte#03weather.subscribe가 호출되면서 callback 함수가 등록됩니다.

그리고 상태 weather 에 등록된 callback 함수는 slide menu를 닫아도 제거되지 않습니다.

무슨 말이냐구요?

Slide menu 를 펼칠 때마다 매번 콜백이 등록됩니다.

  • 메뉴를 4번 열었다면 store 객체 weather에 콜백이 4개 등록되어 있습니다.
  • 상태가 한 번 변경되면 콜백 4개가 호출됩니다.
  • 화면 업데이트가 4번 발생합니다.

버그를 방지하려면 Weather.svelte#03 를 다음과 같이 보완해야 합니다.

Weather.svelte#04
import {onDestroy} from 'svelte'; let _wdata; const unsub = weather.subscribe((weatherData) => { _wdata = weatherData }) onDestory(() => { unsub(); // removes callback internally }
  • store 객체의 subscribe 함수는 반환값으로 또다른 함수를(unsub) 반환해야 합니다.
  • 이 함수는(unsub) 호출되었을 때 등록된 콜백을 해제하는 기능을 구현해야 합니다.
  • 컴포넌트에서 보통 onDestory에서 이 함수를 호출합니다.

이런 규약을 완벽하게 지키는 것은 불가능하기 때문에 svelte에서는 store prefix 문법을 제공합니다.

svelte의 store prefix 문법은 자동으로 subscription, unsubscription이 작동합니다.

4. Svelte store prefix '$'

Weather.svelte#05 - script
import {writable} from 'svelte/store' import {weather, loadWeather} from './weather.store' // let _wdata; // const unsub = weather.subscribe((weatherData) => { // _wdata = weatherData // }) loadWeather()
  • <script> 에서 subscribe 하는 코드를 제거합니다.
  • 임시 변수도 제거합니다.

html 영역에서 store prefix 를 붙여서 store 객체에 접근합니다.

Weather.svelte#05 - html
<div> <h5>weather</h5> {#if $weather} <span>{$weather.temperature}</span> <span>{$weather.humidity}</span> {:else} <span>...loading</span> {/if} </div>

다시 weather.store.js를 보면 store 객체를 업데이트 하는 코드가 있습니다.

weather.store.js
loadWeather: async () => { const data = await api.weather.load(); weather.set(data); // makes view to be updated }
  • weather.store.js는 svelte 파일이 아닌 일반 js 파일이기 때문에 store prefix 를 사용할 수 없습니다.

store.set(...) 으로 상태를 변경하면 컴포넌트 Weather.svelte가 업데이트 됩니다.

Weather.svelte
{#if $weather} ... {/if}

결론: 가급적이면 컴포넌트에서 store 객체를 사용할 때 store prefix를 사용합시다.

1.4. 일반 js(ts) 파일에서 store 객체 사용

위에서 이미 언급했듯이 store preifx 문법은 svelte 파일 내에서만 인식됩니다. js(ts)파일에서는 일반 변수로 취급됩니다.

애플리케이션이 커지면 store 객체끼리 서로 의존하는 경우가 발생하는데, 이런 경우에 subscription을 수동으로 관리합니다.