React 상태 관리 Library에 대해 (feat. Context)

React 상태 관리 Library에 대해 (feat. Context)

요즘 React, Next에서 상태 관리를 위한 라이브러리나 Context API를 사용하는 것은 거의 필수적이다. 하지만 왜 Context API 대신 다른 상태 관리 라이브러리를 쓰는 지, 각 라이브러리가 어떠한 컨셉을 가지고 있는 지에 대해 정리해 본 적이 없어서 이번 기회에 정리해보려 한다.

Context API

Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props.

공식 문서를 보면 Context는 상위 컴포넌트에서 하위컴포넌트로 사용할 수 있는 정보를 전달해 주는 역할을 한다고 적혀있다.

정보를 전달하는 방법으로는 “props drilling”이 있을 텐데 Depth가 매우 깊은 컴포넌트면 props를 계속 하위로 넘겨줘야 한다. 하지만 Context를 사용하면 props drilling이 아니라 다른 방식으로 정보를 전달할 수 있다.

사용법

  1. Context를 생성하고 export한다.
  2. 자식 컴포넌트에 useContext(MyContext) 훅을 전달한다.
  3. 부모 컴포넌트에서 children<MyContext.Provider value={...}> 로 감싼다.

예시

전체 코드

// context.js
import { createContext, useContext, useMemo, useState } from "react";

const initialState = {
  name: "binary",
};

const MyContext = createContext(null);

export const MyProvider = ({ children }) => {
  const [state, setState] = useState(initialState);

  const value = useMemo(() => ({ state, setState }), [state, setState]);

  return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
};

export const useMyContext = () => {
  const context = useContext(MyContext);
  if (context === null) {
    throw new Error();
  }
  return context;
};
// App.js
import { useState } from "react";
import { MyProvider, useMyContext } from "./context";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <MyProvider>
        <MyComponent />
      </MyProvider>

      <button onClick={() => setCount(count + 1)}>count : {count}</button>
    </div>
  );
}

const MyComponent = () => {
  console.log("MyComponent render");
  const { state } = useMyContext();

  return (
    <div>
      <h2>name : {state.name}</h2>
    </div>
  );
};

위 코드에서 버튼을 누르면 App 컴포넌트가 리렌더링되는 것이기 때문에 당연히 자식 컴포넌트인 MyProvider , MyComponent 가 리렌더링된다.

이를 해결하려면 MyComponent 를 메모이제이션 해주면 될 것 같다! const MyMemoComponent = memo(MyComponent);

위 코드에서 이제 이름을 바꿀 수 있는 컴포넌트를 만들어 보자!

const NameChangeComponent = () => {
  console.log("NameChangeComponent render");
  const { setState } = useMyContext();

  return (
    <div>
      <button onClick={() => setState({ name: "진수" })}>Change Name</button>
    </div>
  );
};

위 컴포넌트를 MyProvider 하위로 추가하고 Change Name 버튼을 클릭하면 NameChangeComponent , MyComponent 둘 다 리렌더가 일어나는 것을 확인할 수 있다.

문제점

Context value를 메모이제이션해도 state 또는 setState 가 변경되면 value의 Reference 값이 바뀌기 때문에 useMyContext 를 사용하는 Component는 모두 리렌더가 일어나는 것이다.

리렌더를 회피하기 위해 아래처럼 Context를 분리할 수 있지만 이는 Provider Hell을 야기한다.

const Provider = ({ children }) => {
  const [state, setState] = useState('a');
  
  return (
    <Context1.Provider value={state}>
      <Context2.Provider value={setState}>
        {children}
      </Context2.Provider>
    </Context1.Provider>
  )
}

이러한 문제는 리액트 커뮤니티(RFC 등)에서도 활발하게 논의되고 있지만 아직 리액트에 반영되지는 않았다. (4년째..)

RFC: Context Selectors

https://github.com/reactjs/rfcs/pull/119

해결 방안

use-context-selector 는 Selector를 통해 값을 가져오는 방식으로 문제를 해결한다.

use-context-selector

React useContextSelector hook in userland. Latest version: 2.0.0, last published: a year ago. Start using use-context-selector in your project by running `npm i use-context-selector`. There are 237 other projects in the npm registry using use-context-selector.

