커스텀 Store — 재사용 가능한 상태 로직 설계하기
Store를 그냥 쓰는 건 쉽습니다. 진짜 실력은 "잘 설계된 커스텀 Store"를 만드는 데서 나옵니다.
개념 정의
커스텀 Store 는 writable을 기반으로 도메인 로직을 캡슐화한 상태 관리 모듈입니다. subscribe 메서드를 가진 객체면 무엇이든 Svelte Store로 사용할 수 있습니다.
Store 프로토콜
Svelte에서 $ 자동 구독이 동작하려면 subscribe 메서드만 있으면 됩니다.
// 최소 Store 구현
function createMinimalStore(initial) {
let value = initial;
const subscribers = new Set();
return {
subscribe(fn) {
subscribers.add(fn);
fn(value); // 즉시 현재 값 전달
return () => subscribers.delete(fn);
},
set(newValue) {
value = newValue;
subscribers.forEach(fn => fn(value));
}
};
}
실전 패턴 — Todo Store
// stores/todoStore.js
import { writable, derived } from 'svelte/store';
function createTodoStore() {
const { subscribe, update, set } = writable([]);
return {
subscribe,
add(text) {
update(todos => [...todos, {
id: crypto.randomUUID(),
text,
done: false,
createdAt: new Date(),
}]);
},
remove(id) {
update(todos => todos.filter(t => t.id !== id));
},
toggle(id) {
update(todos => todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
},
clear() {
set([]);
},
clearCompleted() {
update(todos => todos.filter(t => !t.done));
},
};
}
export const todos = createTodoStore();
// 파생 스토어
export const activeTodos = derived(todos, $todos =>
$todos.filter(t => !t.done)
);
export const completedCount = derived(todos, $todos =>
$todos.filter(t => t.done).length
);
비동기 Store
// stores/apiStore.js
import { writable } from 'svelte/store';
function createAsyncStore(fetchFn) {
const { subscribe, set, update } = writable({
data: null,
loading: false,
error: null,
});
return {
subscribe,
async fetch(...args) {
update(s => ({ ...s, loading: true, error: null }));
try {
const data = await fetchFn(...args);
set({ data, loading: false, error: null });
return data;
} catch (error) {
update(s => ({ ...s, loading: false, error: error.message }));
throw error;
}
},
reset() {
set({ data: null, loading: false, error: null });
},
};
}
// 사용
export const usersStore = createAsyncStore(
() => fetch('/api/users').then(r => r.json())
);
localStorage 동기화 Store
// stores/persistentStore.js
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
function persistent(key, initialValue) {
const stored = browser ? localStorage.getItem(key) : null;
const initial = stored ? JSON.parse(stored) : initialValue;
const store = writable(initial);
if (browser) {
store.subscribe(value => {
localStorage.setItem(key, JSON.stringify(value));
});
}
return store;
}
export const theme = persistent('theme', 'light');
export const language = persistent('language', 'ko');
Undo/Redo Store
function createUndoStore(initialValue) {
const { subscribe, set } = writable(initialValue);
let history = [initialValue];
let index = 0;
return {
subscribe,
push(value) {
history = history.slice(0, index + 1);
history.push(value);
index++;
set(value);
},
undo() {
if (index > 0) {
index--;
set(history[index]);
}
},
redo() {
if (index < history.length - 1) {
index++;
set(history[index]);
}
},
get canUndo() { return index > 0; },
get canRedo() { return index < history.length - 1; },
};
}
면접 포인트
- "Store Contract란?": subscribe 메서드를 가진 객체면 Svelte Store로 동작합니다. RxJS Observable도 subscribe가 있으므로
$접두사로 자동 구독이 가능합니다. - "커스텀 Store의 이점은?": 상태 변경 로직을 캡슐화하여 컴포넌트에서 직접 상태를 조작하는 것을 방지합니다. 비즈니스 로직의 단일 책임 원칙을 적용할 수 있습니다.
정리
커스텀 Store는 "상태 + 로직"을 하나의 모듈로 묶는 패턴입니다. writable을 감싸서 도메인별 메서드를 노출하면, 컴포넌트는 "무엇을 할지"만 선택하고 "어떻게 할지"는 Store가 책임집니다.
댓글 로딩 중...