๐ก 06. atomFamily โ ID ๋ณ ๋์ atom ์์ฑ ํจํด
๐ ๊ฐ์
atomFamily์ ๊ฐ๋ , jotai-family๋ก์ ๋ง์ด๊ทธ๋ ์ด์ , ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง, ๋ฆฌ์คํธ ์์ดํ ๋ณ ๋ ๋ฆฝ ์ํ ๊ด๋ฆฌ๋ฅผ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
atomFamily๊ฐ "ํ๋ผ๋ฏธํฐ๋ณ atom ์บ์" ๋ผ๋ ๊ฐ๋ ์ ์ค๋ช ํ ์ ์๋ค.jotai/utils์atomFamily๊ฐ deprecated ๋์ดjotai-familyํจํค์ง๋ก ๋ง์ด๊ทธ๋ ์ด์ ํด์ผ ํ๋ค๋ ๊ฒ์ ์๋ค.- ๋ฌดํ ์คํฌ๋กค ํ๊ฒฝ์์
remove()์setShouldRemove()๋ก ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๋ฐฉ์งํ ์ ์๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ atomFamily ๊ฐ ํ์ํ๊ฐ?
- โ ๏ธ DEPRECATED ๊ฒฝ๊ณ โ jotai-family ๋ก ๋ง์ด๊ทธ๋ ์ด์
- ๐๏ธ atomFamily ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
- ๐งน ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง โ remove ์ setShouldRemove
- ๐ท TypeScript ์ ํจ๊ป โ ์ ๋ค๋ฆญ ํ์ ๋ช ์
- โ๏ธ TanStack Query queryKey ์์ ์ญํ ๋น๊ต
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 12๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[๊ณ ์ atom ์์ญ ๊ฐ ์ ์ธ ์ง์ฅ] โ [atomFamily ๊ฐ๋ ] โ [jotai-family ๋ง์ด๊ทธ๋ ์ด์ ] โ [๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ] โ [๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง] โ [TypeScript ํ์ดํ]
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
์์ฒ ์ด๊ฐ ์คํฐ๋ ์นด๋๋ง๋ค ์ข์์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ์์ ์ ๋งก์ ๋ ์ด์ผ.
๐ฃ ์์ฒ : (์ฌ๋์์ ์ํธ ๋ ๋ฉ์ ) "์ํธ ๋! ์คํฐ๋ ์นด๋ ์ข์์ ๊ธฐ๋ฅ ๊ตฌํํ์ด์. ๋ฆฌ๋ทฐ ๋ถํ๋๋ ค์!"
๐ฆ ์ํธ ๋: (์ฝ๋ ์ด๊ณ ์ ์ ์นจ๋ฌต) "์์ฒ ๋... ์ด๊ฒ
likeCount_001,likeCount_002,likeCount_003...?"๐ฃ ์์ฒ : (์์ ์๊ฒ) "๋ค! ์คํฐ๋ ID ๋ง๋ค atom ๋ฏธ๋ฆฌ ๋ง๋ค์ด๋์ด์. ์คํฐ๋๊ฐ ์ง๊ธ 50๊ฐ๋๊น atom 50๊ฐ ์ ์ธํ์ด์!"
๐ฆ ์ํธ ๋: "๋ง์ฝ ์คํฐ๋๊ฐ 1000๊ฐ๊ฐ ๋๋ฉด์? atom 1000๊ฐ ์ ์ธํ์ค ๊ฑด๊ฐ์?"
๐ฃ ์์ฒ : (๋ง๋ฌธ์ด ๋งํ์) "...์."
๐ฆ ์ํธ ๋: "๊ฒ๋ค๊ฐ API ์์ ์คํฐ๋ ๋ชฉ๋ก์ด ๋์ ์ผ๋ก ๋ด๋ ค์ค์์์. ID ๋ฅผ ๋ฏธ๋ฆฌ ์ ์ ์์ด์.
atomFamily๋ผ๋ ๊ฒ ์์ด์. ํ๋ผ๋ฏธํฐ๋ง๋ค atom ์ ๋์ ์ผ๋ก ์บ์ฑํด์ฃผ๊ฑฐ๋ ์."๐ฃ ์์ฒ : (๋ฉ๋ชจ์ฅ ์ด๋ฉฐ) "๊ทธ๊ฒ ๋ญ๋ฐ์...?"
๐ค ์ atomFamily ๊ฐ ํ์ํ๊ฐ? ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ฆฌ์คํธ ์์ดํ ๋ณ ๋ ๋ฆฝ ์ํ ๊ด๋ฆฌ์์ ๊ณ ์ atom ์ ์ธ ๋ฐฉ์์ ํ๊ณ๋ฅผ ์ค๋ช ํ ์ ์๋ค
atomFamily๊ฐ "ํ๋ผ๋ฏธํฐ โ atom" ์ ์บ์ ๋งต ์ญํ ์ ํ๋ค๋ ๊ฐ๋ ์ ์ดํดํ๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
์คํฐ๋ ์นด๋ 100๊ฐ๊ฐ ์๊ณ , ๊ฐ ์นด๋์ "์ข์์" ์ํ๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ์์ด์ผ ํด. atom ์ ์ด๋ป๊ฒ ๊ด๋ฆฌํ ๊น?
"์คํฐ๋ ID ๋ฅผ key ๋ก ํ๋ Map ์ฒ๋ผ atom ์ ์ ์ฅํ๋ฉด ์ด๋จ๊น?" ๊น์ง ๋ ์ฌ๋ ธ์ผ๋ฉด ์ถฉ๋ถํด.
๊ณ ์ atom ์ ์ธ ๋ฐฉ์์ ํ๊ณ
// โ ๐ฃ ์์ฒ ์ ์ด๊ธฐ ์๋ โ ์คํฐ๋ ID ๋ง๋ค atom ํ๋์ฝ๋ฉ
const likeCountAtom_001 = atom(0)
const likeCountAtom_002 = atom(0)
const likeCountAtom_003 = atom(0)
// ... 50๊ฐ ๋ฐ๋ณต
const likeCountAtom_050 = atom(0)
// ๐ฃ ์์ฒ : "์คํฐ๋ ์ถ๊ฐ๋ ๋๋ง๋ค ์ฌ๊ธฐ๋ค ํ ์ค์ฉ ์ถ๊ฐํ๋ฉด ๋์ฃ ๋ญ!"
// ๐ฆ ์ํธ: "API ์์ ๋์ ์ผ๋ก ID ๊ฐ ๋ด๋ ค์ค๋๋ฐ, ์ฝ๋๋ฅผ ์ด๋ป๊ฒ ๋ฏธ๋ฆฌ ์ ์ธํด์?
// ๊ทธ๋ฆฌ๊ณ ์คํฐ๋ 1000๊ฐ ๋๋ฉด ์ด ํ์ผ ์ด๋ป๊ฒ ๋ ๊ฒ ๊ฐ์์?"// ๐ก ๊ฐ์ ์๋ โ ๊ฐ์ฒด๋ก ๋ฌถ๊ธฐ
const likeCountAtoms: Record<string, PrimitiveAtom<number>> = {
'001': atom(0),
'002': atom(0),
'003': atom(0),
}
// ๐ฃ ์์ฒ : "์ด๊ฑด ์ด๋์? ๊ฐ์ฒด๋ก ๋ฌถ์์ด์!"
// ๐ฆ ์ํธ: "์ฌ์ ํ API ๊ฐ ๋ด๋ ค์ฃผ๋ ID ๋ฅผ ๋ฏธ๋ฆฌ ์ ์ ์์ด์.
// ๊ทธ๋ฆฌ๊ณ ์ด ๊ฐ์ฒด, ๋ชจ๋ ๋ก๋ ์์ ์ ์ ๋ถ ๋ฉ๋ชจ๋ฆฌ ์ฌ๋ผ๊ฐ์.
// ์ฐ์ง ์๋ atom ๋ ์ ๋ถ ์ด์์์ด์."atomFamily ์ ํด๋ฒ โ ํ์ํ ๋ ๋ง๋ค๊ณ , ์บ์ํ๊ณ , ์ฌ์ฌ์ฉ
// โ
๐ฆ ์ํธ: "atomFamily ๋ param โ atom ์ Map ์ด์์.
// ์ฒ์ ์์ฒญํ๋ฉด atom ์ ๋ง๋ค์ด ์บ์ํ๊ณ ,
// ๊ฐ์ param ์ผ๋ก ๋ค์ ์์ฒญํ๋ฉด ์บ์๋ atom ์ ๋ฐํํด์."
import { atomFamily } from 'jotai-family'
import { atom } from 'jotai'
const studyLikeAtomFamily = atomFamily((studyId: string) => atom(0))
// ์ฌ์ฉ
const StudyCard = ({ studyId }: { studyId: string }) => {
// ์ฒ์: atom(0) ์์ฑ ํ ์บ์
// ์ดํ: ์บ์์์ ๊บผ๋ด ์ฌ์ฌ์ฉ
const [likeCount, setLikeCount] = useAtom(studyLikeAtomFamily(studyId))
return (
<button onClick={() => setLikeCount((prev) => prev + 1)}>
โค๏ธ {likeCount}
</button>
)
}atomFamily ๋ด๋ถ ์บ์ (Map)
โโโ '001' โ PrimitiveAtom<number> (value: 5)
โโโ '002' โ PrimitiveAtom<number> (value: 2)
โโโ '003' โ PrimitiveAtom<number> (value: 12)
studyLikeAtomFamily('001') โ ์บ์์์ ๋์ผํ atom config ๋ฐํ
studyLikeAtomFamily('999') โ ์ atom(0) ์์ฑ + ์บ์ ์ ์ฅ
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
atomFamily๋ "ID ๋ณ ๊ฐ์ธ ์ฌ๋ฌผํจ" ์ด์ผ. ์ฒ์ ์ค๋ฉด ์ ์ฌ๋ฌผํจ์ ๋ง๋ค์ด ์ฃผ๊ณ , ๋ค์์ ์ค๋ฉด ๊ฐ์ ์ฌ๋ฌผํจ์ผ๋ก ์๋ดํด์ค.
โ ๏ธ DEPRECATED ๊ฒฝ๊ณ โ jotai-family ๋ก ๋ง์ด๊ทธ๋ ์ด์ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
jotai/utils์atomFamily๊ฐ Jotai v3 ์์ ์ ๊ฑฐ ์์ ์ด๋ผ๋ ๊ฒ์ ์๋คjotai-familyํจํค์ง๋ก ๋ง์ด๊ทธ๋ ์ด์ ํ๋ ๊ตฌ์ฒด์ ์ธ ๋ฐฉ๋ฒ์ ์ ์ ์๋ค
๊ณต์ Deprecated ์ ์ธ
Jotai ๊ณต์ ๋ ํผ๋ฐ์ค์๋ ๋ค์๊ณผ ๊ฐ์ด ๋ช ์๋์ด ์์ด:
"atomFamily is deprecated and will be removed in Jotai v3. Please migrate to the jotai-family package, which provides the same API with additional features like atomTree."
jotai/utils ์์ atomFamily ๋ฅผ import ํ๋ฉด ์ง๊ธ๋ deprecation warning ์ด ๋์. ์ ํ๋ก์ ํธ์์๋ ์ฒ์๋ถํฐ jotai-family ๋ฅผ ์จ์ผ ํด.
๋ง์ด๊ทธ๋ ์ด์ ๋ฐฉ๋ฒ
# 1. jotai-family ํจํค์ง ์ค์น
npm install jotai-family
# ๋๋
pnpm add jotai-family// โ Before (deprecated)
import { atomFamily } from 'jotai/utils'
// โ
After โ import ๊ฒฝ๋ก๋ง ๋ฐ๊พธ๋ฉด ๋!
import { atomFamily } from 'jotai-family'
// API ๋ ์์ ํ ๋์ผํด. ์ฝ๋ ๋ก์ง์ ์์ ๋ถํ์.
const studyLikeAtomFamily = atomFamily((studyId: string) => atom(0))๐ก ํ
jotai-family๋atomFamily์ ๋๋กญ์ธ ๋์ฒด์ ์ผ. import ๊ฒฝ๋ก ํ๋๋ง ๋ฐ๊พธ๋ฉด ๋ผ.atomTree๊ฐ์ ์ถ๊ฐ ๊ธฐ๋ฅ๋ ์ ๊ณตํด.
๐๏ธ atomFamily ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
atomFamily๋ก ์คํฐ๋ ID ๋ณ ๋ ๋ฆฝ ์ํ๋ฅผ ์์ฑํ๋ ๋ฒ์ ์ ์ ์๋ค- primitive atom ๋ฟ ์๋๋ผ derived atom, async atom ๋ family ๋ก ๋ง๋๋ ๋ฒ์ ์ดํดํ๋ค
๊ธฐ๋ณธ ํจํด โ ์คํฐ๋ ์ข์์
import { atom, useAtom } from 'jotai'
import { atomFamily } from 'jotai-family'
// ๐ฆ ์ํธ: "initializeAtom ํจ์๊ฐ param ์ ๋ฐ์์ ์ด๋ค ์ข
๋ฅ์ atom ์ด๋ ๋ฐํํ ์ ์์ด์"
const studyLikeAtomFamily = atomFamily((studyId: string) => atom(false))
// studyId ๋ณ๋ก ๋
๋ฆฝ์ ์ธ PrimitiveAtom<boolean> ์ ์บ์ฑ
// ์ปดํฌ๋ํธ์์ ์ฌ์ฉ โ studyId ๊ฐ key ์ญํ
const StudyLikeButton = ({ studyId }: { studyId: string }) => {
const [isLiked, setIsLiked] = useAtom(studyLikeAtomFamily(studyId))
return (
<button
onClick={() => setIsLiked((prev) => !prev)}
className={isLiked ? 'liked' : ''}
>
{isLiked ? 'โค๏ธ' : '๐ค'} ์ข์์
</button>
)
}
// ์คํฐ๋ ๋ชฉ๋ก โ ๊ฐ ์นด๋๊ฐ ๋
๋ฆฝ ์ํ๋ฅผ ๊ฐ์ง
const StudyList = ({ studies }: { studies: Study[] }) => (
<ul>
{studies.map((study) => (
<li key={study.id}>
<StudyCard study={study} />
{/* ๊ฐ ์คํฐ๋๋ง๋ค ๋
๋ฆฝ์ ์ธ ์ข์์ ์ํ */}
<StudyLikeButton studyId={study.id} />
</li>
))}
</ul>
)๊ฐ์ฒด๋ฅผ ์ด๊ธฐ๊ฐ์ผ๋ก โ ์์ธ ์ํ ๊ด๋ฆฌ
interface StudyCardState {
isExpanded: boolean
commentDraft: string
activeTab: 'info' | 'members' | 'comments'
}
// ๐ฆ ์ํธ: "๊ฐ์ฒด atom ๋ family ๋ก ๋ง๋ค ์ ์์ด์"
const studyCardStateAtomFamily = atomFamily((studyId: string) =>
atom<StudyCardState>({
isExpanded: false,
commentDraft: '',
activeTab: 'info',
})
)
const StudyCard = ({ studyId }: { studyId: string }) => {
const [cardState, setCardState] = useAtom(studyCardStateAtomFamily(studyId))
return (
<div>
<button onClick={() => setCardState((prev) => ({ ...prev, isExpanded: !prev.isExpanded }))}>
{cardState.isExpanded ? '์ ๊ธฐ' : 'ํผ์น๊ธฐ'}
</button>
{cardState.isExpanded && (
<div>
{/* ํญ, ๋๊ธ ์ด์ ๋ฑ ๊ฐ ์คํฐ๋ ์นด๋์ ๋
๋ฆฝ ์ํ */}
</div>
)}
</div>
)
}areEqual ์ต์ โ ๊ฐ์ฒด ํ๋ผ๋ฏธํฐ ๋น๊ต
import deepEqual from 'fast-deep-equal'
// ๐ฆ ์ํธ: "ํ๋ผ๋ฏธํฐ๊ฐ ๊ฐ์ฒด์ผ ๋๋ ์ฐธ์กฐ ๋น๊ต ๋์ deepEqual ์ ์จ์ผ ํด์"
// ๊ธฐ๋ณธ๊ฐ์ Object.is() โ ์ฐธ์กฐ ๋น๊ต๋ผ ๊ฐ์ฒด ํ๋ผ๋ฏธํฐ๋ ๋งค๋ฒ ์ atom ์ด ์์ฑ๋ผ
const studyFilterAtomFamily = atomFamily(
({ category, tag }: { category: string; tag: string }) =>
atom<Study[]>([]),
deepEqual // ๐ฆ ์ํธ: "{ category: 'react', tag: 'beginner' } ๊ฐ ๋์ผํ๋ฉด ๊ฐ์ atom ๋ฐํ"
)๐งน ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง โ remove ์ setShouldRemove ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
atomFamily๊ฐ ๋ด๋ถ์ ์ผ๋กMap์ ์ ์งํ๋ค๋ ๊ตฌ์กฐ๋ฅผ ์ดํดํ๋ค- ๋ฌดํ ์คํฌ๋กค ํ๊ฒฝ์์ ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๋ฐฉ์งํ๋ ๋ฒ์ ์ ์ ์๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
๋ฌดํ ์คํฌ๋กค๋ก ์คํฐ๋ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋ฉด ID ๊ฐ ๊ณ์ ์ถ๊ฐ๋ผ. atomFamily ๋ ID ๋ง๋ค ์บ์๋ฅผ ์ ์งํด. ์ด๊ฒ ๊ณ์ ์์ด๋ฉด ์ด๋ค ๋ฌธ์ ๊ฐ ์๊ธธ๊น?
๋ฉ๋ชจ๋ฆฌ ๋์ ์๋ฆฌ
Jotai ๋ ํผ๋ฐ์ค์ ๋ช ์๋์ด ์์ด:
"Internally, atomFamily is just a Map whose key is a param and whose value is an atom config. Unless you explicitly remove unused params, this leads to memory leaks."
๋ฌดํ ์คํฌ๋กค ์๋๋ฆฌ์ค:
1ํ์ด์ง: ID 001~020 โ atomFamily ์บ์์ 20๊ฐ ์ถ๊ฐ
2ํ์ด์ง: ID 021~040 โ atomFamily ์บ์์ 40๊ฐ ๋์
3ํ์ด์ง: ID 041~060 โ atomFamily ์บ์์ 60๊ฐ ๋์
...
100ํ์ด์ง: ID 001~2000 โ atomFamily ์บ์์ 2000๊ฐ (ํ๋ฉด์ 20๊ฐ๋ง ๋ณด์!)
๐ฃ ์์ฒ : "...์ด๊ฑฐ ๋ฌดํ ์คํฌ๋กค ๊ธฐ๋ฅ ๋ง๋ค๋ฉด ์คํฌ๋กคํ ์๋ก ๋ฉ๋ชจ๋ฆฌ ํญํ์ด๋ค์."
ํด๊ฒฐ์ฑ 1: remove(param) โ ๊ฐ๋ณ ์ญ์
const studyLikeAtomFamily = atomFamily((studyId: string) => atom(false))
const StudyCard = ({ studyId }: { studyId: string }) => {
useEffect(() => {
// ์ปดํฌ๋ํธ unmount ์ ํด๋น ID ์ atom ์ ์บ์์์ ์ญ์
return () => {
studyLikeAtomFamily.remove(studyId)
// ๐ฆ ์ํธ: "ํ๋ฉด์์ ์ฌ๋ผ์ง ์คํฐ๋ ์นด๋์ atom ์ ํ์ ์์ผ๋๊น ์ง์์"
}
}, [studyId])
const [isLiked, setIsLiked] = useAtom(studyLikeAtomFamily(studyId))
return <LikeButton isLiked={isLiked} onToggle={() => setIsLiked((prev) => !prev)} />
}ํด๊ฒฐ์ฑ 2: setShouldRemove โ ์๋ TTL ์ค์
// ๐ฆ ์ํธ: "์บ์์์ ๊บผ๋ผ ๋๋ง๋ค ์ด ํจ์๋ฅผ ์คํํด์ ์ญ์ ์ฌ๋ถ๋ฅผ ํ๋จํด์"
const studyLikeAtomFamily = atomFamily((studyId: string) => atom(false))
// ์์ฑ๋ ์ง 10๋ถ์ด ์ง๋ atom ์ ์๋ ์ญ์
studyLikeAtomFamily.setShouldRemove((createdAt, studyId) => {
const TEN_MINUTES = 10 * 60 * 1000
return Date.now() - createdAt > TEN_MINUTES
// ๐ฆ ์ํธ: "10๋ถ ๋์ ํญ๋ชฉ์ ์บ์์์ ๊บผ๋ผ ๋ ์๋์ผ๋ก ์ญ์ ํด์"
})
// null ์ ๋๊ธฐ๋ฉด shouldRemove ํจ์ ํด์
// studyLikeAtomFamily.setShouldRemove(null)ํด๊ฒฐ์ฑ 3: getParams() ๋ก ์บ์ ํํฉ ํ์ธ (๋๋ฒ๊น )
// ๐ฃ ์์ฒ : "์ง๊ธ ์บ์์ ๋ญ๊ฐ ์๋์ง ํ์ธํ ์ ์์ด์?"
// ๐ฆ ์ํธ: "getParams() ๋ก ํ์ฌ ์บ์ฑ๋ ํ๋ผ๋ฏธํฐ ๋ชฉ๋ก์ ๋ณผ ์ ์์ด์"
for (const studyId of studyLikeAtomFamily.getParams()) {
console.log('์บ์ฑ ์ค์ธ studyId:', studyId)
}
// ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋๋ฒ๊น
์ ํ์ฉ๋ฌดํ ์คํฌ๋กค ์์ ์์
// โ
๋ฌดํ ์คํฌ๋กค + atomFamily ๋ฉ๋ชจ๋ฆฌ ์์ ํจํด
const studyCardStateFamily = atomFamily((studyId: string) =>
atom({ isExpanded: false, isLiked: false })
)
// ๊ฐ๋ณ ์นด๋ โ unmount ์ ์บ์ ์ ๋ฆฌ
const StudyCard = ({ study }: { study: Study }) => {
useEffect(() => {
return () => {
// ๐ฆ ์ํธ: "virtualizer ๋ก ํ๋ฉด ๋ฐ์ผ๋ก ๋๊ฐ ์นด๋๊ฐ unmount ๋๋ฉด atom ๋ ์ ๋ฆฌํด์"
studyCardStateFamily.remove(study.id)
}
}, [study.id])
const [cardState, setCardState] = useAtom(studyCardStateFamily(study.id))
return (
<div>
<h3>{study.title}</h3>
<button onClick={() => setCardState((prev) => ({ ...prev, isLiked: !prev.isLiked }))}>
{cardState.isLiked ? 'โค๏ธ' : '๐ค'}
</button>
</div>
)
}
// ๋ฌดํ ์คํฌ๋กค ๋ชฉ๋ก
const InfiniteStudyList = () => {
const { data, fetchNextPage } = useInfiniteQuery({ ... })
return (
<>
{data.pages.flat().map((study) => (
<StudyCard key={study.id} study={study} />
))}
<button onClick={() => fetchNextPage()}>๋ ๋ณด๊ธฐ</button>
</>
)
}๐ท TypeScript ์ ํจ๊ป โ ์ ๋ค๋ฆญ ํ์ ๋ช ์ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
atomFamily์ TypeScript ์ ๋ค๋ฆญ ํ์ ์ ๋ช ์ํ๋ ๋ฒ์ ์ ์ ์๋ค
import { atom } from 'jotai'
import type { PrimitiveAtom } from 'jotai'
import { atomFamily } from 'jotai-family'
// ๋ฐฉ๋ฒ 1: TypeScript ์๋ ์ถ๋ก (initializeAtom ์ ๋ฐํ ํ์
์์ ์ถ๋ก )
const studyLikeFamily = atomFamily((studyId: string) => atom(false))
// โ AtomFamily<string, PrimitiveAtom<boolean>>
// ๋ฐฉ๋ฒ 2: ๋ช
์์ ์ ๋ค๋ฆญ (ํ๋ผ๋ฏธํฐ ํ์
๊ณผ atom ํ์
์ง์ ์ง์ )
const studyLikeFamily2 = atomFamily<string, PrimitiveAtom<boolean>>(
(studyId: string) => atom(false)
)
// ๊ฐ์ฒด ํ๋ผ๋ฏธํฐ ํ์
๋ช
์
interface StudyKey {
studyId: string
userId: string
}
const userStudyStatusFamily = atomFamily<StudyKey, PrimitiveAtom<'applied' | 'approved' | 'rejected'>>(
({ studyId, userId }) => atom<'applied' | 'approved' | 'rejected'>('applied'),
(a, b) => a.studyId === b.studyId && a.userId === b.userId // ์ปค์คํ
๋น๊ต
)
// ์ฌ์ฉ โ ํ์
์๋ฒฝ ์ ์ฉ
const StatusBadge = ({ studyId, userId }: StudyKey) => {
const status = useAtomValue(userStudyStatusFamily({ studyId, userId }))
// status: 'applied' | 'approved' | 'rejected' โ ์๋์์ฑ ์๋ฒฝ
return <span>{status === 'approved' ? '์น์ธ๋จ' : '๋๊ธฐ ์ค'}</span>
}โ๏ธ TanStack Query queryKey ์์ ์ญํ ๋น๊ต ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
queryKey์atomFamily์ ์ญํ ์ฐจ์ด๋ฅผ ๋ช ํํ ์ค๋ช ํ ์ ์๋ค- ๊ฐ๊ฐ ์ด๋ค ์ํ๋ฅผ ๋ด๋นํด์ผ ํ๋์ง ํ๋จํ ์ ์๋ค
์์๋ค ํ์ ์๋ฒ ์ํ๋ TanStack Query, ํด๋ผ์ด์ธํธ UI ์ํ๋ Jotai ๋ก ๋ถ๋ฆฌํ๊ณ ์์ด:
TanStack Query queryKey ๊ธฐ๋ฐ ์บ์ฑ
โโโ ['study', '001'] โ ์๋ฒ์์ ๋ฐ์ ์คํฐ๋ ์์ธ ๋ฐ์ดํฐ (์๋ ์ฌ๊ฒ์ฆ)
โโโ ['studies'] โ ์คํฐ๋ ๋ชฉ๋ก ๋ฐ์ดํฐ
โโโ ['user', 'me'] โ ๋ด ํ๋กํ ๋ฐ์ดํฐ
atomFamily ๊ธฐ๋ฐ ์บ์ฑ
โโโ '001' โ { isExpanded: false, isLiked: true } (UI ์ธํฐ๋์
์ํ)
โโโ '002' โ { isExpanded: true, isLiked: false }
โโโ '003' โ { isExpanded: false, isLiked: true }
// โ
์์๋ค ํ์ ์ญํ ๋ถ๋ฆฌ ์์
const StudyCard = ({ studyId }: { studyId: string }) => {
// ์๋ฒ ๋ฐ์ดํฐ โ TanStack Query ๊ฐ ๊ด๋ฆฌ (์บ์ฑ, ์ฌ๊ฒ์ฆ, ๋ฐฑ๊ทธ๋ผ์ด๋ ํจ์น)
const { data: study } = useQuery({
queryKey: ['study', studyId],
queryFn: () => fetchStudy(studyId),
})
// UI ์ํ โ Jotai atomFamily ๊ฐ ๊ด๋ฆฌ (์ปดํฌ๋ํธ ๊ฐ ๊ณต์ , ๋
๋ฆฝ์ )
const [uiState, setUiState] = useAtom(studyCardStateFamily(studyId))
// ๐ฆ ์ํธ: "์๋ฒ ๋ฐ์ดํฐ์ UI ์ํ๋ฅผ ๋ถ๋ฆฌํ๋ฉด ๊ฐ์์ ๋ผ์ดํ์ฌ์ดํด์ ๋
๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด์"
if (!study) return <Skeleton />
return (
<div>
<h3>{study.title}</h3> {/* ์๋ฒ ๋ฐ์ดํฐ */}
<button onClick={() => setUiState((prev) => ({ ...prev, isExpanded: !prev.isExpanded }))}>
{uiState.isExpanded ? '์ ๊ธฐ' : 'ํผ์น๊ธฐ'} {/* UI ์ํ */}
</button>
</div>
)
}| ๊ธฐ์ค | TanStack Query (queryKey) | Jotai (atomFamily) |
|---|---|---|
| ๋ฐ์ดํฐ ์ถ์ฒ | ์๋ฒ (API) | ํด๋ผ์ด์ธํธ (์ฌ์ฉ์ ์ธํฐ๋์ ) |
| ์๋ ์ฌ๊ฒ์ฆ | โ
staleTime, refetch | โ (UI ์ํ๋ ์๋ฒ ๋๊ธฐํ ๋ถํ์) |
| ์ญํ | ์๋ฒ ์ํ ๊ด๋ฆฌ | ํด๋ผ์ด์ธํธ UI ์ํ ๊ด๋ฆฌ |
| ์์ | ์คํฐ๋ ์์ธ ์ ๋ณด | ์นด๋ ํผ์นจ/์ ํ ์ฌ๋ถ, ์ข์์ UI ์ํ |
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ ๊ฐ์ ํ๋ผ๋ฏธํฐ์ธ๋ฐ ๋ค๋ฅธ atom ์ด ๋ฐํ๋จ (๊ฐ์ฒด ํ๋ผ๋ฏธํฐ)
์ฆ์: ๋์ผํ { studyId: '001', tag: 'react' } ๋ก ํธ์ถํด๋ ๋งค๋ฒ ์ atom ์ด ์์ฑ๋จ
์์ธ:
// ๐ฃ ์์ฒ : "๋ถ๋ช
๊ฐ์ ๊ฐ์ธ๋ฐ ์ ๋ค๋ฅธ atom ์ด ๋์ค์ฃ ?"
const familyAtom = atomFamily(
({ studyId, tag }: { studyId: string; tag: string }) => atom([])
)
// ๊ธฐ๋ณธ๊ฐ์ Object.is() ๋น๊ต โ ๊ฐ์ฒด๋ ์ฐธ์กฐ ๋น๊ต โ ๋งค๋ฒ ์ ๊ฐ์ฒด = ๋งค๋ฒ ์ atomํด๊ฒฐ์ฑ :
import deepEqual from 'fast-deep-equal'
const familyAtom = atomFamily(
({ studyId, tag }: { studyId: string; tag: string }) => atom([]),
deepEqual // โ
๊ฐ ๋น๊ต๋ก ๋ณ๊ฒฝ
)โ atomFamily import ์ Deprecation Warning
์ฆ์:
Warning: atomFamily from 'jotai/utils' is deprecated.
Please migrate to 'jotai-family' package.
ํด๊ฒฐ์ฑ :
npm install jotai-family// Before
import { atomFamily } from 'jotai/utils'
// After
import { atomFamily } from 'jotai-family'โ ๋ฌดํ ์คํฌ๋กค์์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๊ณ์ ์ฆ๊ฐ
์์ธ: atomFamily ์บ์๊ฐ ๋ฌด์ ํ์ผ๋ก ์ฆ๊ฐ
ํด๊ฒฐ์ฑ :
// ์ปดํฌ๋ํธ unmount ์ remove ํธ์ถ
useEffect(() => {
return () => {
myAtomFamily.remove(id)
}
}, [id])
// ๋๋ TTL ๊ธฐ๋ฐ ์๋ ์ญ์
myAtomFamily.setShouldRemove(
(createdAt) => Date.now() - createdAt > 5 * 60 * 1000 // 5๋ถ
)๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ atomFamily ํต์ฌ ๊ฐ๋ ์์ฝ
| ๊ฐ๋ | ์ค๋ช |
|---|---|
| atomFamily(fn) | (param) => Atom ํํ. param ๋ณ atom ์ Map ์ ์บ์ฑ |
| areEqual | ๋ ๋ฒ์งธ ์ธ์. ๊ฐ์ฒด ํ๋ผ๋ฏธํฐ ๋น๊ต ํจ์ (๊ธฐ๋ณธ: Object.is) |
| remove(param) | ํน์ param ์ atom ์ ์บ์์์ ์ญ์ |
| setShouldRemove(fn) | TTL ๊ธฐ๋ฐ ์๋ ์ญ์ ํจ์ ๋ฑ๋ก |
| getParams() | ํ์ฌ ์บ์ฑ๋ ํ๋ผ๋ฏธํฐ ๋ชฉ๋ก ๋ฐํ (๋๋ฒ๊น ์ฉ) |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| import ๊ฒฝ๋ก | from 'jotai/utils' | from 'jotai-family' |
| ๊ฐ์ฒด ํ๋ผ๋ฏธํฐ ๋น๊ต | ๊ธฐ๋ณธ Object.is() ์ฌ์ฉ | deepEqual ํจ์ ์ง์ |
| ๋ฌดํ ์คํฌ๋กค | ์บ์ ์ญ์ ์์ด ๊ณ์ ์ถ๊ฐ | remove() ๋๋ setShouldRemove() |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ๐ด ์๋์ด ๋ฉด์ ์ง๋ฌธ (์์ฒ ์ ๋ฉด์ ๋์ )
"atomFamily ๋ฅผ ์ฌ์ฉํ ๋ ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํ ์ ์๋ ์ํฉ๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ค๋ช ํด๋ณด์ธ์."
- A) atomFamily ๋ WeakMap ์ ์ฌ์ฉํ๋ฏ๋ก ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ์๋์ผ๋ก ๋ฐฉ์ง๋๋ค
- B) atomFamily ๋ ๋ด๋ถ์ ์ผ๋ก Map ์ ์ฌ์ฉํ๊ณ , ํ๋ผ๋ฏธํฐ๊ฐ ๋ฌดํํ ์ถ๊ฐ๋ ๊ฒฝ์ฐ ์บ์๊ฐ ๋ฌด์ ํ ์ฆ๊ฐํ๋ฏ๋ก
remove()๋๋setShouldRemove()๋ก ๋ช ์์ ์ผ๋ก ์ ๋ฆฌํด์ผ ํ๋ค - C) ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํ๋ฉด
Provider๋ฅผ ์๋ก ๋ง์ดํธํ๋ฉด ํด๊ฒฐ๋๋ค - D) atomFamily ๋
atom()๊ณผ ๋ฌ๋ฆฌ ๊ฐ๋น์ง ์ปฌ๋ ์ ์ด ์๋์ผ๋ก ๋๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: Jotai ๋ ํผ๋ฐ์ค์ ๋ช
์๋ ๊ฒ์ฒ๋ผ, atomFamily ๋ ๋ด๋ถ์ ์ผ๋ก
Map<param, atomConfig>์ผ. Jotai store ๊ฐ WeakMap ์ ์ฐ๋ ๊ฒ๊ณผ ๋ฌ๋ฆฌ, atomFamily ์ ์บ์ Map ์ ์ผ๋ฐ Map ์ด๋ผ ํค ์ฐธ์กฐ๊ฐ ์ฌ๋ผ์ ธ๋ ์๋ GC ๊ฐ ์ ๋ผ. ๋ฌดํ ์คํฌ๋กค์ฒ๋ผ ํ๋ผ๋ฏธํฐ๊ฐ ๊ณ์ ์ถ๊ฐ๋๋ ํ๊ฒฝ์์๋remove(param)์ด๋setShouldRemove(fn)์ผ๋ก ๋ช ์์ ์ ๋ฆฌ๊ฐ ํ์์ผ. - ์ค๋ต ํผ๋๋ฐฑ: A ๋ ํ๋ ธ์ด โ atomFamily ์บ์๋ WeakMap ์ด ์๋ ์ผ๋ฐ Map ์ด์ผ. C ์ D ๋ ํ๋ ธ์ด โ Provider ์ฌ๋ง์ดํธ๋ก๋ atomFamily ์บ์๋ฅผ ์ด๊ธฐํํ์ง ์๊ณ , GC ๋ ์๋์ผ๋ก ๋์ง ์์.
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "Jotai store ๋ WeakMap ์ด์ง๋ง, atomFamily ์บ์๋ ์ผ๋ฐ Map. ์์์ ์ ๋ฆฌ ์ ๋ผ."
Q2. ๐๏ธ ์ํคํ ์ฒ ์ค๊ณ (์์์ UX ํผ๋๋ฐฑ)
์์ ๋์ด ๋์์ธ ๋ฆฌ๋ทฐ์์ ๋งํ์ด: "์คํฐ๋ ์นด๋๋ฅผ ํผ์ณค๋ค๊ฐ ๋ค๋ฅธ ์นด๋ ํด๋ฆญํ๋ฉด ํผ์ณ์ง ์ํ๊ฐ ์ ์ง๋์ผ๋ฉด ์ข๊ฒ ์ด์. ์ง๊ธ์ ์คํฌ๋กคํ๋ฉด ์ด๊ธฐํ๋ผ์."
์์ฒ ์ด๊ฐ ํ์ฌ ํผ์นจ/์ ํ ์ํ๋ฅผ ์ปดํฌ๋ํธ ๋ด๋ถuseState๋ก ๊ด๋ฆฌํ๊ณ ์์ด. ์ด๊ฑธ atomFamily ๋ก ์ ํํ๋ฉด ์์ ๋์ ํผ๋๋ฐฑ์ด ํด๊ฒฐ๋๋ ์ด์ ๋?
- A) atomFamily ๋ ์๋ฒ ์ํ์ ๋๊ธฐํ๋๊ธฐ ๋๋ฌธ์ด๋ค
- B) atomFamily ๋ ํ๋ผ๋ฏธํฐ(studyId) ๋ณ๋ก atom ์ ์บ์ฑํ๋ฏ๋ก, ์ปดํฌ๋ํธ๊ฐ unmount/mount ๋์ด๋ atom ์ด ์ ์ง๋์ด ์ํ๊ฐ ๋ณด์กด๋๋ค
- C) atomFamily ๋ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์๋์ผ๋ก ์ ์ฅํ๊ธฐ ๋๋ฌธ์ด๋ค
- D) React ๊ฐ ์ปดํฌ๋ํธ๋ฅผ virtualize ํด์ ์ํ๋ฅผ ๋ณด์กดํ๊ธฐ ๋๋ฌธ์ด๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
:
useState๋ ์ปดํฌ๋ํธ์ ๋ก์ปฌ ์ํ๋ผ์ ์ปดํฌ๋ํธ๊ฐ unmount ๋๋ฉด ์ํ๋ ์ฌ๋ผ์ ธ. ๊ฐ์ ์คํฌ๋กค(virtualizer)์ด๋ ํ์ด์ง๋ค์ด์ ์์ ํ๋ฉด ๋ฐ์ผ๋ก ๋๊ฐ ์ปดํฌ๋ํธ๊ฐ unmount ๋๋ฉดuseState๊ฐ์ ์ด๊ธฐํ๋ผ. ๋ฐ๋ฉดatomFamily(studyId)๋ก ๊ด๋ฆฌํ๋ฉด atom ์ดstudyId๋ฅผ key ๋ก Jotai store ์ ์ ์ฅ๋๊ธฐ ๋๋ฌธ์, ์ปดํฌ๋ํธ๊ฐ unmount ๋์ด๋ ์ํ๊ฐ ๋ณด์กด๋ผ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "useState ๋ ์ปดํฌ๋ํธ์ ๊ธฐ์ต, atomFamily ๋ store ์ ๊ธฐ์ต. ์ปดํฌ๋ํธ๊ฐ ์ฌ๋ผ์ ธ๋ store ๋ ๊ธฐ์ตํด."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
atomFamily์ TanStack Query ์queryKey๊ฐ ๋น์ทํด ๋ณด์ด๋๋ฐ, ๋ ๊ฐ์ง์ ์ญํ ์ฐจ์ด๋ฅผ ์น๊ตฌ์๊ฒ ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"๋ ๋ค 'ํ๋ผ๋ฏธํฐ๋ณ ์บ์' ํจํด์ด์ง๋ง ๋ด๋ ๋ด์ฉ์ด ๋ฌ๋ผ.
queryKey๋ ์๋ฒ์์ ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํ๊ณ ์๋์ผ๋ก ์ฌ๊ฒ์ฆํด์ค.atomFamily๋ ์ฌ์ฉ์์ ํด๋ฆญ, ํผ์นจ/์ ํ, ์ข์์ ๊ฐ์ ํด๋ผ์ด์ธํธ UI ์ํ๋ฅผ ID ๋ณ๋ก ๋ ๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํด. ์๋ฒ ๋ฐ์ดํฐ๋ Query, UI ์ํ๋ Jotai ๋ผ๋ ์ญํ ๋ถ๋ฆฌ๊ฐ ํต์ฌ์ด์ผ."
๐ก ์ด ๋น์ ๋ฅผ ์ง์ ๋ง๋ค์๋ค๋ฉด: ์ํ ๊ด๋ฆฌ ์ํคํ ์ฒ์ ์ญํ ๋ถ๋ฆฌ๋ฅผ ์ดํดํ ๊ฑฐ์ผ. ๋ค์ ๊ฐ์ด๋๋ก ๋์ด๊ฐ๋ ์ถฉ๋ถํด!
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ ๋ง ์ฐฝํผํ๊ณ ๋ฟ๋ฏํ ํ๋ฃจ๋ฅผ ๋์์ ์ด์์ด.
์์นจ์ ์์ ์๊ฒ PR ์ฌ๋ฆฐ ์ฝ๋๊ฐ likeCountAtom_001 ๋ถํฐ likeCountAtom_050 ๊น์ง 50๊ฐ๊ฐ ์ค์ค์ด ์ ์ธ๋ผ ์์๋๋ฐ, ์ํธ ๋์ด ๊ทธ๊ฑธ ๋ณด๊ณ ์ ์ ์นจ๋ฌตํ์
จ๊ฑฐ๋ . ๊ทธ ์นจ๋ฌต์ด ๋ํํ
๋ ๊ต์ฅํ ๊ธธ๊ฒ ๋๊ปด์ก์ด. "์์ฒ ๋, ์คํฐ๋ 1000๊ฐ ๋๋ฉด ์ด์ฉ๋ ค๊ณ ์?" ๋ผ๋ ํ ๋ง๋์ ๋จธ๋ฆฟ์์ด ํ์์ก๋ค.
atomFamily ๋ฅผ ๋ฐฐ์ฐ๊ณ ๋์ ์ฝ๋๋ฅผ ์ ๋ถ ๊ฐ์์์๋๋ฐ, 50์ค์ด 4์ค๋ก ์ค์์ด. ์ง์ง์ผ. ๊ฒ๋ค๊ฐ ๋์ ์ผ๋ก ์คํฐ๋๊ฐ ์ถ๊ฐ๋ผ๋ ์ฝ๋ ์์ ์์ด ๋์ํ๋ค๋ ๊ฒ ๋๋ฌด ์ ๊ธฐํ์ด.
๐ก ์ค๋์ ๊ตํ: "๋์ ๋ฐ์ดํฐ๋ฅผ ์ํ ์ํ๋ ๋ฏธ๋ฆฌ ์ ์ธํ๋ ๊ฒ ์๋๋ผ, ํ์ํ ๋ ๋์ ์ผ๋ก ๋ง๋ค์ด์ผ ํ๋ค."
๋ฉ๋ชจ๋ฆฌ ๋์ ์๊ธฐ๋ ์ฒ์์ "์ค๋ง ๊ทธ๊ฒ ๋ฌธ์ ๊ฐ ๋๊ฒ ์ด์?" ์ถ์๋๋ฐ, ๋ฌดํ ์คํฌ๋กค์์ ํ์ด์ง๊ฐ ์์ผ์๋ก ์บ์๊ฐ ๋ฌด์ ํ์ผ๋ก ๋์ด๋๋ค๋ ๊ฑฐ ๋ค์ผ๋๊น ๋ฌด์ญ๋๋ผ๊ณ . remove() ์ฑ๊ธฐ๋ ๊ฑฐ ์์ง ๋ง์์ผ์ง.
ํด๊ทผํ๊ณ ํธ์์ ์์ ๋งฅ์ฃผ ํ ์บ ์ฌ์ผ๊ฒ ๋ค. ์ค๋ ๋ญ๊ฐ ๋ง์ด ๋ ๋ ธ๋ค ์๋ก ์ผ๋ค ํด์ ์ง์ด ์ข ๋น ์ง ๊ฒ ๊ฐ์. ๊ทธ๋๋ ์ฝ๋๋ ํจ์ฌ ๊น๋ํด์ก์ผ๋๊น.