Front-End

2024년 상태 관리의 종결 : zustand

나른한 노치 2024. 1. 19. 18:46

얼마 전, [모던 리액트 Deep Dive] 책을 읽고 회사에서 1월 사용하는 상태 관리 라이브러리를 zustand로 변경하였습니다. 왜 zustand를 선택하였고, 그 이야기를 해보려고 합니다.

 

여러분들은 redux, mobx, recoil, jotai, zustand, valtio 이들 중 어떤 것을 쓰시나요?

혹시 valtio를 쓰시는 분이 있다면 굉장히 힙하다는 생각이 드네요.

 

회사에서 zustand 를 쓰기 전인 2022~2023년 약 2년 동안 recoil 을 사용하였습니다. 2021년에 입사했을 땐 redux를 쓰고 있었구요.

 

redux 로 짜여진 프로덕트에서 거의 대부분의 상태가 redux 와 결합되어 매우 거대한 스토어를 이루고 있었습니다. 그것을 사용하기 위해 action 또한 너무 거대하다보니 복잡도가 너무 높아서 정신을 차리기 힘들 정도 였습니다.

 

제 실력이 너무나 부족하여 redux 의 결합도를 이해하지 못하고 코드를 고치다 보니 dispatch 들이 여기저기서 동작하기도 하며, 그것을 발견해서 막으면 저기가 뚫려서 하나를 고치면 사이드 이펙트가 많이 발생하는 상황이었습니다.

 

 

당시 새로운 구원자 분이 나타나 reduxrecoil 로 바꾸기로 하였고, 그 사용법에 매료되었습니다. npm의 생태계를 잘 모르던 저는 META(전 facebook)에서 관리하는 상태관리 라이브러리’ 이기에 ‘가장 react와 잘 맞지 않을까?’ 했었습니다.

 

그렇게 recoil로 전면 수정을 하였고, 관리도 단순해지니 프로덕트의 속도까지 향상되었습니다.

recoil과 이별을 결심하다.

현재 recoil 은 처음 만났던 2년 전과는 달리 현재는 너무 다른 인식을 주고 있습니다. recoil은 굉장히 무거운 라이브러리라는 것을 깨달았습니다.

 

아래는 bundlephobia에서 측정된 recoil @0.7.7 번들 사이즈입니다.

 

 

최적화를 시키면 79.4kB 이고 압축까지 하면 23.5kB 입니다! (…?)

문득 질문을 할 수 있습니다. “단위가 kB인데 뭐가 무겁다는거냐?” 라고 할 수 있습니다.

그래서 npm trends에서 상태 관리 라이브러리를 비교해보면 얼마나 무거운지 알 수 있습니다.

 

가장 우측의 압축까지 완료된 size를 보면 mobx를 제외하고는 recoil이 약 9~10배, 많게는 약 20배나 무겁다는 것을 알 수 있습니다.

recoil과 zustand 비교하기

아래는 저희 회사에서 next13 page-router로 개발된 WA.GG 프로덕트에서 recoilzustand로 수정한 빌드 결과를 비교한 것 입니다.

 

 

왼쪽: recoil ↔ 오른쪽: zustand

 

 

21kB 만 줄어든게 아니라 빌드된 페이지 폴더마다 약 21kb 씩 줄어든 것을 알 수 있습니다. 저는 이것을 통해 유의미한 경량화를 했다고 생각이 들었습니다.

작년에 AWSKRUG 프론트엔드 소모임 네트워킹 때, 한 개발자가 전역 상태 관리를 고민하여서 recoil을 추천했던 기억이 있는데… 이 내용을 써내려가면서 문득 부끄러움이 몰려드네요…

 

zustand는 어떻게 동작하는가?

zustand는 어떻게 구성되어 있길래 가벼운지 살펴보면 zustand의 핵심 파일인 vanilla.ts를 보면 알 수 있습니다.

  • /zustand/src/vanilla.ts

 

이 코드까지 어떻게 진입하는지 zustand demo를 통해서 살펴보겠습니다.

 

 

위 코드에서 컴포넌트 바깥에서 useStore를 통해 zustandcreate 함수를 사용하여 상태관리하는 것을 볼 수 있습니다.

  • /zustand/src/index.ts

 

zustand의 진입점인 index.ts에서는 default./react.ts 를 참조하고 있기에 ./react.ts 로 진입해보겠습니다.

  • /zustand/src/react.ts

여기에 저희가 찾고있던 create 함수가 있는 것을 알 수 있습니다.

 

 

