Recoil과 Jotai는 Context와 Provider, 그리고 훅을 기반으로 가능한 작은 상태를 효율적으로 관리하는데 초점을 맞춘다.
반면 Zustand는 Redux와 비슷하게 하나의 큰 스토어를 기반으로 상태를 관리하는 라이브러리이다. 이 큰 스토어는 Context가 아니라 스토어가 가지는 클로저를 기반으로 생성되며, 이 스토어의 상태가 변경되면 이 상태를 구독하고 있는 컴포넌트에 전파하여 리렌더링을 알리는 방식이다.
1 .Recoil
- 리액트에서 훅의 개념으로 상태관리를 시작한 최초의 라이브러리 중 하나이며, 최소 상태 개념인 Atom을 처음 사용하였다.
- Recoil은 현재 시점에서 아직 1.0.0이 배포되지 않아 안정성이나 성능, 사용성 등을 보장하기는 쉽지 않다.
Recoil을 사용하기 위해서는 RecoilRoot를 앱의 최상단에 선언해 둬야 한다. 이는 Recoil에서 생성되는 상태값을 저장하기 위한 스토어를 생성하는 역할을 한다.
RecoilRoot로 감싸지 않은 컴포넌트에서는 스토어에 접근할 수 없다.
스토어 내부에는 스토어의 아이디 값을 가져올 수 잇는 함수인 getNextStoreId()와 스토어의 값을 가져오는 함수인 getState, 값을 수정하는 함수인 replaceState 등으로 이루어져 있다.
replaceState 내부에는 상태가 변할 때 이 변경된 상태를 하위 컴포넌트로 전파해 컴포넌트에 리렌더링을 일으키는 notifyComponents가 존재한다.
- atom은 다음과 같은 구조로 선언할 수 있다.
atom은 구별하는 식별자를 위해 key를 필수로 가지며, default는 atom의 초기값을 의미한다.
atom의 값을 컴포넌트에서 읽고 이 값의 변화에 따라 컴포넌트를 리렌더링하려면 다음의 두 훅 useRecoilValue와 useRecoilState을 사용한다.
- useRecoilValue는 atom의 값을 읽어오는 훅이다.
이 내부의 getLoadable 함수는 Recoil이 가지고 있는 상태값을 가지고 있는 클래스인 loadable을 반환하는 함수이다.
이 값을 이전값과 비교해 렌더링이 필요한지 확인하기 위해 렌더링을 일으키지 않으면서 값을 저장할 수 있는 ref에 매번 저장한다.
useEffect를 통해 recoilValue가 변경됐을 때 forceUpdate를 호출해 렌더링을 강제로 일으킨다.
- useRecoilState는 좀 더 useState와 유사하게 값을 가져오고, 또 이 값을 변경할 수도 있는 훅이다.
useRecoilState는 useState와 매우 유사한 구조로 작성되어 있으며, 현재 값을 가져오기 위해 이전에 작성한 훅인 useRecoilValue를 그대로 사용한다.
상태를 설정하는 훅으로는 useSetRecoilState 훅을 사용하는데, 이는 내부에서 먼저 스토어를 가져온 다음에 setRecoilValue를 호출해 값을 업데이트한다.
setRecoilValue 내부에서는 queueOrPerformStateUpdate 함수를 호출하여 상태를 업데이트 하거나 업데이트가 필요한 내용을 등록한다.
2. Jotai
- Jotai는 bottom-up 접근법을 취하고 있는데 이는, 작은 단위의 상태를 위로 전파할 수 있는 구조를 가지고 있음을 의미한다.
- 리액트 Context의 문제점인 불필요한 리렌더링이 일어난다는 문제를 해결하고자 설계되어 있으며, 추가적으로 메모이제이션이나 최적화를 거치지 않아도 리렌더링이 발생되지 않도록 설계되어 있다.
- atom은 Recoil과 마찬가지로 최소 단위의 상태를 의미한다. Recoil과는 다르게 atom 하나만으로도 상태를 만들 수도, 이에 파생된 상태를 만들 수도 있다.
Jotai는 atom을 생성할 때 별도의 key를 넘겨주지 않는다. 내부에 key의 변수가 존재하지만, 외부에서 받는 값은 아니며 단순히 toString()을 위한 용도로 한정된다.
또한 config라는 객체를 반환하는데, 여기에는 init, read, write만 존재하며 atom에 따로 상태를 저장하고 있지 않는다.
- useAtomValue 내부를 보자면, atom의 값은 store에 존재한다는 것을 알 수 있다. store에 객체 그 자체로 키를 활용해 값을 저장한다.
또한 내부에 renderIfChanged라는 함수를 통하여 리렌더링을 일으켜 atom의 값이 어디서 변경되더라도 useAtomValue로 값을 사용하는 쪽에서는 언제든 최신 값의 atom을 사용해 렌더링을 할 수 있게 된다.
리렌더링이 일어나는 경우는 첫째, 넘겨 받은 atom이 Reducer를 통해 스토어에 있는 atom과 달라지는 경우, 둘째, subscribe를 수행하고 있다가 어디선가 이 값이 달라지는 경우이다.
- useAtom은 useState와 동일한 형태의 배열을 반환한다. 첫째로 atom의 현재값인 useAtomValue 훅의 결과, 둘째로 useSetAtom 훅을 반환한다. 이는 atom을 수정할 수 있는 기능을 갖고 있다.
Recoil에서는 atom에서 파생된 값을 만들기 위해서는 selector가 필요했지만, Jotai에서는 selector가 없이도 atom 값에서 또 다른 파생된 상태를 만들 수 있다.
객체의 참조를 WeakMap에 보관해 해당 객체 자체가 변경되지 않는 한 별도의 키가 없이도 객체의 참조를 통해 값을 관리할 수 있다.
3. Zustand
- Zustand는 atom이 아닌 하나의 스토어를 중앙 집중형으로 활용해 이 스토어 내부에서 상태를 관리한다.
Zustand 내부에 store를 만드는 코드를 보자면, setState가 partial과 replace로 나눠져 있는데, partial은 state의 일부분만 변경하고 싶을 때 사용하고, replace는 state를 완전히 새로운 값으로 변경하고 싶을 때 사용한다.
subscirbe 함수는 listener를 등록하는데, listener는 마찬가지로 Set 형태로 선언되어 추가와 삭제, 그리고 중복 관리가 용이하게끔 설계되어 있다. 즉, 상태값이 변경될 때 리렌더링이 필요한 컴포넌트에 전파될 목적으로 만들어 진 것이다.
destroy는 listener를 초기화하는 역할을 한다.
createStore는 이러한 getState, setState, subscribe, destory를 반환한다.
- Zustand의 create를 사용해 스토어를 만들고, 반환 값으로 이 스토어를 컴포넌트 내부에서 사용할 수 있는 훅을 받아, 스토어 내부에 있는 getter와 setter 모두에 접근해 사용할 수 있게 되었다.
- 리액트 컴포넌트 외부에 store를 만드는 것도 다음과 같이 가능하다.
- createStore를 사용하여 리액트와 상관없는 바닐라 스토어를 만들 수 있으며, 이는 useStore 훅을 통해 리액트 컴포넌트 내부에서 사용할 수 있게 된다.
- Zustand는 많은 코드를 작성하지 않고도 빠르게 스토어를 만들고 사용할 수 있다는 장점이 있다.
- 이것은 Zustand 자체의 작은 라이브러리 크기도 큰 영향이 있다. (Recoil 79.1kB, Jotai 13.1kB, Zustand 2.9kB)