한준호

리액트 상태관리에 대해 (recoil, jotai, zustand) 본문

Frontend/react

리액트 상태관리에 대해 (recoil, jotai, zustand)

igoman2 2022. 8. 5. 02:18
728x90

Recoil vs Jotai vs Zustand

리액트 진영에서 사용되는 클라이언트 상태 관리 라이브러리에 대한 간략한 스터디로 무거운 redux와 mobx는 고려하지 않음.

위 라이브러리들은 공통적으로 기존 상태관리(redux, mobx, context api)에 대한 성능 개선과 보일러플레이트 개선을 위해 등장했다.

  • Redux
    • 외부 상태로 Store를 취급하기 때문에 동시성 모드를 구현하기 제한됨
    • 복잡한 Boilerplate
      • Store, Action, Reducer 등 코드량이 많음
    • 비동기
    • Saga, Thunk 등 서드파티 라이브러리 필요
  • mobx
    • 클래스 컴포넌트에 맞춰져있음 (hook을 위해 추가 라이브러리 필요)
    • 객체지향
    • 딱히 단점이라곤....?
  • Context
    • value가 업데이트될 때마다 구독 컴포넌트 리렌더링 => 성능 이슈
    • 엄밀하게 말하면 상태 관리는 아님
      • DI를 위한 도구일 뿐 아무것도 관리 하지 않음.
      • 상태 관리 처럼 느껴지는 이유는 context 내부에서 주로 state를 사용하기 때문

 

 

Recoil

  • facebook에서 개발
  • 업데이트가 조금 느림
  • 리액트 "외부 요인" 으로써의 상태가 아니라 내부 state를 이용하기 때문에 스케줄러에 접근 가능
  • 동시성 모드 지원
    • ErrorBoundary, Suspense와 활용
    • 동시성 모드: 렌더링 작업을 쪼개서 스케줄러를 통해 렌더링 작업의 우선순위를 선정하여 부분적으로 DOM트리를 렌더링 (리액트에서 알아서 해줌). 쉽게 말해 현재 렌더링 인터럽트하면서 의도적으로 렌더링을 조절 (디바운스, 쓰로틀로는 한계가 있음 - UI 렌더링을 막을 수는 없기 때문)
  • 파생 상태 지원(redux의 selector, mobx/vuex의 computed)
  • atomic 패턴
    • atoms
      • 상태. 업데이트 되면 구독 컴포넌트 리렌더링
      • atom state 정의와 활용. key와 default만 atom으로 생성해주면 알아서 setter 함수까지 생성된다. selector는 파생 상태이다. 
        • useRecoilState — value와 setter. useState와 동일한 방식으로 사용할 수 있다.
        • useRecoilValue — setter 함수 없이 value 반환한다.
        • useSetRecoilState — setter 함수만 반환한다.
    • selector
      • 파생 상태를 만드는 순수함수. atoms에 의존하기 때문에 변화에 따라 업데이트 및 구독 컴포넌트 리렌더링. 특이한 점은 setter함수를 가질 수 있어 atomFamily로 생성된 모든 값에 대해 재설정이 가능하다.
      • atomFamily: atom과 동일하지만, 다른 인스턴스와 구분 가능한 매개 변수를 받을 수 있음. 예를 들어, Todos로 atomFamily를 만들고 개별 Todo 항목을 구독할 수 있음 => 렌더링 최적화
  • persist: recoil-persist
    • atom을 생성할 때 key를 넣는 이유 (unique key를 통해 디버깅, persist 가능)
  • 캐싱 지원
    • atom의 값이 변경될 때 마다 selector가 변경되는데, atom의 값이 같으면 내부적으로 반환값을 memo 하고 있어 캐싱된 값을 반환하게 된다.
  • devtool이 빈약함

 

 

Jotai

  • atomic 패턴
    • Read-only atom (값을 가져오는 경우) => selector
    • Write-only atom (값을 업데이트)
    • Read-Write atom (둘 다)
  • recoil과 코드가 아주 유사
    • 차이점이 있다면 key값을 넣어주지 않아도 됨 => 코드가 더욱 짧다.

  • 동시성 모드 지원
  • 비동기 처리
  • 파생 상태 지원
  • 캐싱 지원
  • Provider를 지정하지 않으면 기본 저장소를, 지정하면 atom이 저장될 Context를 제공할 수 있음. 즉, 참조하는 Provider가 다르면 같은 atom을 사용해도 값이 다르다!
  • devtool 지원
  • suspense 지원
  • persist: atomWithStorage
  • 다양한 마이그레이션 지원

 