create 함수는 createState가 있으면 createImpl(createState) 실행 결과를 반환 받고, 아니면 createImpl 함수 자체를 반환합니다.

 

 

createImpl 함수 내부에서는 api 변수를 다음과 같이 선언하고 있습니다.

const api = createState가 함수인가? true → createStore(createState) 함수를 실행하고 return 값을 할당합니다. false → createState 를 할당합니다.

 

여기서 createState 인수에는 아래와 같은 화살표 함수가 담겨져 있습니다.

 

 

 

api 변수는 createStore(createState) 함수를 실행하고 반환 값을 할당하게 됩니다. createStore 함수는 저희가 바라던 목적지인 ./vanilla.tsimport 해서 얹는 값임을 알 수 있습니다.

 

  • /zustand/src/vanilla.ts

 

createStore 함수는 인수로 받은 createState 가 값이 있으면 createStoreImpl(createState) 를 실행 후 그 결과를 반환합니다. 값이 없으면 createStoreImpl 함수 자체를 반환합니다.

 

저희는 createState 안에 화살표 함수가 있으니 createStoreImpl(createState) 를 실행하고 그 결과가 무엇인지 확인해보겠습니다.

 

 

 

createStoreImpl 함수에서 createStatestate = createState(setState, getState, api) 로 할당되어지는 것을 알 수 있습니다. 이 식을 보면 조금 혼란스러울 수 있는데 풀어서 보겠습니다.

 

createState는 하나의 인수를 가진 화살표 함수입니다. 그렇기에 동작하는 방식대로 코드를 작성하면 statesetState, getState, apisetState 만 사용하게 됩니다. 그래서 createState(setState)로 동작합니다. 최종적으로 state 에는 아래와 같이 담기게 됩니다.

 

 

이제 setState가 어떻게 동작하는지를 살펴보겠습니다.

 

 

 

현재 partial 인수에는 (state) => ({ count: state.count + 1 })로 된 화살표 함수가 담겨져 있습니다. 그러니 state.count 가 1이라고 가정했을 때, nextState에는 { count: 2 } 객체가 담기게 됩니다.

 

다음 if문에서 Object.is 로 객체 간의 얕은 비교를 수행하고, 비교 결과 같지 않을 경우 replace 에 따라 새 객체를 넣을 것인지 아니면 Object.assign({}, state, nextState) 를 통해 바뀐 것만 할당할 것인지 선택할 수 있습니다.

 

Object.assign 을 잘 사용할 일이 없어서 해당 함수가 어떻게 동작할까 살펴보았습니다.

 

 

nextState 의 값으로 할당되는 것을 확인 했습니다. 그런데 왜 Object.assign 을 선택하였을까 하니,

 

{} (빈) 객체에 state를 먼저 할당하고, 다음 nextState를 할당하는데 그 중 키가 중복될 경우 nextState에 있는 value로 덮어쓰기 방법을 통해 상태를 갱신하는 것을 알 수 있었습니다.

 

그리고 그 값을 마지막으로 listeners.forEach((listener) => listener(state, previousState)) 를 통해 listener 를 호출하는 것을 알 수 있습니다.

 

다시 createStoreImpl로 돌아와서 결과를 반환한 api 변수는 아래와 같은 값을 할당받게 됩니다.

 

 

createImpl 를 수정하여 대입하였습니다.

 

 

이제는 useBoundStore 함수가 무엇을 하는지 알아볼 차례입니다.

 

 

useBoundStore 에서는 useStore를 이용한 중첩 함수임을 알 수 있습니다. useStore 에는 방금 전에 할당을 완료한 api도 같이 보입니다.

 

 

useStoreuseSyncExternalStoreWithSelector 를 이용하여 api 의 속성을 param으로 받는 것을 알 수 있습니다.

 

useSyncExternalStoreWithSelectorreact에서 제공하는 useSyncExternalStore API 입니다. store가 변경되면 api.subscribe를 호출하면서 컴포넌트가 리렌더링되고 api.getState를 반환하여 데이터를 갱신합니다.

 

slice에는 api.getState의 반환 값이 할당되고 useStore 의 반환 값으로 slice가 반환되는 것을 알 수 있습니다.

slice 에는 아래와 같은 값이 담기게 됩니다.

 

 

useBoundStore 에 값이 할당되면 다음과 같습니다.

 

 

그럼 이제 useBoundStore가 반환되고 create 함수의 역할이 끝나게 됩니다.

고로 이제 useState() 를 통해 countinc를 반환 받아서 사용할 수 있게 됩니다.

 

