Front-End(Web)/React - 라이브러리들

[Jotai] React 전역 상태 라이브러리

ttaeng_99 2022. 3. 13. 05:22
반응형

이전, 토이를 진행하면서 React의 전역 상태를 사용하기 위해 Recoil 라이브러리를 적용했던 적이 있다. (링크)

 

[Recoil] 전역 상태관리 라이브러리 - Recoil 정복기

🧐 서론 굉장히 오랜만에 쓰는 서론인 것 같다!! 그만큼 이 글의 길이가 짧진 않을거라는 마음의 준비 차원일지도? 오랜만에 React를 복기하고 Typescript를 숙달할 겸 예전에 면접과제로 받았던 메

abangpa1ace.tistory.com

 

이전에 사용했던 Redux에 비하면 매우 심플한 사용법에 컬쳐쇼크를 느끼고, 그렇게 나는 Recoil의 추종자가 되었다. 😏😏

 

Recoil의 철학(Atomic Model)에 영감을 받아 만들어진 React의 상태관리 라이브러리인 'Jotai' 를 소개해보려고 한다.

 


 

👻 Jotai 는?

* Jotai는 일본어로 '상태(狀態)' 를 의미한다.

 

Jotai는 Recoil의 Atomic Model 기반의 상향식 접근(bottom-up approach)의 방법에 영감을 받아 만들어진 라이브러리라고 공식문서는 소개하고있다. (특히 React 전용인것이 특징이다!)

그렇기에 recoil과 유사하게, 함수형 프로그래밍(state, useState) 및 Context API 형태의 코드 패턴을 보이는 것이 특징이다.

그러면서도, 아래와 같은 차별점을 가지고 있어서 일부 개발자들에겐 Recoil보다 선호되고 있다.

  • 경랑화된 API (Minimalistic API)
  • String key의 미사용
  • 타입스크립트 기반
  • utils 함수들의 제공

 

Jotai는 React의 Context(useState + useContext) 기반 상태관리 모델에서 발생한 주요 이슈들의 개선에 초점을 맞췄다.

  • Provider hell : Root 컴포넌트가 전역상태 테마수 만큼의 Provider로 감싸지고, 때론 이들이 다다른 subtree만 감싸는게 유용하다.
  • Dynamic addition/deletion : Context를 수정할 때, 여기에 속한 컴포넌트들이 상태 참조여부에 상관없이 리렌더된다는 이슈가 존재한다.

이러한 문제점들을 개선하기 위해 use-context-selector 라이브러리도 등장하였지만,

기존의 Context 상태를 Memoization 한 후 변경된 부분만 반영되도록 접근하는 방식에서, 좀 더 관심사를 분리하고자 한 점이 Jotai 개발의 출발점이었다.

 

 

- 다양한 상태관리 라이브러리

일전, Recoil 포스팅에서도 정리했지만, React에서 대표적으로 사용되어온 Redux의 Flux 패턴과 비교를 했었다.

최근, Zustand 라는 Jotai와 같은 pmndrs 개발팀에서 만든 상태관리 라이브러리를 접하면서 다양한 라이브러리들의 특징을 다시금 정리해보고 가려고 한다!

 

1) Proxy 패턴 - Mobx, Valtio

Proxy 패턴의 대표적인 라이브러리는 전통적으로 애용되었던 Mobx 이다.

전체 상태들을 모아놓고 엑세스를 제공하며, 컴포넌트에서 사용되는 일부 상태를 자동으로 감지하고 업데이트를 인지하는 패턴이다.

단순한 패턴인만큼 디버깅은 어렵지만, Store 데이터에 바로 엑세스하여 변경하는 편의성이 있다고 한다.

 

2) Flux 패턴 - Redux, Zustand

현재까지 가장 많이 사용된 Redux가 대표적으로 보이는 패턴이다.

Store라는 상태 저장소를 기반으로, Action 타입을 Reducer에 전달하면 해당 타입에 맞는 동작에 따라 상태값을 갱신한다.

또한, 컴포넌트는 Selector를 사용해 Store에서 필요한 상태값을 구독(subscribing)하는 형태를 보인다.