https://www.npmjs.com/package/use-context-selector
use-context-selector

위 라이브러리와 Context API 사용하여 문제를 해결할 수도 있지만 같은 문제를 해결할 수 있으며 더 많은 기능을 제공하는 상태 관리 라이브러리를 사용하지 않을 이유는 없다.

전역 상태 관리 라이브러리

전역 상태 관리 라이브러리는 웹 애플리케이션에서 상태를 중앙에서 관리하기 위해 사용되는 도구이다. 여러 컴포넌트가 있고 이 컴포넌트 간에 데이터를 공유해야 할 때 전역 상태 관리 라이브러리가 유용하게 사용될 수 있다.

Trends

많은 라이브러리가 있지만 주류인 Flux, Atomic 패턴을 가진(혹은 유사한) 라이브러리에 대해 설명한다. (Proxy (Mobx, Valtio)는 다음 기회에..)

Flux (Redux, Zustand)

Flux 패턴은 2014년 페이스북 컨퍼런스에서 발표된 Architecture이다. 기존의 MVC 패턴은 데이터 흐름이 양방향이기 때문에 복잡성과 예측 불가능성이 증가하는 문제가 있었다. 이를 해결하기 위해 나온 Flux 패턴은 단방향 데이터 흐름을 도입하여 상태 관리를 간소화하고 예측할 수 있는 방식으로 처리할 수 있도록 설계되었다.

Flux 패턴은 아래와 같은 주요 개념으로 구성된다.

  • 액션(Action): 애플리케이션에서 발생하는 어떠한 사건이다.
    • 예를 들어 버튼 클릭, 입력값 변경 등이 액션에 해당한다.
  • 디스패처(Dispatcher): 액션을 전달받아 등록된 스토어에 액션을 분배하는 역할을 한다.
  • 스토어(Store): 애플리케이션의 상태를 저장하고 관리한다.
    • 변경된 상태를 뷰에 알린다.
    • 상태 변경에 대한 로직을 포함하고 있으며, 디스패처로부터 전달받은 액션을 처리한다.
  • 뷰(View): 사용자에게 데이터를 표시하고 사용자 입력을 받는 역할을 한다.
    • 뷰는 스토어의 상태를 구독하고, 상태가 변경될 때마다 업데이트된다.

Flux 패턴은 단방향 데이터 흐름으로써, 액션 → 디스패처 → 스토어 → 뷰의 순서로 데이터가 흘러간다. 이러한 방식은 상태 변화를 추적하고 예측할 수 있는 방식으로 처리할 수 있게 해준다.

Flux 패턴의 구현체인 Redux, Flux 패턴의 구현체는 아니지만 Redux와 유사한 목적을 가지는 Zustand는 하향식 (Top-Down) 접근법을 가지고 있다.

하향식 (Top-Down) 접근법 : 컴포넌트 외부에 Store가 상태를 가지고 컴포넌트에서 상태를 가져다 쓰는 방식

Redux

프론트엔드 개발자라면 한 번쯤은 본 영상이다. Redux는 기본적으로 위와 같이 단방향으로 데이터가 흐른다.

바로 Redux를 사용한 예제를 보겠다.

Counter 예제

코드

// slice.js
import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export const selectCount = (state) => state.counter.value

export default counterSlice.reducer

// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export default configureStore({
  reducer: {
    counter: counterReducer,
  },
});

// Counter.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');

  return (
    <div>
      <div>
        <button
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span>{count}</span>
        <button
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
      <div>
        <input
          value={incrementAmount}
          onChange={e => setIncrementAmount(e.target.value)}
        />
        <button
          onClick={() =>
            dispatch(incrementByAmount(Number(incrementAmount) || 0))
          }
        >
          Add Amount
        </button>
      </div>
    </div>
  );
}

Redux를 사용하는 Counter 관련 코드만 가져왔다. 그럼에도 여전히 코드(보일러 플레이트)는 많다. redux-toolkit 사용 전에는 reselect, immer 등도 추가로 사용해야 했다.. 그나마 redux-toolkit를 사용하며 많이 준거다. 여기에 비동기 통신을 위한 라이브러리 (redux-saga, rtk-query, redux-thunk)까지 덧붙이면 얼마나 코드가 많아질지 상상이 안 된다.

