/ReactForge

Primary LanguageTypeScript

๐Ÿšง ReactForge

๐Ÿ“Œ ์•„์ง ์™„์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋งŒ๋“ค์–ด ๋ณด๊ณ  ์žˆ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค!

  • ์—”ํ‹ฐํ‹ฐ ๊ธฐ๋ฐ˜ ์ƒํƒœ๊ด€๋ฆฌ ๋„๊ตฌ
  • Top-down๊ณผ Bottom-up์˜ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ฐฉ์‹

Basic Example

// State
interface State {
  count:number
  doubledCount:number
}

// Action
interface Actions {
  INCREASE(by:number):void
  DECREASE(by:number):void
  RESET():void
}
// Store.md
export const useStore = createStore<State, Actions>(({store, reducer}) => {

  // Reducer
  store.count = reducer(0, (on) => {
    on.INCREASE((by) => (state) => (state.count += by))
    on.DECREASE((by) => (state) => (state.count -= by))
    on.RESET(() => (state) => (state.count = 0))
  })

  // Computed
  store.doubledCount = reducer((state) => state.count * 2)
})

You can use store in React.

// Component
function Counter() {
  const {dispatch, count, doubledCount} = useStore()

  const ์ฆ๊ฐ€ = () => dispatch.INCREASE(1)

  const ๊ฐ์†Œ = () => dispatch.DECREASE(1)

  const ์ดˆ๊ธฐํ™” = () => dispatch.RESET()

  return (
    <>
      <div>count is {count}</div>
      <div>doubledCount is {doubledCount}</div>
      <button onClick={์ฆ๊ฐ€}>+</button>
      <button onClick={๊ฐ์†Œ}>-</button>
      <button onClick={์ดˆ๊ธฐํ™”}>RESET</button>
    </>
  )
}

ComponentStore

Overview

ComponentStore is a modern state management library for React, designed to offer a more granular and flexible approach to managing state across components. It enables developers to create separate state management contexts for different parts of their application, reducing the complexity and enhancing the reusability of components.

Key Features

  • Separate State Contexts: Enables the creation of separate state contexts (Providers) for different components or component groups.
  • Reduced Props Drilling: By leveraging Providers, the need for prop drilling is significantly reduced, leading to cleaner and more maintainable code.
  • Enhanced Reusability: Components become more reusable and maintainable, as their state management is more self-contained.
  • Flexible State Sharing: Allows for flexible state sharing and interactions between different state contexts, making it suitable for complex state management scenarios.

Usage

Setting Up ComponentStore

  1. createComponentStore Manages the state of individual todo items.
interface Todo {
  id: string
  text: string
  completed: boolean
  creatorId: string
}

interface TodoExtra {
  creator?: User
  ์ˆ˜์ •๊ถŒํ•œ์ด_์žˆ๋Š”๊ฐ€: false
}

interface TodoActions {
  TOGGLE(): void
  SET_TEXT(text: string): void
}

export const [useTodo, TodoProvider, TodoRepo] = createComponentStore<Todo, TodoActions, TodoExtra>(({store: Todo, reducer, key}) => {
  // Todo.id = key

  Todo.text = reducer("", (on) => {
    on.SET_TEXT((text) => (state) => (state.text = text))
  })

  Todo.completed = reducer(false, (on) => {
    on.TOGGLE(() => (state) => (state.completed = !state.completed))
  })
})
  1. createStore: Manages the state of the entire todo list.
interface TodoApp {
  Todo: Record<PropertyKey, Todo>

  todos: Todo[]
  num_todos: number
  num_completed_todos: number
}

interface TodoAppActions {
  ADD_TODO(id: string, text: string): void
  REMOVE_TODO(id: string): void
}

export const useTodoApp = createStore<TodoApp, TodoAppActions>(({store, reducer}) => {
  // Repository
  store.Todo = reducer(TodoRepo, (on) => {
    on.ADD_TODO((id, text) => (state) => {
      state.Todo[id] = {id, text, completed: false, creatorId: "tmp"}
    })

    on.REMOVE_TODO((id) => (state) => {
      delete state.Todo[id]
    })
  })

  // computed value
  store.todos = reducer((state) => Object.values(state.Todo).filter(Boolean))

  store.num_todos = reducer((state) => state.todos.length)

  store.num_completed_todos = reducer((state) => state.todos.filter((todo) => todo.completed).length)
})

Implementing Components

  1. TodoList Component: Uses TodoListProvider to manage the list.
