npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cbcruk/sync-store

v0.0.2

Published

framework-agnostic 싱글턴 store와 useSyncExternalStore 바인딩

Readme

@cbcruk/sync-store

프레임워크에 의존하지 않는 싱글턴 store core와, 그 위에 얇게 얹는 React useSyncExternalStore 레이어.

설치

pnpm add @cbcruk/sync-store

core(createStore, persist)는 의존성이 없고, useStore를 쓸 때만 react(peer dependency)가 필요합니다.

배경

최근 라이브러리들은 react-*로 core를 설계하지 않고, UI 프레임워크를 모르는 순수한 core(싱글턴 store)를 먼저 만든 뒤 useSyncExternalStore 같은 얇은 어댑터로 React에 연결하는 경우가 많습니다. 이렇게 하면:

  • core를 React 없이 테스트할 수 있고 (아래 createStore 테스트 참고)
  • 동일한 core를 React가 아닌 환경(바닐라 JS, 다른 프레임워크)에서도 재사용할 수 있으며
  • 구독/스냅샷 처리는 React가 공식 지원하는 useSyncExternalStore에 위임해 tearing 없이 안전합니다.

이 패키지는 그 패턴의 최소 구현입니다.

구성

| 파일 | 레이어 | 역할 | | ----------------- | ------------------------- | ------------------------------------------------------ | | create-store.ts | core (framework-agnostic) | getState / setState / subscribe 싱글턴 store | | persist.ts | core (framework-agnostic) | localStorage 등에 상태를 영속화하는 store 래퍼 | | use-store.ts | React | useSyncExternalStore 기반 바인딩 + selector 메모이즈 |

사용법

1. core: 싱글턴 store 정의

import { createStore } from '@cbcruk/sync-store'

interface CounterState {
  count: number
}

// 모듈 스코프에 한 번만 생성 → 앱 전역 싱글턴
export const counterStore = createStore<CounterState>({ count: 0 })

// 도메인 동작은 core 밖에서 정의해 store를 얇게 유지
export const counterActions = {
  increment: () => counterStore.setState((prev) => ({ count: prev.count + 1 })),
  reset: () => counterStore.setState(counterStore.getInitialState()),
}

createStore는 React를 전혀 모르므로 순수 함수처럼 단독으로 쓰거나 테스트할 수 있습니다.

counterStore.subscribe(() => console.log(counterStore.getState()))
counterActions.increment() // { count: 1 }

2. React: useStore로 바인딩

import { useStore } from '@cbcruk/sync-store'
import { counterStore, counterActions } from './counter-store'

function Counter() {
  // selector로 필요한 slice만 구독한다.
  const count = useStore(counterStore, (state) => state.count)

  return <button onClick={counterActions.increment}>{count}</button>
}

selector를 생략하면 전체 상태를 구독합니다.

const state = useStore(counterStore) // CounterState

selector와 리렌더 제어

selector가 매 렌더마다 새 참조를 만들어도, 이전에 선택한 값과 isEqual(기본 Object.is)로 비교해 동등하면 리렌더하지 않습니다.

// 세 번째 인자로 커스텀 비교 함수를 넘길 수 있다.
const ids = useStore(
  todoStore,
  (state) => state.todos.map((t) => t.id),
  (a, b) => a.length === b.length && a.every((id, i) => id === b[i]),
)

영속화: persist

createStore 위에 localStorage 영속화를 얹은 store를 만든다. React를 모르므로 core 그대로 쓸 수 있다.

import { persist } from '@cbcruk/sync-store'

// volume / isLooping만 'audio-storage' 키에 저장
const audioStore = persist(
  { volume: 1, isLooping: false, currentTime: 0 },
  {
    name: 'audio-storage',
    partialize: ({ volume, isLooping }) => ({ volume, isLooping }),
  },
)
  • 생성 시 저장된 값을 읽어 초기 상태에 병합(하이드레이트)한다.
  • 상태가 바뀔 때마다 partialize한 결과를 저장하되, 직렬화 결과가 이전과 같으면 쓰기를 건너뛴다. (위 예시에서 currentTime만 바뀌면 localStorage에 쓰지 않는다.)
  • version / migrate로 스키마 변경에 대응하고, storage에 접근할 수 없으면(SSR 등) no-op가 되어 안전하다.

API

createStore<T>(initialState: T): Store<T>

  • getState(): T — 현재 상태
  • getInitialState(): T — 생성 시점의 초기 상태 (reset 등에 사용)
  • setState(updater: T | ((prev: T) => T)): void — 상태 갱신. Object.is 기준으로 값이 같으면 알림을 건너뜀
  • subscribe(listener: () => void): () => void — 구독 등록, 해제 함수 반환

useStore<T>(store) / useStore<T, U>(store, selector, isEqual?)

useSyncExternalStore로 store를 구독한다. selector로 slice를 선택하고 isEqual로 리렌더 조건을 제어한다.

persist<T, P>(initialState, options): Store<T>

createStore와 동일한 Store<T>를 반환하되 저장소에 영속화한다. options: name(키), storage(기본 localStorage), partialize, merge, version, migrate.

storage는 PersistStorage(getItem / setItem, 선택 removeItem)를 만족하면 된다. persist는 읽기·쓰기만 하고 저장분을 지우지 않으므로 removeItem은 구현하지 않아도 된다. localStorage / sessionStorage는 그대로 넘길 수 있다.