Zustand

A small, fast, and scalable bearbones state management solution.

공식 문서 첫 문장에서 “Zustand는 작고, 빠르며 확장할 수 있는 bearbones 상태 솔루션이다” 라고 말한다. Zustand는 독일어로 “상태”라는 뜻이고 Redux와 마찬가지로 특정 라이브러리에 종속되지 않아 여러 라이브러리, 프레임워크에서 사용할 수 있다.

Counter 예제 코드

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
	des: () => set((state) => ({ count: state.count - 1 })),
	incrementByAmount: (value) => set(() => ({ count: value })),
}))

function Counter() {
  const { count, inc, incrementByAmount } = useStore()
	const [incrementAmount, setIncrementAmount] = useState('2');

  return (
    <div>
      <div>
        <button
          onClick={inc}
        >
          +
        </button>
        <span>{count}</span>
        <button
          onClick={des}
        >
          -
        </button>
      </div>
      <div>
        <input
          value={incrementAmount}
          onChange={e => setIncrementAmount(e.target.value)}
        />
        <button
          onClick={incrementByAmount(Number(incrementAmount))}
        >
          Add Amount
        </button>
      </div>
    </div>
  );
}

Zustand는 Pub/Sub 모델을 기반으로 하며 스토어의 상태 변경이 일어날 때 해당 상태를 구독하는 리스너들에게 상태가 변경되었다고 전파한다.

Zustand의 Core 코드는 짧고 이해하기도 쉽다고 들었는데 한번 읽어보자!

타입들은 제거했다. 복잡해보일수도 있지만 listeners , setState , getState , subscribe , subscribeWithSelector 에 각각 포커스를 해가며 읽어보자

const listeners = new Set() : 구독하는 리스너를 자료구조 Set으로 관리한다.

const setState = (partial, replace) => {
  // partial이 함수면 함수형 업데이트, 함수가 아닌 경우 partial을 다음 State로 한다.
  const nextState = typeof partial === "function" ? partial(state) : partial;

  // Object.is로 기존 상태와 다음 상태의 얕은 비교를 수행한다.
  if (!Object.is(nextState, state)) {
    const previousState = state;
    // replace 값이 truthy인 경우, nextState를 그대로 할당하여 현재 상태를 교체한다.
    // replace 값이 falsy인 경우, state와 nextState를 합친 새로운 객체를 현재 상태로 교체한다.
    // replace 값이 undefined 또는 null인 경우, nextState가 object가 아니거나, null인 경우
    // nextState를 그대로 할당 현재 상태를 교체,
    // 그 외의 경우 state와 nextState를 합친 새로운 객체를 현재 상태로 교체한다.
    state =
      replace ?? (typeof nextState !== "object" || nextState === null)
        ? nextState
        : Object.assign({}, state, nextState);

    // Loop를 돌며, listener에 인자을 넣어준다.
    listeners.forEach((listener) => listener(state, previousState));
  }
};
const subscribe = (listener) => {
    // 상태를 구독하는 함수를 등록한다.
    listeners.add(listener)
    // Unsubscribe
    return () => listeners.delete(listener)
}

const getState = () => state : State를 반환한다.

const subscribeWithSelector = (fn) => (set, get, api) => {
  const origSubscribe = api.subscribe;
  api.subscribe = (selector, optListener, options) => {
    let listener = selector; // if no selector
    if (optListener) {
      const equalityFn = options?.equalityFn || Object.is;
      let currentSlice = selector(api.getState());
      listener = (state) => {
        const nextSlice = selector(state);
        if (!equalityFn(currentSlice, nextSlice)) {
          const previousSlice = currentSlice;
          optListener((currentSlice = nextSlice), previousSlice);
        }
      };
      if (options?.fireImmediately) {
        optListener(currentSlice, currentSlice);
      }
    }
    return origSubscribe(listener);
  };
  const initialState = fn(set, get, api);
  return initialState;
};

간단하게 설명하면 store를 만들 때 위 함수를 사용하면 어떠한 상태를 구독할 때 해당 상태의 이전, 이후 값을 비교하는 로직이 추가된다.