export default function TodoList() {
  const {todos, num_todos, dispatch} = useTodoApp()

  const generateUniqueId = () => Math.random().toString(36).slice(2)

  const addTodo = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.nativeEvent.isComposing) return
    if (e.key === "Enter") {
      const text = e.currentTarget.value
      const newId = generateUniqueId()
      dispatch.ADD_TODO(newId, text)

      e.currentTarget.value = ""
    }
  }

  return (
    <>
      <div>num_todos: {num_todos}</div>
      <input type="text" onKeyDownCapture={addTodo} />
      <ul>
        {todos.map((todo) => (
          // extra value๋“ค๋„ ๋„˜๊ธธ์ˆ˜ ์žˆ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค. index๊ฐ™์€...
          <TodoProvider key={todo.id} id={todo.id}>
            <TodoItem />
          </TodoProvider>
        ))}
      </ul>
    </>
  )
}
  1. TodoItem Component: Manages its own state using TodoProvider.
function TodoItem() {
  const {id, text, completed, dispatch} = useTodo()
  const app = useTodoApp()

  const toggleTodo = () => dispatch.TOGGLE()
  const removeTodo = () => app.dispatch.REMOVE_TODO(id)

  return (
    <li className="hbox pointer" style={{textDecoration: completed ? "line-through" : "none"}} onClickCapture={toggleTodo}>
      <div>
        {id} - {text}
      </div>
      <button onClickCapture={removeTodo}>์‚ญ์ œ</button>
    </li>
  )
}

/* ์—ฌ๊ธฐ์— TodoItem์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณต์žกํ•ด์ง€๋ฉด์„œ ๊ธฐ์กด์—๋Š” props-drill์ด ๋ฐœ์ƒํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์—์„œ๋Š” ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋Š” ๊ฒƒ์„ ํ†ตํ•ด์„œ ๋ทฐ ๋ณ€๊ฒฝ์˜ ์ž์œ ๋กœ์›€์„ ๋ณด์—ฌ์ฃผ๋Š” ๋‚ด์šฉ๊ณผ ์˜ˆ์‹œ๋ฅผ ์ถ”๊ฐ€ํ•˜์ž */

๊ธฐ์กด ๋ฐฉ์‹์˜ Props Drilling ๋ฌธ์ œ

  • Props๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ „๋‹ฌํ•˜๊ณ  ํŠนํžˆ Props Type ์ง€์ •์ด ๋„ˆ๋ฌด ๊ดด๋กญ๋‹ค.
  • ์ถ”ํ›„์— ๋””์ž์ธ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๋ณ€๊ฒฝ์ด ์–ด๋ ค์›Œ์ง.
interface TodoItem {
  id: string
  text: string
  completed: boolean
}

// TodoList ์ปดํฌ๋„ŒํŠธ
function TodoList() {
  const [todos, setTodos] = useState([{id: "1", text: "Learn React", completed: false}])

  const toggleTodo = (id: string) => {
    // ํˆฌ๋‘ ์•„์ดํ…œ ์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง
  }

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} />
      ))}
    </ul>
  )
}

// TodoItem์˜ Props ํƒ€์ž…
type TodoItemProps = {
   todo: TodoItem
   onToggle: (id: string) => void
}

// TodoItem ์ปดํฌ๋„ŒํŠธ
function TodoItem({todo, onToggle}: TodoItemProps) {
  return (
    <li>
      <TodoText text={todo.text} />
      <TodoCheckbox completed={todo.completed} onToggle={() => onToggle(todo.id)} />
    </li>
  )
}

// TodoText ์ปดํฌ๋„ŒํŠธ
function TodoText({text}: {text: string}) {
  return <span>{text}</span>
}

// TodoCheckbox ์ปดํฌ๋„ŒํŠธ
function TodoCheckbox({completed, onToggle}: {completed: boolean; onToggle: () => void}) {
  return <input type="checkbox" checked={completed} onChange={onToggle} />
}

export default TodoList

ComponentStore๋ฅผ ์‚ฌ์šฉํ•œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

ComponentStore๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ๊ฐ TodoItem ์ปดํฌ๋„ŒํŠธ๋Š” ์ž์ฒด์ ์œผ๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ๋งŽ์€ props๋ฅผ ์ „๋‹ฌ๋ฐ›์„ ํ•„์š”๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค.

// TodoItemStore ์„ค์ •
const [useTodo, TodoProvider, TodoRepo] = createComponentStore<...>(...)
const useTodoApp = createStore<...>(...)

// TodoList ์ปดํฌ๋„ŒํŠธ
function TodoList() {
  const {todos, dispatch} = useTodoApp()

  const addTodo = (text) => {
    const newId = generateUniqueId()
    dispatch.ADD_TODO(newId)
  }

  return (
    <>
      <input type="text" onKeyPress={(e) => e.key === "Enter" && addTodo(e.target.value)} />
      <ul>
        {todos.map((id) => (
          <TodoProvider key={id} id={id}>
            <TodoItem />
          </TodoProvider>
        ))}
      </ul>
    </>
  )
}