Zustand (쮸스탠드..)

  • Provider가 필요 없다.
    • 부모 - 자식 관계가 사라지기 때문에 불필요한 렌더링을 줄일 수 있다. ContextAPI의 경우 Provider Hell 이 생길 수 있어서 가독성이 매우 좋지 않다. (실제로 맛집쪽은 비슷한 이름의 store Provider가 엄청나게 많이 감싸져 있어서 파악이 어려웠음)
    • 이는 어플리케이션 성능에 문제를 줄 수 있다.
  • 단일 store를 가지는 flux 패턴 (redux 축소판)
  • 가장 가파르게 성장 중
  • immer 를 사용해 nested object 처리할 수 있다.

  • 미들웨어를 통해 redux-devtools 활용 가능

zustand store 생성과 활용.

const {nuts, honey} = useStore((state) => ({nuts: state.nuts, honey: state.honey}), shallow)

const [nuts, honey] = useStore((state) => ([nuts: state.nuts, honey: state.honey]), shallow)

=> state.nuts나 state.honey가 변경되면 리렌더링

와 같이 여러 state로 이루어진 단일 객체를 생성하는 경우 shallow 함수를 전달하여 얕은 비교하도록 할 수 있다.

 

import {devtools} from 'zustand/middleware'

const useStore = create(devtools(store))

만 추가해주면 redux-devtools을 통해 디버깅할 수 있다.

  • 클로저를 활용

zustand는 발행/구독 모델 기반이다. 리스너 함수를 모아두고 store에 구독을 걸어놓으면(구독) store의 상태가 변경될 때 등록된 리스너들이 발동한다.(발행)

상태관리 라이브러리들은 스토어를 주입할 때 Context를 많이 활용하지만 zustand는 클로저를 통해 내부 상태를 관리한다. 이를 통해, 의도치 않게 상태가 오염되는 것을 막는다. 실제로 상태와 ui의 불일치는 생명주기의 클래스 컴포넌트에서 훅 기반으로 넘어오게 된 원인이기도 하다.

하지만 Context로 store를 주입하는 것 역시 지원한다.

 

state가 getter, setter 함수 바깥 스코프에 위치

원본 코드

  • 파생 상태 지원
  • 동시성 모드 지원
  • persist: zustand-persist
  • react가 아니어도 사용할 수 있음
  • 공식문서가 없다
  • suspense 지원 안됨

 

 

 

조타이.. 젤 조타이!!

동시성 모드 테스트 결과

 

테스팅

카운터 앱을 테스트 해본다.

 

 

왼쪽부터  recoil - jotai - zustand

확실히 jotai가 코드량이 적다.

 

테스트 코드 작성 방법은 2가지가 있다.

  1. DOM UI 테스트
  2. 로직(훅 테스트)

세 라이브러리 모두 훅을 기반으로 상태를 활용하기 때문에 훅 테스트를 위해서는 추가 라이브러리 @testing-library/react-hooks 에서 지원하는 hook test가 가능하다.

하지만 문제는 testing-library/react-hooks 가 아직 react 18을 지원하지 않는다.

그래서 구현 주도 테스트가 아닌 행위 주도 테스트로 테스트 코드를 작성하였다.

아래 테스트 코드 하나로 각기 다른 위 3개의 카운터 앱에 대한 테스트를 동일하게 할 수 있다.(좋은데..?) 행위 주도 테스트는 확실히 도메인에 종속적이지 않다.

 

 

결론

zustand가 급격하게 성장하는 모습. (아무래도 redux의 대체제라서?)

 

flux - zustand, redux  /  atomic - jotai, recoil 으로 컨셉이 나누어지며, redux보다 발전된 zustand는 Provider 없이 외부 상태로 존재하고 jotai와 recoil은 리액트 컴포넌트 트리에 존재한다.

zustand가 단일 스토어를 가지는 기존에 많이 사용하던 패턴(redux, vuex)이었어서 확실히 안정적이고 익숙한 느낌이었고, atomic 같은 경우는 꽤 신선했다. 

zustand와 jotai는 엔터급 프로덕트에서 쓰기에는 검증이 덜 되어있지 않냐는 의견이 종종 보이지만 카카오스타일과 화해에서는 jotai를 이미 도입하여 사용 중이다

여담이지만 켄트형님도 jotai를 선호한다고 한다.

728x90
Comments