Jotai

박찬영·2024년 3월 25일
0

👻 설명 👻

Jotai는 리액트 애플리케이션에서 상태 관리를 위한 간단하고 현대적인 라이브러리입니다.
이 라이브러리는 Context API와 리액트 훅을 기반으로 만들어졌으며, 상태를 최소 단위인 "원자 (Atoms)"로 분해하여 관리하는 Atomic 디자인 패턴을 채택합니다.
jotai에 대해 찾아보면서 context와 비슷하다고 느꼈었는데, 실제로 다른 상태 관리 라이브러리들보다는 context나 useState와 비교되는 경우가 더 많습니다.

Atomic 접근 방식
이 패턴은 상태를 최소 단위인 "원자 (Atoms)" 단위로 분해하여 관리하는 방식을 의미합니다.

주의점
next.js 버전이 14가 되었는데 jotai ver2와 일부 충돌이 일어나기 때문에 next 버전을 13으로 다운그레이드 하고 진행해야 합니다.

jotai 를 시작하기 전에 앱 컴포넌트 내의 router 를 Provider 로 감싸 주어야 합니다.
쉽게 생각하자면 jotai 가 해당 요소들을 항상 감시하고 변경사항이 생긴다면 변경사항 부분만 바꿔 준다고 생각하시면 됩니다.

import { Provider } from 'jotai';

function App() {
  return (
    <Provider>
      <Routes>
        <Route path="/home" element={<Home/>}/>
      </Routes>
    </Provider>
  );
}

export default App;

atom

jotai의 atom은 상태 조각 아주 작은 단위의 상태를 의미합니다.
상태들을 만들기 위한 atom 함수는 초기값으로 string, number, object, array와 같은 원시값을 받을 수 있습니다.

const priceAtom = atom(1000);
const priceAtom = atom([1,3,2,4,5]);
...

단순하게 값만 넣고 사용하면 좋겠지만 개발자 입장으로써 이정도의 기능이 필요하다면 useState 를 쓰면 됩니다.
하지만 저희는 똑똑한 개발자로써 atom 의 자세한 기능을 알 필요가 있습니다.😊😊

jotai의 atom은 아래 케이스로 나눌 수 있습니다.

  • 🍎 Read-only atom (값을 가져오는 경우)
  • 🍎 Write-only atom (값을 업데이트)
  • 🍎 Read-Write atom (둘 다)

🍎 Read-only atom (값을 가져오는 경우)

이 같은 경우에는 기존의 있었던 atom 을 가공해서 새로운 값으로 만들 수 있습니다.

const priceAtom = atom(1000)
const readOnlyAtom = atom((get) => get(priceAtom) * 2); // 2000

🍎 Write-only atom (값을 업데이트)

기존에 있었던 아톰을 특정한 조건에 따라 값을 바꿔서 새로운 값으로 만들어 줍니다.

여기서 주의할 점은 기존에 있었던 아톰의 변수값은 바뀌지 않는 불변성 인걸 기억하세요~

interface Point {
    num: number;
}

const priceAtom = atom(1000);

const handleMouseMoveAtom = atom(
	null, // 첫번째 인자로 전달하는 초기값은 null로 전달
  	(get, set, update: Point) => {
      set(priceAtom, (prev) => prev * price);
    }
)

// or

const handleMouseMoveAtom = atom(
	null,
  	(get, set, update: Point) => {
      set(priceAtom, get(priceAtom) * price)
    }
)

----------------------------------------------------

// home.jsx

const [doublePrice, setDoublePrice] = useAtom(handleMouseMoveAtom)

<button onClick={() => setDouble(2)}>Set Doubled Counter Value</button>
<p>{doublePrice}</p>

이렇게 설정하면
set(priceAtom, (prev) => prev + price); -> set(1000, (1000) => 1000 * 2) 형태가 된다.

// get -> 다른 atom의 value를 가져오는 함수이다.
// set -> 다른 atom의 값을 변경하는 함수이며 set(atom, value)의 형태로 사용한다.
// update -> 인자로 받아온 파라미터 값이다.

🍎 Read-Write atom (둘 다)

앞서 설명해 드린 Write-only atom , read-only atom 을 합쳤다고 생각하시면 됩니다.

const counterAtom = atom(1); 
const doubledCounterAtom = atom(
  (get) => get(counterAtom) * 2,
  (get, set, newValue) => {
    set(counterAtom, newValue / 2); 
  }
);

// 설명하자면 useAtom 을 사용하여 값, 상태를 변경하는 함수를 가져올 텐데 
// 값을 가져올 때는 기존에 있었던 counterAtom 의 값에서 * 2 를 해서 가져오고
// 상태를 변경하는 함수는 나누기 2 를 하는 함수이기 때문에 ( 해당 함수의 파라미터값 / 2 ) 를 해줍니다.

----------------------------------------------------

// ExampleComponent.jsx

function ExampleComponent() {
  
  const [doubledCounterValue, setDoubledCounterValue] = useAtom(doubledCounterAtom);

  return (
    <div>
      <p>Counter Value: {doubledCounterValue}</p>
      <button onClick={() => setDoubledCounterValue(10)}>Set Doubled Counter Value</button>
    </div>
  );
}