// TodoItem ์ปดํฌ๋„ŒํŠธ
function TodoItem() {
  return (
    <li>
      <TodoText />
      <TodoCheckbox />
    </li>
  )
}

// TodoText ์ปดํฌ๋„ŒํŠธ
function TodoText() {
  const {text} = useTodo()
  return <span>{text}</span>
}

// TodoCheckbox ์ปดํฌ๋„ŒํŠธ
function TodoCheckbox() {
  const {completed, dispatch} = useTodo()
  const toggleTodo = dispatch.TOGGLE_TODO()
  return <input type="checkbox" checked={completed} onChange={toggleTodo} />
}

์ด ์˜ˆ์ œ์—์„œ ComponentStore๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด TodoItem ๋‚ด๋ถ€์˜ TodoText์™€ TodoCheckbox ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ์ง์ ‘ props๋ฅผ ์ „๋‹ฌ๋ฐ›์ง€ ์•Š๊ณ ๋„ ํ•„์š”ํ•œ ์ƒํƒœ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด Props Drilling ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜๊ณ , ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ๊ฐ€ ๋” ๊ฐ„๊ฒฐํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.


Key Advantages

  • Granular State Management: The use of separate Providers for the todo list and individual todo items allows for more detailed and controlled state management.
  • Independent State Management: Each provider manages its own state independently, reducing inter-component dependencies and enhancing maintainability.
  • Flexible and Efficient State Interactions: The ability to have different state contexts interact with each other provides a powerful tool for managing complex state behaviors in large-scale applications.

In conclusion, ComponentStore provides an innovative approach to state management in React applications. It emphasizes modularity, reusability, and flexibility, making it an ideal choice for developers looking to streamline their state management practices in complex applications.


Core Concept

  • ์ •๋‹ต์ด ์žˆ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋˜์ž.
  • ๊ฐ•๋ ฅํ•œ ์ œํ•œ์„ ๊ฑธ๋ฉด ์ฝ”๋“œ๋Š” ํด๋ฆฐํ•ด์งˆ ์ˆ˜ ์žˆ๋‹ค.
  • ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์“ฐ๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ์ปจ๋ฒค์…˜์ด ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์ž.
  • ๋ˆ„๊ฐ€ ์ž‘์„ฑ์„ ํ•ด๋„ ํด๋ฆฐ์ฝ”๋“œ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋„๋ก ๋„›์ง€๋ฅผ ๋ฐœํœ˜
  • ๊ทธ๋ ‡์ง€๋งŒ Draftํ•œ ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„  ๊ฐœ๋ฐœ ํ›„ ๋ฆฌํŒฉํ† ๋ง๋„ ๊ฐ€๋Šฅํ•˜๊ฒŒ
  • ๋น ๋ฅธ ๊ฐœ๋ฐœ๋ณด๋‹ค ์ถ”์ ๊ณผ ๋””๋ฒ„๊น…์„ ๋” ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ•œ๋‹ค.
  • ๊ทธ๋ ‡๋‹ค๊ณ  ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ—ˆ๋“ค์ด ๋˜์–ด์„œ๋Š” ์•ˆ๋œ๋‹ค.

์›์น™

  • ํ™•์‹คํ•œ CQRS
  • ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ์ปจ์…‰(๋ถˆ๋ณ€์„ฑ, ๋‹จ๋ฐฉํ–ฅ)
  • state๋Š” Action์„ ํ†ตํ•ด์„œ๋งŒ ์ˆ˜์ •์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

More Real World App

export interface Todo {
  id:number
  text:string
  completed:boolean
}

export type VisibilityFilter = "SHOW_ALL"|"SHOW_COMPLETED"|"SHOW_ACTIVE"

export interface TodoState {
  Query: {
    todos:Todo[]
    filteredTodos:Todo[]
  }
  Todo: Record<string, Todo>
  
  visibilityFilter:VisibilityFilter
}

export interface TodoActions {
  ADD_TODO(text:string):void
  TOGGLE_TODO(id:number):void
  REMOVE_TODO(id:number):void

  SET_VISIBILITY_FILTER(filter:VisibilityFilter):void
}
export const useStore = createStore<TodoState, TodoActions>(({store, reducer}) => {

  store.Todo = reducer([], on => {
    on.ADD_TODO((text) => (state) => {
      const newTodo = {id: Date.now(), text, completed: false}
      state.Todo[id] = newTodo
    })

    on.TOGGLE_TODO((id) => (state) => {
      state.Todo[id].completed = !state.Todo[id].completed
    })

    on.REMOVE_TODO((id) => (state) => {
      delete state.Todo[id]
    })
  })

  store.Query.todos = reducer(state => Object.values(state.Todo))

  store.Query.filteredTodos = reducer(state => {
    const todos = state.Query.todos
    const visibilityFilter = state.visibilityFilter

    if (visibilityFilter === "SHOW_ACTIVE") {
      return todos.filter(todo => !todo.completed)
    }

    if (visibilityFilter === "SHOW_COMPLETED") {
      return todos.filter(todo => todo.completed)
    }

    return todos
  })

  store.visibilityFilter = reducer("SHOW_ALL", on => {
    on.SET_VISIBILITY_FILTER((filter) => (state) => state.visibilityFilter = filter)
  })
})

