본문 바로가기

리액트

[react ] Jotai, Recoil, Zustand, Valtio | Jotai (1)

반응형

Jotai

Jotai는 원자(atom) 단위로 전역 React 상태 관리를 수행하는 라이브러리이다. 즉, 상태를 작은 단위인 atom으로 분리하여 관리하고, 렌더링은 자동으로 최적화된다.

쉽게 말해서, 의존 관계에 놓여 있지 않은 atom(원자) 간에는 서로 영향을 미치지 않는다는 것을 의미한다. 예를 들어, A라는 원자 변하더라도, B라는 원자는 아무런 영향을 받지 않는다. 

 

이는 React 컨텍스트의 불필요한 리렌더링 문제를 해결하고, 메모이제이션 필요성을 없애며, 선언적 프로그래밍 모델을 유지하면서 시그널과 유사한 개발자 경험을 제공한다.

여기서, 선언적이란 알고리즘을 통해 얻은 결과는 이거야 라고 선언하는 것과 같다. 즉, 결과를 얻기 위해 알고리즘이 어떻게 구현되어야 하는지를 일일이 명시하는 명령형 프로그래밍의 반대라고 이해할 수 있다.


Jotai는 단순한 useState 대체물에서 복잡한 요구 사항을 가진 엔터프라이즈 타입스크립트 애플리케이션까지 다양한 규모의 프로젝트에 적합하다고 한다.

 

등장배경

등장배경은 솔직히 앞 전에서 다 언급이 되었지만, 재정리 차 각각 나눠서 설명을 해본다면 다음과 같다.

React 컨텍스트의 불필요한 리렌더링 문제 해결

React 컨텍스트는 상태를 전역적으로 관리하는 데 유용한 도구이지만, 불필요한 리렌더링 문제를 일으킬 수 있다. Jotai는 atom의 의존 관계를 기반으로 렌더링을 자동으로 최적화하여, React 컨텍스트의 불필요한 리렌더링 문제를 해결한다.

 

메모이제이션 필요성 없음

상태 관리 라이브러리에서 메모이제이션을 사용하면 성능을 향상시킬 수 있지만, 메모리 사용량을 증가시킬 수 있다. 즉, 증가폭이 커질수록 오히려 성능 저하를 일으킬 수도 있다는 약점이 존재한다. 이 때, Jotai는 atom의 의존 관계를 기반으로 값을 계산하므로, 메모이제이션을 사용하지 않고도 값을 효율적으로 계산할 수 있다.

 

선언적 프로그래밍 모델 유지

상태 관리 라이브러리에서 선언적 프로그래밍 모델을 유지하면 코드가 더 간결하고 이해하기 쉽다.  Jotai는 값을 도출하는 과정을 명시하는 것에 초점을 두지 않으며,  도출된 결과 자체에 초점을 두기 때문에  atom의 값을 값 자체로 표현하여, 선언적 프로그래밍 모델을 유지한다.

 

>> 위 설명을 조금 더 자세히 정리해보자

Totai는 atom의 값을 값 자체로 표현하여, 선언적 프로그래밍 모델을 유지한다고 하였다. 예를 들어, 다음과 같이 atom을 선언할 수 있다.

const count = atom(0);


이 코드는 count라는 atom을 선언하고, 초기 값을 0으로 설정한다. count atom의 값은 0이라는 값 자체로 표현된다. Jotai는 atom의 값이 변경될 때, 의존 관계에 있는 다른 atom을 리렌더링하는데, 이것이 atom의 값이 변경된 것을 알리는 방법이다

 

시그널과 유사한 개발자 경험 제공

시그널은 상태를 관리하는 데 유용한 도구이지만, React에서 사용하기에는 어려움이 있다. Jotai는 atom을 사용하여 시그널과 유사한 방식으로 상태를 관리할 수 있도록 한다.

참고로, 시그널은  SolidJS, Svelte  에서 주로 사용되는 상태관리 라이브러리 이다. 리액트에서는 이 상태관리 라이브러리를 적용하는 것이 어렵다. 이 때, Jotai 는 시그널과 유사한 방식으로 상태관리를 수행할 수 있는 리액트 라이브러리로서 시그널에 익숙한 개발자에게 친숙한 도구로 사용될 수 있다.


설치

# npm
npm i jotai

# yarn
yarn add jotai

# pnpm
pnpm add jotai

 

 

구성

최상의 사용자 경험을 제공하기 위한 별도의 구성을 설정한다(각  환경에 따라 별도 지정).

 

Nest.js(SWC)

# npm
npm install --save-dev @swc-jotai/react-refresh

# next.config.js
experimental: {
swcPlugins: [['@swc-jotai/react-refresh', {}]],
}

 

Next.js(Babel)

# .babelrc
{
"presets": ["next/babel"],
"plugins": ["jotai/babel/plugin-react-refresh"]
}

 

Gatsby ( Babel)

# npm
npm install --save-dev babel-preset-gatsby

# .babelrc
{
"presets": ["babel-preset-gatsby"],
"plugins": ["jotai/babel/plugin-react-refresh"]
}