코드 수도 적고, 복잡하지도 않아서 라이브러리 탐구하는데 어려움은 크게 없었습니다.

React에 종속적이지 않는 라이브러리

저는 주변에 가끔 “리액트가 언제까지 생존할 수 있을 것 같은가?”에 대한 이야기를 합니다. 새로운 시대가 왔을 때, 리액트 보다 더 간편한 UI 라이브러리 또는 프레임워크가 나왔을 때 옮길 수 있으면 좋겠단 생각이 들었습니다.

 

그렇기 때문에 redux를 잘 쓰는게 가장 좋은 방법이 아닐까 고민을 하고 있었습니다. 그러나 zustand를 알게 되었고 문서를 살펴보던 중 아래와 같은 내용이 있었습니다.

 

 

어디에 종속되지 않는 특성을 가졌기 때문에 zustand가 적합하는 생각을 하였습니다. 종속적이지 않기 때문에 라이브러리 또한 가볍다는 생각이 들었습니다.

내가 생각하는 zustand 장점과 단점

제가 생각하는 zustand의 장점으로 앞서 설명한 크기와 종속적이지 않는 것 등 있지만 무엇보다 추상화, 즉 의도를 명확히 할 수 있다는 점이 마음에 들었습니다.

 

2년간 recoil을 썼을 때, 불편했던 점이라면 다른 사람이 쓴 setState를 보면 어떤 일을 하는지 명확하지 않아서 지나치는 경우가 많았습니다.

const [value, setValue] = useRecoilState(ValueAtom);

 

useState가 지역적일 때는 복잡하지 않았다면, 전역적일 때는 상태관리가 복잡해 질 수 있기에 단순하고 직관적이게 만드는 방법에 고민을 하게 되지만 zustand에는 상태를 변경할 때 사용할 수 있는 정의할 수 있다는 점에서 저는 매력적으로 다가왔습니다.

 

그러나 단점이라고 지역으로 상태관리 하고 싶은 경우가 있는데, 찾아보니 Context API를 사용하는 것도 있어서 Context API의 단점인 상태가 변경될 경우, 하위가 모두 리렌더링이 되는 문제가 있었는데 이 문제를 해결하는지 직접 해봐야겠습니다.

 

Daishi Kato

여기서 TMI를 조금 던져보자면 zustand 를 따라 올라가다보면 Daishi Kato 님이 나오게 됩니다. Pinned 된 목록만 봐도 상태관리에 진심이신 것을 알 수 있습니다.

 

 

오늘 이야기한 zustand, jotai, valtio 모두 이 분이 관리한다고 하네요. Repositories를 보면 재밌는 것들이 많이 있으니 들어가서 봐도 좋을 것 같아서 잠깐 소개하였습니다.

마무리

코드를 전체를 다 파진 않았지만 zustand가 어떻게 상태관리 하고 있는지 살펴볼 수 있었습니다. 그리고 얼마나 간단하게 만들어져 있는지 알 수 있었습니다. 저보다 더 설명을 잘한 블로그도 많아서 쓰지말까 했지만 제가 직접 파보는 시간을 따로 갖고 싶어서 실험도 해보고 재밌는 결과를 낼 수 있어서 좋은 경험이었습니다.

블로그를 쓰는 분들을 만나보면 많은 사람들이 “나보다 잘하는 사람이 좋은 글을 많이 써놨어”라고 합니다. 저도 그럴 때마다 글을 쓰기 전에 찾아보면서 “이 사람보다 잘 쓸 수 있을까?”하며 망설였습니다. (여전히 다른 분이 더 잘 썼지만..) 제가 블로그를 쓰면서 잘못된 정보를 전하지 않고자 찾아보는 과정에서 배우는 게 많다는 것을 깨달았습니다.

 

 

요즘 프론트엔드적으로 깊게 파고 공유할 수 있는 자리를 만들고 싶어서 슬랙 채널을 만들었습니다! 관심있으신 분이 있다면 kthrkdals@kakao.com 또는 댓글 달아주시면 초대하도록 하겠습니다! 긴 글 읽어주셔서 감사합니다!

'Front-End' 카테고리의 다른 글

React State 관리: 10년간의 교훈  (0) 2024.03.03
React-Native에 대해서 알아보자!  (0) 2024.02.17
Next.js Project Structure  (1) 2024.02.03
Virtualize List with react-window  (2) 2023.12.17
[FE RoadMap] 00.Internet 좀 봐봐  (0) 2023.03.27