์ฃผ์š” ๊ฐœ๋…

  • State

  • Store

  • Action

  • Dispatch

  • On

  • Reducer(+Computed)

  • Draft

์˜๊ฐ์„ ๋ฐ›์€ ๊ฐœ๋…

  • CQRS
  • Redux(Single Source, Reducer)
  • NgRx(Effect)

ํŠน์ง•

๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ๊ณผ ์ž๋™์™„์„ฑ

  • StateForge๋Š” TypeScript์˜ ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐœ๋ฐœ ์ค‘์— ๋†’์€ ์ˆ˜์ค€์˜ ์ž๋™์™„์„ฑ ์ง€์›์„ ์ œ๊ณตํ•˜๋ฉฐ, ํƒ€์ž… ๊ด€๋ จ ์˜ค๋ฅ˜๋ฅผ ์‚ฌ์ „์— ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ํ•„์š”ํ•œ ์†์„ฑ์ด๋‚˜ ์•ก์…˜์„ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

์ตœ์†Œํ•œ์˜ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ

  • Redux์™€ ๊ฐ™์€ ๊ธฐ์กด์˜ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ๋งŽ์€ ์„ค์ •๊ณผ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. StateForge๋Š” ์ด๋Ÿฐ ๋ถ€๋ถ„์„ ๋Œ€ํญ ๊ฐ„์†Œํ™”ํ•˜์—ฌ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋” ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ๋ช‡ ์ค„์˜ ์ฝ”๋“œ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Redux Toolkit + Jotai + Zustand + Valtio = ?

  • StateForge๋Š” Redux Toolkit์˜ ์˜๊ฐ์„ ๋ฐ›์•„ ๋” ๋‚˜์€ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ StateForge๋Š” ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ ์ž๋™์™„์„ฑ์„ ๊ฐœ์„ ํ•˜์—ฌ Redux Toolkit์ด ์ œ๊ณตํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์ง๊ด€์ ์ธ API

  • StateForge์˜ API๋Š” ์ง๊ด€์ ์ด๋ฉฐ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. ์Šฌ๋ผ์ด์Šค ์ƒ์„ฑ, ์•ก์…˜ ์ •์˜, ์Šคํ† ์–ด ๊ตฌ์„ฑ ๋“ฑ์˜ ๊ณผ์ •์ด ๋‹จ์ˆœํ™”๋˜์–ด ์žˆ์–ด, ์ƒˆ๋กœ์šด ๊ฐœ๋ฐœ์ž๋„ ์‰ฝ๊ฒŒ ์ƒํƒœ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ์ดํ•ดํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

State & Action

  • Interface๋ฅผ ๋จผ์ € ์„ค๊ณ„ํ•˜๊ณ  ๊ฐœ๋ฐœํ•˜๋Š” ๋ฐฉ์‹
  • State์™€ Action์„ ๋ถ„๋ฆฌํ•ด์„œ ๊ฐœ๋ฐœํ•˜๊ธฐ ์‰ฝ๊ฒŒ! BDD, SDD
  • ์“ธ๋ฐ์—†์€ ActionType, ActionCreator ์ด๋Ÿฐ๊ฑฐ NoNo!
  • Proxy ๊ธฐ๋ฐ˜์œผ๋กœ ์“ธ๋ฐ์—†์ด ๋ถˆ๋ณ€์„ฑ์„ ์ง€ํ‚ค๊ธฐ ์œ„ํ•œ ์ฝ”๋”ฉ ํ•˜์ง€ ์•Š๋Š”๋‹ค.

Core Concept

Store

