
Stores
Svelte는 상태 관리 라이브러리를 따로 지원하지 않고, Svelte 내부(svelte/store)에 포함되어 있다. store는 서로 관계없는 컴포넌트끼리 같은 데이터에 접근해야할 때 사용된다. A 컴포넌트의 데이터를 B와 C 컴포넌트에서 사용해야 한다고 할 때, store를 사용하면 간편하게 데이터 전달할 수 있다. Svelte의 store은 subscribe 함수를 포함하는 단순한 객체이다. subscribe 함수는 관찰하고 있는 값이 변경될 때마다 컴포넌트에게 알려주는 콜백 함수이다.
Writable stores
보통의 store는 읽기, 쓰기(수정)이 가능해야한다. 읽기, 쓰기 모두 가능한 sotre(writable store)를 생성해보자.
<!-- App.svelte
count가 관찰하고 있는 값이 변경될 때마다 subscribe 함수의 콜백 함수가 실행 된다. -->
<script>
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/>
<!-- Decrementer.svelte
- 버튼을 클릭하게 되면, decrement 함수가 호출된다. increment 함수와 동일하게 count.update를 사용한다. n => n - 1로 n - 1을 리턴하기 때문에, 이전에 관찰하는 값보다 1 작은 값으로 업데이트된다. -->
<script>
import { count } from './stores.js';
function decrement() {
count.update(n => n - 1);
}
</script>
<button on:click={decrement}>
-
</button>
<!-- Incrementer.svelte
+ 버튼을 클릭하게 되면, increment 함수가 호출된다. count.update의 n은 현재의 count가 관찰하고 있는 값을 저장하고 있다. n => n + 1은 n + 1을 리턴한다는 의미이다. 리턴된 값으로 count가 관찰하는 값을 업데이트한다. 즉, 이전에 관찰하는 값보다 1이 더 큰 값으로 업데이트된다. 또한 writable(0)로 count 변수를 생성하였기 때문에, count가 관찰하고 있는 값의 초깃값은 0이 된다.-->
<script>
import { count } from './stores.js';
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>
+
</button>
<!-- Resetter.svelte
reset 버튼을 클릭하게 되면, reset 함수가 호출된다. count.set(0)를 호출하는데, 이 의미는 count가 관찰하고 있는 값을 0으로 세팅한다는 것이다 -->
<script>
import { count } from './stores.js';
function reset() {
count.set(0);
}
</script>
<button on:click={reset}>
reset
</button>
// stores.js
// count 변수가 writable(0)로 생성되었다. 이렇게 생성된 변수는 set과 update, subscribe 함수를 포함하는 객체가 된다.
import { writable } from 'svelte/store';
export const count = writable(0);
Auto-subscriptions
subscribe 만되고 unsubscribed 되지 않을 경우에 컴포넌트가 지속적으로 인스턴스화되고 이는 메모리 누수를 야기시킬 수 있다. 따라서 반드시 unsubscribe도 선언해주어야한다.
const unsubscribe = count.subscribe(value => {
countValue = value;
});
Auto-subscriptions
subscribe를 사용하여 관찰하는 값이 변경되었을 경우 화면에 표시될 변수를 업데이트하고, 컴포넌트가 제거 되었을 때 subscribe의 리턴 값을 onDestroy 라이프 사이클 함수에서 호출하는 이 일련의 과정을 자동화해주는 $ 기능이 제공된다.
<!-- App.svelte -->
<script>
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>
<Incrementer/>
<Decrementer/>
<Resetter/>
store 이름 앞에 $를 붙여 사용하면 자동 구독을 할 수 있다. 자동구독을 사용하더라도 subscribe 함수는 <script> 태그 어디에서도 사용할 수 있다.
- 주의사항
store 변수 정의는 최상위 스코프에 있어야 한다.
: 최상위 스코프에 있다는 것은, 블록 안에서 변수가 정의되지 않고 <scirpt> 태그 하위에 바로 정의되어야 하는 것을 의미한다. import 된 store 변수 혹은 최상위 스코프에서 정의된 store 변수는 자동 구독을 할 수 있다.
$를 접두사로 사용하는 변수를 선언할 수 없다.
: 자동 구독을 위해 $ 접두사를 사용하기 때문에, Svelte는 $ 접두사를 사용하는 변수 선언을 막았다.
Readable stores
마우스의 위치나, 현재 사용자 위치, 현재 시간 등... 읽기만 가능한 store의 선언하기
<!-- App.svelte -->
<script>
import { time } from './stores.js';
const formatter = new Intl.DateTimeFormat('en', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit'
});
</script>
<h1>The time is {formatter.format($time)}</h1>
// stores.js
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 함수
readable(initial, function start (set) {
...
return function stop () {
...
};
})
- initial: 첫 번째 파라미터는 초깃값이다. 초깃값을 설정할 필요가 없을 경우, null이나 undefined를 사용할 수 있다.
- start: 두 번째 파라미터는 첫 구독자가 발생했을 때 호출되는 함수이다. set 콜백 함수를 파라미터로 가지고 stop 함수를 리턴하는 함수.
- set: 관찰하고 있는 값을 변경하는 콜백 함수.
- stop: 모든 구독자가 구독을 중단하면 호출되는 함수. start 함수에서 사용된 자원들이 있다면, 이 함수 내에서 자원을 해제해야 한다.
Derviced stores
이미 존재하는 store 를 이용해 새로운 store를 만들 수 있다.
<!-- App.svelte -->
<script>
import { time, elapsed } from './stores.js';
const formatter = new Intl.DateTimeFormat('en', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit'
});
</script>
<h1>The time is {formatter.format($time)}</h1>
<p>
This page has been open for
{$elapsed} {$elapsed === 1 ? 'second' : 'seconds'}
</p>
// stores.js
import { readable, derived } from 'svelte/store';
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
});
const start = new Date();
export const elapsed = derived(
time,
$time => Math.round(($time - start) / 1000)
);
- dervied 함수
// derived 함수의 4가지 형태
store = derived(a, callback: (a: any) => any)
store = derived(a, callback: (a: any, set: (value: any) => void) => void | () => void, initial_value: any)
store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any)
store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any)
- 첫 번째 파라미터는 참고하는 store이다. 참고하는 store가 하나라면 derived(a, ...)으로 객체가 되지만 참고하는 store가 여러 개라면 derived([a, ...b], ...)로 배열이 된다.
- 두 번째 파라미터는 새로운 store의 값을 리턴하는 콜백 함수이다. 콜백 함수의 파라미터는 참고하는 store가 된다. 콜백 함수의 마지막 파라미터는 set 함수이다. derived([a, ...b], ([$a, ...$b], set) => ...)와 같은 형태이다.
- 세 번째 파라미터는 새로운 store의 초깃값
Custom stores
subscribe 함수가 바르게 구현되어 있는 것들은 모두 store 이다. 커스텀 한 store을 만드는 것은 어렵지 않다. 왜냐하면 subscribe가 구현되어 있으면 모두 store이기 때문에 subscribe 함수를 가지는 객체를 만들면 되기 때문이다.
<!-- App.svelte -->
<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>
// stores.js
// count는 subscribe, increment, decrement, reset 함수를 가지는 store 이다.
// increment: update(n => n + 1)로 관찰하는 값을 1 증가시킨다.
// decrement: update(n => n - 1)로 관찰하는 값을 1 감소시킨다.
// reset: set(0)로 관찰하는 값을 0으로 초기화 시킨다.
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const count = createCount();
store bindings
만약 writable store 이고 set 메소드가 함께 있다면 store도 바인딩이 가능하다.
<!-- App.svelte
<input>: <input bind:value={$name}>로 <input> 태그의 value 속성에 $name을 바인딩한다. <input> 태그의 입력 값이 들어오면 $name에 반영된다.
<button>: <button> 태그에 click이벤트가 발생하면 $name에 ! 문자를 추가한다.-->
<script>
import { name, greeting } from './stores.js';
</script>
<h1>{$greeting}</h1>
<input bind:value={$name}>
<button on:click="{() => $name += '!'}">
Add exclamation mark!
</button>
// stores.js
// writable store 인 name과 derived store 인 greeting 2개의 store가 있다.
// greeting는 name이 관찰하는 값에 약간의 문구를 추가한 store이다.
import { writable, derived } from 'svelte/store';
export const name = writable('world');
export const greeting = derived(
name,
$name => `Hello ${$name}!`
);