[갓생팟 스터디] - Jotai(기본)

수연·2024년 1월 8일

study

목록 보기
2/8
post-thumbnail

이번 리팩토링을 하며 Context API 기반의 코드에서 Jotai 로 바꾸게 되면서 Jotai 에 대한 개념을 보다 잘 이해하기 위해 발표 주제로 선정하게 되었어요.

이번 포스트에선 Jotai 와 Jotai 공식 문서에 Core 로 분류되는 네가지 개념에 대해 소개하려 해요.

Jotai 목차

  • Atom
  • useAtom
  • Store
  • Provider

Jotai

Jotai, 상태 관리를 위해 쓰는 라이브러리예요. 일본어로 '상태' 라는 뜻을 가지고 있어요.

Atom

Atom 은 Jotai 에서 전역 상태를 만드는 가장 기본적인 함수예요.

import { atom } from 'jotai';

const messageAtom = atom('hello');
  • atom 함수는 atom config 를 생성해요. atom config 는 값이 아니라 불변성 객체예요.
  • atom 을 만들 때, 초기 값을 제공해줄 수 있어요.
  • atom 함수를 사용해 읽기, 쓰기, 읽기-쓰기 전용의 파생아톰을 만들 수 있어요.

파생 Atom

위 아톰은 읽기와 쓰기가 모두 되는 아톰이에요.

여기서 아톰의 콜백함수를 통해 읽기 전용, 쓰기 전용 아톰을 만들아 Redux 의 actions 처럼 특정 동작만 허용되는 용도로 사용할 수 있어요.

읽기 전용 Atom

const messageAtom = atom('hello');

const readOnlyAtom = atom((get) => get(messageAtom));
  • 읽기 전용 함수의 get 메서드는 아톰의 값을 가져올 수 있어요.
  • 반응성을 가지고 있고, 의존성을 추적할 수 있어요.

쓰기 전용 Atom

const messageAtom = atom('hello');

const writeOnlyAtom = atom(null, (get, set, update) => {
	set(messageAtom, update);
})
  • 쓰기 전용을 만들기 위해 읽기 전용 Atom 의 부분은 null 값을 넣어줘요.
  • get 은 아톰의 값을 읽어올 수 있지만 의존성이 추적되지 않아요.
  • set 은 아톰의 값을 새로 쓸 수 있어요.
  • update 는 새로 넣어줄 아톰의 값을 의미해요.

읽기-쓰기 전용 Atom

// 읽기-쓰기 전용 아톰
const readWriteAtom = atom((get) => get(message), 
	(get, set, update) => {
		set(messageAtom, update);
	}
)

주의할 점

  • 아톰은 어디서든 생성될 수 있지만 참조값이 늘 동일해야해요.
  • 따라서 렌더링이 발생하는 컴포넌트 안에서 선언하게 되면 아톰도 새로 생성돼요.
  • useMemouseRef 등을 사용하는 것이 좋아요.

useAtom

기본 아톰

위에서 만든 아톰과 파생아톰들은useAtom 을 통해 사용할 수 있어요.

const someAtom = atom('??');

const [value, setValue] = useAtom(someAtom);

React.useState 처럼 useAtom 을 사용해 첫번째 값은 읽기 전용 값을, 두번째론 값을 변경시킬 수 있는 메서드를 받아올 수 있어요.

읽기 전용, 쓰기 전용 아톰

읽기, 쓰기 전용 아톰은 다음과 같이 사용할 수 있어요.

const value = useAtom(readAtom);
const setValue = useAtom(writeAtom);

useAtomValue, useSetAtom

아톰의 읽기, 쓰기 중 둘 중 하나만 하고 싶다면 useAtomValue, useSetAtom 을 사용할 수 있어요.

const value = useAtomValue(readWriteAtom);
const setValue = useSetAtom(readWriteAtom);

이렇게 명확하게 가져오는 것은 [, setValue] = useAtom(atom) 을 사용했을 때보다 렌더링 성능을 높여준다고 해요.

Store

createStore()

createStore 메서드는 새로운 스토어를 만들어요. 이렇게 만들어진 스토어는 Provider 내부로 전달될 수 있어요.

const myStore = createStore();

const Root = () => {
	<Provider store={myStore} />
  		<App />
  	</Provider>
}

스토어를 왜 나누나요?
스토어를 나누지 않으면 하나의 Root 스토어만 존재해요.
해당 Root 스토어에 모든 컴포넌트에서 자유롭게 접근할 수 있다면, 예상치 못한 곳에서 상태가 바뀔 수도 있어요.
따라서 store 를 나누어 각 컴포넌트에서만 사용되는 아톰들을 안전하게 관리할 수 있어요.

스토어의 메서드

스토어는 get, set, sub 의 세가지 메서드를 가질 수 있어요.

  • get
    아톰의 값들을 가져와요.
  • set
    아톰의 값을 바꿔요.
  • sub
    아톰을 구독하고, 아톰이 변화할 시 행동을 지정할 수 있어요.
const myStore = createStore();

