컴포넌트 트리 깊숙이 데이터를 전달해야 할 때, Props를 10단계나 내려보내는 건 고통입니다 — Store가 그 문제를 해결합니다.

개념 정의

Store 는 컴포넌트 외부에서 반응형 상태를 관리하는 메커니즘입니다. 여러 컴포넌트에서 공유해야 하는 데이터를 중앙에서 관리하며, 값이 변경되면 구독 중인 모든 컴포넌트가 자동 업데이트됩니다.

writable — 읽기/쓰기 스토어

JAVASCRIPT
// stores.js
import { writable } from 'svelte/store';

// 초기값을 인자로 전달
export const count = writable(0);
export const user = writable({ name: '', loggedIn: false });
export const theme = writable('light');
SVELTE
<!-- Counter.svelte -->
<script>
  import { count } from './stores.js';
</script>

<!-- $ 접두사로 자동 구독/해제 -->
<p>카운트: {$count}</p>
<button onclick={() => $count++}>증가</button>
<button onclick={() => count.set(0)}>초기화</button>
<button onclick={() => count.update(n => n + 10)}>+10</button>

Store API

JAVASCRIPT
const myStore = writable(initialValue);

// set — 값 교체
myStore.set(newValue);

// update — 현재 값을 기반으로 업데이트
myStore.update(currentValue => currentValue + 1);

// subscribe — 값 변경 구독 ($ 접두사 대신 수동 구독)
const unsubscribe = myStore.subscribe(value => {
  console.log('값 변경:', value);
});
// 구독 해제
unsubscribe();

readable — 읽기 전용 스토어

외부에서 값을 설정할 수 없고, 스토어 내부에서만 값을 변경할 수 있습니다.

JAVASCRIPT
// stores.js
import { readable } from 'svelte/store';

// 현재 시간을 1초마다 업데이트
export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  // 정리 함수 — 마지막 구독자가 해제될 때 호출
  return () => clearInterval(interval);
});

// 마우스 위치 추적
export const mousePosition = readable({ x: 0, y: 0 }, (set) => {
  function handleMove(e) {
    set({ x: e.clientX, y: e.clientY });
  }
  window.addEventListener('mousemove', handleMove);
  return () => window.removeEventListener('mousemove', handleMove);
});
SVELTE
<script>
  import { time, mousePosition } from './stores.js';
</script>

<p>현재 시간: {$time.toLocaleTimeString()}</p>
<p>마우스: ({$mousePosition.x}, {$mousePosition.y})</p>

derived — 파생 스토어

다른 스토어로부터 계산된 값을 제공합니다.

JAVASCRIPT
// stores.js
import { writable, derived } from 'svelte/store';

export const items = writable([
  { name: '사과', price: 1000, quantity: 3 },
  { name: '바나나', price: 500, quantity: 5 },
]);

// items가 변경되면 자동 재계산
export const totalPrice = derived(items, ($items) =>
  $items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const itemCount = derived(items, ($items) => $items.length);

// 여러 스토어 조합
export const firstName = writable('길동');
export const lastName = writable('홍');
export const fullName = derived(
  [firstName, lastName],
  ([$first, $last]) => `${$last}${$first}`
);

$ 자동 구독의 원리

Svelte 컴파일러는 $ 접두사가 붙은 스토어 변수를 자동으로 구독/해제 코드로 변환합니다.

SVELTE
<script>
  import { count } from './stores.js';

  // 컴파일러가 내부적으로 이렇게 변환:
  // let $count;
  // const unsubscribe = count.subscribe(v => $count = v);
  // onDestroy(unsubscribe);
</script>

<!-- $count를 쓰면 자동 구독됨 -->
<p>{$count}</p>

**주의 **: $ 접두사는 .svelte 파일 안에서만 동작합니다. 일반 .js 파일에서는 수동으로 subscribe()를 호출해야 합니다.

Svelte 5에서의 Store

Svelte 5에서는 $state.svelte.js 파일에서 사용할 수 있어, 전통적인 Store 대신 이 방식을 쓸 수도 있습니다.

JAVASCRIPT
// counter.svelte.js
let count = $state(0);

export function getCount() {
  return count;
}

export function increment() {
  count++;
}

export function reset() {
  count = 0;
}
SVELTE
<!-- App.svelte -->
<script>
  import { getCount, increment, reset } from './counter.svelte.js';
</script>

<p>{getCount()}</p>
<button onclick={increment}>증가</button>
<button onclick={reset}>초기화</button>

실전 패턴 — 인증 상태 관리

JAVASCRIPT
// authStore.js
import { writable, derived } from 'svelte/store';

function createAuthStore() {
  const { subscribe, set, update } = writable({
    user: null,
    token: null,
    loading: false,
  });

  return {
    subscribe,
    login: async (email, password) => {
      update(s => ({ ...s, loading: true }));
      try {
        const res = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify({ email, password }),
          headers: { 'Content-Type': 'application/json' },
        });
        const data = await res.json();
        set({ user: data.user, token: data.token, loading: false });
      } catch (e) {
        update(s => ({ ...s, loading: false }));
        throw e;
      }
    },
    logout: () => {
      set({ user: null, token: null, loading: false });
    },
  };
}

export const auth = createAuthStore();
export const isLoggedIn = derived(auth, ($auth) => !!$auth.user);
export const userName = derived(auth, ($auth) => $auth.user?.name ?? '게스트');

면접 포인트

  • "Store와 Context API의 차이는?": Store는 컴포넌트 트리와 무관하게 어디서든 import해서 사용할 수 있습니다. Context API는 컴포넌트 트리 내에서만 접근 가능하며, 서로 다른 트리에서 독립적인 인스턴스를 만들 수 있습니다.
  • "Svelte 5에서 Store가 여전히 필요한가요?": .svelte.js 파일에서 $state를 사용하면 Store 없이도 전역 상태를 관리할 수 있습니다. 하지만 Store는 subscribe 프로토콜을 제공해 외부 라이브러리와 호환성이 좋고, $ 자동 구독이 편리합니다.

정리

Store는 Svelte에서 컴포넌트를 넘어서는 상태를 관리하는 핵심 도구입니다. writable로 읽기/쓰기, readable로 읽기 전용, derived로 파생 값 — 이 세 가지를 조합하면 대부분의 상태 관리 시나리오를 커버할 수 있습니다.

댓글 로딩 중...