"Store"๋ผ๋Š” ์šฉ์–ด๋Š” ์ƒ์ (store)์—์„œ ์œ ๋ž˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒ์ ์ฒ˜๋Ÿผ ๋‹ค์–‘ํ•œ ๋ฌผ๊ฑด์„ ํ•œ ๊ณณ์— ๋ชจ์•„๋‘๊ณ  ํ•„์š”ํ•  ๋•Œ ๊บผ๋‚ด ์“ฐ๋Š” ๊ฒƒ๊ณผ ๋น„์Šทํ•˜๊ฒŒ, ์ƒํƒœ ๊ด€๋ฆฌ์—์„œ์˜ store๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ(State)๋ฅผ ํ•˜๋‚˜์˜ ์žฅ์†Œ์— ์ €์žฅํ•˜๊ณ  ํ•„์š”ํ•  ๋•Œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ ‘๊ทผํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ค‘์•™ ์ง‘์ค‘์‹ ๊ด€๋ฆฌ ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ์˜ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ณ , ์ƒํƒœ ๋ณ€ํ™”์— ๋Œ€ํ•œ ์ถ”์ ๊ณผ ๋””๋ฒ„๊น…์„ ์šฉ์ดํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ƒํƒœ๋ฅผ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•จ์œผ๋กœ์จ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๋ณด๋‹ค ๋ช…ํ™•ํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ , ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๋‹จ์ˆœํ™”ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

Store์˜ ์—ญํ• 

  • ์ƒํƒœ ๋ณด๊ด€: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ํ•˜๋‚˜์˜ ๊ฐ์ฒด๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒํƒœ ์ ‘๊ทผ: ์ปดํฌ๋„ŒํŠธ์—์„œ store์˜ ์ƒํƒœ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒํƒœ ๊ฐฑ์‹ : ์•ก์…˜์„ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ , ์ด์— ๋Œ€์‘ํ•˜๋Š” ๋ฆฌ๋“€์„œ๋กœ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ๊ตฌ๋… ๊ด€๋ฆฌ: ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์— ๋ณ€ํ™”๋ฅผ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.

Reducer

  • ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๋Š” ๋™์ž‘์„ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ
  • ex) turnOnLight() vs isLight = true
  • ๊ทธ๋Ÿฐ๋ฐ ํ”„๋กœ๊ทธ๋žจ์ด ๋ณต์žกํ•ด์ง€๋ฉด ๊ฐ’์œผ๋กœ๋งŒ ๊ธฐ์ˆ ํ•˜๋‹ค ์ด๊ฒŒ ์–ด๋–ค ๋™์ž‘์ธ์ง€ ์ดํ•ด๊ฐ€ ์–ด๋ ค์›Œ์ง„๋‹ค.
  • ์ง์ ‘ ๊ฐ’์„ ์ˆ˜์ •ํ•˜๋‹ค ๋ณด๋ฉด ์‹ค์ˆ˜๋ฅผ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.
  • "ํ•ด๋ฒ•: ์ƒํƒœ(State)์˜ ๋ณ€ํ™”๋ฅผ ์•ก์…˜๊ณผ ๋ฆฌ๋“€์„œ๋กœ ๋ถ„๋ฆฌํ•˜๊ธฐ"

์žฅ์ :

  • ๋ฐ์ดํ„ฐ์˜ ๋ณ€ํ™”์˜ ๊ธฐ์ˆ ์„ ๋ฐ์ดํ„ฐ scope๋‚ด์—์„œ ์ž‘์„ฑํ•ด์„œ ๋ฐ์ดํ„ฐ์˜ ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ”„๋กœ๊ทธ๋žจ์„ ๊ฐ’์˜ ๋ณ€ํ™”๊ฐ€ ์•„๋‹ˆ๋ผ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ๋น„์Šทํ•œ ํ˜•ํƒœ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

๋‹จ์ :

  • ์กฐ๊ธˆ ๋” ๊ท€์ฐฎ์•„์•ผ ํ•œ๋‹ค.
  • ๋ฌธ๋ฒ•์ด ๋ณต์žกํ•ด์ง„๋‹ค(?)
    • => ๊ฐ„๋‹จํ•˜๊ฒŒ on์ด๋ผ๋Š” helper๊ฐ์ฒด๋ฅผ ์ œ๊ณตํ•ด์„œ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ๋ฅผ ์ค„์˜€๋‹ค!
    • => immer์™€ ๊ฐ™์ด ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋‹จ์ˆœํ•œ ๋ฌธ๋ฒ•์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋‹ค.
store.Todo = reducer([], on => {
  on.ADD_TODO((text) => (state) => {
    const newTodo = {id: Date.now(), text, completed: false}
    state.Todo[id] = newTodo
  })

  on.TOGGLE_TODO((id) => (state) => {
    state.Todo[id].completed = !state.Todo[id].completed
  })

  on.REMOVE_TODO((id) => (state) => {
    delete state.Todo[id]
  })
})

์™œ Reducer์—์„œ state๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋‚˜์š”?

  • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋Š” ๋ถˆ๋ณ€์„ฑ์„ ์–ธ์–ด์ฐจ์›์—์„œ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ƒ๋‹นํžˆ ๋ถˆํŽธํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ReactForge์—์„œ๋Š” ์ž๋™์œผ๋กœ ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ค๋‹ˆ๋‹ค. (like Immer)
