๐งฉ 14. ๋๊ท๋ชจ ์ฑ Jotai ์ํคํ ์ฒ ์ค๊ณ
๐ ๊ฐ์
Feature ๋จ์ atom ์ฌ๋ผ์ด์ฑ, cross-feature ์์กด์ฑ ๊ด๋ฆฌ, atom effect, write-only action atom ํจํด, Jotai vs Zustand ์ ํ ๊ธฐ์ค์ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Feature ๋จ์๋ก atom ์ ์ฌ๋ผ์ด์ฑํ๋ ํด๋ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ๊ณ ์คํํ ์ ์๋ค.
- cross-feature ์์กด์ฑ์์ ์ํ ์ฐธ์กฐ๊ฐ ์๊ธฐ์ง ์๋๋ก ์์กด์ฑ ๋ฐฉํฅ์ ์ค๊ณํ ์ ์๋ค.
- write-only action atom ํจํด์ผ๋ก ๋น์ฆ๋์ค ๋ก์ง์ atom ์ผ๋ก ๊น๋ํ๊ฒ ์บก์ํํ ์ ์๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์ํคํ ์ฒ๊ฐ ์ค์ํ๊ฐ?
- ๐๏ธ Feature ๋จ์ atom ์ฌ๋ผ์ด์ฑ
- ๐ cross-feature ์์กด์ฑ ๊ด๋ฆฌ โ ์ํ ์ฐธ์กฐ ๋ฐฉ์ง
- โก atom effect ํจํด โ atom ๋ณํ์ ๋ฐ์ํ๋ ์ฌ์ด๋ ์ดํํธ
- ๐ฌ write-only action atom โ ๋น์ฆ๋์ค ๋ก์ง ์บก์ํ
- โ๏ธ Jotai vs Zustand โ ์ธ์ ์ด๋ค ๊ฒ์ ์ ํํ๋๊ฐ?
- ๐ฆ Before/After โ ํผ์ฌ๋ ๊ตฌ์กฐ โ Feature ์ฌ๋ผ์ด์ฑ
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 40๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 25๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: ์ฑ์ด ์ปค์ ธ๋ฒ๋ ธ๋ค
์คํ๋ฆฐํธ 7์ฃผ์ฐจ. ์์๋ค ์ปค๋ฎค๋ํฐ ์ฑ์ด ์๊ฐ๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ฒ ์ฑ์ฅํ์ด. ๊ธฐ๋ฅ์ด ๋์ด๋๋ฉด์ atom ํ์ผ๋ ๋ฉ๋ฌ์ ๋์ด๋ฌ๋๋ฐ...
- ๐ฃ ์์ฒ : (atoms/index.ts ๋ฅผ ์ด์ด๋ณด๋ฉฐ ๊ฒฝ์ ) "์ํธ ๋... atom ์ด ์ง๊ธ ๋ช ๊ฐ์์? ์ฌ๊ธฐ์ userAtom, studyAtom, chatAtom, filterAtom, modalAtom... ๋ค ์์ฌ์์ด์."
- ๐ฆ ์ํธ ๋: "ํ์ผ์ด ๋ช ์ค์ด์์?"
- ๐ฃ ์์ฒ : "800์ค์ด์..."
- ๐ฆ ์ํธ ๋: (ํ์จ) "๋์ด์. Feature ๋จ์๋ก ์ฌ๋ผ์ด์ฑํด์ผ ํ ๋๊ฐ ๋๋ค์."
- ๐ฃ ์์ฒ : "Feature ์ฌ๋ผ์ด์ฑ์ด์?"
- ๐ฆ ์ํธ ๋: "Redux Toolkit ์ slice ํจํด์ฒ๋ผ, Jotai ๋ ๋๋ฉ์ธ ๋จ์๋ก atom ์ ๋ถ๋ฆฌํ๋ฉด ๊ด๋ฆฌ๊ฐ ํจ์ฌ ์ฌ์์ ธ์. ๊ฐ์ด ๋ฆฌํฉํ ๋งํด๋ด์."
- ๐ ์์ ๋: "๊ทธ๊ฑฐ ํ๋ฉด์ ๊ธฐ๋ฅ ์ถ๊ฐ๋ ๋์์ ์งํํด์ผ ํด."
- ๐ฃ ์์ฒ : (๋์ด ๋์๊ฐ๋ฉฐ) "๋ต..."
๐ค ์ ์ํคํ ์ฒ๊ฐ ์ค์ํ๊ฐ? ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ํคํ ์ฒ ์์ด atom ์ ๋๋ฆฌ๋ฉด ์ด๋ค ๊ณ ํต์ด ์๊ธฐ๋์ง ๊ตฌ์ฒด์ ์ผ๋ก ์ค๋ช ํ ์ ์๋ค
- "ํ์ผ ํฌ๊ธฐ"๋ณด๋ค "์์กด์ฑ ๋ฐฉํฅ"์ด ์ ๋ ์ค์ํ์ง ์ดํดํ๋ค
์ํคํ
์ฒ ์๋ atoms/index.ts (800์ค):
- userAtom, sessionAtom, isLoggedInAtom (auth ๊ด๋ จ)
- studyListAtom, selectedStudyAtom, studyFiltersAtom (study ๊ด๋ จ)
- messagesAtom, roomAtom, isConnectedAtom (chat ๊ด๋ จ)
- isModalOpenAtom, modalTypeAtom, drawerOpenAtom (ui ๊ด๋ จ)
- notificationsAtom, unreadCountAtom (notification ๊ด๋ จ)
๋ฌธ์ ๋ค:
1. ํ์ผ ํ๋๊ฐ ๋๋ฌด ์ปค์ git blame ์ด ์๋ฏธ ์์ด์ง
2. auth atom ์ ๊ณ ์น๋ ค๋๋ฐ study ๋ก์ง์ด ๊ฐ์ ํ์ผ์ ์์ด์ ์ถฉ๋ ๋ฐ์
3. ์ ํ์์ด "์คํฐ๋ ๊ด๋ จ atom ์ด ์ด๋์ ์์ด์?" ๋ผ๊ณ ๋ฌผ์ผ๋ฉด ๋ตํ๊ธฐ ์ด๋ ค์
4. ์ํ ์์กด์ฑ์ด ์๊ฒจ๋ ํ์
ํ๊ธฐ ์ด๋ ค์
5. ํ
์คํธ ์ ํ์ ์๋ atom ๊น์ง ๋ชจ๋ import ๋จ
๐ก ์ํํธ์จ์ด ์ํคํ ์ฒ์ ํต์ฌ ์์น
"์ฝ๋๋ ๋ณ๊ฒฝ๋๋ ์ด์ ๊ฐ ๊ฐ์ ๊ฒ๋ผ๋ฆฌ ๋ฌถ์ด์ผ ํ๋ค." โ Robert C. Martin (ํด๋ฆฐ ์ํคํ ์ฒ)
auth ๊ฐ ๋ฐ๋๋ ์ด์ ์ chat ์ด ๋ฐ๋๋ ์ด์ ๋ ๋ฌ๋ผ. ๊ทธ๋์ ๋ถ๋ฆฌํด์ผ ํด.
๐๏ธ Feature ๋จ์ atom ์ฌ๋ผ์ด์ฑ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋๋ฉ์ธ๋ณ๋ก atom ์ ํด๋์ ์ ๋ฆฌํ๋ ์ค์ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ ์ ์๋ค
- ๊ฐ feature ํด๋์ atoms, hooks, types ๋ฅผ ํจ๊ป ๋๋ ์ฅ์ ์ ์ดํดํ๋ค
src/
โโโ features/ โ Feature ๋จ์๋ก ๋ถ๋ฆฌ
โ โ
โ โโโ auth/ โ ์ธ์ฆ ๋๋ฉ์ธ
โ โ โโโ atoms/
โ โ โ โโโ index.ts โ userAtom, sessionAtom, isLoggedInAtom
โ โ โโโ hooks/
โ โ โ โโโ useAuth.ts โ useAtom ๋ฅผ ๊ฐ์ผ ์ปค์คํ
ํ
โ โ โ โโโ usePermission.ts
โ โ โโโ types/
โ โ โโโ index.ts โ User, Session ํ์
โ โ
โ โโโ study/ โ ์คํฐ๋ ๋๋ฉ์ธ
โ โ โโโ atoms/
โ โ โ โโโ index.ts โ studyListAtom, selectedStudyAtom, studyFiltersAtom
โ โ โโโ hooks/
โ โ โ โโโ useStudySearch.ts
โ โ โ โโโ useStudyActions.ts
โ โ โโโ types/
โ โ โโโ index.ts โ Study, StudyFilter ํ์
โ โ
โ โโโ chat/ โ ์ฑํ
๋๋ฉ์ธ
โ โ โโโ atoms/
โ โ โ โโโ index.ts โ messagesAtom, roomAtom, isConnectedAtom
โ โ โโโ hooks/
โ โ โโโ useChat.ts
โ โ
โ โโโ notification/ โ ์๋ฆผ ๋๋ฉ์ธ
โ โโโ atoms/
โ โ โโโ index.ts โ notificationsAtom, unreadCountAtom
โ โโโ hooks/
โ โโโ useNotification.ts
โ
โโโ shared/
โโโ atoms/ โ Feature ์ ์ข
์๋์ง ์๋ ๊ณตํต UI ์ํ
โโโ index.ts โ isModalOpenAtom, drawerOpenAtom, themeAtom
๊ฐ feature ์ atoms/index.ts ์์
// features/auth/atoms/index.ts
import { atom } from 'jotai'
import type { User, Session } from '../types'
// ๐ฆ ์ํธ: "debugLabel ์ ํ์์์. DevTools ์์ ์ด๋ค atom ์ธ์ง ๋ฐ๋ก ์์์ผ ํด์."
export const userAtom = atom<User | null>(null)
userAtom.debugLabel = 'auth/userAtom'
export const sessionAtom = atom<Session | null>(null)
sessionAtom.debugLabel = 'auth/sessionAtom'
// derived atom โ user ๊ฐ ์์ผ๋ฉด ๋ก๊ทธ์ธ ์ํ
export const isLoggedInAtom = atom((get) => get(userAtom) !== null)
isLoggedInAtom.debugLabel = 'auth/isLoggedInAtom'
// derived atom โ ์ญํ ํ์ธ
export const isAdminAtom = atom((get) => get(userAtom)?.role === 'admin')
isAdminAtom.debugLabel = 'auth/isAdminAtom'// features/study/atoms/index.ts
import { atom } from 'jotai'
import type { Study, StudyFilters } from '../types'
export const studyListAtom = atom<Study[]>([])
studyListAtom.debugLabel = 'study/studyListAtom'
export const selectedStudyIdAtom = atom<string | null>(null)
selectedStudyIdAtom.debugLabel = 'study/selectedStudyIdAtom'
export const studyFiltersAtom = atom<StudyFilters>({
category: 'all',
sortBy: 'latest',
tags: [],
})
studyFiltersAtom.debugLabel = 'study/studyFiltersAtom'
// derived โ ์ ํ๋ ์คํฐ๋ ๊ฐ์ฒด
export const selectedStudyAtom = atom((get) => {
const list = get(studyListAtom)
const id = get(selectedStudyIdAtom)
return list.find((s) => s.id === id) ?? null
})
selectedStudyAtom.debugLabel = 'study/selectedStudyAtom'์ปค์คํ ํ ์ผ๋ก atom ์บก์ํ
// features/study/hooks/useStudySearch.ts
'use client'
import { useAtom, useAtomValue } from 'jotai'
import { studyFiltersAtom, studyListAtom } from '../atoms'
// ๐ฆ ์ํธ: "์ปดํฌ๋ํธ๊ฐ atom ์ ์ง์ import ํ์ง ๋ง๊ณ , ํ
์ ํตํด์ ์ฌ์ฉํ๋ฉด
// ๋์ค์ atom ๊ตฌ์กฐ๊ฐ ๋ฐ๋์ด๋ ํ
๋ง ์์ ํ๋ฉด ๋ผ์."
export function useStudySearch() {
const studies = useAtomValue(studyListAtom)
const [filters, setFilters] = useAtom(studyFiltersAtom)
const filteredStudies = studies.filter((s) => {
if (filters.category !== 'all' && s.category !== filters.category) return false
if (filters.tags.length > 0 && !filters.tags.some((t) => s.tags.includes(t))) return false
return true
})
return {
studies: filteredStudies,
filters,
setCategory: (category: string) =>
setFilters((prev) => ({ ...prev, category })),
setSortBy: (sortBy: typeof filters.sortBy) =>
setFilters((prev) => ({ ...prev, sortBy })),
resetFilters: () =>
setFilters({ category: 'all', sortBy: 'latest', tags: [] }),
}
}๐ cross-feature ์์กด์ฑ ๊ด๋ฆฌ โ ์ํ ์ฐธ์กฐ ๋ฐฉ์ง ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Feature ๊ฐ ์์กด์ฑ ๋ฐฉํฅ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ค๊ณํ ์ ์๋ค
- ์ํ ์์กด์ฑ์ด ์๊ธฐ๋ ํจํด๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ดํดํ๋ค
์์กด์ฑ ๋ฐฉํฅ ์์น
์์กด ํ์ฉ ๋ฐฉํฅ:
shared โ (๋ชจ๋ feature ๊ฐ shared ์ ์์กด ๊ฐ๋ฅ)
auth โ study (study ๋ auth ์ ์์กด ๊ฐ๋ฅ)
auth โ chat (chat ์ auth ์ ์์กด ๊ฐ๋ฅ)
์์กด ๊ธ์ง ๋ฐฉํฅ:
auth โ study โ (auth ๋ study ์ ์์กดํ๋ฉด ์ ๋จ โ ์ํ ์ํ)
study โ chat โ (์ง์ ์์กด ๋์ shared ๋ฅผ ํตํด์)
// โ
์ฌ๋ฐ๋ฅธ ํจํด โ study atom ์์ auth atom ์ import (๋จ๋ฐฉํฅ)
// features/study/atoms/index.ts
import { atom } from 'jotai'
import { userAtom } from '@/features/auth/atoms' // auth โ study ๋ฐฉํฅ โ
// ํ์ฌ ์ ์ ๊ฐ ์คํฐ๋ ๋ฉค๋ฒ์ธ์ง ํ์ธํ๋ ํ์ atom
export const isMemberOfSelectedStudyAtom = atom((get) => {
const user = get(userAtom) // auth atom ์ฐธ์กฐ โ
const study = get(selectedStudyAtom)
if (!user || !study) return false
return study.memberIds.includes(user.id)
})// โ ์ํํ ํจํด โ ์ํ ์์กด์ฑ
// features/auth/atoms/index.ts
import { studyListAtom } from '@/features/study/atoms' // study โ auth โ
// auth ๊ฐ study ์ ์์กดํ๋ ์๊ฐ ์ํ ์ฐธ์กฐ ์ํ!
// โ
ํด๊ฒฐ์ฑ
โ shared atom ์ ์ค๊ฐ์ ๋๊ธฐ
// shared/atoms/index.ts
export const currentUserStudiesAtom = atom((get) => {
// auth ์ study ์์ชฝ์ ์ฐธ์กฐํ์ง๋ง, shared ๋ ์ด์ด์์ ์กฐํฉ
const user = get(userAtom)
const studies = get(studyListAtom)
return studies.filter((s) => s.memberIds.includes(user?.id ?? ''))
})๋ ์ด์ด ์ํคํ ์ฒ ๋ค์ด์ด๊ทธ๋จ
์์กด์ฑ ๋ฐฉํฅ (์ โ ์๋๋ง ํ์ฉ):
components/
โ (์ปดํฌ๋ํธ๋ ํ
/atom ์ ์์กด)
features/study/hooks, features/auth/hooks
โ (ํ
์ feature atom ์ ์์กด)
features/study/atoms, features/auth/atoms
โ (feature atom ์ shared atom ์ ์์กด ๊ฐ๋ฅ)
shared/atoms
โ (shared atom ์ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์๋ง ์์กด)
jotai, tanstack-query
โก atom effect ํจํด โ atom ๋ณํ์ ๋ฐ์ํ๋ ์ฌ์ด๋ ์ดํํธ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
jotai-effect์atomEffect๋ก atom ๊ฐ ๋ณํ์ ๋ฐ์ํ๋ ์ฌ์ด๋ ์ดํํธ๋ฅผ ์ ์ธ์ ์ผ๋ก ์ ์ํ ์ ์๋คuseEffect์ ๋น๊ตํ์ ๋ atomEffect ๊ฐ ์ ๋ฆฌํ ์ํฉ์ ์ดํดํ๋ค
npm install jotai-effect// ๐ฆ ์ํธ: "atomEffect ๋ ์ปดํฌ๋ํธ ์์ด atom ๋ณํ์ ๋ฐ์ํ ์ ์์ด์.
// useEffect ๋ ์ปดํฌ๋ํธ๊ฐ mount ๋์ด์ผ ์คํ๋์ง๋ง,
// atomEffect ๋ atom ์ด ์ฒ์ ๊ตฌ๋
๋ ๋๋ถํฐ ์คํ๋ผ์."
import { atomEffect } from 'jotai-effect'
import { userAtom } from '@/features/auth/atoms'
// ์ ์ ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ์ ๋ถ์ ์ด๋ฒคํธ ์ ์ก
const userAnalyticsEffect = atomEffect((get, set) => {
const user = get(userAtom)
if (user) {
// ๋ก๊ทธ์ธ ์
analytics.identify(user.id, { name: user.name, role: user.role })
analytics.track('user_login')
} else {
// ๋ก๊ทธ์์ ์
analytics.reset()
}
// cleanup ํจ์ ๋ฐํ ๊ฐ๋ฅ (WebSocket close ๋ฑ)
return () => {
// user atom ๊ตฌ๋
์ด ๋๋ ๋ ์คํ
}
})
// ๋คํฌ๋ชจ๋ ๋ณ๊ฒฝ ์ document ํด๋์ค ๋๊ธฐํ
import { atomWithStorage } from 'jotai/utils'
const isDarkModeAtom = atomWithStorage('darkMode', false)
const darkModeEffect = atomEffect((get) => {
const isDark = get(isDarkModeAtom)
// ๐ฃ ์์ฒ : "์ด๋ฌ๋ฉด ์ปดํฌ๋ํธ์์ useEffect ๋ก document ๋ฅผ ๋ฐ๊ฟ ํ์๊ฐ ์๋ค์!"
document.documentElement.classList.toggle('dark', isDark)
})// atomEffect ๋ Provider ํ์์์ useAtom ์ผ๋ก ํ์ฑํํด์ผ ํจ
function EffectActivator() {
// ํจ๊ณผ๋ฅผ ํ์ฑํํ๊ธฐ ์ํด atom ์ ๊ตฌ๋
useAtomValue(userAnalyticsEffect)
useAtomValue(darkModeEffect)
return null
}
// Provider ํ์ ์ด๋๊ฐ์ ๋ฐฐ์น
function App() {
return (
<Provider>
<EffectActivator />
<MainContent />
</Provider>
)
}useEffect vs atomEffect ๋น๊ต
// โ ์ปดํฌ๋ํธ์ ๋ถ์ฐ๋ side effect
const Header = () => {
const user = useAtomValue(userAtom)
// ์ด ์ฌ์ด๋ ์ดํํธ๋ Header ๊ฐ mount ๋์ด์ผ๋ง ์คํ๋จ
// Header ๊ฐ ์์ผ๋ฉด? ๋ถ์ ์ด๋ฒคํธ ์ ๊ฐ
useEffect(() => {
if (user) analytics.identify(user.id, { name: user.name })
}, [user])
return <div>...</div>
}
// โ
atomEffect โ atom ๋ ๋ฒจ์์ ์ค์ ์ง์ค ๊ด๋ฆฌ
// Header ์ปดํฌ๋ํธ ์กด์ฌ ์ฌ๋ถ์ ๋ฌด๊ดํ๊ฒ userAtom ์ด ๊ตฌ๋
๋ ๋๋ถํฐ ์คํ
const userAnalyticsEffect = atomEffect((get) => {
const user = get(userAtom)
if (user) analytics.identify(user.id, { name: user.name })
})๐ฌ write-only action atom โ ๋น์ฆ๋์ค ๋ก์ง ์บก์ํ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- write-only action atom ์ผ๋ก ๋น์ฆ๋์ค ๋ก์ง์ ์ปดํฌ๋ํธ์์ ๋ถ๋ฆฌํ ์ ์๋ค
- ๋ณต์กํ async ๋ก์ง์ atom ์ ์บก์ํํด์ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๊ฒฐํ๊ฒ ๋ง๋ค ์ ์๋ค
// ๐ฆ ์ํธ: "action atom ์ ์ฝ๊ธฐ ๊ฐ์ด null ์ด๊ณ , write ํจ์์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ด์์.
// ์ปดํฌ๋ํธ์ API ํธ์ถ ๋ก์ง์ด ์์ผ๋ฉด ํ
์คํธ๊ฐ ์ด๋ ค์์ง๊ฑฐ๋ ์."
// features/study/atoms/actions.ts
import { atom } from 'jotai'
import { userAtom } from '@/features/auth/atoms'
import { studyListAtom, selectedStudyIdAtom } from './index'
// ์คํฐ๋ ์ ์ฒญ action atom
export const submitStudyApplicationAtom = atom(
null, // ์ฝ๊ธฐ ๊ฐ ์์ (write-only)
async (get, set, studyId: string) => {
// 1. ์ ์ ์กฐ๊ฑด ํ์ธ โ auth atom ์์ ์ ์ ํ์ธ
const user = get(userAtom)
if (!user) throw new Error('๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค')
const studies = get(studyListAtom)
const study = studies.find((s) => s.id === studyId)
if (!study) throw new Error('์คํฐ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค')
if (study.memberCount >= study.maxMembers) throw new Error('์ ์์ด ์ด๊ณผ๋์์ต๋๋ค')
// 2. ๋๊ด์ ์
๋ฐ์ดํธ โ UI ๋จผ์ ๋ฐ์
set(studyListAtom, (prev) =>
prev.map((s) =>
s.id === studyId
? { ...s, memberCount: s.memberCount + 1, isApplied: true }
: s
)
)
// 3. ์๋ฒ API ํธ์ถ
try {
await fetch(`/api/studies/${studyId}/apply`, {
method: 'POST',
headers: { Authorization: `Bearer ${user.token}` },
body: JSON.stringify({ userId: user.id }),
})
} catch (error) {
// 4. ์คํจ ์ ๋กค๋ฐฑ
set(studyListAtom, (prev) =>
prev.map((s) =>
s.id === studyId
? { ...s, memberCount: s.memberCount - 1, isApplied: false }
: s
)
)
throw error
}
}
)
// ์คํฐ๋ ์ข์์ ํ ๊ธ action atom
export const toggleStudyLikeAtom = atom(
null,
async (get, set, studyId: string) => {
const user = get(userAtom)
if (!user) throw new Error('๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค')
// ํ์ฌ ์ข์์ ์ํ ํ์ธ
const studies = get(studyListAtom)
const study = studies.find((s) => s.id === studyId)
if (!study) return
const isLiked = study.likedBy?.includes(user.id) ?? false
// ๋๊ด์ ์
๋ฐ์ดํธ
set(studyListAtom, (prev) =>
prev.map((s) =>
s.id === studyId
? {
...s,
likeCount: isLiked ? s.likeCount - 1 : s.likeCount + 1,
likedBy: isLiked
? s.likedBy?.filter((id) => id !== user.id)
: [...(s.likedBy ?? []), user.id],
}
: s
)
)
// ์๋ฒ ๋๊ธฐํ
await fetch(`/api/studies/${studyId}/like`, {
method: isLiked ? 'DELETE' : 'POST',
})
}
)// ์ปดํฌ๋ํธ๋ action atom ์ ํธ์ถํ๊ธฐ๋ง ํ๋ฉด ๋จ โ ๋น์ฆ๋์ค ๋ก์ง ์์
'use client'
import { useSetAtom, useAtomValue } from 'jotai'
import { submitStudyApplicationAtom, toggleStudyLikeAtom } from '@/features/study/atoms/actions'
export function StudyCard({ study }: { study: Study }) {
// ๐ฃ ์์ฒ : "์ปดํฌ๋ํธ๊ฐ ์ง์ง ๋จ์ํด์ก์ด์! ๋ก์ง์ด ์ ๋ถ atom ์ ์์ผ๋๊น์"
const submitApplication = useSetAtom(submitStudyApplicationAtom)
const toggleLike = useSetAtom(toggleStudyLikeAtom)
const handleApply = async () => {
try {
await submitApplication(study.id)
// ์ฑ๊ณต toast ๋ฑ
} catch (e) {
// ์๋ฌ ์ฒ๋ฆฌ
alert((e as Error).message)
}
}
return (
<div>
<h3>{study.title}</h3>
<button onClick={() => toggleLike(study.id)}>
โค๏ธ {study.likeCount}
</button>
<button onClick={handleApply} disabled={study.isApplied}>
{study.isApplied ? '์ ์ฒญ ์๋ฃ' : '์ ์ฒญํ๊ธฐ'}
</button>
</div>
)
}โ๏ธ Jotai vs Zustand โ ์ธ์ ์ด๋ค ๊ฒ์ ์ ํํ๋๊ฐ? ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Jotai ์ Zustand ์ ์ฒ ํ์ ์ฐจ์ด๋ฅผ ์ค๋ช ํ ์ ์๋ค
- ํ๋ก์ ํธ ํน์ฑ์ ๋ฐ๋ผ ์ด๋ค ๋๊ตฌ๊ฐ ๋ ์ ํฉํ์ง ํ๋จํ ์ ์๋ค
์ฒ ํ์ ์ฐจ์ด
Jotai (Bottom-up):
- ๋
๋ฆฝ์ ์ธ ์์ atom ์ ์กฐํฉํด์ ์ํ ๊ตฌ์ฑ
- React ์ ๊ธด๋ฐํ๊ฒ ํตํฉ (React Suspense, Concurrent features)
- atom ๋จ์ ์ ํ์ ๋ฆฌ๋ ๋๋ง
- ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR) ์นํ์ (Provider ์ค์ฝํ ๊ฒฉ๋ฆฌ)
- ์ฝ๋ ๋ถํ ์ ์ ๋ฆฌ (atom ์ด ํ์ํ ๋๋ง ๋ฒ๋ค์ ํฌํจ)
Zustand (Top-down):
- ํ๋์ ์คํ ์ด ๊ฐ์ฒด์ ๋ชจ๋ ์ํ๋ฅผ ๋ฃ๋ ๋ฐฉ์
- React ์ ๋ ์ข
์์ (React ์์ด๋ ์ฌ์ฉ ๊ฐ๋ฅ)
- ์
๋ ํฐ(selector)๋ก ํน์ ๋ถ๋ถ๋ง ๊ตฌ๋
- Redux DevTools ์ ์์ฐ์ค๋ฝ๊ฒ ํตํฉ
- ๋ฏธ๋ค์จ์ด(middleware) ์์คํ
์ ๊ณต
์ ํ ๊ธฐ์ค ๋น๊ตํ
| ์ํฉ | Jotai | Zustand |
|---|---|---|
| Next.js App Router (SSR ํ์) | โ ๊ฐ๋ ฅ ์ถ์ฒ (Provider ์ค์ฝํ ๊ฒฉ๋ฆฌ) | ๐ก ๊ฐ๋ฅํ์ง๋ง ์ค์ ๋ณต์ก |
| React Suspense ์ ๋น๋๊ธฐ ์ํ | โ ๋ค์ดํฐ๋ธ ์ง์ | ๐ก ๋ณ๋ ์ค์ ํ์ |
| ์ฝ๋ ๋ถํ / ํธ๋ฆฌ ์ ฐ์ดํน | โ atom ๋จ์ ๋ถํ | ๐ก ์คํ ์ด ๋จ์ ๋ถํ |
| Redux DevTools ํ์ | ๐ก jotai-devtools ๋ก ๊ฐ๋ฅ | โ ๊ธฐ๋ณธ ์ง์ |
| React ์ธ๋ถ์์ ์ํ ์ ๊ทผ | ๐ก createStore ๋ก ๊ฐ๋ฅ | โ
getState() ๋ก ์ฝ๊ฒ ๊ฐ๋ฅ |
| ๋ฏธ๋ค์จ์ด๊ฐ ํ์ | ๐ก ์ ํ์ | โ ๋ฏธ๋ค์จ์ด ์์คํ ์ ๊ณต |
| useState ์์ ์์ฐ์ค๋ฌ์ด ์ ํ | โ ๋์ผํ ์ฌ์ฉ๊ฐ | ๐ก ์ ํจํด ํ์ต ํ์ |
| ์ด๋ฏธ Redux ๋ฅผ ์ฐ๊ณ ์๋ค๊ฐ ์ ํ | ๐ก ์ ํจ๋ฌ๋ค์ | โ ์ ์ฌํ ํจํด |
๊ณต์กด๋ ๊ฐ๋ฅํ๋ค
// ๐ฆ ์ํธ: "๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ด ์จ๋ ๋ผ์. ๊ฐ์ ์ํ๋ ์์ญ์ด ์์ผ๋๊น์."
// Jotai โ SSR ์ด ํ์ํ UI ์ํ (Provider ์ค์ฝํ๋ก ๊ฒฉ๋ฆฌ)
const userAtom = atom<User | null>(null) // SSR ์์ ์์
const studyFiltersAtom = atom<StudyFilters>({ ... }) // UI ์ํ
// Zustand โ React ์ธ๋ถ์์ ์ ๊ทผํด์ผ ํ๋ ๊ฒฝ์ฐ (e.g., WebSocket ํธ๋ค๋ฌ)
const useNotificationStore = create<NotificationStore>((set) => ({
notifications: [],
addNotification: (n) => set((state) => ({
notifications: [...state.notifications, n],
})),
}))
// WebSocket ํธ๋ค๋ฌ (React ๋ฐ๊นฅ)์์ Zustand store ์ ๊ทผ
ws.onmessage = (event) => {
const notification = JSON.parse(event.data)
// Zustand: React ์์ด๋ store ์ ์ง์ ์ ๊ทผ
useNotificationStore.getState().addNotification(notification)
}๐ฆ Before/After โ ํผ์ฌ๋ ๊ตฌ์กฐ โ Feature ์ฌ๋ผ์ด์ฑ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ํผ์ฌ๋ atom ๊ตฌ์กฐ๋ฅผ Feature ๋จ์๋ก ๋ฆฌํฉํ ๋งํ๋ ๊ณผ์ ์ ์ดํดํ๋ค
// โ Before โ atoms/index.ts (800์ค ํผ์ฌ)
// ๐ฃ ์์ฒ : "๋ชจ๋ ๊ฒ ์ฌ๊ธฐ ์์ด์. ๋ญ๊ฐ ๋ญ์ง ๋ชจ๋ฅด๊ฒ ์ด์."
export const userAtom = atom<User | null>(null)
export const sessionAtom = atom<Session | null>(null)
export const studyListAtom = atom<Study[]>([])
export const selectedStudyAtom = atom<Study | null>(null)
export const studyFiltersAtom = atom({ category: 'all', sortBy: 'latest' })
export const messagesAtom = atom<Message[]>([])
export const roomAtom = atom<Room | null>(null)
export const isModalOpenAtom = atom(false)
export const notificationsAtom = atom<Notification[]>([])
// ... 300์ค ๋// โ
After โ Feature ๋จ์๋ก ๋ถ๋ฆฌ
// ๐ฆ ์ํธ: "์ด๋ ๊ฒ ๋๋๋ฉด ๊ฐ ํ์ผ์ด ํ๋์ ๋๋ฉ์ธ์๋ง ์ง์คํด์."
// features/auth/atoms/index.ts (30์ค)
export { userAtom, sessionAtom, isLoggedInAtom, isAdminAtom }
// features/study/atoms/index.ts (50์ค)
export { studyListAtom, selectedStudyIdAtom, studyFiltersAtom, selectedStudyAtom }
// features/study/atoms/actions.ts (80์ค)
export { submitStudyApplicationAtom, toggleStudyLikeAtom }
// features/chat/atoms/index.ts (30์ค)
export { messagesAtom, roomAtom, isConnectedAtom }
// features/notification/atoms/index.ts (20์ค)
export { notificationsAtom, unreadCountAtom }
// shared/atoms/index.ts (20์ค)
export { isModalOpenAtom, themeAtom, drawerOpenAtom }
// โ ๊ฐ ํ์ผ์ด ์๊ธฐ ๋๋ฉ์ธ์๋ง ์ง์ค, ํ๊ท 30~80์ค
// โ git blame ์ด ์๋ฏธ ์์ด์ง
// โ ํ์์ด "์คํฐ๋ atom ์ด๋?" ํ๋ฉด ๋ฐ๋ก features/study/atoms ๋ก ์๋ด๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F ๋ก ๊ฒ์ํด๋ด. ๋๋ถ๋ถ ์ฌ๊ธฐ ์์ด.
โ ์ํ ์์กด์ฑ ์๋ฌ โ "Cannot access before initialization"
์์ธ: feature A ์ atom ์ด feature B ๋ฅผ import ํ๊ณ , feature B ์ atom ์ด feature A ๋ฅผ import
ํด๊ฒฐ์ฑ :
// ๊ณตํต ์์กด ํญ๋ชฉ์ shared/atoms ๋ก ์ด๋
// ๋๋ ์์กด์ฑ ๋ฐฉํฅ์ ๋จ๋ฐฉํฅ์ผ๋ก ์ฌ์ค๊ณโ atomEffect ๊ฐ ์๋ํ์ง ์์
์์ธ: Provider ํ์์์ useAtomValue(myEffect) ๋ก ๊ตฌ๋
ํ์ง ์์
ํด๊ฒฐ์ฑ :
// EffectActivator ์ปดํฌ๋ํธ๋ฅผ Provider ํ์์ ์ถ๊ฐ
function EffectActivator() {
useAtomValue(myAtomEffect) // ๊ตฌ๋
ํ์ฑํ ํ์
return null
}โ action atom ์ ํ์ ์๋ฌ
์์ธ: write-only atom ์ ํ์ ํ๋ผ๋ฏธํฐ ์ค์ ์ค์
ํด๊ฒฐ์ฑ :
// write-only atom ์ฌ๋ฐ๋ฅธ ํ์
์ ์ธ
const myActionAtom = atom<
null, // ์ฝ๊ธฐ ๊ฐ ํ์
[string], // ์ธ์ ํ์
(ํํ)
Promise<void> // ๋ฐํ ํ์
>(
null, // ์ด๊ธฐ ์ฝ๊ธฐ ๊ฐ
async (get, set, studyId: string) => {
// ...
}
)๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ ๋๊ท๋ชจ Jotai ์ํคํ ์ฒ ํต์ฌ ์์น
| ์์น | ์ค๋ช | ์ ์ฉ |
|---|---|---|
| Feature ์ฌ๋ผ์ด์ฑ | ๋๋ฉ์ธ๋ณ๋ก atoms ํด๋ ๋ถ๋ฆฌ | features/study/atoms/index.ts |
| ๋จ๋ฐฉํฅ ์์กด์ฑ | ์์ feature โ ํ์ feature ๋ง ํ์ฉ | auth โ study โ , study โ auth โ |
| shared ๋ ์ด์ด | ๊ณตํต ์ํ๋ shared ํด๋ | shared/atoms/index.ts |
| ํ ์ผ๋ก ์บก์ํ | ์ปดํฌ๋ํธ๋ ํ ์ ํตํด atom ์ ๊ทผ | useStudySearch() |
| action atom | ๋น์ฆ๋์ค ๋ก์ง์ atom ์ผ๋ก ๋ถ๋ฆฌ | submitStudyApplicationAtom |
| debugLabel ํ์ | ๋ชจ๋ atom ์ ์ด๋ฆ ๋ถ์ฌ | atom.debugLabel = 'feature/atomName' |
โ๏ธ Jotai vs Zustand ํ ์ค ์์ฝ
Jotai โ SSR + React Suspense + ์ฝ๋ ๋ถํ ์ด ์ค์ํ Next.js ํ๋ก์ ํธ
Zustand โ React ์ธ๋ถ ์ ๊ทผ + Redux DevTools + ๋ฏธ๋ค์จ์ด๊ฐ ํ์ํ ํ๋ก์ ํธ
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ๐๏ธ ์ํคํ ์ฒ ์ค๊ณ ํ๋จ
์์ฒ ์ด๊ฐ "์คํฐ๋ ์ ์ฒญ ๋ก์ง์ด StudyCard ์ปดํฌ๋ํธ ์์ 100์ค์ด๋ ์์ด์. ์ด๋ป๊ฒ ์ ๋ฆฌํด์?" ๋ผ๊ณ ๋ฌผ์์ด.
์ํธ ๋์ด ์ ์ํ ๊ฐ์ฅ ์ฐ์ํ ํจํด์?
- A) ๋ก์ง์ ๋ณ๋ ์ ํธ๋ฆฌํฐ ํจ์๋ก ๋ถ๋ฆฌํ๊ณ ์ปดํฌ๋ํธ์์ ํธ์ถํ๋ค
- B) ์ปค์คํ
ํ
(
useStudyApplication)์ผ๋ก ๋ก์ง์ ๋ถ๋ฆฌํ๋ค - C) write-only action atom(
submitStudyApplicationAtom)์ผ๋ก ๋น์ฆ๋์ค ๋ก์ง์ ์บก์ํํ๊ณ , ์ปดํฌ๋ํธ์์๋useSetAtom์ผ๋ก ํธ์ถ๋ง ํ๋ค - D) ๋ก์ง์ ์๋ฒ API ๋ก ์ด๋ํ๋ค
โ ์ ๋ต: C (B ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ง๋ง Jotai ์ํ๊ณ์ ์ด์์ ์ธ ํจํด์ C)
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: write-only action atom ํจํด์ ํต์ฌ ์ฅ์ ์ ๋น์ฆ๋์ค ๋ก์ง์ด atom ๋ ๋ฒจ์ ์์ด์ ์ปดํฌ๋ํธ์ ๋
๋ฆฝ์ ์ด๋ผ๋ ๊ฑฐ์ผ. ์ด๋ค ์ปดํฌ๋ํธ์์๋
useSetAtom(submitStudyApplicationAtom)์ผ๋ก ํธ์ถํ ์ ์๊ณ , ๋ก์ง์ ๋ณ๊ฒฝํด๋ ์ปดํฌ๋ํธ ์ฝ๋๋ฅผ ๊ฑด๋๋ฆด ํ์๊ฐ ์์ด. ๋ํcreateStore()๋ก ํ ์คํธํ ๋store.set(submitStudyApplicationAtom, studyId)๋ก ์ง์ ์คํํ ์ ์์ด์ ํ ์คํธ๊ฐ ์ฌ์. - ์ค๋ต ํผ๋๋ฐฑ: A ์ B ๋ ๊ฐ์ ์ด์ง๋ง, action atom ์ Jotai ์ store ์ปจํ ์คํธ๋ฅผ ์์ ํ ํ์ฉํด์ ๋ค๋ฅธ atom ์ ์ ๊ทผํ๊ณ ์์ ํ ์ ์๋ค๋ ์ถ๊ฐ ์ฅ์ ์ด ์์ด.
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "action atom = ๋น์ฆ๋์ค ๋ก์ง์ ์ง." ์ปดํฌ๋ํธ๋ "์ง์ ์ด์ธ์ข ๋ง ๋๋ฅด๋ฉด ๋ผ."
Q2. โ๏ธ Jotai vs Zustand
์์ ๋์ด "WebSocket ์ผ๋ก ์ค์๊ฐ ์๋ฆผ์ด ์ค๋ฉด React ๋ฐ๊นฅ์์ ๋ฐ๋ก ์ํ๋ฅผ ์ ๋ฐ์ดํธํด์ผ ํด. Jotai ๋ ๊ทธ๊ฒ ํ๋ ๊ฐ?" ๋ผ๊ณ ๋ฌผ์์ด.
์ํธ ๋์ ๋ต๋ณ์ผ๋ก ๊ฐ์ฅ ์ ํํ ๊ฒ์?
- A) Jotai ๋ React ๋ฐ๊นฅ์์ ์ํ ์ ๊ทผ์ด ๋ถ๊ฐ๋ฅํ๋ค. Zustand ๋ก ๊ต์ฒดํด์ผ ํ๋ค
- B) Jotai ๋
createStore()์store.set()์ผ๋ก React ๋ฐ๊นฅ์์ atom ์ ์ ๋ฐ์ดํธํ ์ ์๋ค.useStore()ํ ์ผ๋ก store ์ฐธ์กฐ๋ฅผ ๊ฐ์ ธ์จ ๋ค WebSocket ์ฝ๋ฐฑ์์ ์ฌ์ฉํ๋ฉด ๋๋ค - C) WebSocket ์ ํญ์ ์๋ฒ์์ ์ฒ๋ฆฌํด์ผ ํ๋ค
- D) Zustand ๊ฐ ๋ ์ ํฉํ์ง๋ง Jotai ์์ ๋ถ๊ฐ๋ฅํ ๊ฑด ์๋๋ค โ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ด ์ฐ๋ ๊ฒ๋ ๊ณ ๋ คํ ์ ์๋ค
โ ์ ๋ต: B (D ๋ ์ํฉ์ ๋ฐ๋ผ ์ ํจํ์ง๋ง B ๊ฐ ๋ ์์ ํ ๋ต๋ณ)
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: Jotai ๋
createStore()์store.set()์ผ๋ก React ๋ฐ๊นฅ์์๋ atom ์ ์ ๋ฐ์ดํธํ ์ ์์ด (08๋ฒ ๊ฐ์ด๋).useStore()ํ ์ผ๋ก ํ์ฌ Provider ์ store ๋ฅผ ๊ฐ์ ธ์จ ํ,useEffect์์ WebSocket ์ฝ๋ฐฑ์ ์ ๋ฌํ๋ฉด ๋ผ. Zustand ๋useNotificationStore.getState().addNotification()์ฒ๋ผ ๋ ์ง๊ด์ ์ด๊ธด ํด. - ์ค๋ต ํผ๋๋ฐฑ: A ๋ ํ๋ ธ์ด. Jotai ๋ React ๋ฐ๊นฅ ์ ๊ทผ์ด ๊ฐ๋ฅํด (08๋ฒ ๊ฐ์ด๋์ useStore ์น์ ์ฐธ๊ณ ).
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "Jotai ๋ React ๋ฐ์์ ๋๋ค. ๋จ, useStore() ๋ก store ๋ฅผ ๊บผ๋ด์ผ ํด."
Q3. ๐ ์์กด์ฑ ์ค๊ณ
์์ฒ ์ด๊ฐ "auth atom ์์ ์คํฐ๋ ๋ชฉ๋ก์ ํํฐ๋งํด์ผ ํด์ study atom ์ import ํ๋๋ ์ํธ ๋์ด ๋ง์์ด์. ์์?" ๋ผ๊ณ ๋ฌผ์์ด.
์ํธ ๋์ ์ค๋ช ์ ์ฌํํด๋ด.
์์ ๋ต๋ณ:
"auth ์ study ๊ฐ ์๋ก๋ฅผ import ํ๋ฉด ์ํ ์์กด์ฑ(Circular Dependency)์ด ์๊ฒจ์. study ๊ฐ auth ์ ์์กดํ๋๋ฐ, auth ๋ study ์ ์์กดํ๋ฉด JavaScript ๋ชจ๋ ๋ก๋๊ฐ ์ด๋ ๊ฒ์ ๋จผ์ ์ด๊ธฐํํด์ผ ํ ์ง ๋ชฐ๋ผ์ 'Cannot access before initialization' ์๋ฌ๊ฐ ๋ ์ ์์ด์. ํด๊ฒฐ์ฑ ์ ๋ ๊ฐ์ง์์: (1) ์์กด์ฑ ๋ฐฉํฅ์ ๋จ๋ฐฉํฅ์ผ๋ก ๊ณ ์ โ auth โ study ๋ง ํ์ฉํ๊ณ , study โ auth ๋ ๊ธ์ง. (2) ๋ feature ๊ฐ ๊ณตํต์ผ๋ก ํ์ํ ๋ก์ง์
shared/atoms๋ ์ด์ด๋ก ์ฌ๋ฆฌ๊ธฐ. ์ด ๊ฒฝ์ฐ ํ์ฌ ์ ์ ์ ์คํฐ๋ ๋ชฉ๋ก์ ํ์ํ๋ atom ์shared/atoms์ ๋๋ฉด auth ์ study ์ด๋ ์ชฝ๋ ์๋ก๋ฅผ import ํ์ง ์์๋ ๋ผ์."
๐ก ํต์ฌ: "์์กด์ฑ์ ํญ์ ํ ๋ฐฉํฅ์ผ๋ก. ์๋ฐฉํฅ ์์กด์ฑ์ ์ค๊ณ ๋ฌธ์ ์ ์ ํธ์ผ."
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
atoms/index.ts ๊ฐ 800์ค์ด ๋์ ๋ ์์งํ "์ด๊ฒ ๋ง๋?" ์ถ์์ด. ๊ทผ๋ฐ ๋น์ฅ ๊ธฐ๋ฅ ๋ง๋ค๊ธฐ ๋ฐ๋น ์ ๊ทธ๋ฅ ๊ฑฐ๊ธฐ๋ค ๊ณ์ ์์๊ฑฐ๋ . ๊ฒฐ๊ตญ ์ํธ ๋์ด "๋์ด์. Feature ์ฌ๋ผ์ด์ฑ ํด์ผ ํ ๋๊ฐ ๋๋ค์" ๋ผ๊ณ ํ์ ๋ ์คํ๋ ค ์๋๊ฐ์ด ๋ค์์ด.
Feature ๋จ์๋ก ๋๋๊ณ ๋์ ๊ฐ ํ์ผ์ด 30~80์ค์ด ๋๋๊น ์ง์ง ๋์ ์์ ๋ค์ด์์ด. "์คํฐ๋ ๊ด๋ จ atom ์ด ์ด๋ ์์ด?" โ features/study/atoms ๋ก ๋ฐ๋ก ๊ฐ๋ฉด ๋ผ. ์ด๊ฒ ์ข์ ์ํคํ
์ฒ๊ตฌ๋ ์ถ์์ด.
๊ทธ๋ฆฌ๊ณ write-only action atom ํจํด... ์ฒ์์ "๊ทธ๋ฅ ์ปดํฌ๋ํธ์์ API ํธ์ถํ๋ฉด ์ ๋๋?" ์ถ์๋๋ฐ, action atom ์ผ๋ก ๋ถ๋ฆฌํ๊ณ ๋์ ์ปดํฌ๋ํธ ์ฝ๋๊ฐ ์ผ๋ง๋ ๊น๋ํด์ง๋์ง ๋ณด๊ณ ์๊ฐ์ด ๋ฐ๋์์ด. ์ปดํฌ๋ํธ๋ "์ ์ฒญํ๊ธฐ ๋ฒํผ ํด๋ฆญ" ๋ง ํ๊ณ , ๋๋จธ์ง๋ atom ์ด ๋ค ์ฒ๋ฆฌํ๋ ๊ฑฐ์ผ.
๐ก ์ค๋์ ๊ตํ: "์ฝ๋๋ ๋ณ๊ฒฝ๋๋ ์ด์ ๊ฐ ๊ฐ์ ๊ฒ๋ผ๋ฆฌ ๋ฌถ์ด์ผ ํ๋ค. Feature ์ฌ๋ผ์ด์ฑ์ ์ด ์์น์ ์ค์ฒ์ด๋ค."
Jotai vs Zustand ๋น๊ต๋ ์ฌ๋ฏธ์์์ด. ์์ด์์ ์๋ค๋ ๊ฑธ ๋ค์ ๋ฐฐ์ ์ด. ์ํฉ์ ๋ฐ๋ผ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ด ์ฐ๋ ๊ฒ๋ ์ ํ ์ด์ํ ๊ฒ ์๋๊ณ .
๋๋์ด ์ค๋ ๋ฆฌํฉํ ๋ง์ด ๋๋ฌ๋ค! ์ด์ ์ง์ง ํด๊ทผ์ด๋ค. ์ค๋์ ์ค๋๋ง์ ์ผ์ฐ ํด๊ทผํ๋ ๋ ์ด๋ผ ๋ญ ํด๋ ๊ธฐ๋ถ์ด ์ข์. ํธ์์ ์์ ์์ด์คํฌ๋ฆผ ํ๋ ์ง์ด๋ค๊ณ ์ง ๊ฐ์ ํน ์ฌ์ด์ผ์ง.