빠른 시작

설치

npm
pnpm
yarn
bun
npm install @violetflux/kerros

Kerros는 npm, pnpm, Yarn, Bun에서 사용할 수 있으며 React 17, 18, 19를 지원합니다.

Store 만들기

어떤 custom Hook이든 createStore로 감싸면 컴포넌트 사이에서 상태를 공유할 수 있습니다.

import { createStore } from '@violetflux/kerros'
import { useState } from 'react'

interface Task {
  id: string
  title: string
}

function useTaskStoreValue() {
  const [tasks, setTasks] = useState<Task[]>([])

  const addTask = (task: Task) => {
    setTasks(v => [...v, task])
  }

  const finishTask = (taskId: string) => {
    setTasks(v => v.filter(task => task.id !== taskId))
  }

  return { tasks, addTask, finishTask }
}

export const [useTask, TaskProvider] = createStore(useTaskStoreValue)

createStore는 컴포넌트가 사용할 Hook과 대응하는 Provider를 반환합니다. Store 안에서는 useState, useReducer, Context, SDK Hook, custom Hook을 그대로 사용할 수 있습니다.

initializer는 useTaskStoreValue처럼 모듈 최상위의 이름 있는 Hook으로 유지하세요. 익명 initializer도 동작하지만 React Compiler의 infer 모드에서는 Hook으로 자동 컴파일되지 않습니다.

Provider 마운트하기

useTaskTaskProvider의 하위 컴포넌트에서만 호출할 수 있습니다.

function App() {
  return (
    <TaskProvider>
      <Header />
      <TaskList />
    </TaskProvider>
  )
}

Provider 밖에서 호출하면 Kerros가 명확한 오류를 던집니다.

컴포넌트에서 Store 사용하기

selector 객체에는 컴포넌트가 실제로 사용하는 필드만 넣습니다.

function TaskList() {
  const { tasks, finishTask } = useTask(s => ({
    tasks: s.tasks,
    finishTask: s.finishTask,
  }))

  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          {task.title}
          <button onClick={() => finishTask(task.id)}>완료</button>
        </li>
      ))}
    </ul>
  )
}

Kerros는 selector 객체의 최상위 필드를 얕게 비교합니다. 선택한 필드가 같다면 Store의 다른 업데이트로 TaskList가 다시 렌더링되지 않습니다. selector는 인라인으로 작성할 수 있고 useCallback이 필요하지 않습니다.

여러 Store 인스턴스

Provider를 마운트할 때마다 독립된 Store가 만들어집니다.

<TaskProvider>
  <h2>개인 작업</h2>
  <TaskList />
</TaskProvider>

<TaskProvider>
  <h2>팀 작업</h2>
  <TaskList />
</TaskProvider>

TaskList는 가장 가까운 Provider를 읽으며 데이터는 서로 영향을 주지 않습니다.

Store 간 의존성

한 Store 안에서 다른 Store를 직접 호출할 수 있습니다.

function useTaskStoreValue() {
  const { user } = useAccount(s => ({ user: s.user }))
  const [tasks, setTasks] = useState<Task[]>([])

  const addTask = (title: string) => {
    if (!user)
      return

    setTasks(v => [...v, {
      id: crypto.randomUUID(),
      title,
      assigneeId: user.id,
    }])
  }

  return { tasks, addTask }
}

export const [useTask, TaskProvider] = createStore(useTaskStoreValue)

의존되는 Provider를 바깥에 두고 의존성은 단방향으로 유지합니다.

<AccountProvider>
  <TaskProvider>
    <App />
  </TaskProvider>
</AccountProvider>

Provider에 props 전달하기

Provider props는 Store Hook의 인수로 전달됩니다.

function useCounterStoreValue({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount)
  return { count, setCount }
}

const [useCounter, CounterProvider] = createStore(useCounterStoreValue)
<CounterProvider initialCount={42}>
  <Counter />
</CounterProvider>

직접 사용해 보기

아래 카운터도 같은 createStore + Provider + selector API를 사용합니다.

다음으로 SelectorStore 조합을 읽어보세요.