상태가 분리되어 있으며 플로우가 일방적인 점이 유지보수에 용이하지만, 그만큼 보일러 플레이트와 작성할 코드가 방대해진다는 단점이 있었다.

 

3) Atomic 패턴 - Recoil, Jotai

React의 state와 비슷하게, 컴포넌트 트리 안에 상태들이 존재하며 이들이 상향식(bottom-up)으로 수집 및 공유되는 패턴이다.

상태들은 atom이라고 불리는 객체에서 설정하며, 값의 참조와 조작은 React.useState와 유사하게 [state, setState] 튜플로 수행한다.

Store에서 하향식(top-down)으로 관리되던 기존 패턴과 매우 다르기에, 다른 라이브러리보단 React의 Hooks 및 Context API와 많이 비교된다.

 

* 공식문서 링크 : https://jotai.org/

 

Jotai, primitive and flexible state management for React

Jotai takes a bottom-up approach to React state management with an atomic model inspired by Recoil. One can build state by combining atoms and renders are optimized based on atom dependency. This solves the extra re-render issue of React context and avoids

jotai.org

 

 

- 설치 및 적용

yarn add jotai

npm 혹은 yarn으로 우선 설치해준다.

 

// index.js

import { Provider } from "jotai"
ReactDOM.render(
  <Provider>
    <App />
  </Provider>,
  document.getElementById(‘root’)
);

그리고, React 진입점인 루트의 index.js에서 <App /> 컴포넌트를 jotai의 <Provider>로 감싸기만 하면 된다!

Recoil의 <RecoilRoot>와 유사하며, 이제 내부 모든 컴포넌트에서 jotai atom을 사용할 수 있다.

 

 

 

👻 Jotai 주요 문법

Jotai의 주요 문법들을 간단하게 정리해보고자 한다. (상태관리 컨셉은 Recoil 포스팅을 참고할 것을 추천한다!)

 

1. atom()

jotai의 내장 API인 atom이다. 상태의 단위(조각)이자 state를 생성하는 함수이다.

아래와 같이 심플한 문법으로 사용할 수 있으며, Recoil과 달리 key값(string)을 설정하지 않는 것이 특징이다.

import { atom } from 'jotai'

const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })

 

* 읽기 / 쓰기 전용 atom

 

Jotai에서는 읽기 전용(Recoil의 selector와 유사), 혹은 쓰기 전용, 읽기/쓰기 전용 3가지 케이스의 atom이 존재할 수 있다.

import { atom } from 'jotai'

const priceAtom = atom(10)

const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
  null, // 첫번째 인자로 전달하는 초기값은 null로 전달
  (get, set, update) => {
    // update는 atom을 업데이트하기 위해 받아오는 값
    set(priceAtom, get(priceAtom) - update.discount)
  }
)
const readWriteAtom = atom(
  (get) => get(priceAtom) * 2,
  (get, set, newPrice) => {
    set(priceAtom, newPrice / 2)
    // set 로직은 원하는 만큼 지정할 수 있다.
  }
)

atom 내 인자로 콜백함수를 작성하면, 위 예시처럼 첫 번째는 읽기, 두 번째는 쓰기에 관련된 인자를 받는 것을 알 수 있다.

get은 다른 atom을 참조, set은 atom을 새로운 값으로 갱신, update(newPice) 등은 갱신을 위해 입력받을 값이다.

 

 

2. useAtom() 

useAtom 훅은 atom을 인자로 받아, [atom, setAtom] 값과 세터함수를 튜플로 반환한다. (React의 useState와 유사하다)

// Component.jsx

import { useAtom } from 'jotai'
import { countAtom } from '../store'

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return (
    <h1>
      {count}
      <button onClick={() => setCount(c => c + 1)}>one up</button>
    </h1>
  )
}

위처럼, 상태를 적용하고자 하는 컴포넌트 내에서 useAtom을 import해서 상태 & 세터함수를 선언해주면 된다.

내부에 전달하는 인자는 컴포넌트 혹은 별도 파일에서 선언한 atom이다. 이를 불러오면 atom에서 설정한 초기값이 상태에 저장된다.

또한, 세터함수를 통해 atom의 값을 갱신할 수 있다.

 

