Svelte Store 사용하기

Svelte 저장소 가이드

쓰기 가능한 저장소

모든 응용 프로그램의 상태가 응용 프로그램의 구성 요소 계층 구조에 속하는 것은 아니야. 때로는 서로 관련이 없는 여러 구성 요소나 일반 JavaScript 모듈에서 액세스해야 하는 값이 있을 수 있는데, Svelte에서는 store라는 저장소를 통해 이를 수행할 수 있어.

store는 값이 변경될 때마다 관련 프로그램에게 알림을 보낼 수 있는 subscribe 메서드가 있는 객체인데, 다음과 같이 App.svelte 컴포넌트에서는 저장소인 countcount.subscribe라는 메서드의 콜백을 통해 countValue변수의 값을 구독하고 있는 것을 확인할 수 있어.

count store 사용하기
App.svelte
<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/>

countstores.js 파일에 정의되어 있는 쓰기 가능한 저장소로, 쓰기가능한 저장소는 subscribe 외에도 set, update와 같은 메서드도 사용할 수 있어.

Store 정의
stores.js
import { writable } from 'svelte/store';
export const count = writable(0);

stores.js에서 정의한 쓰기가능한 저장소인 countIncrementer.svelte 컴포넌트에서 다음과 같이 increment 함수와 + 버튼을 연결하여 Count 프로그램을 구현할 수 있어.

Store와 버튼 연결
Incrementer.svelte
function increment() {
    count.update(n => n + 1);
}

Resetter.svelte 컴포넌트에서는 다음과 같이 reset 함수를 구현하여 store 초기화를 할 수 있어.

store 초기화
Resetter.svelte
function reset() {
    count.set(0);
}

Writable store 예제 전체 코드

App.svelte
<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
<script>
    import { count } from './stores.js';

    function decrement() {
        count.update(n => n - 1);
    }
</script>

<button on:click={decrement}>
    -
</button>
Incrementer.svelte
<script>
    import { count } from './stores.js';

    function increment() {
        count.update(n => n + 1);
    }
</script>

<button on:click={increment}>
    +
</button>
Resetter.svelte
<script>
    import { count } from './stores.js';

    function reset() {
        count.set(0);
    }
</script>

<button on:click={reset}>
    reset
</button>
stores.js
import { writable } from 'svelte/store';
export const count = writable(0);

자동 구독

이전 예제에서 앱은 작동하지만 미묘한 버그가 존재하는데, store를 구독한 이후에 구독은 취소되지 않기 때문에, 컴포넌트가 여러 번 인스턴스화되고 파괴되면 메모리 누수가 발생하게 돼. 그래서 App.svelte 컴포넌트에서는 unsubscribe를 선언하여 시작할 필요가 있어.

unsubscribe 선언
App.svelte
const unsubscribe = count.subscribe(value => {
    countValue = value;
});

위 코드는 subscribe 메서드를 호출하면 unsubscribe 함수가 반환되는데, unsubscribe를 선언한 후에는 다음과 같이 onDestroy 수명 주기 훅을 통해 호출해줘야 돼.

onDestroy 수명 주기로 호출
App.svelte
<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의 값을 참조할 수 있어.

store 참조하기
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>

자동 구독은 컴포넌트의 최상위 범위에서 선언되거나 가져온 store 변수에서만 작동하는데, $ 참조는 마크업 내에서 $count를 사용하는 것에 국한되지 않고 이벤트 핸들러나 반응 선언과 같이 <script>의 어느 곳에서나 사용될 수 있어.

Svelte에서 $로 시작하는 모든 이름은 store를 참조하는 것으로 간주되는데, $는 사실상 예약된 문자이기 때문에 Svelte에서는 $ 접두사를 사용하여 변수를 선언할 수 없어.

Auto-subscriptions 예제 전체 코드

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/>
Decrementer.svelte
<script>
    import { count } from './stores.js';

    function decrement() {
        count.update(n => n - 1);
    }
</script>

<button on:click={decrement}>
    -
</button>
Incrementer.svlelte
<script>
    import { count } from './stores.js';

    function increment() {
        count.update(n => n + 1);
    }
</script>

<button on:click={increment}>
    +
</button>
Resetter.svelte
<script>
    import { count } from './stores.js';

    function reset() {
        count.set(0);
    }
</script>

<button on:click={reset}>
    reset
</button>
stores.js
import { writable } from 'svelte/store';
export const count = writable(0);

읽기 전용 저장소

참조하는 모든 저장소에 쓰기 작업이 필요한 것은 아니야. 예를 들어 마우스 위치나 사용자의 지리적 위치를 나타내는 저장소와 같이 ‘외부’에서 값을 설정할 필요가 없는 경우에는 읽기 전용으로 설정할 필요가 있어.

다음 코드와 같이 stores.js에 구현된 저장소의 readable에 대한 첫 번째 인수는 초기 값으로, 만약 초기 값이 없는 경우에는 null 또는 undefined일 수 있고, 두 번째 인수는 집합 콜백을 받아 stop 함수를 반환하는 start 함수로 start 함수는 store가 첫 번째 subscriber를 얻을 때 호출되고, stop은 마지막 subscriber가 구독을 취소할 때 호출되는 함수야.

stores.js
export const time = readable(new Date(), function start(set) {
    const interval = setInterval(() => {
        set(new Date());
    }, 1000);

    return function stop() {
        clearInterval(interval);
    };
});

Readable stores 예제 전체 코드

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);
    };
});

파생 저장소

derived를 사용하면 하나 이상의 다른 store의 값을 기반으로 하는 또 다른 store를 생성할 수 있어.

stores.js
export const elapsed = derived(
    time,
    $time => Math.round(($time - start) / 1000)
);

Derived stores 예제 전체 코드

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)
);

사용자 정의 저장소

subscribe 메서드를 올바르게 구현한다면 모든 객체는 저장소가 될 수 있어. 즉 사용자 지정 저장소도 매우 간단하게 만들 수 있는데, 다음과 같이 count 저장소에 increment, decrement, reset 메서드를 포함하고 set, update 메서드는 숨기는 사용자 정의 저장소를 만들 수도 있어.

stores.js
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 예제 전체 코드

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
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 메서드가 있는 경우, 즉 저장소가 쓰기 가능한 경우에는 로컬 컴포넌트의 상태에 바인딩할 수 있는 것처럼 해당 값도 바인딩할 수 있어.

App.svelte
<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 예제 전체 코드

App.svelte
<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
import { writable, derived } from 'svelte/store';

export const name = writable('world');
export const greeting = derived(
    name,
    $name => `Hello ${$name}!`
);

답글 남기기