사용법

const store = create(
  subscribeWithSelector((set) => ({
    count: 0,
    text: "",
    inc: () => set((state) => ({ count: state.count + 1 })),
    setText: (text) => set({ text }),
  }))
);

store.subscribe((state) => console.log("State is changed: ", state));

store.subscribe(
  (state) => state.count,
  (count) => console.log("Count is changed: ", count)
);

store.subscribe(
  (state) => state.text,
  (text) => console.log("Text is changed: ", text)
);

store.getState().inc();
store.getState().setText('Changed');

리액트와 같이 사용하기

현재 최신버전 (v4.4.7)의 Zustand는 React의 useSyncExternalStore을 활용해 React Component에서 사용할 수 있는 방법을 제공한다.

import ReactExports from 'react'
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'
import { createStore } from './vanilla.ts'

const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports

let didWarnAboutEqualityFn = false

...

export function useStore<TState, StateSlice>(
  api: WithReact<StoreApi<TState>>,
  selector: (state: TState) => StateSlice = api.getState as any,
  equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
	...
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn,
  )
  return slice
}

const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  const api =
    typeof createState === 'function' ? createStore(createState) : createState

  const useBoundStore: any = (selector?: any, equalityFn?: any) =>
    useStore(api, selector, equalityFn)

  Object.assign(useBoundStore, api)

  return useBoundStore
}

...

이전 Zustand 버전에서는 useEffect, useLayoutEffect, useReducer, useRef 등 hooks를 활용해서 useStore 를 구현하였는데 최신버전은 React 18에서 사용할 수 있는 useSyncExternalStore 를 사용했다.

Counter 예제코드의 import { create } from 'zustand' 에서 createcreateImpl 에 해당한다. createStore 함수를 실행하고 useBoundStore 를 반환한다. (useBoundStore 가 상태 변화를 감지한다.)

Atomic (Recoil, Jotai)

Atomic 패턴은 어디서 기인한 것인지는 명확하지 않지만, 디자인 시스템에서의 Atomic 디자인, 함수형 언어 클로저의 Atom 패턴과 거의 동일하다고 생각될 수 있다.

Recoil의 Atom은 불변성과 원자성을 가진 상태 관리를 위한 데이터 구조이다.

Recoil은 아래와 같은 주요 개념으로 구성된다.

  • Atom : 상태의 단위이며, 업데이트와 구독이 가능하다. atom이 업데이트되면 각각 구독된 컴포넌트는 새로운 값을 반영하여 다시 렌더링 된다. atoms는 런타임에서 생성될 수도 있다. Atoms는 React의 로컬 컴포넌트의 상태 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다. atom의 키 값은 전역적으로 고유해야 한다. (Jotai는 별도 키가 필요 없다.)
  • Selectors : atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)다. 상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다. 컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링 된다. Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다.

Atom 패턴을 가지고 있는 Recoil, Jotai는 상향식 (Bottom-Up) 접근법을 가지고 있다.

상향식 (Bottom-Up) 접근법 : 상태를 원자 단위로 가지며, 상태와 셀렉터들을 조합하여 상태를 확장하는 방식

Recoil

Recoil 현재 마지막 업데이트가 된 지 약 9개월이며 Major 버전이 0이다. 또한 다음에서 소개할 Jotai와 컨셉, 사용법이 매우 유사하기 때문에 Recoil은 다루지 않겠다.

Jotai

Primitive and flexible state management for React

소개처럼 Zustand, Redux와 달리 Jotai는 리액트를 위해 만들어진 라이브러리이다. Recoil의 Atomic 패턴에 영감을 받아 만든 라이브러리로 컨셉과 사용법이 매우 유사하다.

한 가지 특이한 점은 Zustand, Valtio 등을 만든 개발자가 Jotai를 만들었다는 것이다. 그래서 코드를 보면 폴더 구조 등이 유사한 것을 알 수 있다.

아래 링크를 보면 해당 그룹에서 react-spring, react-three-fiber 등을 만들었다. 이 그룹을 팔로잉하면 현재 개발 트렌드 등을 알 수 있을 것이다.

Poimandres

