[ 살펴보기 ] Jotai - Atom

[ 살펴보기 ] Jotai - Atom

·

4 min read

Global state management 중 Recoil과 유사한 library를 찾고 있다면 아마 1순위가 Jotai가 되지 않을까 싶다. Recoil을 이미 사용해 본 경험이 있다면 Jotai 역시 금방 익숙하게 사용할 수 있으리라 생각한다. 이번 포스팅에서는 Jotai의 기본이 되는 atom에 대해 살펴보자

Atom config

Jotai에서는 state의 단위를 atom config ( 또는 atom )이라고 부른다. Recoil과는 다르게 기본적으로 app을 Provider로 따로 wrapping하지 않고 바로 사용할 수 있다.( provider-less mode ) 아래 예제를 통해 기본적인 사용법을 살펴보자

import { atom } from 'jotai'

const animalNameAtom = atom('jack')
const personAtom = atom({ name: 'jack', age:20 })

위의 예제에서 살펴볼 수 있듯이 atom의 선언은 굉장히 심플하게 할 수 있다. Recoil과 차이점이 있다면 Jotai의 atom은 별개로 atom의 id를 설정할 필요가 없다는 것이다.

그리고 Recoil도 또 다른 차이점이 있다면 Jotai에서는 selector가 별도로 존재하지 않는다, Derived state 관리 역시 또 다른 atom을 통해 이루어 지며 atom을 선언하는 방식에 따라 read-only atom 또는 write-only atom 또는 read-write atom인지 결정된다. 다음의 예를 살펴보자

const personAtom = atom({ name: 'jack', age:20 })

const readOnlyAtom = atom((get) => get(personAtom))

const writeOnlyAtom = atom(
  null,
  (get, set, newName) => {
    set(personAtom, (person) => ({...person, name:newName}))
  },
)

const readAndWriteAtom = atom(
  (get) => get(personAtom),
  (get, set, newName) => {
   set(personAtom, (person) => ({...person, name:newName}))
  },
)

readOnlyAtom 처럼 atom의 첫 번째 parameter에 함수를 넘기면 해당 atom은 업데이트를 할 수 없는 read-only atom이 된다. 만약 writeOnlyAtom처럼 두 번째 parameter만 함수를 넘기면 write 기능만 수행할 수 있는 write-only atom이 되며 readAndWriteAtom의 예제처럼 둘 다 넘기게 되면 read와 write가 모두 되는 atom이 되는 것이다

Read-only Atom

const personAtom = atom({ name: 'jack', age:20 })

const readOnlyAtom = atom((get) => get(personAtom))

위의 예제 처럼 첫 번째 parameter로 함수를 넘길 때 반환되는 get function을 통해 다른 atom의 값을 읽어올 수 있다.

Read & Write Atom

const personAtom = atom({ name: 'jack', age:20 })

const readAndWriteAtom = atom(
  (get) => get(personAtom),
  (get, set, newName) => {
   set(personAtom, (person) => ({...person, name:newName}))
  },
)

하지만 마냥 read만을 위해 Jotai와 같은 global state library를 사용하진 않을 것이다, 만약 업데이트도 가능한 atom을 만들고자 한다면 위의 예제와 같이 두 번째 parameter로 함수를 전달한다, 그리고 해당 함수에서 return해주는 set을 통해 다른 atom을 업데이트 할 수 있다.

useAtom

React에서 useState를 통해 state 값에 접근 및 수정을 할 수 있듯이 Jotai에서는 useAtom을 통해 atom 값에 접근 및 수정이 가능하다, 간단한 예시를 살펴보자

const UserComp = () => {
  const [user, setUser] = useAtom(readAndWriteAtom);
  const handleAtomUpdate = () => {
    setUser("new name");
  };
  ...
}

위의 예제에서 상단에서 정의했던 readAndWriteAtom atom의 값을 업데이트 하고 있다 setUser를 통해 parameter를 전달하면 readAndWriteAtom atom 두 번째 parameter로 전달했던 함수의 세 번째 parameter로 해당 값을 받게 된다

