svelte에는 전역 상태를 저장하는 store가 계층구조에 포함되어있어 유용하게 사용할 수 있습니다. 그러나 모든 어플리케이션이 상태가 어플리케이션 계층 구조에 속하는 것은 아닙니다. 상태 관리를 위해서 자바스크립트 모듈 등을 사용해야 하는 경우도 많습니다. ( redux, recoil … )
스벨트에서는 상태관리를 stores
를 통해 관리합니다. store는 값이 변경될 때 마다 subscribe
메소드를 사용하는 간단한 객체입니다.
import { writable } from 'svelte/store';
export const count = writable(0);
import { count } from './stores.js';
function reset() {
count.set(0);
}
</script>
<button on:click={reset}>
reset
</button>
import { count } from './stores.js';
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>
+
</button>
import { count } from './stores.js';
function decrement() {
count.update(n => n - 1);
}
</script>
<button on:click={decrement}>
-
</button>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
let countValue;
count.subscribe(value => {
countValue = value;
});
</script>
<h1>The count is {countValue}</h1>
<Incrementer/>
<Decrementer/>
<Resetter/>
이전의 예제는 스토어가 구독되지만 구독 취소는 되지 않았습니다.
const unsubscribe = count.subscribe(value => {
countValue = value;
});
subscribe
메소드를 호출하면 unsubscribe
함수가 반환됩니다.
이제 unsubscribe
를 선언할 수 있으며 onDestroy
lifecycle hook을 통해 호출할 수 있습니다.
import { onDestroy } from 'svelte';
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
let countValue;
const unsubscribe = count.subscribe(value => {
countValue = value;
});
onDestroy(unsubscribe);
</script><h1>The count is {countValue}</h1>
svelte에서는 컴포넌트가 여러 스토어를 구독하는 경우 등을 위해서 자동 구독을 지원합니다. 구독하는 변수 앞에 $
를 붙여 사용할 수 있습니다.
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
</script>
<h1>The count is {$count}</h1>
자동 구독은 컴포넌트의 최상위 스코프에서 선언된 store 변수에서만 작동합니다.
$count
는 위 예제와 같이 마크업 내에서만 사용 가능한 것이 아닌, 이벤트 핸들러나 반응성 선언과 같이 어디에서나 사용 가능합니다.
$
가 앞에 붙어있는 변수는 모두 store 값을 참조하는 값입니다. 따라서 Svelte에서는 $
로 시작하는 변수명을 짓는 것은 피해야 합니다.
만약 마우스 위치나 사용자의 지리적 위치 등을 나타내는 store 가 있다면 외부에서 해당 스토어의 값을 설정하지 못합니다. 이러한 경우을 위하여 읽기만 가능한 store를 지정할 수 있습니다.
import { readable } from 'svelte/store';
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
});
readable
에 대한 첫번째 인자는 초기 값을 지정해줍니다. 아직 값이 없는 경우라면 null값이나 undefined 값이 될 수도 있습니다. 두번째로는 set을 콜백으로 받고 stop 함수를 리턴하는 start 함수를 입니다.
store에서 첫번째 구독자를 얻을 때 start 함수가 호출되며 마지막 구독자가 구독을 해지하게 된다면 stop 함수가 호출하게 되는 구조입니다.
Derived
를 통해서 다른 store에 기초되는 값을 가지는 store를 만들 수 있습니다. 이전 예제를 기반으로 page가 열린 시간을 파생하는 store를 만들 수 있습니다.
export const elapsed = derived(
time, $time => Math.round(($time - start) / 1000)
);
여러 입력에서 저장소를 파생하고, 값을 반환하는 대신 명시적으로 값을 ‘set’ 할 수 있습니다 ( 비동기적으로 값을 파생하는데 유용)
Derived
는 기존에 생성되어있는 쓰기 가능한 store, 읽기 가능한 store의 데이터를 기반으로 동작하게 됩니다.
import { writable, derived } from 'svelte/store'
export let count = writable(1)
export let double = derived(count,$count => $count * 2 )
//구독의 의미가 아니라 count 함수의 데이터라는 뜻으로 $를 추가한다.
//svelte 파일이 아닌 js 파일 내 이므로 자동구독으로 실행되지 않음
export let total = derived([count,double], ([$count, $double], set) => {
set($count+$double)
})
// export let total = derived([count,double], ([$count, $double])=> { return $count+$double } 과 동일하다
export let initialValue = derived(count, ($countm set) => {
setTimeout(() => {
set($count + 1)
},1000)
},'최초 계산중 ...')
import { count, double, total } from './store.js'
console.log(double)
</script>
<button on:click={()=> $count += 1 }> click! </button>
<h1>total: {$total}</h1>
<h2>count: {$count}</h2>
<h2>double: {$double}</h2>
<h2>count + 1 : {$initialValue}</h2>
derived는 계산된 store입니다. 따라서 함수의 첫번째 인자로 계산할 대상의 store를 매개변수로 입력하고 두번째는 실행 될 코드를 입력합니다. 두가지 값을 입력할 때는 배열을 사용해서 입력할 수 있습니다. 연결되어있는 스토어의 값이 바뀌면 매번 실행되며 초기화 되었다가 재 구독 하는 형식으로 다른 store들과 차이점이 있습니다.
store에 구독 하지 않고 단순히 해당하는 값만 읽어오고 싶은 경우 사용합니다.
import { get } from 'svelte/store'
import { count, double, user } from './store.js'
console.log(get(count))
console.log(get(double))
console.log(get(user))
import { writable, readable, derived, get } from 'svelte/store'
export let count = writable(1)
export let double = derived(count, $count => $count * 2)
export let user = readable({
name: 'Heropy'
age: 85,
email:'thesecon@gmail.com'
})
console.log(get(count))
console.log(get(double))
console.log(get(user))
custom store는 기존의 store에서 추가로 원하는 메소드를 추가하여 커스텀해서 사용 가능한 store입니다. writable, readable 등
<script>
import { count } from './stores.js';
</script>
<h1>The count is {$count}</h1>
<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>
import { writable } from 'svelte/store';
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe, //subscribe를 가지고있으면 App.svelte에서 '$'자동구독을 이용해서 사용 가능
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const count = createCount();
예제와 같이 기본 writable store에서 increment, decrement, reset 메소드가 추가로 포함되게 구성할 수 있습니다. return 값에서 set 과 update는 입력하지 않아도 됩니다.
import { writable } from 'svelte/store'
const { set, update, subscribe } = writable(0)
export let count = {
set,
update,
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
}
위와 같이 작성할 수도 있습니다.
예제 2)
import { fruits } from './fruits.js'
let value
<input bind:value/>
<button on:click={() => fruits.setItem(value)}>
Add fruit!
</button>
<button on:click={() => console.log(fruits.getList())}>
Log fruit list!
</button>
<ul>
{#each $fruits as {id, name} (id)}
<li>{name}</li>
{/each}
</ul>
import { writable , get } from 'svelte/store'
const _fruits = writable([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
export let fruits = {
..._fruits, // writable이기 때문에 set, update, subscribe가 포함되어있다
getList: () => get(_fruits).map((f) => f.name),
setItem: () => {
_fruits.update((f) => {
f.push({
id: f.length + 1,
name
})
return f
})
}
}
set
메소드를 가지고 있는 writable store라면 컴포넌트에 bind하는 것과 동일하게 값을 bind 할 수 있습니다.
예를 들어 쓰기 가능한 저장소에 name을 가지고 있고 파생된 저장소에는 greeting을 가지고 있을 때 input 요소에 해당 값을 업데이트 할 수 있습니다.
<input bind:value={$name}>
컴포넌트 내부에 값을 저장하도록 직접 할당할 수도 있습니다.
<button on:click="{() => $name += '!'}">
Add exclamation mark!
</button>
$name += '!'
와 name.set($name + '!')
는 동일합니다.
<script>
import { name, greeting } from './stores.js';
</script>
<h1>{$greeting}</h1>
<input bind:value={$name}>
<button on:click="{() => $name += '!'}">
Add exclamation mark!
</button>
import { writable, derived } from 'svelte/store';
export const name = writable('world');
export const greeting = derived(
name,
$name => `Hello ${$name}!`
);