아래처럼 상태 혹은 세터함수 중 필요한 값만 사용할 수 있으며,

후에 설명할 'jotai/utils' 의 useAtomValue, useUpdateAtom 유틸함수로도 대체할 수 있다.

const [onlyState] = useAtom(myAtom)		// useAtomValue()
const [, onlySetState] = useAtom(myAtom)	// useUpdateAtom()

 

 

3. atom.onMount

atom이 <Provider>에서 처음으로 사용되는 시점에 호출되는 onMount() 메서드 프로퍼티가 존재한다.

인자는 새터함수로, Mount 후 초기값을 재지정하고 싶을때 사용할 수 있다.

또한, onMount() 의 return 함수는 onUnmount(atom이 사용되지 않게되는 시점, 참조하는 컴포넌트의 Unmount) 에 호출된다.

// mount, unmount 기본 형태
const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
  console.log('atom is mounted in provider')
  setAtom(c => c + 1) // increment count on mount
  return () => { ... } // return optional onUnmount function
}

// mount를 활용한 초기값 설정 예제
const countAtom = atom(1)
const derivedAtom = atom(
  (get) => get(countAtom),
  (get, set, action) => {
    if (action.type === 'init') {
      set(countAtom, 10)
    } else if (action.type === 'inc') {
      set(countAtom, (c) => c + 1)
    }
  }
)
derivedAtom.onMount = (setAtom) => {
  setAtom({ type: 'init' })
}

 

 

4. Async

Recoil은 selector를 활용해 비동기 전역상태를 관리했다.

Jotai는 atom이 동기/비동기를 모두 담당하는게 특징이며, 초기 fetch를 위해 write 함수인자를 활용하면 된다.

const fetchUrlAtom = atom(async (get) => {
  const response = await fetch(get('https://my-api.com'))
  return await response.json()
})

 

특히, 비동기 상태 fetch간 노출할 목적으로, <Suspense> 컴포넌트로 감싸서 fallback을 설정해줘야 한다.

(통상 App을 감싸나, 컴포넌트별로 여러번 감싸도 무방하다.)

const App = () => (
  <Provider>
    <Suspense fallback="Loading...">
      <Layout />
    </Suspense>
  </Provider>
)

 

 

5. Utils

'jotai/utils' 패키지는 atom을 사용하는데 있어 유용한 함수들을 지원한다.

위에서 언급한 useAtomValue(), useUpdateAtom() 부터, 리셋이나 스토리지 저장 등 다양한 메서드들이 있다.

여기서는 유용한 몇 가지 메서드들만 언급하고 넘어가겠다. (공식문서 링크 참고)

 

 

- useAtomValue, useUpdateAtom

 

앞서 언급했던 상태 혹은 세터함수만을 사용하기 위한 유틸리티 함수이다.

import { atom, useAtom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'

const exampleAtom = atom(0)

const Example = () => {
  // 기존 useAtom
  const [myAtom, setMyAtom] = useAtom(exampleAtom)
  
  // useAtomValue, useUpdateAtom 각각 적용
  const myAtom = useAtomValue(exampleAtom)
  const setMyAtom = useUpdateAtom(exampleAtom)
  
  return <div>atom: {myAtom}</div>
}

 

 

- AtomWithStorage

 

Atom 상태값을 스토리지에 저장하는 유틸리티 함수이다. 토큰 등 스토리지와 연관되는 전역상태에 유용하다.

인자는 키네임, 값, 옵션을 받으며, 옵션의 기본값은 localStorage 이다.

const anAtom = atomWithStorage('ls_key', [], {
    ...createJSONStorage(() => localStorage),
  delayInit: true,
})

 

이외에도, selector처럼 사용하는 selectAtom, SSR 시 사용하는 useHydrateAtom, Reset이 가능한 atomWithReset, useResetAtom 등 다양한 메서드들이 존재한다.

 


📎 출처

- [Jotai] Jotai 공식문서 : https://jotai.org/

- [Jotai 적용기] 화해 기술 블로그 : http://blog.hwahae.co.kr/all/tech/tech-tech/6099/

- https://velog.io/@ggong/%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-jotai

 

반응형