쓰기 가능한 저장소
모든 응용 프로그램의 상태가 응용 프로그램의 구성 요소 계층 구조에 속하는 것은 아닙니다. 때로는 서로 관련이 없는 여러 구성 요소나 일반 JavaScript 모듈에서 액세스해야 하는 값이 있을 수 있는데, Svelte에서는 store라는 저장소를 통해 이를 수행할 수 있습니다.
store
는 값이 변경될 때마다 관련 프로그램에게 알림을 보낼 수 있는 subscribe
메서드가 있는 객체인데, 다음과 같이 App.svelte
컴포넌트에서는 저장소인 count
의 count.subscribe
라는 메서드의 콜백을 통해 countValue
를 변수의 값을 구독하고 있는 것을 확인할 수 있습니다.
<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/>
count
는 stores.js
파일에 정의되어 있는 쓰기 가능한 저장소로, 쓰기가능한 저장소는 subscribe
외에도 set
, update
와 같은 메서드도 사용할 수 있습니다.
import { writable } from 'svelte/store'; export const count = writable(0);
stores.js
에서 정의한 쓰기가능한 저장소인 count
는 Incrementer.svelte
컴포넌트에서 다음과 같이 increment
함수와 + 버튼을 연결하여 Count 프로그램을 구현할 수 있습니다.
function increment() { count.update(n => n + 1); }
Resetter.svelte
컴포넌트에서는 다음과 같이 reset
함수를 구현하여 store 초기화를 할 수 있습니다.
function reset() { count.set(0); }
Writable store 예제 전체 코드
<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/>
<script> import { count } from './stores.js'; function decrement() { count.update(n => n - 1); } </script> <button on:click={decrement}> - </button>
<script> import { count } from './stores.js'; function increment() { count.update(n => n + 1); } </script> <button on:click={increment}> + </button>
<script> import { count } from './stores.js'; function reset() { count.set(0); } </script> <button on:click={reset}> reset </button>
import { writable } from 'svelte/store'; export const count = writable(0);
자동 구독
이전 예제에서 앱은 작동하지만 미묘한 버그가 존재했는데, store를 구독한 이후에 구독은 취소되지 않기 때문에, 컴포넌트가 여러 번 인스턴스화되고 파괴되면 메모리 누수가 발생하게 됩니다. 그래서 App.svelte
컴포넌트에서는 unsubscribe
를 선언하여 시작할 필요가 있습니다.
const unsubscribe = count.subscribe(value => { countValue = value; });
위 코드는 subscribe
메서드를 호출하면 unsubscribe
함수가 반환되는데, unsubscribe
를 선언한 후에는 다음과 같이 onDestroy
수명 주기 훅을 통해 호출해줘야 합니다.
<script> 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는 다음과 같이 store
앞에 $를 붙여서 store
의 값을 참조할 수 있습니다.
<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>
자동 구독은 컴포넌트의 최상위 범위에서 선언되거나 가져온 store
변수에서만 작동하는데, $ 참조는 마크업 내에서 $count
를 사용하는 것에 국한되지 않고 이벤트 핸들러나 반응 선언과 같이 <script>
의 어느 곳에서나 사용될 수 있습니다.
Svelte에서 $로 시작하는 모든 이름은 store
를 참조하는 것으로 간주되는데, $는 사실상 예약된 문자이기 때문에 Svelte에서는 $ 접두사를 사용하여 변수를 선언할 수 없습니다.
Auto-subscriptions 예제 전체 코드
<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/>
<script> import { count } from './stores.js'; function decrement() { count.update(n => n - 1); } </script> <button on:click={decrement}> - </button>
<script> import { count } from './stores.js'; function increment() { count.update(n => n + 1); } </script> <button on:click={increment}> + </button>
<script> import { count } from './stores.js'; function reset() { count.set(0); } </script> <button on:click={reset}> reset </button>
import { writable } from 'svelte/store'; export const count = writable(0);
읽기 전용 저장소
참조하는 모든 저장소에 쓰기 작업이 필요한 것은 아닙니다. 예를 들어 마우스 위치나 사용자의 지리적 위치를 나타내는 저장소와 같이 ‘외부’에서 값을 설정할 필요가 없는 경우에는 읽기 전용으로 설정할 필요가 있습니다.
다음 코드와 같이 stores.js
에 구현된 저장소의 readable
에 대한 첫 번째 인수는 초기 값으로, 만약 초기 값이 없는 경우에는 null
또는 undefined
일 수 있고, 두 번째 인수는 집합 콜백을 받아 stop
함수를 반환하는 start
함수로 start
함수는 store
가 첫 번째 subscriber를 얻을 때 호출되고, stop
은 마지막 subscriber가 구독을 취소할 때 호출되는 함수입니다.
export const time = readable(new Date(), function start(set) { const interval = setInterval(() => { set(new Date()); }, 1000); return function stop() { clearInterval(interval); }; });
Readable stores 예제 전체 코드
<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>
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); }; });
파생 저장소
derived를 사용하면 하나 이상의 다른 store의 값을 기반으로 하는 또 다른 store를 생성할 수 있습니다.
export const elapsed = derived( time, $time => Math.round(($time - start) / 1000) );
Derived stores 예제 전체 코드
<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>
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) );
사용자 정의 저장소
subscribe 메서드를 올바르게 구현한다면 모든 객체는 저장소가 될 수 있습니다. 즉 사용자 지정 저장소도 매우 간단하게 만들 수 있는데, 다음과 같이 count
저장소에 increment
, decrement
, reset
메서드를 포함하고 set
, update
메서드는 숨기는 사용자 정의 저장소를 만들 수도 있습니다.
function createCount() { const { subscribe, set, update } = writable(0); return { subscribe, increment: () => update(n => n + 1), decrement: () => update(n => n - 1), reset: () => set(0) }; }
Custom stores 예제 전체 코드
<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, increment: () => update(n => n + 1), decrement: () => update(n => n - 1), reset: () => set(0) }; } export const count = createCount();
저장소 바인딩
set
메서드가 있는 경우, 즉 저장소가 쓰기 가능한 경우에는 로컬 컴포넌트의 상태에 바인딩할 수 있는 것처럼 해당 값도 바인딩할 수 있습니다.
<script> import { name, greeting } from './stores.js'; </script> <h1>{$greeting}</h1> <input value={$name}>
위의 코드는 쓰기 가능한 저장소인 name
과 파생 저장소인 greeting
이 있고, <input>
요소를 업데이트하는 예제로 입력 값을 변경하면 name
과 모든 종속 항목이 업데이트되는데, <button>
요소를 추가하여 컴포넌트의 내부에 값을 저장할 때 직접 할당할 수도 있습니다.
<button on:click="{() => $name += '!'}"> Add exclamation mark! </button>
위 코드에서 $name += '!'
할당은 name.set($name + '!')
과 동일하게 동작합니다.
Store bindings 예제 전체 코드
<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}!` );