Open source developer collective. Poimandres has 83 repositories available. Follow their code on GitHub.

https://github.com/pmndrs
Poimandres

Counter 예제 코드

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return (
    <h1>
      {count}
      <button onClick={() => setCount(c => c + 1)}>one up</button>
    </h1>);
}

Jotai는 리액트를 위한 라이브러리이지만 atom과 store는 vanilla로 작성되어 있다. 또한 Zustand와 마찬가지로 Pub/Sub 모델을 가지고 있다.

Jotai 내부 코드를 보며 이해해 보자!

atom

function atom(
    read,
    write,
  ) {
    const key = `atom${++keyCount}`
    const config = {
      toString: () => key,
    }
    if (typeof read === 'function') {
      config.read = read
    } else {
      config.init = read
      config.read = function (get) {
        return get(this)
      }
      config.write = function (
        this,
        get,
        set,
        arg,
      ) {
        return set(
          this,
          typeof arg === 'function'
            ? (arg)(get(this))
            : arg,
        )
      }
    }
    if (write) {
      config.write = write
    }
    return config
  }

atom 함수가 반환하는 configread, write, init 함수를 가지고 있다. 또한 atom이 생성될 때마다 keyCount 를 늘려주면서 키 값이 중복되지 않게 한다.

read : atom 을 읽을 때마다 평가되는 함수다. Dependency를 추적해서 atom 에 대해 한 번이라도 get 을 호출했다면 atom 값이 변경될 때마다 read 는 평가된다.

write : useAtom[, set] 에서 set이 호출될 때마다 write가 실행된다. atom 값을 바꾸는 역할을 한다.

Provider

The Provider component is to provide state for a component sub tree. Multiple Providers can be used for multiple subtrees, and they can even be nested. This works just like React Context.

원문 그대로 쉽게 받아들일 수 있다. Provider 가 있다면 하나의 store 가지고 되는 셈이다. 없다면 Jotai에서 제공하는 기본 store를 사용한다.

store

Jotai의 Core(store) 코드는 너무 길어서 생략하고 간단히 설명한다.

Zustand의 createStoreImpl 의 반환값과 동일하게 Jotai도 createStoreget, set, sub 를 반환한다.

const createStore = () => {

	...

	return {
	    get: readAtom, // atom의 값을 읽어온다.
	    set: writeAtom, // atom의 값을 변경한다.
	    sub: subscribeAtom,
	  }
}

subscribeAtom

const subscribeAtom = (atom, listener) => {
    const mounted = addAtom(atom);
		const flushed = flushPending();
    const listeners = mounted.l;
    listeners.add(listener);

    return () => {
      listeners.delete(listener);
      delAtom(atom);
    };
  };

const flushPending = () => {
    ...

    mounted.l.forEach((listener) => listener())

    ...
}

어디서 많이 본 패턴이다. 바로 Zustand에서 사용하는 구독 모델과 거의 유사하다.

리액트와 같이 사용하기

export function useAtom(
  atom,
  options,
) {
  return [
    useAtomValue(atom, options),
    // We do wrong type assertion here, which results in throwing an error.
    useSetAtom(atom),
  ]
}

밥 먹듯 사용하는 Jotai의 useAtomuseAtomValue, useSetAtom 을 반환한다.

export const useAtomValue(atom, options) {
	const store = useStore(options)

	...

	const [[valueFromReducer, storeFromReducer, atomFromReducer], rerender] =
	    useReducer(
	      (prev) => {
	        const nextValue = store.get(atom)
	        if (
	          Object.is(prev[0], nextValue) &&
	          prev[1] === store &&
	          prev[2] === atom
	        ) {
	          return prev
	        }
	        return [nextValue, store, atom]
	      },
	      undefined,
	      () => [store.get(atom), store, atom]
	    )

	let value = valueFromReducer;

	value = store.get(atom)

	...

	useEffect(() => {
	    const unsub = store.sub(atom, () => {
	      if (typeof delay === 'number') {
	        // delay rerendering to wait a promise possibly to resolve
	        setTimeout(rerender, delay)
	        return
	      }
	      rerender()
	    })
	    rerender()
	    return unsub
	  }, [store, atom, delay])

	return isPromiseLike(value) ? use(value) : (value)
}

