ilokesto

통합

@ilokesto/store를 React, Vue, Svelte 같은 프레임워크 어댑터에 연결하는 방식입니다.

@ilokesto/store는 의도적으로 작게 유지됩니다. 상태 저장, 상태 교체, 구독은 맡지만 UI 런타임 자체를 책임지지는 않습니다. 그 사이를 메우는 것이 어댑터입니다.

스토어 기반 구축하기가 안정적인 계약을 설명하는 페이지라면, 이 페이지는 그 계약이 실제 프레임워크 API로 어떻게 번역되는지를 보여줍니다.

공통 어댑터 흐름

대부분의 프레임워크 통합은 같은 네 단계를 따릅니다.

  1. 스냅샷 읽기store.getState()로 현재 값을 읽습니다.
  2. 구독store.subscribe(listener)로 이후 변경을 듣습니다.
  3. 쓰기 브리지store.setState(next)를 그대로 노출하거나 액션으로 감쌉니다.
  4. 정리 — 프레임워크 범위가 끝날 때 unsubscribe 함수를 호출합니다.

높은 수준에서 보면 어댑터는 보통 이런 모양입니다.

import type { Store } from '@ilokesto/store';

function connectStore<T>(store: Store<T>) {
  let current = store.getState();

  const unsubscribe = store.subscribe(() => {
    current = store.getState();
  });

  return {
    getSnapshot: () => current,
    setState: (nextState: T | ((prevState: Readonly<T>) => T)) =>
      store.setState(nextState),
    destroy: unsubscribe,
  };
}

프레임워크마다 세부 구현은 달라지지만, 큰 흐름은 대체로 같습니다. 한 번 읽고, 구독하고, 쓰기 경로를 노출하고, 정리를 정확히 하는 방식입니다.

어댑터가 현재 살아 있는 스냅샷이 아니라 생성자에 전달했던 원래 값을 다시 써야 하는 경우에는 StoregetInitialState()를 사용할 수도 있습니다. 다만 일반적인 렌더링과 동기화 경로는 여전히 getState()를 기준으로 두는 편이 맞고, getInitialState()는 reset이나 재부트스트랩 흐름에 더 가깝습니다.

React

React는 외부 스토어를 위한 내장 모델인 useSyncExternalStore를 이미 제공합니다. 그래서 React 어댑터는 보통 두 가지를 중심으로 구성됩니다.

  • subscribe는 언제 변경이 일어났는지 React에 알려줍니다.
  • getSnapshot은 지금 렌더링해야 할 값을 React에 제공합니다.

최소 hook 형태:

import { useSyncExternalStore } from 'react';
import type { Store } from '@ilokesto/store';

export function useStore<T>(store: Store<T>) {
  return useSyncExternalStore(
    store.subscribe.bind(store),
    () => store.getState(),
    () => store.getState(),
  );
}

React에서 중요한 점은 다음과 같습니다.

  • 렌더 동기화: 렌더 중 안정적인 snapshot reader가 필요합니다.
  • 구독 소유권: 정리는 hook 생명주기 안에서 처리됩니다.
  • selector 계층: 더 풍부한 어댑터는 이 최소 구조 위에 selector를 얹는 경우가 많습니다.

즉 React는 외부 스토어를 위한 자리가 이미 있고, 어댑터는 Store를 그 자리에 맞게 끼워 넣는 역할을 합니다.

Vue

Vue는 useSyncExternalStore를 쓰지 않지만, 통합 아이디어 자체는 비슷합니다. 외부 스토어의 스냅샷을 Vue ref와 동기화하고, 컴포넌트 범위가 끝나면 정리합니다.

최소 composable 형태:

import { onUnmounted, shallowRef } from 'vue';
import type { Store } from '@ilokesto/store';

export function useStore<T>(store: Store<T>) {
  const state = shallowRef(store.getState());

  const unsubscribe = store.subscribe(() => {
    state.value = store.getState();
  });

  onUnmounted(unsubscribe);

  const setState = (nextState: T | ((prevState: Readonly<T>) => T)) => {
    store.setState(nextState);
  };

  return { state, setState };
}

Vue에서 중요한 점은 다음과 같습니다.

  • 반응성 브리지: 스토어 스냅샷을 Vue ref로 옮겨야 합니다.
  • 생명주기 정리: 컴포넌트 범위가 끝날 때 구독을 해제해야 합니다.
  • computed 계층: selector 역할은 ref 위에 computed()로 얹는 경우가 많습니다.

Vue에서는 “렌더에 직접 구독한다”기보다, “외부 상태를 Vue 반응성 그래프 안으로 흘려 넣는다”는 감각이 더 강합니다.

Svelte

Svelte 역시 작은 브리지가 필요하지만, 도착점의 모양이 다릅니다. hook이나 ref 대신, Svelte는 값 전달용 subscribe 메서드를 가진 자기만의 store contract를 선호합니다.

최소 wrapper 형태:

import { readable } from 'svelte/store';
import type { Store } from '@ilokesto/store';

export function fromStore<T>(store: Store<T>) {
  return readable(store.getState(), (set) => {
    set(store.getState());

    return store.subscribe(() => {
      set(store.getState());
    });
  });
}

Svelte에서 중요한 점은 다음과 같습니다.

  • store contract 브리지: Store<T>를 Svelte가 $ 문법으로 구독할 수 있는 형태로 바꿔야 합니다.
  • 자동 정리: Svelte는 자신의 store 사용 모델 안에서 구독 해제를 처리합니다.
  • 쓰기 노출 방식: 쓰기 함수는 readable store 위에 직접 올리기보다 별도 액션 helper로 두는 경우가 많습니다.

