상태관리 라이브러리 종류
- proxy patten : Mobx, Valtio
- flux patten: Redux, Zustand
- atomic patten: Recoil, Jotai
위 언급한 것과 같이 아토믹 패턴에 포함되는 것이 조타이이다.
조타이 공식문서를 보면 다음과 같이 설명한다.
Jotai는 Recoil 에서 영감을 받은 원자 모델로 상향식 접근 방식을 취합니다 . 원자를 결합하는 상태를 구축하고 원자 종속성을 기반으로 렌더링을 최적화할 수 있습니다. 이렇게 하면 메모이제이션이 필요하지 않습니다.
특징:
- 최소 코어 API(2kb)
- 많은 유틸리티 및 통합TypeScript 지향
- Next.js, Gatsby, Remix 및 React Native와 함께 작동
- SWC 및 Babel 플러그인으로 빠른 새로 고침 반응
리코일보다 더 쉽게 사용할 수 있다는 이야기를 듣고 한번 사용해보고 싶다는 생각이 들었다.
모든 것들은 공식문서를 기반으로 찾아보았다.
1. 조타이 기본 구성 요소
조타이에는 atom과 useAtom이 있다. 이는 리코일과 비슷하다.
대신 리코일에 있는 셀렉터가 없다.
아톰은 다른 아톰을 가지고 올 수 있고, 가공할 수 도 있다.
다른 부분으로 store와 provider가 있다.
이는 다른 redux에 있는 개념인 것 같은데 리덕스를 몰라서 이해가 어려웠다.
공식문서에는 provider을 통해 store를 하위 컴포넌트들로 내려주는 것 같다.
근데 아톰을 사용하면 그럴 이유가 없는데 왜 그런지 모르겠다...
그래서 일단은 모르겠는 것은 빼고 아톰을 중심으로 연습을 해봤다.
2. 기본 아톰 만들고 사용해보기
//기본값 지정
const priceAtom = atom(기본값)
//읽기전용
const readOnlyAtom = atom((get) => get(priceAtom) * 2)
//쓰기전용(그럼 첫번째 인수에 null)
const writeOnlyAtom = atom(
null,
(get, set, update) => {
set(priceAtom, get(priceAtom) - update.discount)
}
)
//읽기+쓰기 가능
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2)
}
)
//첫번째 인수로 read 지정
//두번째 인수로 write 지정
const anotherAtom = atom(
(get) => get(다른아톰)+가공,
(get, set, 업데이트할 값) => {
set(바꿀 아톰, 없데이트할 값)
}
)
이런식으로 atom에 초기값을 넣을 수 있다.
그리고 이렇게 get/set을 할 수 있다.
이건 다른 곳에서 useState를 이용해 사용할 수 있다.
const anAtom = atom(0);
//값, setting 모두 가능
const [value, setValue] = useAtom(anAtom);
//setting만 가지고 오고 싶을 때
const setCount = useSetAtom(countAtom)
//값만 가지고 오고 싶을때
const count = useAtomValue(countAtom)
이런식으로 사용할 수 있다.
확실히 리코일보다 키값이 없고 셀렉터가 없어서 더 단출하고 좋았다.
useState와 비슷해서 사실 atom, useAtom만 사용해도 정말 좋다고 생각한다.
그럼 실제로는 어떻게 사용할 수 있을까?
3. 실제 실험 코드
최상위에 페이지 > 그 안에 유저 페이지 > 그 안에 취미 페이지 > 그 안에 input이 있다.
그 각 컴포넌트는 오른쪽의 아톰을 사용한다.
초기 데이터는 임의로 만들 프로미스로 fetch처럼 만들었다.
이걸 가지고 와서 setting을 하고 사용자의 상호작용에 따라 취미정보를 수정한다.
//atom.js
import { atom } from "jotai";
//사용자 promise객체
const promiseUser = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ name: "gildong", level: 3, id: 1 });
}, 5000);
});
//이름은 신경쓰지 마세요.. 사용자 정보 아톰 만들기
export const fetchUserAtom = atom(promiseUser);
//조타이 onMount 이용해서 해당 아톰이 만들어질때? 사용될때? 불러질때? 수행되는 함수로 프로미스 객체 setting
fetchUserAtom.onMount = (setAtom) => {
console.log("mount user fetch");
return async () => await promiseUser.then((res) => setAtom(res));
};
//사용자 정보 getter atom
export const userAtom = atom((get) => get(fetchUserAtom));
//취미 promise객체
const promiseHobby = new Promise((resolve, reject) => {
console.log("fetch");
setTimeout(() => {
resolve({ 영화보기: true, 운동하기: false, 요리하기: true });
}, 3000);
});
//이름은 신경쓰지 마세요.. 취미 정보 아톰 만들기
const fetchHobbyAtom = atom({
영화보기: false,
운동하기: false,
요리하기: false,
});
//조타이 onMount 이용해서 해당 아톰이 만들어질때? 사용될때? 불러질때? 수행되는 함수로 프로미스 객체 setting
fetchHobbyAtom.onMount = (setAtom) => {
console.log("mount fetch");
return async () => await promiseHobby.then((res) => setAtom(res));
};
//취미 getter + value수정 setter
export const hobbyAtom = atom(
(get) => get(fetchHobbyAtom),
(get, set, updateData) =>
set(fetchHobbyAtom, (origin) => ({
...origin,
[updateData]: !origin[updateData],
}))
);
//취미 getter + 취미 추가 setter
export const addHobbyAtom = atom(
(get) => get(fetchHobbyAtom),
(get, set, updateData) =>
set(fetchHobbyAtom, (origin) => {
console.log(updateData);
return {
...origin,
...updateData,
};
})
);
여기서 보면 취미의 경우 초기값으로 {..}를 주고 유저의 경우 프로미스 객체를 바로 주었는데, 이게 차이가 있다.
조타이의 경우 suspense를 지원하기 때문에 아톰에 프로미스가 있는 경우 suspense를 이용해 대체컴포넌트를 그려줄 수 있기 때문이다.
다만 취미처럼 안에 초기값을 넣고 onMount를 사용하는 경우 이것이 적용되지 않는다.
그럼 promise객체를 넣고 suspense를 이용하면 되지 왜 초기값을 넣어서 이용하지 않느냐?
현재에는 promise객체를 넣으면 프로미스객체가 초기값이 되어서 다음 setting이 제대로 되지 않는다...
그래서 일단 user에만 넣었다.
왜 또 onMount를 해서 동일한 promise를 또 setting하냐?
저 안에 함수를 넣으면 readOnly처리가 된다. 때문에 그렇기때문에 변경이 불가능해진다.
일단 임시방편으로 프로미스 객체를 풀어 value를 꺼내기 위해 onMount를 했는데 권장할만한 방법은 아니라고 생각된다.
근데, 이게 만약 그냥 fetch함수였다고 해도 동일한 결과(readonly가 되는것)가 벌어질 것이라 예상된다...
suspense를 이용하기 위해 어떻게 해야할지 잘 모르겠다.
암튼.. 이렇게 아톰을 만들었으면 사용을 해야한다.
차례로 상위 컴포넌트이다.
//page.jsx
import { Suspense } from "react";
import UserPage from "../component/UserPage";
export default function JotaiTest() {
return (
<div>
<h1>조타이 연습하기</h1>
<Suspense fallback={<div>"Loading..."</div>}>
<UserPage />
</Suspense>
</div>
);
}
위처럼 프로미스 객체가 pending일때 suspense를 통해 다른 컴포넌트를 보여줄 수 있다.
이건 조금 다른 개념이라서 더 검색해보면 좋다.
//userPage.jsx
import { useAtom } from "jotai";
import { hobbyAtom, userAtom } from "../hook/atom";
import HobbyPage from "./HobbyPage";
export default function UserPage() {
const [userData] = useAtom(userAtom);
const [hobby] = useAtom(hobbyAtom);
const hobbyName = Object.keys(hobby);
return (
<>
<div>{userData.name}</div>
{hobbyName.map((name) => {
return hobby[name] && <span key={name}>{name}</span>;
})}
<HobbyPage />
</>
);
}
useState처럼 useAtom의 첫번째 변수는 값이기때문에 이렇게 작성해도 문제없이 돌아간다.
//AtomPage.jsx
import { useAtom } from "jotai";
import { hobbyAtom } from "../hook/atom";
import InputHobby from "./InputHobby";
export default function HobbyPage() {
const [hobbyData, toggleHobby] = useAtom(hobbyAtom);
const hobbyName = Object.keys(hobbyData);
return (
<>
<InputHobby />
<fieldset>
<legend>취미</legend>
{hobbyName.map((name) => {
return (
<label key={name}>
<input
type="checkbox"
checked={hobbyData[name]}
onChange={() => toggleHobby(`${name}`)}
/>
{name}
</label>
);
})}
</fieldset>
</>
);
}
여기에선 체크박스를 클릭하면 hobby toggle이 일어나도록 만들었다.
//InputHobby.jsx
import { useState } from "react";
import { addHobbyAtom } from "../hook/atom";
import { useAtom } from "jotai";
export default function InputHobby() {
const [content, setContent] = useState("");
const [_, addHobby] = useAtom(addHobbyAtom);
const handleFormSubmit = (e) => {
e.preventDefault();
addHobby({ [content]: true });
setContent("");
};
return (
<form onSubmit={handleFormSubmit}>
<input value={content} onChange={(e) => setContent(e.target.value)} />
<button type="submit">제출</button>
</form>
);
}
마지막 취미 입력 컴포넌트는 addHobbyAtom을 이용한다. 해당 setter는 사용하지만 값은 사용하지 않아 _,처리를 했다.
개발을 할때 해당 순서 인수를 비워야할때 임의값을 입력하기보단 _를 입력해 자리는 채우되 사용은 하지 않는 경우가 있다.
암튼 이렇게 사용할 수도 있고, 혹은 useSetAtom을 이용할 수도 있다.
결과적으로 리코일보다 편한 것 같기는한데..
내가 잘 사용하지 못하는거일지도..?
패치하는게 어렵다.
04. 결과화면
05. 궁금증
- 아톰을 한번 초기값으로 넣으면 해당 타입을 꼭 지켜야 하나? 왜 다음에 setting을 해도 안바뀌지?
> 현재는 취미 아톰에 초기값으로 프로미스를 넣었을때 onMount가 적용 되서 value로 초기값이 될 거라고 생각했는데 안됨.. 그냥 promise가 아톰에 있음
- get api로 가지고 온 값들을 저장하고 이용하고 싶으면 어떻게 해야 하지?
- get api로 가지고 온 값들을 변환해서 저장하고 싶을때는 어떻게 해야 하지?
- store와 provider은 뭐지?
참고자료
조타이 공식문서: https://jotai.org/docs/introduction
Introduction — Jotai
Table of contents
jotai.org
https://velog.io/@eunbinn/you-might-not-need-react-query-for-jotai
[번역] Jotai에 React Query가 필요하지 않을 수도 있습니다
Jotai를 만든 Daishi Kato의 글로 Jotai를 위한 데이터 페칭 API는 무엇일지 찾아가는 여정을 담고 있습니다.
velog.io
https://blog.hwahae.co.kr/all/tech/6099
'개인공부' 카테고리의 다른 글
[React] useState(value) vs useState(callbackFn) (1) | 2023.12.28 |
---|---|
[REACT] React 렌더링 과정? (0) | 2023.04.26 |
[IntersectionObserver] 바닐라자스_무한 스크롤 만들기 (1) | 2023.03.22 |
[비동기] promise (0) | 2023.03.17 |
[CSS] display속성 발표자료 (0) | 2023.03.05 |