스토어(Stores): 컴포넌트 간 반응형 상태 공유
스토어(Stores): 컴포넌트 간 반응형 상태 공유
여러 컴포넌트가 같은 값을 읽고 바꿔야 할 때가 있어요. 예를 들어 사용자 정보나 알림 개수를 여러 화면이 함께 참조하는 상황이죠. Svelte에서 이런 컴포넌트 간 반응형 상태를 다루는 전통적인 방법이 스토어(store) 예요.
스토어는 간단한 store contract 를 통해 값에 대한 반응형 접근을 허용하는 객체예요. svelte/store 모듈이 이 계약을 만족하는 최소한의 구현을 제공해요. 이 글에서는 스토어를 쓰는 법과 svelte/store 의 핵심 함수들을 정리할게요.
기본 사용법
스토어 참조가 있으면, 컴포넌트 안에서 그 값에 $ 문자를 붙여 접근할 수 있어요. Svelte가 $ 접두사가 붙은 변수를 선언하고, 컴포넌트 초기화 시점에 스토어를 구독하며, 적절한 시점에 구독 해제까지 처리해 줘요.
$ 접두사가 붙은 변수에 할당하면 그 변수가 writable store여야 하고, 스토어의 .set 메서드를 호출하게 돼요. 스토어는 컴포넌트 최상위 레벨에 선언해야 하고, if 블록이나 함수 안에는 넣으면 안 돼요. 스토어가 아닌 로컬 변수에는 $ 접두사를 붙이면 안 돼요.
<script>
import { writable } from 'svelte/store';
const count = writable(0);
console.log($count); // logs 0
count.set(1);
console.log($count); // logs 1
$count = 2;
console.log($count); // logs 2
</script>
스토어를 써야 할 때
Svelte 5 이전에는 스토어가 컴포넌트 간 반응형 상태를 만들거나 로직을 분리하는 대표적인 수단이었어요. 룬이 등장하면서 이런 용도는 크게 줄었어요.
- 로직 추출: 룬의 보편적 반응성을 활용하는 게 더 좋아요. 컴포넌트 최상위뿐 아니라
.svelte.js/.svelte.ts파일에서도 룬을 쓸 수 있어요. - 공유 상태: 필요한 값들을 담은
$state객체를 만들고 그 상태를 조작하면 돼요.
/// file: state.svelte.js
export const userState = $state({
name: 'name',
/* ... */
});
<!--- file: App.svelte --->
<script>
import { userState } from './state.svelte.js';
</script>
<p>User name: {userState.name}</p>
<button onclick={() => {
userState.name = 'new name';
}}>
change name
</button>
스토어는 여전히 복잡한 비동기 데이터 흐름을 다루거나, 값을 갱신·변화를 듣는 과정을 수동으로 더 잘 제어해야 할 때 좋은 선택이에요. RxJS에 익숙하고 그 지식을 재사용하고 싶다면 $ 접두사가 유용하게 느껴질 거예요.
svelte/store
svelte/store 모듈은 store contract를 충족하는 최소한의 구현을 제공해요. 외부에서 갱신할 수 있는 스토어, 내부에서만 갱신할 수 있는 스토어, 그리고 스토어를 조합·파생하는 함수들을 담고 있어요.
writable
외부 컴포넌트에서 값을 설정할 수 있는 스토어를 만드는 함수예요. set과 update 메서드를 가진 객체로 생성돼요.
set— 인자 하나(설정할 값)를 받아요. 현재 값과 같지 않을 때만 스토어 값을 그 인자로 설정해요.update— 인자 하나(콜백)를 받아요. 콜백은 기존 스토어 값을 인자로 받고 새 값을 반환해요.
/// file: store.js
import { writable } from 'svelte/store';
const count = writable(0);
count.subscribe((value) => {
console.log(value);
}); // logs '0'
count.set(1); // logs '1'
count.update((n) => n + 1); // logs '2'
두 번째 인자로 함수를 넘기면, 구독자가 0에서 1로 늘어날 때(1에서 2로 늘 때는 아님) 그 함수가 호출돼요. 이 함수는 스토어 값을 바꾸는 set 과, 기존 값에서 새 값을 계산하는 update 를 받아요. 그리고 구독자 수가 1에서 0으로 줄 때 호출되는 stop 함수를 반환해야 해요.
/// file: store.js
import { writable } from 'svelte/store';
const count = writable(0, () => {
console.log('got a subscriber');
return () => console.log('no more subscribers');
});
count.set(1); // does nothing
const unsubscribe = count.subscribe((value) => {
console.log(value);
}); // logs 'got a subscriber', then '1'
unsubscribe(); // logs 'no more subscribers'
writable 값은 파괴되면(예: 페이지 새로고침) 사라져요. localStorage 같은 곳에 직접 동기화하는 로직을 짜면 돼요.
readable
외부에서 값을 설정할 수 없는 스토어를 만들어요. 첫 번째 인자가 초기값이고, 두 번째 인자는 writable 의 두 번째 인자와 같아요.
import { readable } from 'svelte/store';
const time = readable(new Date(), (set) => {
set(new Date());
const interval = setInterval(() => {
set(new Date());
}, 1000);
return () => clearInterval(interval);
});
const ticktock = readable('tick', (set, update) => {
const interval = setInterval(() => {
update((sound) => (sound === 'tick' ? 'tock' : 'tick'));
}, 1000);
return () => clearInterval(interval);
});
derived
하나 이상의 스토어에서 파생 스토어를 만들어요. 콜백은 첫 구독자가 구독할 때 처음 실행되고, 그 후로는 스토어 의존성이 바뀔 때마다 실행돼요.
가장 단순한 형태는 단일 스토어를 받아 파생 값을 반환하는 거예요.
import { derived } from 'svelte/store';
const doubled = derived(a, ($a) => $a * 2);
콜백은 두 번째 인자 set 과 선택적 세 번째 인자 update 를 받아 값을 비동기로 설정할 수 있어요. 이 경우 set/update 가 처음 호출되기 전까지 쓸 파생 스토어의 초기값을 세 번째 인자로 derived 에 넘길 수도 있어요. 초기값을 지정하지 않으면 undefined 예요.
import { derived } from 'svelte/store';
const delayed = derived(
a,
($a, set) => {
setTimeout(() => set($a), 1000);
},
2000
);
const delayedIncrement = derived(a, ($a, set, update) => {
set($a);
setTimeout(() => update((x) => x + 1), 1000);
// every time $a produces a value, this produces two
// values, $a immediately and then $a + 1 a second later
});
콜백에서 함수를 반환하면, a) 콜백이 다시 실행되거나 b) 마지막 구독자가 구독을 해제할 때 그 함수가 호출돼요.
import { derived } from 'svelte/store';
const tick = derived(
frequency,
($frequency, set) => {
const interval = setInterval(() => {
set(Date.now());
}, 1000 / $frequency);
return () => {
clearInterval(interval);
};
},
2000
);
두 경우 모두 첫 번째 인자로 단일 스토어 대신 배열을 넘길 수 있어요.
import { derived } from 'svelte/store';
const summed = derived([a, b], ([$a, $b]) => $a + $b);
const delayed = derived([a, b], ([$a, $b], set) => {
setTimeout(() => set($a + $b), 1000);
});
readonly
이 단순한 도우미 함수는 스토어를 읽기 전용으로 만들어요. 원본 스토어의 변화는 여전히 이 read-only 스토어로 구독할 수 있어요.
import { readonly, writable } from 'svelte/store';
const writableStore = writable(1);
const readableStore = readonly(writableStore);
readableStore.subscribe(console.log);
writableStore.set(2); // console: 2
// @errors: 2339
readableStore.set(2); // ERROR
get
일반적으로 스토어 값은 구독해서 시간에 따라 변하는 값을 읽는 게 좋아요. 가끔 구독하지 않은 스토어의 값을 한 번 꺼내야 할 때가 있는데, get 이 그 일을 해요.
⚠️
get은 구독을 만들고 값을 읽고 구독을 해제하는 방식으로 동작해요. 그래서 성능이 중요한 코드 경로에서는 권장하지 않아요.
import { get } from 'svelte/store';
const value = get(store);
Store contract
store = { subscribe: (subscription: (value: any) => void) => (() => void), set?: (value: any) => void }
svelte/store 에 의존하지 않고 직접 스토어를 만들 수도 있어요. store contract만 구현하면 돼요.
- 스토어는
.subscribe메서드를 가져야 하고, 그 인자로 구독 함수를 받아요..subscribe가 호출되면 이 구독 함수는 스토어의 현재 값과 함께 즉시·동기적으로 호출돼야 해요. 이후 스토어 값이 바뀔 때마다 활성 구독 함수들이 동기적으로 호출돼야 해요. .subscribe는 구독 해제 함수를 반환해야 해요. 구독 해제 함수를 호출하면 해당 구독은 중단되고, 그 구독 함수는 더 이상 호출되지 않아야 해요.- 스토어는 선택적으로
.set메서드를 가질 수 있어요..set은 스토어의 새 값을 인자로 받고, 활성 구독 함수를 모두 동기적으로 호출해요. 이런 스토어를 writable store 라고 불러요.
RxJS Observable과의 호환을 위해, .subscribe 는 구독 해제 함수 대신 .unsubscribe 메서드를 가진 객체를 반환해도 돼요. 다만 .subscribe 가 (Observable 스펙상 필수는 아닌) 동기 호출을 하지 않으면, 그때까지 Svelte는 스토어 값을 undefined 로 볼 수 있어요.
더 알아보기
- 공식 문서 (1차): Stores
- 관련 모듈: svelte/store
- 이어지는 개념: 반응형 상태($state) · 컨텍스트