// (get) => get(counterAtom) * 2 의 뜻은
// 초기값의 2배를 해서 나타내 준다는 뜻이다.
// 따라서 doubledCounterValue 는 2가 된다.

// set(counterAtom, newValue / 2) 의 뜻은 
// counterAtom = newValue / 2 라는 뜻이랑 똑같다.
// setDoubledCounterValue(10) 이니까 counterAtom = 10 / 2 가 된다.

그럼 버튼을 눌렀을 때 doubledCounterValue 의 값은 무엇이 될까요??
바로 5입니다.

useAtom

useAtom hook은 상태에 있는 atom 값을 읽어오고, 값과 업데이트 함수를 배열 형태로 반환합니다. (useState랑 엄청 비슷하네요😊😊)
초기에는 state에 저장된 값이 없다가 useAtom 함수를 통해 atom이 최초로 불러와지면, 초기값이 상태에 저장됩니다

atom 의 엄청난 장점
atom이 더이상 사용되지 않으면 (그 상태를 사용하는 모든 컴포넌트들이 언마운트되면) 값은 가비지 콜렉트됩니다.

useAtom 도 마찬가지로 값, 업데이트, 둘다 할수 있는 함수로 나누어져 있습니다.

  • 🍎 useAtomValue (값을 가져오는 경우)
  • 🍎 useSetAtom (값을 업데이트)
  • 🍎 useAtom (둘 다)
const [value, setValue] = useAtom(atom)
const [value] = useAtomValue(atom)
const [setValue] = useSetAtom

atomWithStorage

atomWithStorage 는 localStorage, Session Storage에 상태를 저장을 해주는 좋은 기능입니다.

import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

const darkModeAtom = atomWithStorage('darkMode', false)

const Page = () => {
  const [darkMode, setDarkMode] = useAtom(darkModeAtom)

  return (
    <>
      <h1>Welcome to {darkMode ? 'dark' : 'light'} mode!</h1>
      <button onClick={() => setDarkMode(!darkMode)}>toggle theme</button>
    </>
  )
}

비동기 데이터 가져오기

jotai 의 다양한 기능 중 비동기 데이터를 서버에서 가져오는 기능도 있습니다.

🍎 atomWithQuery
🍎 atomWithMutation
🍎 atomWithInfiniteQuery
🍎 atomWithSuspenseQuery
🍎 atomWithSuspenseInfiniteQuery

이중에서 대표적으로 사용하는 atomWithQuery, atomWithMutation 을 설명하겠습니다.

나머지는 예시를 들기에 소~~올직히 코드를 만들기 귀찮아서 공식 문서를 보시면 좋겠습니다 ㅎㅎ..

공식문서에 따르면 비동기 데이터를 가져오기 위해서는 추가적인 라이브러리 설치가 필요합니다.

npm i jotai-tanstack-query @tanstack/query-core wonka

👻 atomWithQuery

react-query 를 사용하고 있는 개발자라면 함수명을 보자마자 이해하실 거라고 생각합니다.
atomWithQuery 는 서버에서 비동기 데이터를 가져오는 함수입니다.
서버에서 데이터를 가져오는 get 이 이에 해당됩니다.

import { atom, useAtom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)
const userAtom = atomWithQuery((get) => ({
  queryKey: ['users', get(idAtom)],
  queryFn: async ({ queryKey: [, id] }) => {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
    return response.data
  },
}))

const UserData = () => {
  const [{ data, isPending, isError }] = useAtom(userAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return <div>{JSON.stringify(data)}</div>
}

👻 atomWithMutation

atomWithMutation 은 CRUD 기능 중 C,U,D 기능을 담당합니다.
즉 post, delete 등이 이에 해당됩니다.

해당 함수를 사용하면 mutate, status 라는 두개의 변수를 받습니다.
mutate 는 서버에 보내고 싶은 데이터를 넣어주는 함수입니다.

status 는 해당 atomWithMutation 으로 서버와 통신할 때 상태를 나타냅니다.
🍎 loading
뮤테이션이 진행 중인지 여부를 나타내는 부울 값입니다.
🍎 error
뮤테이션이 실패하였을 때의 에러 메시지 또는 객체입니다.
🍎 data
뮤테이션이 성공적으로 완료되었을 때의 결과 데이터입니다.

const postAtom = atomWithMutation(() => ({
  mutationKey: ['posts'],
  mutationFn: async ({ title }: { title: string }) => {
    const response = await axios.post(`https://jsonplaceholder.typicode.com/posts`, title)
    return response.data
  },
}))

const Posts = () => {
  const [{ mutate, status }] = useAtom(postAtom)
  
  if(status.loading) return <div>로딩중입니다..</div>
  if(status.error) return <div>에러가 발생했습니다..</div>
  
  return (
    <div>
      <button onClick={() => mutate({ title: 'foo' })}>Click me</button>
      <pre>{status.data}</pre>
    </div>
  )
}

더 많은 기능은 현재 공부중입니다. 시간이 생길때 마다 추가할 예정입니다.👻👻

jotai 공부중.....👻👻👻👻👻

profile
오류는 도전, 코드는 예술

0개의 댓글

관련 채용 정보