// ๊ฒฐ๊ตญ ์ˆœ์ˆ˜ํ•จ์ˆ˜ ํ˜•ํƒœ๋กœ ์ œ๊ณต๋œ๋‹ค. 
function createReducer(state, action, reducerFn) {
  const draft = clone(state) // ๊ตฌ์กฐ์  ๊ณต์œ ๋ฅผ ํ†ตํ•œ ํšจ๊ณผ์ ์ธ ๋ณต์‚ฌ
  const on = helper(action)
  reducerFn(on)(draft)
  return draft
}

classicReducer

๐Ÿค” (์ƒ์ƒ) classicReducer๋„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์šฉ์œผ๋กœ ์ œ๊ณตํ•ด ๋ณผ๊นŒ??

function todoReducer(state = {Todo:{}}, action) {
  
  switch (action.type) {
    case "ADD_TODO": {
      const {text} = action.payload
      const todo = {id: Date.now(), text, completed: false}
      return {...state, Todo: {...state.Todo, [todo.id]: todo}} 
    }

    case "TOGGLE_TODO": {
      const {id} = action.payload
      const completed = !state.Todo[id].completed
      return {...state, Todo: {...state.Todo, [id]: {...state.Todo[id], completed}}}
    }

    case "REMOVE_TODO": {
      const {id} = action.payload
      const {[id]: value, ...newTodo} = state.Todo;
      return {...state, Todo: newTodo};
      delete newState.Todo[id];
    }
  }
  
  return state
}

store.todo = classicReducer(TodoReducer)

On

// on.ACTION(params => (state) => state.value++))
on.INCREASE(by => (state) => state.count)

// on ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜๋ฉด, ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๋กœ 2๊ฐ€์ง€ ์•ก์…˜์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋‹ค.
on(on.INCREASE, on.DECREASE)((inc, dec) => (state) => {
  acount = inc ? inc : -dec
  state.count += amount
})
        
// on ํ•จ์ˆ˜์™€ store๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ํ•ด๋‹น ๊ฐ’์ด ๋ฐ”๋€”๋•Œ ์•ก์…˜์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.        
// like Rxjs's combineLastest
// each value changes, but every value is not undefined
on(store.account.id, store.User)((accountId, User) => (state) => {
  const user = User[accountId]
  if(!user) return
  state.account.name = user.name
})

// SUCCESS or FAILURE or onChange(store.User)
on(on.SUCCESS, on.FAILURE, store.User)((succ, fail, user) => (state) => {
  if (succ) state.ok = true
  else if (fail) state.ok = false
  else state.ok = !!user
})

Advanced

Advanced Action

Action Slot Pilling

interface Actions {
  ADD_TODO(text:string, id?:number):void
}

// action middleware
store.dispatch.ADD_TODO = (text:string, id:number = Date.now()) => {
  return [text, id]
}

store.Todo = reducer([], (on) => {
  on.ADD_TODO((text, id) => (state) => {
    state.Todo[id] = {id, text, complted: false}
  })

  /* Bad 
  on.ADD_TODO((text) => (state) => {
    // Date.now() is not pure!
    const newTodo = {id: Date.now(), text, completed: false}
    state.Todo[id] = newTodo
  })
  */
})


function Component() {
  const someHandler = (msg:string) => dispatch.ADD_TODO(msg)
  return <></>
}

Async Action

Promise๋ฅผ returnํ•˜๋ฉด SUCCESS, FAILTURE, SETTLED ์•ก์…˜์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜์–ด ํ˜ธ์ถœ๋œ๋‹ค.

