Jotai 기본 개념 익히기

dana·2022년 11월 8일
8

상태관리 시리즈

목록 보기
1/2
post-thumbnail

Jotai?

간단 요약 : 추가 리렌더링 없음, 리액트에 속한 상태, 그리고 서스펜스와 병렬 기능들의 장점들을 모두 취할 수 있고, 심플한 react.useState 대체재부터 복잡한 요구사항을 가진 큰 스케일의 애플리케이션까지 커버 가능한 😎Jotai😎

주요 기능

  • Minimal API
  • TypeScript oriented
  • Tiny bundle size (3kb)
  • Many extra utils and official integrations
  • Supports Next.js and React Native

콘셉

리렌더링 이슈를 기존 useContext + useState 조합으로 해결하기엔 다음의 문제가 존재하기 때문에, Jotai는 리렌더링 이슈를 해결하기 위해 만들어졌습니다.

  1. Provider hell: 루트 컴포넌트가 너무 많은 Provider를 가지게 됨. 기술적으로는 문제 없지만, 다른 서브트리에 컨텍스트를 전달해야하는 경우 문제 발생
  2. Dynamic addition/deletion: 런타임에 새로운 컨텍스트를 추가하는 것은 새로운 provider를 생성하고 그 자식 요소들이 리마운트되어야하기 때문에 옳지 않음.

전통적으로 top-down 방식의 해결방법은 selector를 이용하는 것입니다. (ex. use-context-selector 라이브러리) 이 방식은 selector 함수는 재렌더링을 막기 위해 같은 값을 리턴해야하는 문제가 존재하고 가끔은 메모이제이션 기술을 요구한다는 단점이 존재합니다.🥲

Jotai는 리코일에서 영감을 받아 아토믹 모델과 함께 bottom-up 방식으로 접근합니다. 아톰과 함께 상태를 생성하고 아톰 의존성에 따라 렌더링 최적화를 하는데, 이 방식을 통해 리액트 컨텍스트의 리렌더링 이슈를 해결하고, 메모이제이션의 필요를 줄일 수 있습니다.😎

다른 상태관리 도구와의 차이점

zustand

JotaiZustand
기반recoilredux
상태가 속하는 곳리액트 컴포넌트 트리외부 스토어
상태 모델 초기 아톰들로 구성여러 스토어를 생성하더라도 하나의 스토어 안의 여러 스토어일 뿐

JotaiuseState와 useContext 조합의 대체재로 여러 컨텍스트를 생성하는 대신에 아톰이 큰 컨텍스트를 쉐어합니다.

Zustand는 외부 스토어로 리액트와 외부를 연결하기 위해 을 사용합니다.

🍀 어떨 때 무엇을 쓰면 좋을지?

  • 만약 useState+useContext 대체재를 찾는다면 , Jotai 👍
  • 리액트 외부의 상태 (서버 등)을 다룬다면, Zustand 👍
  • 코드 분할이 중요하다면 Jotai 👍
  • 리덕스 개발자도구를 선호한다면 Zustand 👍
  • suspense 사용을 원한다면 Jotai 👍

Recoil

JotaiRecoil
기반쉽게 익힐 수 있도록 집중복잡한 요구사항이 있는 큰 애플리케이션을 위한 모든 기능
의존성참조형 ID가 있는 아톰 객체아톰 ID

🍀 어떨 때 무엇을 쓰면 좋을지?

  • Zustand를 편하게 사용했다면 , Jotai 👍
  • 스토리지나 URL, 서버 등에 상태를 저장하는 것과 같이 많은 양의 상태 직렬화가 필요하다면, Recoil 👍
  • React Context 대체재가 필요하다면, Jotai 👍
  • 만약 새로운 라이브러리를 만들고 싶다면, Jotai 👍
  • suspense 사용을 원한다면 Jotai 👍

기본 개념

Atom

atom상태의 일부분을 의미하며, 숫자, 불리언, 문자열부터 문자열, 객체와 같은 복잡한 구조의 값을 가질 수 있습니다.. 리액트의 useState와 다르게 특정 컴포넌트에 구속되어있지 않습니다.

atom configs를 생성하기 위해 atom이라고 하는 함수를 export합니다. 이 함수는 단순한 정의일 뿐, 을 가지고 있지 않기 때문에 config라고 부릅니다. 그리고 만약 컨텍스트가 아무 값도 가지고 있지 않다면 atom이라고 부르기도 합니다.

기본 atom(config)을 만들기 위해선, 초기값을 설정해주어야 합니다..

import { atom } from 'jotai'

const priceAtom = atom(10) // 원하는 형태의 값으로 초기값 설정
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })

파생된 atom

파생된 atom이란 다른 atom의 값을 이용해 계산된 값을 의미합니다. jotai atom에는 기본 atom에서 파생된 3가지 패턴의 atom 형식이 있습니다.

  1. read-only atom
  2. write-only atom
  3. read-write atom