createStore 의 반환 값인 get, sub 를 사용하는 것을 알 수 있다.

추가로 store.sub 으로 atom 를 구독하며 변경되면 rerender() 을 호출하는 것을 볼 수 있다.

2) useEffect Execute and Subscribe Store”가 위 코드의 useEffect 절에 해당하는 것을 알 수 있다.

export function useSetAtom(
  atom,
  options,
) {
  const store = useStore(options)
  const setAtom = useCallback(
    (...args: Args) => {
      return store.set(atom, ...args)
    },
    [store, atom],
  )
  return setAtom
}

예상했듯이 useSetAtomcreateStoreset 을 사용한다.

마지막으로 atom, Provider, Store, Component 간의 연관 관계를 보여주는 그림이다.

정리

짧게 쓰려고 했는데 코어한 부분까지 보려고 하다 보니 생각보다 길어졌다.. 개인적으로는 상태를 원자 단위로 관리하며 그 원자들을 조합하며 확장하는 방식에 좀 더 공감하지만, “무엇이 더 우월하다”라고 말하기는 힘든 것 같다. 자신(팀)의 현재 상황, 프로젝트의 구조, 규모 등 상황에 따라 적용하면 좋을 상태 관리 라이브러리는 달라진다고 생각한다.

Reference

Passing Data Deeply with Context – React

The library for web and native user interfaces

https://react.dev/learn/passing-data-deeply-with-context
Passing Data Deeply with Context – React

Context API의 최대 단점은 무엇일까

이번 시간에는 Context API에 대해 좀 더 자세히 알아보고 왜 Context API 대신 Redux나 Recoil 같은 전역 상태 라이브러리를 많이 사용하는지 알아보도록 하겠습니다.

https://velog.io/@ckstn0777/Context-API의-최대-단점은-무엇일까#context-api의-최대-단점
Context API의 최대 단점은 무엇일까

Quick Start | Redux Toolkit

&nbsp;

https://redux-toolkit.js.org/tutorials/quick-start
Quick Start | Redux Toolkit

React 상태 관리 라이브러리 Zustand의 코드를 파헤쳐보자

TOAST UI Calendar의 새로운 상태 관리 방법을 도입하기 위해 참고한 라이브러리 Zustand의 코드를 분석해본다.

https://ui.toast.com/weekly-pick/ko_20210812
React 상태 관리 라이브러리 Zustand의 코드를 파헤쳐보자

주요 개념 | Recoil

개요

https://recoiljs.org/ko/docs/introduction/core-concepts
주요 개념 | Recoil

Recoil을 이용한 손쉬운 상태관리

Recoil의 장단점과 함께 효과적으로 상태관리 할 수 있는 방법을 소개하려고 합니다!

https://techblog.yogiyo.co.kr/recoil을-이용한-손쉬운-상태관리-b70b32650582
Recoil을 이용한 손쉬운 상태관리

Documentation — Jotai

Table of contents

https://jotai.org/docs/introduction
Documentation — Jotai

Atomic state management - Jotai – 화해 블로그 | 기술 블로그

상태 관리 jotai 과거에는 상태 관리를 위해 당연하듯 Redux를 고려했었는데 리액트 v16.8의 hook의 등장 이후로 많은 선택지가 생겨나게 되었고 이에 따라 저희 역시 새로운 시도에 대한 고민을 해왔습니다. 자료를 조사해가던 중 Jotai라는 상태 관리를 언급한 문장을 보았습니다.

https://blog.hwahae.co.kr/all/tech/tech-tech/6099
Atomic state management - Jotai – 화해 블로그 | 기술 블로그

seungahhong.github.io

https://seungahhong.github.io/blog/2023/09/2023-09-10-react-state/

zustand/src/vanilla.ts at main · pmndrs/zustand

🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.

https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts
zustand/src/vanilla.ts at main · pmndrs/zustand

jotai/src/vanilla/store.ts at main · pmndrs/jotai

👻 Primitive and flexible state management for React - pmndrs/jotai

https://github.com/pmndrs/jotai/blob/main/src/vanilla/store.ts
jotai/src/vanilla/store.ts at main · pmndrs/jotai