const someAtom = atom(0);

myStore.set(someAtom, 1); 

// sub 메서드로 만들어진 unsub은 구독을 취소할 수 있어요
const unsub = myScore.sub(someAtom, () => {
	console.log('Atom 이 변경되었어요!');
})

getDefaultStore

getDefaultStore 는 Provider 가 없는 경우 기본 스토어를 반환해주는 함수예요.

Provider

Provider 컴포넌트는 서브 트리 컴포넌트를 위한 상태를 제공해요. React 의 Context 처럼 사용된다고 생각하면 정답 🙆‍♀️

각 서브트리마다 다른 상태를 제공해주고 싶은 경우

같은 atom 을 사용하더라도 참조하는 Provider 가 다른 경우, 다른 값을 가지게 돼요.

import { Provider } from 'jotai';

export const someAtom = useAtom(1);

const Root = () => {
	return (
    	<Provider>
        	<Child1 /> 
        </Provider>
		<Provider>
        	<Child2 />
        </Provider>
    )
}

<Child1 /> 컴포넌트와 <Child2 /> 컴포넌트는 서로 다른 아톰의 값을 가지게 돼요.

아톰의 초기값을 받고 싶은 경우

아톰의 값을 각각 다르게 가지는 Provider 의 특성과useHydrationAtoms 을 사용하여 아톰의 초기값을 설정할 수 있어요.

<Provider>
  <AtomHydrator initialValues={[[someAtom, 1]]}>
	<Component />
  </AtomHydrator>
</Provider>
// AtomHydrator.js
import { useHydrateAtoms } from 'jotai';

const AtomHydrator = ({initalValues, children}) => {
	useHydrateAtoms(new Map(atomValues));
	 return chilren;
}

⚠️ 타입스크립트에선 useHydrateAtoms 가 일종의 오버로드 함수이지만, 타입스크립트는 오버로드된 함수의 타입을 추측할 수 없기 때문에 Map 을 사용해 초기값을 전달하는 것이 권장돼요

remounting 시 모든 아톰을 초기화하고 싶은 경우

마찬가지로 Provider 와 atomWithReset, useResetAtom 으로 아톰을 초기화시켜줄 수 있어요.

atomWithReset 으로 Resettable 한 아톰을 만들고, useResetAtom 훅을 사용해서 초기 값으로 되돌려줄 수 있어요.

import { atomWithReset } from 'jotal/utils';

export const dollarsAtom = atomWithReset(0);

const Root = () => {
	<Provider>
    	<App />
    </Provider>
}
// App.js
import { useResetAtom } from 'jotai/utils';

const App = () => {
	const resetDollars = useResetAtom(dollarsAtom);
  
  	useEffect(() => {
    	resetDollars();
    }, []);
}

발표 Q & A

❓ 실제로 store.set() 을 써본 적이 있나요? 언제 쓰나요?


  • 다른 컴포넌트에서의 무분별한 아톰 사용을 막기 위해서 응용할 수 있을 것 같아요
  • 특정 Atom을 store 에 저장해두고 Provider 내부의 컴포넌트만 사용할 수 있도록 하는 방법 (Context API 와 유사)
    <Provider store={MyStore}>
    	<MyComponent /> 
    </Provider>
    <YourComponent />
  • 현재 아래 코드는 전역 store 에 Atom 을 저장해놓고 아무 곳에서나 export 가능한 상태예요
    // /stores.js
    
    export const atom1 = useAtom(0);
    export const atom2 = useAtom(1)
  • 하지만 store 를 만들어서 store 자체를 반환하면 해당 store 안에만 있는 Atom 만 사용 가능해요.
    • 이때 Atom을 store 에 저장하기 위해 store.set() 을 사용할 수 있어요.
	const myStore = createStore();

	const atom1 = useAtom(0);
	const atom2 = useAtom(1);

	myStore.set(atom1);
	myStore.set(atom2);

	export default myStore;

❓ useHydrateAtoms 는 초기화시켜줄 때 사용하나요?

원래 utils 함수 설명하면서 다루려고 했는데, 잘 사용하지 않을 것 같아서 질문으로 대체할게요!

  • 여러 아톰을 초기화하는 데 사용해요.
    • Provider 내부에서 초기화 시켜주는 경우
    • Next JS 와 같이 서버에서 초기값을 가져와서, props 을 통해 컴포넌트에 전달하는 경우
import { atom, useAtom } from 'jotai';
import { useHydrateAtoms } from 'jotai/utils';

const countAtom = atom(0);

const CounterPage = ({ countFromServer }) => {
  useHydrateAtoms([[countAtom, countFromServer]]);

  const [count] = useAtom(countAtom);
	// `countFromSever` 의 값으로 바뀐다
}
  • values: Iterable<readonly [Atom<unknown>, unkonwn]> [atom, value] 로 이뤄져있는 iterable 한 튜플을 받아요.
  • options? 특정 스토어를 지정하거나, 강제 redydrate 를 할수 있는 옵션이에요.

0개의 댓글