// async action (promise)
store.dispatch.ADD_TODO_ASYNC = (text:string, id:string) => {
  return new Promise(resolve => setTimeout(() => resolve([text, id]), 1000)
}

store.Todo = reducer({}, on => {
  on.ADD_TODO_ASYNC.REQUEST(res => (state) => {
    /* ... */
  })

  on.ADD_TODO_ASYNC.SUCCESS(res => (state) => {
    /* ... */
  })

  on.ADD_TODO_ASYNC.FAILTURE(res => (state) => {
    /* ... */
  })

  on.ADD_TODOADD_TODO.COMPLETED(res => (state) => {
    /* ... */
  })
})

Mutation

์‹ค์ „ ์˜ˆ์ œ: API ์—ฐ๋™,

interface Todo {
  id:string
  text:string
  completed:boolean
}

interface Actions {
  ADD_TODO: {
    (text:string):(dispatch)=>Promise
    REQUEST(todo:Todo)
    SUCCESS(todo:Todo)
    FAILTURE(error:Error)
  }

  REMOVE_TODO: {
    
  }
}

store.dispatch.ADD_TODO = (text:string) => (dispatch) => {
  const id = Math.random().toString(36).slice(2)
  const newTodo = {id, text, completed: false}
  return dispatch.REQUEST(newTodo, api.POST("/todos")(newTodo).then(res => res.data))
}

store.dispatch.REMOVE_TODO = (id:string) => (dispatch) => {
  return dispatch.REQUEST(id, api.DELETE("/todos/:id")(id))
}


store.dispatch.REMOVE_TODO = mutation((id:string) => api.DELETE("/todos/:id")(id))

store.dispatch.ADD_TODO = mutation(
  (text) => {
    const id = Math.random().toString(36).slice(2)
    return {id, text, completed: false}
  },
  (newTodo) => api.POST("/todos")(newTodo)
)




store.dispatch.ADD_TODO = mutation()
        .onMutate(text => {
          const id = Math.random().toString(36).slice(2)
          return {id, text, completed: false}
        })
        .mutateFn(newTodo => api.POST("/todos")(newTodo))
        .onSuccess(() => invalidate("/todos/", id)
)




store.Todo = reducer([], (on, effect) => {
  
  on.ADD_TODO.REQUEST(newTodo => (state) => {
    state.Todo[id] = newTodo
  })

  on.ADD_TODO.SUCCESS((todo, context) => {
    delete state.Todo[context.id]
    state.Todo[todo.id] = todo
  })

  // request๋•Œ ์ปจํ…์ธ ๋Š” ์ž๋™์œผ๋กœ ๋ณต์›๋œ๋‹ค.
  on.ADD_TODO.FAILTURE((todo, context) => {
    /* TODO */
  })
  
  on.TOGGLE_TODO((id) => (state) => {
    state.Todo[id].completed = !state.Todo[id].completed
  })

  on.REMOVE_TODO.REQUEST((id) => (state) => {
    delete state.Todo[id]
  })
})
store.dispatch.REMOVE_TODO = (id:string) => (dispatch, transaction) => {
  return dispatch.REQUEST(id, transaction(state => {
    delete state.Todo[id]
    return api.DELETE("/todos/:id")(id)
  }))
}


๊ทธ๋ฐ–์— Action ํ™•์žฅ ์ƒ๊ฐ๊ฑฐ๋ฆฌ๋“ค..

  • .CANCELED: ์ง„ํ–‰ ์ค‘์ธ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ทจ์†Œํ•˜๋Š” ์•ก์…˜์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญ์„ ์ค‘๋‹จํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์ž‘์—…์œผ๋กœ ์ „ํ™˜ํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • .RETRY: ์‹คํŒจํ•œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋‹ค์‹œ ์‹œ๋„ํ•˜๋Š” ์•ก์…˜์ž…๋‹ˆ๋‹ค. FAILURE ํ›„์— ๋„คํŠธ์›Œํฌ ์ƒํƒœ๊ฐ€ ๊ฐœ์„ ๋˜๊ฑฐ๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ์ˆ˜์ •๋œ ๊ฒฝ์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • .THROTTLE / .DEBOUNCE: ์š”์ฒญ์˜ ๋นˆ๋„๋ฅผ ์กฐ์ ˆํ•˜๋Š” ์•ก์…˜์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž ์ž…๋ ฅ์— ๋”ฐ๋ฅธ ์ž๋™ ์™„์„ฑ ๊ธฐ๋Šฅ์—์„œ ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • .UPDATE: ์ง„ํ–‰ ์ค‘์ธ ๋น„๋™๊ธฐ ์ž‘์—…์— ๋Œ€ํ•œ ์ค‘๊ฐ„ ์—…๋ฐ์ดํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์•ก์…˜์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํŒŒ์ผ ์—…๋กœ๋“œ์˜ ์ง„ํ–‰ ์ƒํ™ฉ์„ ํ‘œ์‹œํ•  ๋•Œ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • .POLLING_START / ,POLLING_STOP: ์ •๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” ํด๋ง(polling) ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ ์ค‘๋‹จํ•˜๋Š” ์•ก์…˜์ž…๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๊ฐœ๋ฐœ ๋ฉ˜ํƒˆ ๋ชจ๋ธ

  1. ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” 1)๊ฐ’๊ณผ 2)dispatch๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.
    1. ๋ณ€ํ•˜๋Š” ๊ฐ’์„ value๋กœ ๋งŒ๋“ค๊ณ  State์— ๋“ฑ๋กํ•œ๋‹ค.
    2. ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ๊ทธ๋Œ€๋กœ dispatchํ•˜๊ณ  Action์„ ๋“ฑ๋กํ•œ๋‹ค.
  2. ํ•ด๋‹น Action์„ ํ•˜๊ณ  ๋‚˜๋ฉด ์–ด๋–ค ๊ฐ’์ด ๋ฐ”๋€Œ์–ด์•ผ ํ•˜๋Š”์ง€ ์ƒ๊ฐํ•ด๋ณธ๋‹ค.
    1. ๋ฐ”๋€Œ๋Š” ๊ฐ’์˜ reducer์— ๊ฐ€์„œ on.ACTION ์ดํ›„ ๊ฐ’์„ ๋ณ€ํ™” ์‹œํ‚จ๋‹ค.
  3. ์š”๊ตฌ์‚ฌํ•ญ์„ ์ƒ๊ฐํ•ด๋ณธ๋‹ค.
    1. ์–ด๋–ค ๊ฐ’์ด ๋ฐ”๋€Œ์–ด์•ผ ํ•˜๋Š”๊ฐ€?
    2. ๊ทธ ๊ฐ’์ด ๋ฐ”๋€Œ๊ธฐ ์œ„ํ•ด์„œ ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€?
    3. ์–ธ์ œ ๊ทธ๊ฐ’์ด ๋ฐ”๋€Œ์–ด์•ผ ํ•˜๋Š”๊ฐ€?
      1. ํ•ญ์ƒ ํŠน์ • ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋กœ ํ•„์š”ํ•˜๋‹ค๋ฉด on(store.data.path)๋ฅผ ์ด์šฉํ•œ๋‹ค.
      2. ํŠน์ • ์‹œ์ ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด disaptch.ACTION์„ ํ†ตํ•ด์„œ ํ•ด๊ฒฐํ•œ๋‹ค.

