๐Ÿงฉ 14. ๋Œ€๊ทœ๋ชจ ์•ฑ Jotai ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

๐Ÿ“‹ ๊ฐœ์š”

Feature ๋‹จ์œ„ atom ์Šฌ๋ผ์ด์‹ฑ, cross-feature ์˜์กด์„ฑ ๊ด€๋ฆฌ, atom effect, write-only action atom ํŒจํ„ด, Jotai vs Zustand ์„ ํƒ ๊ธฐ์ค€์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • Feature ๋‹จ์œ„๋กœ atom ์„ ์Šฌ๋ผ์ด์‹ฑํ•˜๋Š” ํด๋” ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • cross-feature ์˜์กด์„ฑ์—์„œ ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ์ƒ๊ธฐ์ง€ ์•Š๋„๋ก ์˜์กด์„ฑ ๋ฐฉํ–ฅ์„ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • write-only action atom ํŒจํ„ด์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ atom ์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์บก์Аํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 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) ์‹œ์Šคํ…œ ์ œ๊ณต

์„ ํƒ ๊ธฐ์ค€ ๋น„๊ตํ‘œ

์ƒํ™ฉJotaiZustand
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 ๋น„๊ต๋„ ์žฌ๋ฏธ์žˆ์—ˆ์–ด. ์€์ด์•Œ์€ ์—†๋‹ค๋Š” ๊ฑธ ๋‹ค์‹œ ๋ฐฐ์› ์–ด. ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ™์ด ์“ฐ๋Š” ๊ฒƒ๋„ ์ „ํ˜€ ์ด์ƒํ•œ ๊ฒŒ ์•„๋‹ˆ๊ณ .

๋“œ๋””์–ด ์˜ค๋Š˜ ๋ฆฌํŒฉํ† ๋ง์ด ๋๋‚ฌ๋‹ค! ์ด์ œ ์ง„์งœ ํ‡ด๊ทผ์ด๋‹ค. ์˜ค๋Š˜์€ ์˜ค๋žœ๋งŒ์— ์ผ์ฐ ํ‡ด๊ทผํ•˜๋Š” ๋‚ ์ด๋ผ ๋ญ˜ ํ•ด๋„ ๊ธฐ๋ถ„์ด ์ข‹์•„. ํŽธ์˜์ ์—์„œ ์•„์ด์Šคํฌ๋ฆผ ํ•˜๋‚˜ ์ง‘์–ด๋“ค๊ณ  ์ง‘ ๊ฐ€์„œ ํ‘น ์‰ฌ์–ด์•ผ์ง€.


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