Tech
Atomic state management – Jotai
2021. 07. 01
안녕하세요. 화해팀 프론트엔드 플랫폼 박제훈입니다. 상태 관리 jotai
최근 플랫폼 내에서 상태 관리에 대한 기술 흐름 파악 및 팀 내 표준화를 위해서 논의를 진행하려 하고 있었고 관련해서 조사하던 내용들을 글로 정리해보려 합니다.
매번 해왔던 고민
저희 프론트엔드 플랫폼은 UI 구현을 위한 라이브러리로 리액트를 메인 기술 구조로 채택하여 사용하고 있습니다. 그런데 새로운 프로젝트가 시작되면 리액트와 함께 구현할 기반 기술들을 선택할 때 자주 논의되었던 주제가 상태 관리였습니다. 과거엔 상태 관리를 위해 당연하듯 Redux를 고려했었는데 리액트 v16.8의 hook
의 등장 이후로 많은 선택지가 생겨나게 되었고 이에 따라 저희 역시 새로운 시도에 대한 고민을 해왔습니다. 이럴 때마다 논의의 내용은 항상 비슷했었습니다.
- 요즘의 상태 관리 라이브러리들은 어떠한 변화를 겪고 있고 어떤 라이브러리가 선호될까?
- 상태 관리 라이브러리를 사용하지 않고 Context 만으로 충분히 관리될 규모일까?
- 대부분의 상태 관리를 해야 하는 것들이 response 데이터라면
swr
이나react-query
만으로 구현할 수도 있지 않을까?
위와 같은 내용으로 몇번의 논의 이후 플랫폼 내 스터디를 통해서 리액트 상태 관리에 대한 기반 지식들을 되짚고 갈 필요가 있다고 생각되었고, 플랫폼 내부에서 나눠서 여러 상태 관리 라이브러리에 대한 조사를 시작하고 각자 세미나를 진행하기로 하였습니다. 저는 자료를 조사해가던 중 Kent C. Dodds의 블로그 글을 보다가 Jotai라는 상태 관리를 언급한 문장을 보았습니다.
💡 recoil과 jotai는 매우 유사하며 같은 문제를 해결한다. 하지만 내 경험상 jotai를 선호한다. (Recoil and jotai are very similar (and solve the same types of problems). But based on my (limited) experience with them, I prefer jotai.)
20년 7월 즈음 작성된 글이어서 약 1년 정도 지난 시점에선 두 라이브러리에 변화가 많이 있었겠지만, jotai
를 특별히 언급한 부분에 흥미를 느끼게 되었습니다. 이후 jotai
의 철학과 컨셉, 그리고 코드를 훑어보면서 조사한 내용들을 플랫폼 인원들과 공유하면 좋겠다는 생각으로 글을 시작하게 되었습니다.
re-render, re-render
jotai
의 컨셉 문서를 읽어보면 시작은 Context의 re-rendering 문제에서 출발한 것 같다는 생각이 들었습니다. Context를 활용해 상태 관리를 할 때 발생하는 대표적인 문제는 아래와 같은 케이스입니다.
위 코드는 일반적으로 Context를 활용한 상태 관리의 예시가 될 수 있다고 생각되어 작성해보았습니다.
Title
과 Color
의 state 업데이트 여부와 관계없이 Context가 업데이트되는 순간 Context 하위에 있는 컴포넌트는 전부 re-rendering이 됩니다. 예제가 극단적인 예시일 수 있지만 위와 같이 사용할 때 dispatch
와 state
를 분리해서 별도의 Context로 관리하는 방법, 필요한 값만 memoization 하는 방법, Context를 지연 업데이트하는 방법 등으로 회피를 해볼 수 있지만 이런 부분까지 최적화하여 사용해야 하는 것인지 항상 의문이 들었습니다. 이와 관련해 리액트 팀에서도 문제를 느껴 Context Selector RFC와 Context 변경 지연 전파 RFC 와 같은 Context 개선 과제를 진행했습니다. 이를 기반으로 한 useContextSelector
가 PR로 올라온 상태였으며 개선은 꾸준히 진행 중입니다. (참고: Context 변경 지연 전파는 얼마 전 merge 되었습니다.)
위와 비슷하게 jotai
의 maintainer는 상태 관리, 그중에서도 Context의 효율적인 관리에 대한 접근이 많았다고 생각됩니다. jotai
를 만들기 전 use-context-selector
라는 라이브러리로 Context의 변경점만 반영될 수 있도록 접근하였습니다.
use-context-selector
는 아래와 같은 방식으로 사용됩니다.
현재 리액트 팀의 useContextSelector
RFC에 올라온 방식도 굉장히 유사한 방식으로 접근하고 있습니다.
위 내용을 소개 한 이유는 jotai
는 리액트의 useState
와 useContext
만으로 충분히 상태 관리가 가능한 프로젝트 규모에서 고려해볼 만하고, Context의 re-rendering 문제를 해소하기 위한 접근으로 만들어졌다고 생각하기 때문입니다. 위 컨셉은 jotai
에서 계속 활용되고 있습니다.
Jotai?
jotai
는 recoil이라는 리액트 팀에서 직접 만든 상태 관리 라이브러리가 등장한 후, atomic model 기반의 상향식 접근 방법에 영감을 받아 만들어졌습니다. jotai
는 일본어로 상태를 뜻합니다. 같은 개발 그룹인 Pmndrs에서 만든 Zustand라는 상태 관리 라이브러리도 있는데 이는 독일어로 상태라는 뜻이라고 합니다. jotai
는 recoil
과 zustand
는 redux
와 비슷하다고 비교 문서에 작성이 되어있습니다.
메인테이너의 블로그에는 그동안 상태 관리에 대해 고민한 흔적들이 보이는데 이 글을 읽어보면 jotai
의 전신 프로젝트였던 use-atom을 만들면서 global state management에 대해 많이 고민한 것을 알 수 있습니다. 기본 철학은 useState
와 useReducer
같은 기존 리액트의 사용성을 그대로 가져오면서 core api는 minimalistic 하게 디자인되어 있습니다.
기본 컨셉은 atomic
한 상태로 작은 단위로 관리하고, atomic
상태에서 파생된 “계산된” 결과를 받아봅니다.
위 Context로 작성한 예제를 jotai
로 아래와 같이 작성할 수 있습니다.
그러면, 아래와 같이 업데이트 되는것만 렌더가 됩니다.
기본 사용법은 여기까지고, 이후부터는 조합된 사용법 혹은 확장된 사용법 그리고 다른 라이브러리와 integration 하는 응용 사례들이 있습니다.
Concurrent Mode
리액트에서는 동시 모드라는 실험적인 기능을 도입하고 있습니다. 최종적으로 리액트의 stable 버전에 도입이 된다면 미래 리액트를 사용할 때의 큰 변곡점이 될 것이라 기대가 되는데, 저는 이 모드에서의 가장 중요한 개념은 tearing이라 생각했습니다. tearing이란 요약하자면 동시 모드에서 컴포넌트의 렌더 트리가 외부 상태 업데이트를 통해서 갱신되는 도중에 다른 업데이트가 다시 발생하여 렌더링이 깨진 듯한 현상입니다. 스택오버플로우의 질문 글에서 redux의 메인테이너가 답변을 작성한 내용도 있고, React Next 2019의 발표 영상에서 구체적으로 설명한 내용도 있으니 참고하시기 바랍니다.
jotai
의 메인테이너는 여러 상태 관리 라이브러리들에서 다양한 방법으로 tearing 현상 발생 테스트를 진행하였고 tearing 현상에 대한 고려를 많이 한 것 같습니다. 이 저장소에서 테스트에 대한 내용을 확인해볼 수 있습니다.
위 tearing 현상에 대한 이미지는 해당 저장소에서 가져왔습니다.
동시 모드는 react state를 사용하지 않는 redux와 같은 외부 상태 관리 라이브러리들에게 상태 관리 관점에서 해결해야 할 많은 과제들을 주었다고 생각됩니다. 리액트 팀에서는 동시 모드에서 안전한 상태 관리를 위한 useMutableSource RFC를 제안합니다. 아직 실험 모드에서 사용할 수 있는 상태이지만 이 PR에서 오랫동안 논의가 되었고 동시 모드에서 상태 관리를 하는 방법에 대한 고민을 많은 리뷰어들이 진행했습니다.
jotai
의 내부 상태 관리는 useMutableSource
를 활용할 계획으로 보이며 인터페이스를 맞추고 내부에서 관리하는 코드에서 아래와 같이 리액트 stable 버전이 release 될 경우 차용할 대비를 하는 것 같습니다.
useMutableSource
의 동작에 대해서는 해당 RFC를 제안했던 저자의 Gist에 정리가 더 되어있으니 참고하면 좋을 것 같습니다. 동시 모드로 개발하는 시기가 오게 될 때도 jotai
에서 같이 고민하고 발전할 것으로 보여서 향후 소스코드를 지켜보는 것도 즐거울 것 같습니다.
Features
사실 공식 문서가 생각보다 굉장히 잘 되어있는 편이라 공식 문서를 정독하는 것을 추천드립니다. 저는 간략하게 소개하고 넘어가려 합니다.
atom & useAtom
생성은 아래와 같이 config라 부르는 초기 값을 넣어주면 됩니다.
atom
은 상태를 원자 단위로 관리하기 위한 하나의 단위로 보면 좋을 것 같습니다. atom
은 상태이고 useAtom
hook으로 관리됩니다. recoil
과 차이점은 atom
을 생성할 때는 key가 필요 없습니다.
atom
은 아래 3가지 형태로 파생될 수 있습니다.
- 예제의
readOnlyAtom
과 같이 기반이 되는atom
값이 변경될 때 반응하는 값을 읽을 수만 있는atom
타입 - 예제의
writeOnlyAtom
과 같이atom
이 갖는 값은 없으며, 파생된atom
의 값에 쓰기가 가능한 타입 - 예제의
readWriteAtom
과 같이 기반이 되는atom
값이 변경될 때 반응하는 값도 읽을 수 있고 쓰기도 가능한 타입
useAtom
은 리액트의 useState
와 비슷하게 사용 가능하고 값과 update로 구성된 Tuple로 전달됩니다. useAtom
에서 사용되는 atom
의 값이 더 이상 사용되지 않으면 GC 된다는 가이드가 있는데 내부에 Context에서 관리되는 Global State를 확인해보면 WeakMap으로 관리되는 것을 확인할 수 있습니다.
아마도 jotai
의 상태가 원자 단위로 확장되어서 모든 상태가 atom
으로 관리가 되어야 저자의 의도대로 활용될 것으로 보이고 그 예제는 글 아래 atom in atom 파트에 추가 내용이 있습니다. 규모가 커지거나 효율적인 상태 관리를 고민하다 보면 많은 atom으로 구성이 될 수 있다 보니 메모리 관리에 신경 쓴 것 같습니다.
atom.onMount
atom
이 provider에 처음으로 사용되는 시점을 활용하고 싶다면 onMount
프로퍼티를 사용하면 됩니다. onMount
함수의 return 값은 onUnmount
시점에 트리거 됩니다.
Async
간단한 비동기 액션은 아래와 같이 사용합니다.
그리고 <Suspense />
와 함께 사용이 가능합니다.
<Suspense />
를 사용하지 않고 아래와 같이 Promise를 반환하지 않는 형태로 write 속성을 활용하여 fetching 상태를 직접 관리할 수도 있습니다.
Provider
jotai
는 atom
을 위한 Provider를 제공합니다. 내부 구현은 scope
를 키로 갖는 Map으로 관리하고 있고, 별도로 선언을 해주지 않아도 됩니다. 다만 초기값을 선언해야 하거나, scope 별로 업데이트 영역을 나누고 싶다거나, 개발 중 debug 정보를 보고 싶다면 Provider를 활용하여 이점을 챙길 수 있으니 선택적으로 사용하라고 가이드가 되어있습니다.
내부 구현 코드를 보면 Provider는 scope를 키값으로 갖는 Map으로 관리되고 있습니다. scope는 문서에는 Symbol을 권장하고 있습니다. 라이브러리를 jotai를 이용하여 만드는 방법같이 scope를 분리하고 싶을 때 사용하면 좋을 것 같습니다.
특이한 점은 Provider 없이 Context를 활용하는 것 같은 사용성을 가진 Provider-less 모드를 제공합니다. 아래와 같이 Provider가 없어도 동작하도록 설계되어 있습니다.
이와 관련해서는jotai
개발에 참여하고 있는 컨트리뷰터의 글에 소개되어있습니다. 블로그의 내용은 useContext
를 useMutableSource
의 리턴 값으로 대체하고 global state를 mutable하게 관리하면서 실제 변화 사항은 listener를 통해 구독하는 흐름으로 설명되어있습니다. jotai
전체 설계에서 중요한 부분이라 생각되고 이 설계는 useAtom
까지 이어집니다. 다만 jotai
에서는 Context를 잘 활용하고 있습니다.
Atom in atom
jotai
의 maintainer가 렌더링 요소에서 이점을 많이 볼 수 있도록 설계한 특징을 제일 많이 반영한 가이드가 이 파트라고 생각합니다. atom
이 atom
을 포함할 수 있고 atom
들의 array, object로 관리될 수 있어서 atom
으로 관리되는 상태들은 상향식 접근으로 확장할 수 있습니다. 이 문서에 가이드가 잘 되어 있으니 살펴보시고, 여기서는 한 가지 예제를 소개합니다.
이 예제는 여러 개의 Counter
중 업데이트가 발생하는 Counter
만 re-rendering 되게 하고 싶은 케이스에서 이점을 볼 수 있습니다.
Appendix
Utils
jotai
를 import 하여 사용하는 경우 외에 추가적인 기능이 필요한 경우 jotai/utils
에 확장된 헬퍼들이 포함되어 있습니다. 이 문서를 통해서 확인 가능하고, 재미있는 것은 util이 생성된 이유들을 히스토리처럼 PR 링크로 대부분 달아놓았습니다. 또한 recoil
에 영감을 받아 작성해서인지 recoil
에 포함되어 있는 기능 중에 jotai
를 확장하여 구현한 듯한 PR들이 많이 보였습니다. 대표적으로 atomFamily PR 이 인상적이었습니다. Maintainer가 recoil에 있는 기능들의 필요성을 이해하고 jotai에 녹여내는 과정 또한 인상적이었습니다.
그리고 다른 유틸들의 PR을 읽어보는 것도 jotai
를 이해하는 데 도움이 많이 되었습니다.
Large object – focus or split
대용량 데이터를 관리할 때 단일 원자로 저장하고 이를 나눠서 관리하는 레시피도 인상적이었습니다. 대용량 데이터의 특정 부분만 업데이트를 하면서 렌더링 최적화를 하는 등의 응용이 가능해 보입니다. optics
라는 외부 라이브러리를 integration 해서 사용하는 방법이 아래와 같이 있습니다.
array를 config 값으로 사용하는 atom을 split 해서 사용할 수도 있습니다. 마찬가지로 변화된 상태가 있는 컴포넌트만 렌더링 하도록 사용합니다.
atomFailmy
그리고 splitAtom
과 유사하게 atom
을 배열로 관리하는 TODO 예제에서 같은 UI를 다르게 구현하는 방법으로 비교해볼 수 있도록 공식 문서에서 제공합니다.
Integration
jotai
는 다른 상태 관리 라이브러리들과의 Integration을 공식적으로 제공합니다. 모든 상태 관리의 기본 사용성을 유지한 채로 다른 라이브러리와 연결하여 쓰는 이 부분이 정말 큰 강점이라고 생각합니다. 아래는 react-query
와 integration 한 예제입니다.
공식적으로 아래의 라이브러리 들을 지원합니다.
- immer
- optics
- react-query
- xstate
- valtio
- zustand
- redux
마무리
jotai
라는 상태 관리 라이브러리를 찾아보면서 이 오픈소스는 왜 만들어졌는지, 무엇을 신경 썼는지를 여행 다녀온 느낌으로 즐겁게 찾아보았습니다. 상태 관리는 요즘 많이 관심을 갖고 있던 주제였는데 본격적으로 찾아보면서 다시 한번 미진하게 공부했던 자신을 발견하며 반성했고 이 글도 즐겁게 작성했습니다. 덤으로 jotai
maintainer의 팬이 되어버렸습니다.
react-query
, Redux
, XState
등과 같이 확장하여 사용 가능해서 큰 규모의 프로젝트에서도 충분히 활용할 수 있다는 생각이 들었습니다. 첫 메이저 버전 release가 지난 6월 15일 배포되었는데, 앞으로 관심 있게 지켜볼 예정이고 후속 포스트가 jotai
를 사용해본 경험이나 다른 상태 관리 라이브러리의 비교 글이었으면 좋겠다는 생각을 하며 마무리합니다.
이 콘텐츠가 마음에 드셨다면 화해 프론트엔드 플랫폼의 다른 글도 확인해보세요!
✅ <htmI> | 프론트엔드 플랫폼 , “성장도 공유도 멈추지 않는다”