์ถ”๊ฐ€ ์˜ˆ์ •

  • ๋น„๋™๊ธฐ ์•ก์…˜ ์ฒ˜๋ฆฌ
  • ์ดํŽ™ํŠธ ์ฒ˜๋ฆฌ
  • ์ƒํƒœ ์ถ”์  ๋ฐ ๋””๋ฒ„๊น…
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ
  • ์ƒํƒœ๊ด€๋ฆฌ ๋ฉ˜ํƒˆ ๋ชจ๋ธ
  • ์กฐ๊ฑด๋ถ€ ์Šคํ† ๋ฆฌ
  • ์—”ํ‹ฐํ‹ฐ์™€ ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”(Normalize)
  • createComponentStore()
  • ๋“ฑ๋“ฑ...

Create API

๋ชฉํ‘œ

  • fetchXXX, getXXX, ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ์—†์• ๊ธฐ
  • d.ts ํŒŒ์ผ์—๋‹ค๊ฐ€ interface๋กœ ๋“ฑ๋กํ•˜๋ฉด ๋ฒˆ๋“ค์— ํฌํ•จ์ด ๋˜์ง€ ์•Š๋Š”๋‹ค.
  • proxy์™€ typescript๋ฅผ ํ†ตํ•ด ์ž๋™์™„์„ฑ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
  • ์ด ํ˜•์‹์„ ์Šค์›จ๊ฑฐ๋ฅผ ํ†ตํ•ด์„œ ์ž๋™์ƒ์„ฑ ํ•  ์ˆ˜ ์žˆ๋‹ค
type Response<State> = createResponse<{
  status:number,
  data:State
}>

interface API_Post {
  GET:{
    ["/posts/recommend"]():Response<{lastKey:string, list:Post[]}>
    ["/posts/:postId"](postId:string):Response<Post>
    ["/posts/:postId/comments"](postId:string, params?:unknown):Response<Comment[]>
  }
}

interface API_Calendar {
  GET:{
    ["/calendars"]():Response<Calendar[]>
    ["/calendars/:calendarId"](calendarId:string):Response<Calendar>
  }

  POST:{
    ["/calendars/:calendarId"](calendarId:string, body:Calendar, q:{lastKey:string}):Response<Calendar>
  }

  PUT:{
    ["/calendars/:calendarId"]():Response<Calendar>
  }
}

type API
  = API_Post
  & API_Calendar

export const api = createAPI<API>({
  baseURL: "https://example.com/api",
  fetchOptions: {
    /* @TODO: ์—ฌ๊ธฐ ํ—ค๋”์™€ ๋ณด์•ˆ ์ž‘์—… ์ถ”๊ฐ€ ๋˜์–ด์•ผ ํ•จ.*/
  }
})

API ์‚ฌ์šฉ๋ฐฉ๋ฒ•

// GET /posts/recommend
const res = await api.GET["/posts/recommend"]()
console.log(res.data.data.list)

// GET /posts/7yKG9ccNK82?lastKey=100
const res2 = await api.GET["/posts/:postId"]("7yKG9ccNK82", {lastKey:100})
console.log(res2)

// POST /calendars/7yKG9ccNK82?lastKey=100 body:{x:100}
const res3 = await api.POST["/calendars/:calendarId"]("7yKG9ccNK82", {x:100}, {lastKey:100})