# gatsby-config.js
flags: {
DEV_SSR: false,
}

 

 

Atom 생성하기

기본 원자(Primitive atoms) |  각 atom 에 데이터 저장

기본 원자는 어떤 타입이든 될 수 있다. 예를 들어 불리언, 숫자, 문자열, 객체, 배열, 집합, 맵 등이 될 수 있다.

 

import { atom } from 'jotai'

const countAtom = atom(0) // 숫자 0을 저장하는 원자

const countryAtom = atom('Japan') // 문자열 "Japan"을 저장하는 원자

const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka']) // 문자열 배열을 저장하는 원자

const animeAtom = atom([
  {
    title: 'Ghost in the Shell', // 객체 배열을 저장하는 원자
    year: 1995,
    watched: true
  },
  {
    title: 'Serial Experiments Lain',
    year: 1998,
    watched: false
  }
])

 

파생원자 (Derived atoms) | 여러 atom 중 선택한 atom 에 저장된 값을 읽어와서 자신의 값으로 반환할 때 사용하는 원자

파생 원자는 다른 원자의 값을 읽어 자신의 값을 반환한다.

 

const animeAtom = atom([
  {
    title: 'Ghost in the Shell', // 객체 배열을 저장하는 원자
    year: 1995,
    watched: true
  },
  {
    title: 'Serial Experiments Lain',
    year: 1998,
    watched: false
  }
])

const progressAtom = atom((get) => {
  const anime = get(animeAtom) // animeAtom의 값을 가져옴(즉, animeAtom에 저장된 객체를 가져온다)
  
  // watched가 true인 anime의 개수 / 전체 anime 개수를 계산하여 반환
  return anime.filter((item) => item.watched === true ).length / anime.length 
  
})

 

Atom 사용

원자는 React 컴포넌트 내에서 상태를 읽거나 쓰기 위해 사용된다. 

 

동일 컴포넌트에서 읽기 및 쓰기 | useAtom :  state와 setState 모두 반환

원자를 동일 컴포넌트 내에서 읽고 쓰는 경우 단순성을 위해 결합된 useAtom 훅을 사용한다. 

import { useAtom } from 'jotai'

const AnimeApp = () => {
  const [anime, setAnime] = useAtom(animeAtom) // animeAtom 값을 읽고 쓰는 useAtom 훅 사용

  return (
    <>
      <ul>
        {anime.map((item) => ( // anime 배열 각 요소를 렌더링하는 리스트
          <li key={item.title}>{item.title}</li> // 각 요소의 title 속성을 출력
        ))}
      </ul>
      <button onClick={() => { // Cowboy Bebop 추가 버튼 클릭 이벤트
        setAnime((anime) => [ // animeAtom 업데이트 함수
          ...anime, // 기존 anime 배열 복사
          { // 새로운 anime 객체 추가
            title: 'Cowboy Bebop',
            year: 1998,
            watched: false
          }
        ])
      }}>
        Add Cowboy Bebop
      </button>
    <>
  )
}


별도 컴포넌트에서 읽기 및 쓰기 | useAtomValue : 값만 반환 , useSetAtom : setState 만 반환

원자 값을 읽거나 쓰기만 하는 경우 성능 최적화를 위해 별도의 useAtomValue와 useSetAtom 훅을 사용한다. 즉, state 와 setState 를 각각 관리하는 훅을 사용

import { useAtomValue, useSetAtom } from 'jotai'

const AnimeList = () => { // Anime 목록 컴포넌트
  const anime = useAtomValue(animeAtom) // animeAtom 값 읽기

  return (
    <ul>
      {anime.map((item) => ( // anime 배열 각 요소를 렌더링하는 리스트
        <li key={item.title}>{item.title}</li> // 각 요소의 title 속성을 출력
      ))}
    </ul>
  )
}

const AddAnime = () => { // Anime 추가 컴포넌트
  const setAnime = useSetAtom(animeAtom) // animeAtom 값 쓰기

  return (
    <button onClick={() => { // Cowboy Bebop 추가 버튼 클릭 이벤트
      setAnime((anime) => [ // animeAtom 업데이트 함수
        ...anime, // 기존 anime 배열 복사
        { // 새로운 anime 객체 추가
          title: 'Cowboy Bebop',
          year: 1998,
          watched: false
        }
      ])
    }}>
      Add Cowboy Bebop
    </button>
  )
}

const ProgressTracker = () => { // 진행률 추적 컴포넌트
  const progress = useAtomValue(progressAtom) // progressAtom 값 읽기

  return (
    <div>{Math.trunc(progress * 100)}% watched</div> // 진행률 표시
  )
}

const AnimeApp = () => { // 전체 앱 컴포넌트
  return (
    <>
      <AnimeList />
      <AddAnime />
      <ProgressTracker />
    </>
  )
}

 

이렇게 각 컴포넌트는 필요한 훅만 사용하여 성능을 최적화할 수 있도록 지원한다는 점을 알 수 있다.

 

참고자료

https://jotai.org/

반응형