const readAndWriteAtom = atom(
  (get) => get(personAtom),
  (get, set, newName) => {
   /* 
      위의 예제 기준으로
      newName parameter에는 "new name"이라는 값이 전달된다 
   */
   set(personAtom, (person) => ({...person, name:newName}))
  },
)

useSetAtom

만약 특정 state의 변화를 subscribe하지 않고 update하는 기능만 필요할 때는 useSetAtom을 사용할 수 있다. 아래의 예제같이 처리되었을 때 readAndWriteAtom atom이 변경되었다 하더라도 해당 component는 다시 render되지 않는다.

const UserComp = () => {
  const setUser = useSetAtom(readAndWriteAtom);
  return (
    ...
  );
};

export default UserComp;

Provider를 통해 전역 scope 분리

Jotai에 대한 또 다른 흥미로운 점은 Provider를 통해 전역 scope를 분리할 수 있다는 것이다. 다음의 예를 살펴보자

// state/user.ts
export const userCountAtom = atom(1);

// page/user
import { Provider } from "jotai";

const UserJotai = () => {
  return (
    <div>
      <UserChild />
      <Provider>
        <UserGrandChild />
        <UserAnotherChild />
      </Provider>
    </div>
  );
};

export default UserJotai;

만약 위의 코드에서 UserChild, UserGrandChild, UserAnotherChild component 모두에서 userCountAtom을 사용하고 있다면 UserChild에서 관리되고 있는 userCountAtom과 Provider로 wrapping되어 있는UserGrandChild, UserAnotherChild component에서 사용하는 userCountAtom은 서로 다르게 관리된다

예를들어 UserChild에서 다음과 같이 atom을 업데이트 한다고 해도 Provider로 wrapping한 UserGrandChild, UserAnotherChild component의 userCountAtom의 atom에는 변화가 생기지 않는다. 즉, React Context처럼 Provider에 의해 영향을 주는 scope가 나뉘어 진다

const UserChild = () => {
  const [count, setCount] = useAtom(userCountAtom);
  const handleAtomUpdate = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <h3>User Child</h3>
      /*
         update button을 클릭하면 count는 2가 됨
      */
      <div>{count}</div> 
      <div>
        <button onClick={handleAtomUpdate}>Update</button>
      </div>
    </div>
  );
};
export default UserChild;
const UserGrandChild = () => {
  const [count, setCount] = useAtom(userCountAtom);
  const handleAtomUpdate = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <h3>User Grand Child </h3>
      <div>{count}</div> 
      /*
        UserChild에서 userCountAtom가 변경되어도 여기선
        변경이 발생하지 않는다
        ( 해당 component를 wrapping하고 있는 Provider로 인해 )
      */
      <div>
        <button onClick={handleAtomUpdate}>Update</button>
      </div>
    </div>
  );
};
export default UserGrandChild;

이런 특징에 대해서는 잘 인지하고 적용할 때 주의가 필요할 듯 하다. Global 하게 관리하고자 했던 state가 Provider로 인해 원치 않은 동작으로 이어질 수도 있다. Global State를 어떻게 관리할 것인지에 대한 보다 섬세한 설계가 요구될 것 같다.

Store

만약 Provider로 구획한 부분의 특정 atom의 default value를 다른 부분과 다르게 설정하고 싶다면 어떻게 해야할까? 이럴 경우에는 store는 통해 특정 atom의 대한 데이터를 설정하여 Provider에 전달할 수 있다.

다음의 예제를 살펴보자

// state/user.ts
export const userCountAtom = atom(1);

// page/user
import { Provider, createStore } from "jotai";
import { userCountAtom } from "state/user.ts"

const myStore = createStore();
myStore.set(userCountAtom, 2);

const UserJotai = () => {
  return (
    <div>
      <UserChild />
      <Provider store={myStore}>
        <UserGrandChild />
        <UserAnotherChild />
      </Provider>
    </div>
  );
};

export default UserJotai;

위와 같이 store를 Provider에 제공하면 Provider 아래의 component에서 userCountAtom atom을 사용할 때 그 default 값이 2로 설정되어 있다는 것을 확인할 수 있다