즉 가장 큰 차이는 소스 스토어가 아니라, 프레임워크가 기대하는 대상 계약이 무엇인가입니다.

Angular

Angular의 현재 반응성 모델은 signals를 중심으로 움직입니다. 그래서 Angular 어댑터는 hook보다는, 외부 스토어의 스냅샷을 signal()로 옮겨 Angular 템플릿과 computed 값이 읽을 수 있게 하는 브리지에 가깝습니다.

최소 형태:

import { computed, signal } from '@angular/core';
import type { Store } from '@ilokesto/store';

export function connectStore<T>(store: Store<T>) {
  const state = signal(store.getState());

  const unsubscribe = store.subscribe(() => {
    state.set(store.getState());
  });

  return {
    state,
    setState: (nextState: T | ((prevState: Readonly<T>) => T)) =>
      store.setState(nextState),
    destroy: unsubscribe,
  };
}

Angular에서 중요한 점은 다음과 같습니다.

  • signal 브리지: 스토어 스냅샷을 대개 signal로 옮기게 됩니다.
  • computed 계층: 파생 값은 그 signal 위에 computed()로 놓는 경우가 많습니다.
  • 생명주기 소유권: 어댑터를 소유한 Angular 범위가 끝날 때 정리가 일어나야 합니다.

즉 Angular는 감각상 Vue와 비슷하지만, 연결 어휘가 ref가 아니라 signal이라는 점이 다릅니다.

Solid

Solid는 본래 세밀한 반응성을 전제로 하기 때문에, 어댑터도 외부 스토어의 스냅샷을 createSignal() 쌍으로 옮기고 createMemo()로 파생 값을 만드는 경우가 많습니다.

최소 형태:

import { createMemo, createSignal, onCleanup } from 'solid-js';
import type { Store } from '@ilokesto/store';

export function useStore<T>(store: Store<T>) {
  const [state, setState] = createSignal(store.getState());

  const unsubscribe = store.subscribe(() => {
    setState(store.getState());
  });

  onCleanup(unsubscribe);

  return {
    state,
    setState: (nextState: T | ((prevState: Readonly<T>) => T)) =>
      store.setState(nextState),
    selected: createMemo(() => state()),
  };
}

Solid에서 중요한 점은 다음과 같습니다.

  • 세밀한 업데이트: 브리지가 signal을 채우면 이후 읽기는 더 세밀하게 유지됩니다.
  • memo 계층: selector는 별도 구독 시스템보다 createMemo()가 되는 경우가 많습니다.
  • 반응 범위 정리: 구독은 owner scope와 함께 끝나야 합니다.

Solid는 Vue 다음으로 가장 직접적인 축에 가깝습니다. 어댑터는 얇고, 이후 작업은 반응성 시스템이 맡습니다.

Preact

Preact는 두 가지 방향으로 연결할 수 있지만, 가장 직접적인 길은 React와 같은 external-store 모델을 쓰는 것입니다. preact/compatuseSyncExternalStore를 쓰면 같은 subscription-and-snapshot 구조를 거의 그대로 재사용할 수 있습니다.

최소 hook 형태:

import { useSyncExternalStore } from 'preact/compat';
import type { Store } from '@ilokesto/store';

export function useStore<T>(store: Store<T>) {
  return useSyncExternalStore(
    store.subscribe.bind(store),
    () => store.getState(),
    () => store.getState(),
  );
}

Preact에서 중요한 점은 다음과 같습니다.

  • external-store 호환성: 어댑터 모양이 React와 거의 같습니다.
  • 작은 wrapper 표면: React 사례를 이해했다면, Preact는 런타임만 바뀐 가까운 변형에 가깝습니다.
  • 대안으로서의 signal 경로: Preact Signals를 쓰고 있다면 hook 모델 대신 signal 브리지를 택할 수도 있습니다.

즉 Preact는 완전히 새로운 통합 모델이라기보다, React와 매우 가까운 형제 모델에 가깝습니다.

프레임워크마다 실제로 무엇이 다른가

공통 흐름은 같지만, 각 프레임워크는 브리지의 다른 부분을 더 강하게 요구합니다.

  • React는 렌더 시점의 snapshot 읽기와 external-store 동기화를 가장 중요하게 봅니다.
  • Vue는 외부 상태를 ref와 computed 값으로 옮기는 과정을 가장 중요하게 봅니다.
  • Svelte는 자기 store subscription contract에 맞는 형태로 바꾸는 것을 가장 중요하게 봅니다.
  • Angular는 외부 상태를 signals와 computed 값으로 흘려 넣는 과정을 가장 중요하게 봅니다.
  • Solid는 signals와 memos를 통해 세밀한 반응형 읽기를 유지하는 것을 가장 중요하게 봅니다.
  • Preact는 React와 같은 external-store 자리에 맞추되, 필요하면 signals 모델을 선택할 수 있다는 점이 중요합니다.

그래서 코어 Store API는 아주 작게 유지되더라도, 어댑터의 모양은 프레임워크마다 꽤 달라질 수 있습니다.

어떤 방식으로 통합할지 고르기

@ilokesto/store 위에 무언가를 만든다면 다음 기준이 유용합니다.

  • 바닐라 스토어를 source of truth로 유지하고,
  • 프레임워크 계층은 가능한 한 얇게 만들고,
  • selector, action, 사용성 helper는 raw bridge 바로 위 계층에 얹는 편이 좋습니다.

이 통합들이 의존하는 더 낮은 수준의 보장은 스토어 기반 구축하기, API 레퍼런스, 구독, 업데이트 시맨틱에서 설명합니다.

목차