React에서 가장 많이 쓰는 전역 상태관리 프레임워크를 삼대장을 꼽자면,
한국에서 가장 많이 쓰는 Redux,
한국에서 한참 뜨고 있는 Recoil,
그리고, 한국에서 최근 주목받기 시작한 Zustand가 있다.
이 중, Zustand는 가벼우면서도 강력한 확장성으로 요즘 주목받고 있는 프레임워크다.
오늘은 이 Zustand를 만든 모임, Poimandres 에서 만드는 리액트 상태관리 라이브러리를 알아보고자 한다.
이들이 만드는 리액트 상태관리는 기본적으로 아래 공통점을 가지고 있다.
그렇기 때문에, Astro 같이 여러 프론트엔드 기술 짬뽕으로 섞어먹는 아일랜드 식탁같은 프레임워크에서도 쓸 수 있다는 장점을 가지고 있다.
이 모임을 이끌고 있는 @dai-shi 라는 일본인이 있다. (본명은 링크에 있으니 봐라)
이사람이 누구냐 하면, 공식적인 리액트 상태관리 시스템에 적극적으로 참여하고 있는 한 사람일 뿐이다.
하지만, 이사람이 리액트의 답답한 기존 상태관리 방식을 벗어나게 해준 인물임에는 확실하다.
또한, 리액트 상태관리의 희망이자 미래인 use
함수도 참여하고 있다.
물론 이 외에도 많은 사람들이 연구하고 테스트하고 개선해 나가고 있기 때문에, 꼭 이사람 손에 달린 문제는 아니다.
근데 왜 일본은 리액트보다 뷰의 사용률이 높은지는 그냥 시장에 맡기고 넘어가자.
이 사람은 Poimandres 모임에서 아래 3개의 상태관리 프레임워크를 주도하고 있다.
오늘은 이 주요 3개의 상태관리 라이브러리를 알아보도록 하자.
@dai-shi 가 주도하는 리액트 상태관리 라이브러리 중 가장 많은 사용률, 그리고 Redux 대체제로 주목받고 많이 쓰기 시작한 라이브러리가 바로 이놈이다.
내가 전에 리액트는 Redux, Vue는 vuex 싫어했는데, 이유는 그냥 간단하다.
차라리 뷰의 경우 추적이 어렵지만 의외로 가벼운 이벤트 버스가 오히려 선녀스러운 느낌이었다.
하지만, Zustand는 그냥 전략 구성하고, 갖다 쓰면 끝이다.
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
함수를 통해 현재값을 받기 때문에, 반환 타입을 객체나 배열 등 마음껏 꾸밀 수 있으니 입맛대로 갖다 쓰면 된다.
이 라이브러리는 한국에서 좀 쓰다 보니 검색하면 메뉴얼 찾을 수 있기 때문에 귀찮아서 여기까지만 쓰겠다.
죠타이라고 읽고, 状態 한자를 일본어 음차로 발음하며, '상태(狀態)'의 일본식 한자어를 가진 라이브러리다.
ㅇㅇ 니가 아는 그 '상태' 맞아.
이녀석은 원자적(Atomic) 성격을 가진 상태관리 라이브러리다.
백문이 불여일견, 상태 정의하는 법을 보도록 하자.
import { atom } from 'jotai'
const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const mangaAtom = atom({ 'Dragon Ball': 1984, 'One Piece': 1997, Naruto: 1999 })
대충 봐도 어떻게 쓰는지 감이 올 것이다.
사용법은 이렇다.
import { useAtom } from 'jotai'
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<h1>
{count}
<button onClick={() => setCount((c) => c + 1)}>one up</button>
</h1>
)
그냥 useState
의 전역 버전이라 생각하면 될 것이다.
더 재밌는 점은 비동기를 지원하는데, 그것도 모자라 <Suspense>
를 고려한 설계로 상당히 리액트스러운 컴포넌트를 만들 수 있다는 것이다.
const urlAtom = atom('https://json.host.com')
const fetchUrlAtom = atom(async (get) => {
const response = await fetch(get(urlAtom))
return await response.json()
})
function StatusText() {
// Re-renders the component after urlAtom is changed and the async function above concludes
const [json] = useAtom(fetchUrlAtom)
return <p>{JSON.stringify(json)}</p>
}
function Status() {
return (
<div>
<Suspense fallback={'Loading...'}>
<StatusText />
</Suspense>
</div>
)
}
만약 간단하고 소규모로 전역 이하의 상태 관리가 필요하다면 이녀석을 고려해봄직 하다.
너희들 Proxy
객체 아는가? Vue 쓰는 개발자라면 그나마 익숙할 것이다. 모르면 링크 가서 뭐하는 놈인지 보도록.
어쨋든, 이녀석은 Proxy 객체에 리액트 상태관리를 입히는 것을 도와주어 가장 유연한 상태관리를 제공하는 라이브러리라 하겠다.
먼저, Proxy
객체를 래핑한다.
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
리액트 외에서도 언제든 프록시답게 조작이 가능하다.
setInterval(() => {
++state.count
}, 1000)
리액트에서는 이렇게 쓰면 되겠다.
// `state.count` 속성이 바뀌면 재렌더링 하지만, `state.text` 속성은 여기에 안 쓰기 때문에 재렌더링하지 않는다.
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
주석은 공식 문서를 번역한 것인데, 리액트 내에서 갖다 쓰지 않은 속성값이 바뀌어도 재렌더링하지 않는다는 특징은 이건 진짜 꿈의 상태관리 그 자체 아닌가 싶다.
게다가, 비동기도 지원하며, jotai와 마찬가지로 <Suspense>
호환이 가능하여 리액트스러운 비동기 컴포넌트를 설계할 수 있다.
const state = proxy({ post: fetch(url).then((res) => res.json()) })
function Post() {
const snap = useSnapshot(state)
return <div>{snap.post.title}</div>
}
function App() {
return (
<Suspense fallback={<span>waiting...</span>}>
<Post />
</Suspense>
)
}
어... 근데 어째서인지 Zustand 의 경우 비동기는 그냥 지원하는데 <Suspense>
에 대한 메뉴얼이 없다. 왜지?
이유는 간단한데, Zustand 는 useExternalStore
를 바탕으로 만든 라이브러리다. 따라서 절차에 나타나는 티어링을 막을 수 있지만 (즉, 절차를 따르는데 중간에 짤리는 일 없다고), 동시성에는 안맞기 때문에 고려하지 않았다는 게 개발자 설명.
그래서 @dai-shi 는 트위터에 예제를 올려 use
함수와 같이 사용하여 <Suspense>
와 같이 사용하는 방법을 제시했다.
물론 이 방법은 use
함수가 실험적 기능이라 우리는 쓰지도 못하니, 어자피 <Suspense>
컴포넌트 하나 재대로 사용할 수 있는 생태계부터가 실험적이고 도전적이기 때문에 변경하는 업무에 비동기를 쓰는 방법을 추천한다.
오죽했으면 Next.js 가 앱라우터 만들면서 loading.js
파일을 통해 <Suspense fallback>
대신으로 배려했냐 싶을 정도지.
즉, <Suspense>
컴포넌트는 시기상조라는 얘기다... 아직도...
끗.
zustand는 rtk ( react-toolkit ) 과 완전 유사하군요