이를 생성하기 위해선 read functionwrite function(옵션)을 넘겨주어야 합니다. 쉽게 생각해서 useState에서 만약 우리가 value에 대한 상태를 관리하려고 한다면

const [value, setValue] = useState('')

여기서 value가 read function, setValue가 write function(옵션) 이 됩니다.

const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
  null, // 첫 argument로 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)
    // 한번에 원하는 만큼의 atom의 값을 설정할 수 있음
  }
)

하나씩 살펴보면,
get in read function : atom의 값을 읽기 위함. 읽기 의존성이 기록되며, 반응적(?)
get in write function : read function에서와 동일하게 atom 값을 읽지만, 기록되진 않음. 특히 비동기상태의 값은 읽지 못함

비동기 상태인 경우, 참고
추후 블로그 글 작성...! (목표)

set in write function : atom 값 작성을 위함. 지정한 atom에 쓰기 함수가 실행될 수 있도록 해줌.

파일 어디에서든 import {atom} from 'jotai'를 통해 Atom config를 생성할 수 있습니다. 하지만 참조적 동일성이 매우 중요한데, 렌더 함수에서 atom을 사용하려면 useMemouseRef를 이용해 안정적으로 참조할 수 있어야합니다. 만약 memoization을 위해 둘 중 무엇을 사용할 지 고민 된다면 useMemo를 사용하는 것을 추천합니다.

const Component = ({ value }) => {
  const valueAtom = useMemo(() => atom({ value }), [value])
  // ...
}

useAtom

useAtom은 atom값을 읽기 위한 Hook.
atom configs와 atom 값의 WeakMap 처럼 보이기도 합니다.

WeakMap??
Map과 비슷하지만 키로 객체나 함수만 사용할 수 있고, non-iterable이라는 차이점이 있습니다. 또한 Map은 GC에 의해 삭제되지 않지만, WeakMap은 GC에 의해 사용되지 않는 키값이 소거됩니다.

useState와 같이 atom값과 업데이트 함수가 튜플 형식으로 리턴되며, atom()에 의해 생성된 설정값을 가지게 됩니다.
처음에는 값을 가지고 있지 않다가, useAtom에 의해 사용되면서 초기값이 상태에 저장됩니다. (만약 atom이 파생된 atom이라면 read function이 초기값을 계산하기 위해 호출됩니다.) atom이 더 이상 사용되지 않으면 가비지 콜렉터에 의해 제거됩니다. (WeakMap의 특징)

const [value, updateValue] = useAtom(anAtom)

여기서 업데이트 함수인 updateValue는 하나의 인자값만 가집니다. 이 인자값은 어떻게 write function이 실행되는지에 따라 atom write function의 세번째 인자값으로 넘겨지는데, atom 참조 핸들링시 무한루프에 빠지지 않도록 주의해야함.

const stableAtom = atom(0)
const Component = () => {
  const [atomValue] = useAtom(atom(0)) // ❌ 무한루프 발생
  const [atomValue] = useAtom(stableAtom) // ✅
  const [derivedAtomValue] = useAtom(
    useMemo(
      // This is also fine
      () => atom((get) => get(stableAtom) * 2),
      []
    )
  )
}

리액트는 컴포넌트 호출에 영향을 미치기 때문에 여러번 불려도 항상 같은 값이 나와야함(멱등성). 프롭스나 atom이 변하지 않았는데도 추가적인 리렌더링이 일어나는 경우가 종종있는데, commit 없는 추가 리렌더링은 의도된 동작입니다. React18에서는 useReducer 기본 동작입니다..

Provider

provider는 컴포넌트 서브트리에 상태를 제공합니다. 다중 provider는 다중 트리에서 사용 가능하며, provider끼리 감싼 형태도 가능합니다. React Context처럼 동작합니다.

만약 atom이 provider없이 사용된다면, 기본 상태를 사용하는 것 입니다. (provider-less mode)

provider는 다음과 같은 상황에서 유용합니다.

  • 각각의 서브트리에서 다른 상태를 사용하는 경우
  • atom의 초기값을 받는 경우
  • remounting을 통해 모든 atom의 값을 지우는 경우
const SubTree = () => (
  <Provider>
    <Child />
  </Provider>
)

개념만 공부하고 난 뒤 정리
내 기준 Recoil이 더 쉽게 사용할 수 있는 것 같다. 그만큼 jotai에 기능이 많기 때문이겠지,, jotai가 weakmap으로 되어있어 GC가 불필요한 메모리를 정리해주고, 덕분에 가볍게 사용할 수 있다는 장점이 가장 인상깊었다. 트리별로 상태를 다르게 사용할 수도 있고, 같은 값을 사용할 수 있어 더 자유롭게 사용 가능하기 때문에 챙겨야할 부분이 더 많은 느낌. 사용하면서 익히는게 중요할 것 같다.

profile
PRE-FE에서 PRO-FE로🚀🪐!

0개의 댓글