🧩 13. Jotai ν…ŒμŠ€νŒ… & DevTools μ‹€μ „

πŸ“‹ κ°œμš”

createStore둜 isolated store λ§Œλ“€κΈ°, atom μ΄ˆκΈ°κ°’ override, React Testing Library 톡합, DevTools 섀정을 λ°°μ›λ‹ˆλ‹€.

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • createStore() 둜 ν…ŒμŠ€νŠΈλ§ˆλ‹€ λ…λ¦½λœ store λ₯Ό λ§Œλ“€μ–΄ μƒνƒœ μ˜€μ—Όμ„ μ™„μ „νžˆ 차단할 수 μžˆλ‹€.
  • store.set() 으둜 atom μ΄ˆκΈ°κ°’μ„ override ν•΄μ„œ λ‹€μ–‘ν•œ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό ν…ŒμŠ€νŠΈν•  수 μžˆλ‹€.
  • jotai-devtools 와 debugLabel 둜 개발 κ²½ν—˜(DX)을 크게 ν–₯μƒμ‹œν‚¬ 수 μžˆλ‹€.

πŸ“‹ λͺ©μ°¨


πŸ“Œ 이 λ¬Έμ„œλ₯Ό 읽기 전에

⏱️ μ˜ˆμƒ 읽기 μ‹œκ°„: 30λΆ„ (전체) / 핡심 파트만: 18λΆ„

πŸ—ΊοΈ 이 λ¬Έμ„œμ˜ λ°°κ²½ 세계관: ν…ŒμŠ€νŠΈ κ°„ μƒνƒœ μ˜€μ—Ό 사건

영수 λ‹˜μ΄ "이제 ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό λ†’μ—¬μ•Ό ν•œλ‹€" κ³  ν–ˆμ–΄. μ˜μ² μ΄κ°€ μ˜μš• λ„˜μΉ˜κ²Œ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν–ˆλŠ”λ°...

  • 🐣 영철: "ν…ŒμŠ€νŠΈκ°€ μ΄μƒν•΄μš”. 혼자 μ‹€ν–‰ν•˜λ©΄ ν†΅κ³Όν•˜λŠ”λ°, 전체 μ‹€ν–‰ν•˜λ©΄ μ‹€νŒ¨ν•΄μš”."
  • 🦁 영호 λ‹˜: "μ–΄λ–€ μ—λŸ¬μ˜ˆμš”?"
  • 🐣 영철: "'영호' μœ μ €κ°€ ν•„μš”ν•œλ° '영철' μœ μ €κ°€ λ‚˜μ˜¨λ‹€κ³  ν•΄μš”. 근데 μ „ '영철' μœ μ €λ₯Ό μ„€μ •ν•œ 적 μ—†λŠ”λ°?"
  • 🦁 영호 λ‹˜: (μ½”λ“œ 확인) "이전 ν…ŒμŠ€νŠΈμ—μ„œ userAtom 에 '영철' 을 set ν•˜κ³  정리λ₯Ό μ•ˆ ν–ˆλ„€μš”. μ „μ—­ atom 은 ν…ŒμŠ€νŠΈ 간에 κ³΅μœ λΌμš”."
  • 🐣 영철: "μ•„... μ–΄λ–»κ²Œ ν•΄κ²°ν•΄μš”?"
  • 🦁 영호 λ‹˜: "각 ν…ŒμŠ€νŠΈλ§ˆλ‹€ μƒˆ createStore() λ₯Ό μ“°λ©΄ μ™„μ „νžˆ κ²©λ¦¬λΌμš”. 같이 λ³Όκ²Œμš”."

πŸ€” μ™œ ν…ŒμŠ€νŠΈκ°€ μ–΄λ €μš΄κ°€? 🟒

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • μ „μ—­ atom 이 ν…ŒμŠ€νŠΈ κ°„ μƒνƒœ μ˜€μ—Όμ„ μΌμœΌν‚€λŠ” λ©”μ»€λ‹ˆμ¦˜μ„ μ΄ν•΄ν•œλ‹€
  • afterEach 둜 μ •λ¦¬ν•˜λŠ” λ°©μ‹μ˜ ν•œκ³„λ₯Ό μ•ˆλ‹€
// atoms/user.ts
export const userAtom = atom<User | null>(null)
 
// λ¬Έμ œκ°€ λ˜λŠ” ν…ŒμŠ€νŠΈ νŒ¨ν„΄
describe('StudyCard', () => {
  test('둜그인 μœ μ €λŠ” μ’‹μ•„μš” λ²„νŠΌμ„ λ³Ό 수 μžˆλ‹€', () => {
    // ν…ŒμŠ€νŠΈ Aμ—μ„œ userAtom 에 '영호' λ₯Ό set
    // 🐣 영철: "이 ν…ŒμŠ€νŠΈλ§Œ λ‹¨λ…μœΌλ‘œ 보면 μ™„λ²½ν•œλ°μš”..."
    store.set(userAtom, { name: '영호', role: 'admin' })
    // ... ν…ŒμŠ€νŠΈ μ‹€ν–‰ ...
    // ❌ ν…ŒμŠ€νŠΈ λλ‚˜λ„ atom 정리 μ•ˆ 됨
  })
 
  test('λΉ„λ‘œκ·ΈμΈ μœ μ €λŠ” μ’‹μ•„μš” λ²„νŠΌμ„ λ³Ό 수 μ—†λ‹€', () => {
    // ν…ŒμŠ€νŠΈ B β€” 이전 ν…ŒμŠ€νŠΈμ˜ '영호' κ°€ atom 에 λ‚¨μ•„μžˆμŒ!
    // 🐣 영철: "userAtom 이 null 이어야 ν•˜λŠ”λ° '영호' κ°€ λ‚˜μ™€μš”..."
    const user = store.get(userAtom) // '영호'!!
    // ν…ŒμŠ€νŠΈ μ‹€νŒ¨ πŸ’₯
  })
})
ν…ŒμŠ€νŠΈ μˆœμ„œ:
[ν…ŒμŠ€νŠΈ A] userAtom = '영호' β†’ μ„€μ • β†’ ν…ŒμŠ€νŠΈ 톡과 βœ…
[ν…ŒμŠ€νŠΈ B] userAtom = '영호' (이전 ν…ŒμŠ€νŠΈ μž”μž¬) β†’ null κΈ°λŒ€ν–ˆλŠ”λ° μ‹€νŒ¨ ❌

root 원인: λͺ¨λ“ˆ 레벨의 default store κ°€ λͺ¨λ“  ν…ŒμŠ€νŠΈμ—μ„œ κ³΅μœ λ˜μ–΄, 이전 ν…ŒμŠ€νŠΈμ˜ μƒνƒœκ°€ λ‹€μŒ ν…ŒμŠ€νŠΈμ— 영ν–₯을 쀌

πŸ”§ createStore 둜 isolated store λ§Œλ“€κΈ° 🟑

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • 각 ν…ŒμŠ€νŠΈλ§ˆλ‹€ μƒˆ store λ₯Ό λ§Œλ“€μ–΄ μ™„μ „ν•œ 격리λ₯Ό 달성할 수 μžˆλ‹€
  • beforeEach 와 createStore() λ₯Ό μ‘°ν•©ν•˜λŠ” νŒ¨ν„΄μ„ μ•ˆλ‹€
import { createStore } from 'jotai'
import { userAtom, studyListAtom } from '@/atoms'
 
describe('StudyCard', () => {
  // 각 ν…ŒμŠ€νŠΈλ§ˆλ‹€ μƒˆ store μΈμŠ€ν„΄μŠ€ 생성
  let store: ReturnType<typeof createStore>
 
  beforeEach(() => {
    // 🦁 영호: "beforeEach λ§ˆλ‹€ createStore() λ₯Ό ν˜ΈμΆœν•˜λ©΄
    //          각 ν…ŒμŠ€νŠΈλŠ” μ™„μ „νžˆ 빈 store μ—μ„œ μ‹œμž‘ν•΄μš”"
    store = createStore()
  })
 
  // afterEach μ—μ„œ cleanup 을 λ”°λ‘œ ν•  ν•„μš” μ—†μŒ
  // store λ³€μˆ˜κ°€ λ‹€μŒ beforeEach μ—μ„œ μƒˆ μΈμŠ€ν„΄μŠ€λ‘œ κ΅μ²΄λ˜λ‹ˆκΉŒ
 
  test('둜그인 μœ μ €λŠ” μ’‹μ•„μš” λ²„νŠΌμ„ λ³Ό 수 μžˆλ‹€', () => {
    // 이 ν…ŒμŠ€νŠΈλ§Œμ˜ 독립 store 에 μ„€μ •
    store.set(userAtom, { name: '영호', role: 'admin' })
    // ... ν…ŒμŠ€νŠΈ ...
    // store λŠ” 이 ν…ŒμŠ€νŠΈλ§Œμ˜ 것 β€” λ‹€μŒ ν…ŒμŠ€νŠΈμ— 영ν–₯ μ—†μŒ
  })
 
  test('λΉ„λ‘œκ·ΈμΈ μœ μ €λŠ” μ’‹μ•„μš” λ²„νŠΌμ„ λ³Ό 수 μ—†λ‹€', () => {
    // store = μ™„μ „νžˆ μƒˆ μΈμŠ€ν„΄μŠ€ β€” 이전 ν…ŒμŠ€νŠΈμ™€ 무관
    // 🐣 영철: "μ•„! beforeEach μ—μ„œ createStore() λ₯Ό ν˜ΈμΆœν•˜λ‹ˆκΉŒ 항상 κΉ¨λ—ν•˜κ²Œ μ‹œμž‘ν•˜λ„€μš”!"
    const user = store.get(userAtom) // null βœ…
    // ν…ŒμŠ€νŠΈ 톡과
  })
})

store 직접 μ‚¬μš© (React 없이 atom λ‹¨μœ„ ν…ŒμŠ€νŠΈ)

import { createStore, atom } from 'jotai'
 
// derived atom ν…ŒμŠ€νŠΈ β€” μ»΄ν¬λ„ŒνŠΈ 없이도 κ°€λŠ₯
const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)
 
test('doubledAtom 은 countAtom 의 두 λ°°λ₯Ό λ°˜ν™˜ν•œλ‹€', () => {
  const store = createStore()
 
  store.set(countAtom, 5)
  expect(store.get(doubledAtom)).toBe(10) // βœ…
 
  store.set(countAtom, 100)
  expect(store.get(doubledAtom)).toBe(200) // βœ…
})
 
// async atom ν…ŒμŠ€νŠΈ
const asyncDataAtom = atom(async () => {
  const res = await fetch('/api/studies')
  return res.json()
})
 
test('asyncDataAtom 은 μŠ€ν„°λ”” λͺ©λ‘μ„ λ°˜ν™˜ν•œλ‹€', async () => {
  // fetch λ₯Ό mock 처리
  global.fetch = jest.fn().mockResolvedValue({
    json: () => Promise.resolve([{ id: '1', title: 'ν…ŒμŠ€νŠΈ μŠ€ν„°λ””' }]),
  })
 
  const store = createStore()
  const data = await store.get(asyncDataAtom)
  expect(data).toHaveLength(1)
  expect(data[0].title).toBe('ν…ŒμŠ€νŠΈ μŠ€ν„°λ””')
})

πŸ—οΈ renderWithStore β€” ν…ŒμŠ€νŠΈμš© 헬퍼 ν•¨μˆ˜ 🟑

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • ν…ŒμŠ€νŠΈ μ „μš© renderWithStore 헬퍼 ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄ μž¬μ‚¬μš©ν•  수 μžˆλ‹€
  • useHydrateAtoms λ₯Ό μ‚¬μš©ν•œ μ΄ˆκΈ°κ°’ μ£Όμž… νŒ¨ν„΄κ³Ό 비ꡐ할 수 μžˆλ‹€
// test/utils.tsx β€” ν…ŒμŠ€νŠΈ μœ ν‹Έλ¦¬ν‹°
import React, { type ReactElement } from 'react'
import { render, type RenderOptions } from '@testing-library/react'
import { Provider, createStore } from 'jotai'
 
type Store = ReturnType<typeof createStore>
 
interface RenderWithStoreOptions extends Omit<RenderOptions, 'wrapper'> {
  store?: Store
}
 
// 🦁 영호: "이 헬퍼 ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄λ‘λ©΄ λͺ¨λ“  ν…ŒμŠ€νŠΈμ—μ„œ μž¬μ‚¬μš©ν•  수 μžˆμ–΄μš”"
export function renderWithStore(
  ui: ReactElement,
  { store = createStore(), ...options }: RenderWithStoreOptions = {},
) {
  function Wrapper({ children }: { children: React.ReactNode }) {
    return <Provider store={store}>{children}</Provider>
  }
 
  return {
    store, // store λ₯Ό λ°˜ν™˜ν•΄μ„œ ν…ŒμŠ€νŠΈμ—μ„œ μ ‘κ·Ό κ°€λŠ₯
    ...render(ui, { wrapper: Wrapper, ...options }),
  }
}
 
// useHydrateAtoms λ₯Ό μ΄μš©ν•œ μ΄ˆκΈ°κ°’ μ£Όμž… νŒ¨ν„΄ (곡식 레퍼런슀 방식)
import { useHydrateAtoms } from 'jotai/utils'
import type { Atom } from 'jotai'
 
type InitialValues = Iterable<readonly [Atom<unknown>, unknown]>
 
function HydrateAtoms({
  initialValues,
  children,
}: {
  initialValues: InitialValues
  children: React.ReactNode
}) {
  useHydrateAtoms(initialValues)
  return <>{children}</>
}
 
export function TestProvider({
  initialValues,
  children,
}: {
  initialValues: InitialValues
  children: React.ReactNode
}) {
  return (
    <Provider>
      <HydrateAtoms initialValues={initialValues}>
        {children}
      </HydrateAtoms>
    </Provider>
  )
}

🎭 atom μ΄ˆκΈ°κ°’ override β€” λ‹€μ–‘ν•œ μ‹œλ‚˜λ¦¬μ˜€ ν…ŒμŠ€νŠΈ 🟑

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • store.set() 으둜 ν…ŒμŠ€νŠΈ μ‹œμž‘ μ „ atom 의 초기 μƒνƒœλ₯Ό μ •ν™•νžˆ μ„€μ •ν•  수 μžˆλ‹€
  • μ—¬λŸ¬ μƒνƒœ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό λ…λ¦½μ μœΌλ‘œ ν…ŒμŠ€νŠΈν•  수 μžˆλ‹€
// atoms/index.ts
export const userAtom = atom<User | null>(null)
export const studyListAtom = atom<Study[]>([])
export const isAdminAtom = atom((get) => get(userAtom)?.role === 'admin')
 
// tests/StudyCard.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { StudyCard } from '@/components/StudyCard'
import { userAtom, studyListAtom } from '@/atoms'
import { renderWithStore } from '../utils'
 
const mockStudy: Study = {
  id: 'study-1',
  title: 'React 심화 μŠ€ν„°λ””',
  likeCount: 42,
  authorId: 'user-영호',
}
 
describe('StudyCard', () => {
  // μ‹œλ‚˜λ¦¬μ˜€ 1: λΉ„λ‘œκ·ΈμΈ μœ μ €
  test('λΉ„λ‘œκ·ΈμΈ μœ μ €λŠ” μ’‹μ•„μš” λ²„νŠΌμ΄ λΉ„ν™œμ„±ν™”λœλ‹€', () => {
    const { store } = renderWithStore(<StudyCard study={mockStudy} />)
    // userAtom κΈ°λ³Έκ°’ = null (λΉ„λ‘œκ·ΈμΈ)
    // store.set 을 ν˜ΈμΆœν•˜μ§€ μ•ŠμœΌλ©΄ atom 의 initialValue μ‚¬μš©
 
    const likeButton = screen.getByRole('button', { name: /μ’‹μ•„μš”/i })
    expect(likeButton).toBeDisabled()
  })
 
  // μ‹œλ‚˜λ¦¬μ˜€ 2: 일반 둜그인 μœ μ €
  test('둜그인 μœ μ €λŠ” μ’‹μ•„μš” λ²„νŠΌμ„ 클릭할 수 μžˆλ‹€', async () => {
    const { store } = renderWithStore(<StudyCard study={mockStudy} />)
 
    // 🦁 영호: "store.set 으둜 νŠΉμ • μ‹œλ‚˜λ¦¬μ˜€μ˜ 초기 μƒνƒœλ₯Ό μ„€μ •ν•΄μš”"
    store.set(userAtom, { id: 'user-영철', name: '영철', role: 'member' })
 
    const likeButton = screen.getByRole('button', { name: /μ’‹μ•„μš”/i })
    expect(likeButton).toBeEnabled()
 
    await userEvent.click(likeButton)
    // μ’‹μ•„μš” 수 증가 확인
    expect(screen.getByText('43')).toBeInTheDocument()
  })
 
  // μ‹œλ‚˜λ¦¬μ˜€ 3: μ–΄λ“œλ―Ό μœ μ €
  test('μ–΄λ“œλ―Ό μœ μ €λŠ” μ‚­μ œ λ²„νŠΌμ„ λ³Ό 수 μžˆλ‹€', () => {
    const { store } = renderWithStore(<StudyCard study={mockStudy} />)
 
    store.set(userAtom, { id: 'user-영호', name: '영호', role: 'admin' })
 
    // isAdminAtom = true (userAtom μ—μ„œ νŒŒμƒ)
    expect(screen.getByRole('button', { name: /μ‚­μ œ/i })).toBeInTheDocument()
  })
 
  // μ‹œλ‚˜λ¦¬μ˜€ 4: μž‘μ„±μž 본인
  test('μž‘μ„±μž 본인은 μˆ˜μ • λ²„νŠΌμ„ λ³Ό 수 μžˆλ‹€', () => {
    const { store } = renderWithStore(<StudyCard study={mockStudy} />)
 
    // authorId κ°€ 'user-영호' 인 μŠ€ν„°λ””μ—μ„œ 영호둜 둜그인
    store.set(userAtom, { id: 'user-영호', name: '영호', role: 'member' })
 
    expect(screen.getByRole('button', { name: /μˆ˜μ •/i })).toBeInTheDocument()
  })
})

TestProvider νŒ¨ν„΄μœΌλ‘œ 더 κ°„κ²°ν•˜κ²Œ

// useHydrateAtoms νŒ¨ν„΄ μ‚¬μš© β€” 더 선언적
test('둜그인 μœ μ €λŠ” μ’‹μ•„μš” λ²„νŠΌμ„ 클릭할 수 μžˆλ‹€', () => {
  const mockUser = { id: 'user-영철', name: '영철', role: 'member' as const }
 
  render(
    <TestProvider initialValues={[[userAtom, mockUser]]}>
      <StudyCard study={mockStudy} />
    </TestProvider>
  )
 
  expect(screen.getByRole('button', { name: /μ’‹μ•„μš”/i })).toBeEnabled()
})

πŸ§ͺ React Testing Library 톡합 μ‹€μ „ πŸ”΄

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • μ˜μˆ˜λ„€ μ•±μ˜ μ‹€μ œ μŠ€ν„°λ”” λͺ©λ‘ μ»΄ν¬λ„ŒνŠΈμ— λŒ€ν•œ μ™„μ „ν•œ ν…ŒμŠ€νŠΈ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό μž‘μ„±ν•  수 μžˆλ‹€
// components/StudyList.tsx
'use client'
 
import { useAtomValue, useAtom } from 'jotai'
import { studyListAtom, studyFiltersAtom } from '@/atoms'
 
export function StudyList() {
  const studies = useAtomValue(studyListAtom)
  const [filters, setFilters] = useAtom(studyFiltersAtom)
 
  const filtered = studies.filter(
    (s) => filters.category === 'all' || s.category === filters.category
  )
 
  return (
    <div>
      <select
        aria-label="μΉ΄ν…Œκ³ λ¦¬ ν•„ν„°"
        value={filters.category}
        onChange={(e) => setFilters((prev) => ({ ...prev, category: e.target.value }))}
      >
        <option value="all">전체</option>
        <option value="frontend">ν”„λ‘ νŠΈμ—”λ“œ</option>
        <option value="backend">λ°±μ—”λ“œ</option>
      </select>
      <ul>
        {filtered.map((study) => (
          <li key={study.id}>{study.title}</li>
        ))}
      </ul>
    </div>
  )
}
// tests/StudyList.test.tsx β€” μ™„μ „ν•œ 톡합 ν…ŒμŠ€νŠΈ
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { StudyList } from '@/components/StudyList'
import { studyListAtom, studyFiltersAtom } from '@/atoms'
import { renderWithStore } from '../utils'
 
const mockStudies: Study[] = [
  { id: '1', title: 'React 심화 μŠ€ν„°λ””', category: 'frontend', likeCount: 42 },
  { id: '2', title: 'Node.js λ§ˆμŠ€ν„°', category: 'backend', likeCount: 30 },
  { id: '3', title: 'TypeScript 완전정볡', category: 'frontend', likeCount: 15 },
]
 
describe('StudyList', () => {
  test('전체 ν•„ν„°μ—μ„œ λͺ¨λ“  μŠ€ν„°λ””κ°€ 보인닀', () => {
    const { store } = renderWithStore(<StudyList />)
    store.set(studyListAtom, mockStudies)
 
    // 3개 λͺ¨λ‘ λ Œλ”λ˜μ–΄μ•Ό 함
    expect(screen.getByText('React 심화 μŠ€ν„°λ””')).toBeInTheDocument()
    expect(screen.getByText('Node.js λ§ˆμŠ€ν„°')).toBeInTheDocument()
    expect(screen.getByText('TypeScript 완전정볡')).toBeInTheDocument()
  })
 
  test('ν”„λ‘ νŠΈμ—”λ“œ ν•„ν„° 선택 μ‹œ ν”„λ‘ νŠΈμ—”λ“œ μŠ€ν„°λ””λ§Œ 보인닀', async () => {
    const user = userEvent.setup()
    const { store } = renderWithStore(<StudyList />)
    store.set(studyListAtom, mockStudies)
 
    // μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° λ³€κ²½
    await user.selectOptions(
      screen.getByRole('combobox', { name: /μΉ΄ν…Œκ³ λ¦¬ ν•„ν„°/i }),
      'frontend'
    )
 
    // ν”„λ‘ νŠΈμ—”λ“œλ§Œ 보여야 함
    expect(screen.getByText('React 심화 μŠ€ν„°λ””')).toBeInTheDocument()
    expect(screen.queryByText('Node.js λ§ˆμŠ€ν„°')).not.toBeInTheDocument() // λ°±μ—”λ“œ β†’ 숨겨짐
    expect(screen.getByText('TypeScript 완전정볡')).toBeInTheDocument()
  })
 
  test('초기 ν•„ν„°κ°€ λ°±μ—”λ“œλ‘œ μ„€μ •λ˜μ–΄ 있으면 λ°±μ—”λ“œ μŠ€ν„°λ””λ§Œ 보인닀', () => {
    const { store } = renderWithStore(<StudyList />)
 
    // 🦁 영호: "store.set 으둜 초기 ν•„ν„° μƒνƒœλ„ μ„€μ •ν•  수 μžˆμ–΄μš”"
    store.set(studyListAtom, mockStudies)
    store.set(studyFiltersAtom, { category: 'backend', sortBy: 'latest' })
 
    expect(screen.getByText('Node.js λ§ˆμŠ€ν„°')).toBeInTheDocument()
    expect(screen.queryByText('React 심화 μŠ€ν„°λ””')).not.toBeInTheDocument()
  })
 
  test('μŠ€ν„°λ”” λͺ©λ‘μ΄ λΉ„μ–΄μžˆμœΌλ©΄ 빈 μƒνƒœ λ©”μ‹œμ§€λ₯Ό 보여쀀닀', () => {
    const { store } = renderWithStore(<StudyList />)
    store.set(studyListAtom, []) // 빈 λͺ©λ‘
 
    expect(screen.getByText('λ“±λ‘λœ μŠ€ν„°λ””κ°€ μ—†μŠ΅λ‹ˆλ‹€')).toBeInTheDocument()
  })
})

πŸ” DevTools μ„€μ • β€” atom 을 눈으둜 보기 🟑

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • jotai-devtools λ₯Ό μ„€μΉ˜ν•˜κ³  μ„€μ •ν•˜λŠ” 방법을 μ•ˆλ‹€
  • DevTools 의 μ£Όμš” κΈ°λŠ₯(atom κ°’ μ‹€μ‹œκ°„ 쑰회, νƒ€μž„ νŠΈλž˜λΈ”)을 ν™œμš©ν•  수 μžˆλ‹€
npm install jotai-devtools
// app/providers.tsx β€” DevTools 톡합
'use client'
 
import { Provider } from 'jotai'
import { DevTools } from 'jotai-devtools'
import 'jotai-devtools/styles.css'
 
export function JotaiProvider({ children }: { children: React.ReactNode }) {
  return (
    <Provider>
      {/*
        DevTools λŠ” process.env.NODE_ENV !== 'production' ν™˜κ²½μ—μ„œλ§Œ λ Œλ”λ¨
        ν”„λ‘œλ•μ…˜ λΉŒλ“œμ—μ„œ μžλ™ tree-shake 됨
      */}
      <DevTools
        isInitialOpen={false}     // μ²˜μŒμ— λ‹«νžŒ μƒνƒœλ‘œ
        theme="dark"              // 닀크 ν…Œλ§ˆ
        position="bottom-right"  // 우츑 ν•˜λ‹¨
      />
      {children}
    </Provider>
  )
}

Provider 와 ν•¨κ»˜ DevTools μ‚¬μš© (custom store μ£Όμž… μ‹œ)

// createStore λ₯Ό 직접 μ‚¬μš©ν•  λ•Œ DevTools 에 같은 store λ₯Ό λ„˜κ²¨μ•Ό ν•΄
'use client'
 
import { Provider, createStore } from 'jotai'
import { DevTools } from 'jotai-devtools'
import 'jotai-devtools/styles.css'
import { useRef } from 'react'
 
export function JotaiProvider({ children }: { children: React.ReactNode }) {
  const storeRef = useRef<ReturnType<typeof createStore>>()
  if (!storeRef.current) {
    storeRef.current = createStore()
  }
 
  return (
    <Provider store={storeRef.current}>
      {/* store prop 을 Provider 와 λ™μΌν•˜κ²Œ λ§žμΆ°μ€˜μ•Ό 함 */}
      <DevTools store={storeRef.current} />
      {children}
    </Provider>
  )
}

useAtomsDevtools β€” λͺ¨λ“  atom 을 Redux DevTools μ—μ„œ 보기

// 🦁 영호: "Redux DevTools ν™•μž₯을 μ‚¬μš©ν•œλ‹€λ©΄ useAtomsDevtools κ°€ λͺ¨λ“  atom 을 λ³΄μ—¬μ€˜μš”"
'use client'
 
import { useAtomsDevtools } from 'jotai-devtools/utils'
 
// Provider ν•˜μœ„μ˜ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ‚¬μš©
function AtomsDevtools({ children }: { children: React.ReactNode }) {
  // λͺ¨λ“  atom 을 Redux DevTools 의 'μ˜μˆ˜λ„€μ•±' μΈμŠ€ν„΄μŠ€μ—μ„œ 좔적
  useAtomsDevtools('μ˜μˆ˜λ„€μ•±')
  return <>{children}</>
}
 
export function JotaiProvider({ children }: { children: React.ReactNode }) {
  return (
    <Provider>
      <AtomsDevtools>
        {children}
      </AtomsDevtools>
    </Provider>
  )
}

🏷️ debugLabel β€” ν…ŒμŠ€νŠΈ μ—λŸ¬ λ©”μ‹œμ§€ κ°œμ„  🟒

🎯 이 μ„Ήμ…˜μ„ 읽고 λ‚˜λ©΄:

  • debugLabel 둜 atom 에 이름을 λΆ™μ—¬μ„œ DevTools 와 μ—λŸ¬ λ©”μ‹œμ§€μ—μ„œ μ‰½κ²Œ 식별할 수 μžˆλ‹€
  • SWC ν”ŒλŸ¬κ·ΈμΈμœΌλ‘œ μžλ™ν™”ν•˜λŠ” 방법을 μ•ˆλ‹€
// 🐣 영철: "DevTools μ—μ„œ atom1, atom2, atom3 으둜 λ‚˜μ™€μ„œ 뭐가 λ­”μ§€ λͺ¨λ₯΄κ² μ–΄μš”"
 
// ❌ debugLabel 없을 λ•Œ
const userAtom = atom<User | null>(null)
const studyListAtom = atom<Study[]>([])
// DevTools: atom1, atom2 (μ•Œμ•„λ³΄κΈ° 어렀움)
 
// βœ… debugLabel μΆ”κ°€
const userAtom = atom<User | null>(null)
userAtom.debugLabel = 'userAtom'
 
const studyListAtom = atom<Study[]>([])
studyListAtom.debugLabel = 'studyListAtom'
// DevTools: userAtom, studyListAtom (λͺ…ν™•ν•˜κ²Œ λ³΄μž„)

ν…ŒμŠ€νŠΈ μ—λŸ¬ λ©”μ‹œμ§€ κ°œμ„ 

// debugLabel 이 있으면 μ—λŸ¬ λ©”μ‹œμ§€λ„ 더 λͺ…ν™•ν•΄μ Έ
// ❌ debugLabel μ—†μŒ: "atom1 μ—μ„œ μ˜ˆμƒμΉ˜ λͺ»ν•œ κ°’"
// βœ… debugLabel 있음: "userAtom μ—μ„œ μ˜ˆμƒμΉ˜ λͺ»ν•œ κ°’"
 
// μ‹€μ œ ν…ŒμŠ€νŠΈ μ—λŸ¬ μ‹œλ‚˜λ¦¬μ˜€
test('userAtom μ΄ˆκΈ°κ°’μ€ null 이닀', () => {
  const store = createStore()
  const user = store.get(userAtom)
  // expect(user).toBe(null) μ‹€νŒ¨ μ‹œ:
  // "Expected: null, Received: { name: '영철' }" + atom 이름이 ν‘œμ‹œλ˜λ©΄ 훨씬 νŒŒμ•…μ΄ μ‰¬μ›Œ
})

SWC ν”ŒλŸ¬κ·ΈμΈμœΌλ‘œ μžλ™ν™” (11번 κ°€μ΄λ“œ μ°Έκ³ )

// next.config.js β€” 이미 μ„€μ •ν–ˆλ‹€λ©΄ μƒλž΅
experimental: {
  swcPlugins: [
    ['@swc-jotai/debug-label', {}], // λͺ¨λ“  atom 에 μžλ™μœΌλ‘œ debugLabel μΆ”κ°€
  ],
},

useAtomsDebugValue β€” React DevTools μ—μ„œ 보기

// 🦁 영호: "React DevTools μ—μ„œ atom 값을 ν™•μΈν•˜κ³  μ‹ΆμœΌλ©΄ 이걸 μ¨μš”"
import { useAtomsDebugValue } from 'jotai-devtools/utils'
 
// Provider ν•˜μœ„ 아무 μ»΄ν¬λ„ŒνŠΈμ—μ„œλ‚˜ μ‚¬μš©
function DebugAtoms() {
  useAtomsDebugValue() // React DevTools 의 Hooks νƒ­μ—μ„œ λͺ¨λ“  atom κ°’ 확인 κ°€λŠ₯
  return null
}
 
// μ•± λ£¨νŠΈμ— μΆ”κ°€
function App() {
  return (
    <Provider>
      {process.env.NODE_ENV === 'development' && <DebugAtoms />}
      <MainContent />
    </Provider>
  )
}

πŸ’₯ μ—λŸ¬ ν•΄κ²° μΉ΄νƒˆλ‘œκ·Έ

μ—λŸ¬ λ©”μ‹œμ§€κ°€ 뜨면 Ctrl+F 둜 검색해봐. λŒ€λΆ€λΆ„ μ—¬κΈ° μžˆμ–΄.


❌ ν…ŒμŠ€νŠΈ κ°„ atom 값이 μ˜€μ—Όλ¨

원인: μ „μ—­ default store λ₯Ό κ³΅μœ ν•˜κ±°λ‚˜ beforeEach μ—μ„œ store μ΄ˆκΈ°ν™”λ₯Ό μ•ˆ 함

ν•΄κ²°μ±…:

beforeEach(() => {
  store = createStore() // ← λ§€ ν…ŒμŠ€νŠΈλ§ˆλ‹€ μƒˆ store
})

❌ "act() λˆ„λ½" κ²½κ³  β€” μƒνƒœ μ—…λ°μ΄νŠΈ ν›„ κ²½κ³ 

원인: atom μƒνƒœ μ—…λ°μ΄νŠΈ ν›„ React κ°€ λ¦¬λ Œλ”λ₯Ό μ™„λ£Œν•˜κΈ° 전에 검증

ν•΄κ²°μ±…:

import { act } from '@testing-library/react'
 
// store.set 도 act 둜 감싸면 더 μ•ˆμ „
act(() => {
  store.set(countAtom, 5)
})
// λ˜λŠ” userEvent (μžλ™μœΌλ‘œ act 처리)
await userEvent.click(button) // λ‚΄λΆ€μ μœΌλ‘œ act 처리

❌ DevTools κ°€ ν”„λ‘œλ•μ…˜μ—μ„œλ„ λ³΄μž„

원인: ν™˜κ²½ λ³€μˆ˜ 체크 없이 항상 λ Œλ”

ν•΄κ²°μ±…:

// DevTools λŠ” μžλ™μœΌλ‘œ production μ—μ„œ tree-shake λ˜μ§€λ§Œ
// λͺ…μ‹œμ μœΌλ‘œ 체크해도 μ’‹μŒ
{process.env.NODE_ENV !== 'production' && <DevTools />}

❌ useAtomsDevtools κ°€ μž‘λ™ν•˜μ§€ μ•ŠμŒ

원인: Redux DevTools λΈŒλΌμš°μ € ν™•μž₯이 μ„€μΉ˜λ˜μ§€ μ•ŠμŒ

ν•΄κ²°μ±…: Chrome/Firefox 에 Redux DevTools ν™•μž₯ μ„€μΉ˜ ν›„ μž¬μ‹œλ„


🏁 μ΄λ²ˆμ— 배운 λ‚΄μš© 총정리

πŸ“‹ ν…ŒμŠ€νŒ… μ „λž΅ μš”μ•½

μƒν™©νŒ¨ν„΄μ½”λ“œ
ν…ŒμŠ€νŠΈ 격리beforeEach + createStore()store = createStore()
μ»΄ν¬λ„ŒνŠΈ λ Œλ”renderWithStore 헬퍼renderWithStore(<Comp />, { store })
μ΄ˆκΈ°κ°’ μ„€μ •store.set(atom, value)λ Œλ” μ „ λ˜λŠ” λ Œλ” 직후
atom λ‹¨μœ„ ν…ŒμŠ€νŠΈstore.get/set 직접 μ‚¬μš©μ»΄ν¬λ„ŒνŠΈ 없이 순수 둜직 ν…ŒμŠ€νŠΈ

⚠️ ν…ŒμŠ€νŠΈ 함정

μƒν™©βŒ λ‚˜μœ μ˜ˆβœ… 쒋은 예
store κ³΅μœ μ „μ—­ store μž¬μ‚¬μš©beforeEach 에 createStore()
atom μ΄ˆκΈ°κ°’κΈ°λ³Έ atom κ°’λ§ŒμœΌλ‘œ ν…ŒμŠ€νŠΈstore.set 으둜 λͺ…μ‹œμ  μ„€μ •
정리 λˆ„λ½ν…ŒμŠ€νŠΈ ν›„ atom μˆ˜λ™ μ΄ˆκΈ°ν™”createStore() 둜 μƒˆ μΈμŠ€ν„΄μŠ€ μ‚¬μš©

πŸ“ 마무리 ν€΄μ¦ˆ

Q1. πŸ”₯ κΈ΄κΈ‰ 디버깅 (영수의 ν˜Έν†΅)

ν…ŒμŠ€νŠΈλ₯Ό 전체 μ‹€ν–‰ν•˜λ©΄ μ‹€νŒ¨ν•˜λŠ”λ° κ°œλ³„ μ‹€ν–‰ν•˜λ©΄ ν†΅κ³Όν•œλ‹€. 영수 λ‹˜μ΄ "CI κ°€ 자꾸 μ‹€νŒ¨ν•œλ‹€" λ©° 둜그λ₯Ό λ˜μ‘Œμ–΄.
κ°€μž₯ λ¨Όμ € 확인해야 ν•  것은?

  • A) ν…ŒμŠ€νŠΈ 파일의 import μˆœμ„œλ₯Ό λ°”κΎΌλ‹€
  • B) 각 ν…ŒμŠ€νŠΈκ°€ createStore() 둜 독립 store λ₯Ό μ‚¬μš©ν•˜λŠ”μ§€ ν™•μΈν•˜κ³ , μ „μ—­ store λ‚˜ 곡유 μƒνƒœλ₯Ό ν…ŒμŠ€νŠΈ 간에 μž¬μ‚¬μš©ν•˜κ³  μžˆλŠ”μ§€ μ κ²€ν•œλ‹€
  • C) ν…ŒμŠ€νŠΈ λŸ¬λ„ˆλ₯Ό Jest μ—μ„œ Vitest 둜 κ΅μ²΄ν•œλ‹€
  • D) ν…ŒμŠ€νŠΈ νŒŒμΌμ„ ν•˜λ‚˜λ‘œ ν•©μΉœλ‹€

βœ… μ •λ‹΅: B

πŸ’‘ 상세 ν•΄μ„€:

  • 원리 μ„€λͺ…: "κ°œλ³„ μ‹€ν–‰ 톡과, 전체 μ‹€ν–‰ μ‹€νŒ¨" λŠ” μ „ν˜•μ μΈ ν…ŒμŠ€νŠΈ κ°„ μƒνƒœ μ˜€μ—Ό νŒ¨ν„΄μ΄μ•Ό. ν…ŒμŠ€νŠΈ A κ°€ μ „μ—­ atom 에 값을 set ν•˜κ³ , ν…ŒμŠ€νŠΈ B κ°€ κ·Έ 값을 μ½μ–΄μ„œ μ˜ˆμƒμΉ˜ λͺ»ν•œ κ²°κ³Όκ°€ λ‚˜μ™€. createStore() 둜 각 ν…ŒμŠ€νŠΈλ§ˆλ‹€ 독립 store λ₯Ό λ§Œλ“€λ©΄, 이전 ν…ŒμŠ€νŠΈμ˜ μƒνƒœκ°€ λ‹€μŒ ν…ŒμŠ€νŠΈμ— 영ν–₯을 쀄 수 μ—†μ–΄.
  • μ˜€λ‹΅ ν”Όλ“œλ°±: A 와 C λŠ” κ·Όλ³Έ 원인(μƒνƒœ μ˜€μ—Ό)을 ν•΄κ²°ν•˜μ§€ λͺ»ν•΄. D λŠ” 였히렀 상황을 μ•…ν™”μ‹œν‚¬ 수 μžˆμ–΄.
  • πŸ“Œ 핡심 기얡법: "ν…ŒμŠ€νŠΈ 격리 = createStore(). 각 ν…ŒμŠ€νŠΈλŠ” 빈 냉μž₯κ³ μ—μ„œ μ‹œμž‘ν•΄μ•Ό ν•΄."

Q2. 🎭 atom μ΄ˆκΈ°κ°’ override

μ˜μ² μ΄κ°€ "κ΄€λ¦¬μžλ§Œ λ³Ό 수 μžˆλŠ” λ²„νŠΌμ„ ν…ŒμŠ€νŠΈν•˜κ³  μ‹Άμ–΄μš”. μ–΄λ–»κ²Œ κ΄€λ¦¬μžλ‘œ λ‘œκ·ΈμΈν•œ μƒνƒœλ₯Ό λ§Œλ“€μ–΄μš”?" 라고 λ¬Όμ—ˆμ–΄.
영호 λ‹˜μ΄ μ„€λͺ…ν•œ λ°©λ²•μœΌλ‘œ κ°€μž₯ μ μ ˆν•œ 것은?

  • A) μ‹€μ œ 둜그인 API λ₯Ό ν˜ΈμΆœν•΄μ„œ κ΄€λ¦¬μžλ‘œ μΈμ¦ν•œλ‹€
  • B) renderWithStore μ—μ„œ λ°˜ν™˜ν•œ store 에 store.set(userAtom, { role: 'admin' }) 으둜 초기 μƒνƒœλ₯Ό 직접 μ„€μ •ν•œλ‹€
  • C) userAtom 의 μ΄ˆκΈ°κ°’μ„ κ΄€λ¦¬μžλ‘œ λ³€κ²½ν•˜κ³  ν…ŒμŠ€νŠΈ ν›„ μ›λž˜λŒ€λ‘œ λŒλ¦°λ‹€
  • D) userAtom 을 jest.mock 으둜 mocking ν•œλ‹€

βœ… μ •λ‹΅: B

πŸ’‘ 상세 ν•΄μ„€:

  • 원리 μ„€λͺ…: createStore() 둜 λ§Œλ“  독립 store 에 store.set() 으둜 μ›ν•˜λŠ” 초기 μƒνƒœλ₯Ό μ„€μ •ν•˜λŠ” 게 κ°€μž₯ κΉ”λ”ν•œ 방법이야. μ‹€μ œ API λ₯Ό ν˜ΈμΆœν•  ν•„μš”λ„ μ—†κ³ , μ „μ—­ atom 을 μˆ˜μ •ν•˜μ§€λ„ μ•Šμ•„μ„œ λ‹€λ₯Έ ν…ŒμŠ€νŠΈμ— 영ν–₯이 μ—†μ–΄.
  • μ˜€λ‹΅ ν”Όλ“œλ°±: A λŠ” λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό 톡합 ν…ŒμŠ€νŠΈλ‘œ λ§Œλ“œλŠ” κ±°μ•Ό. C λŠ” atom 의 μ΄ˆκΈ°κ°’μ„ μˆ˜μ •ν•˜λ©΄ λ‹€λ₯Έ ν…ŒμŠ€νŠΈμ—λ„ 영ν–₯을 쀄 수 μžˆμ–΄. D λŠ” Jotai atom 을 mock ν•˜λŠ” 건 κ°€λŠ₯ν•˜μ§€λ§Œ, store.set() 이 훨씬 κ°„λ‹¨ν•˜κ³  μ˜¬λ°”λ₯Έ 방법이야.
  • πŸ“Œ 핡심 기얡법: "ν…ŒμŠ€νŠΈμ—μ„œ μ›ν•˜λŠ” μƒνƒœ = store.set(atom, value). 냉μž₯고에 μ›ν•˜λŠ” 재료λ₯Ό 직접 λ„£μ–΄."

Q3. πŸ” DevTools ν™œμš©

μ˜μ² μ΄κ°€ DevTools μ—μ„œ atom 이름이 "atom1", "atom2" 둜 λ‚˜μ™€μ„œ μ–΄λ–€ atom 인지 λͺ¨λ₯΄κ² λ‹€κ³  ν–ˆμ–΄.
영호 λ‹˜μ΄ μ œμ•ˆν•œ 두 κ°€μ§€ 해결책을 μ„€λͺ…해봐.

μ˜ˆμ‹œ λ‹΅λ³€:

"첫 λ²ˆμ§ΈλŠ” 각 atom 에 debugLabel 을 직접 μΆ”κ°€ν•˜λŠ” κ±°μ˜ˆμš”. userAtom.debugLabel = 'userAtom' μ΄λ ‡κ²Œμš”. DevTools 와 μ—λŸ¬ λ©”μ‹œμ§€μ—μ„œ 이름이 ν‘œμ‹œλΌμš”. 두 λ²ˆμ§ΈλŠ” Next.js 의 next.config.js 에 @swc-jotai/debug-label SWC ν”ŒλŸ¬κ·ΈμΈμ„ μ„€μ •ν•˜λŠ” κ±°μ˜ˆμš”. λΉŒλ“œ μ‹œ λͺ¨λ“  atom 에 μžλ™μœΌλ‘œ λ³€μˆ˜λͺ… 기반 debugLabel 을 μΆ”κ°€ν•΄μ€˜μ„œ μˆ˜λ™μœΌλ‘œ 뢙일 ν•„μš”κ°€ μ—†μ–΄μš”."

πŸ’‘ 핡심: DevTools λ₯Ό μ œλŒ€λ‘œ μ“°λ €λ©΄ debugLabel 이 ν•„μˆ˜μ•Ό. SWC ν”ŒλŸ¬κ·ΈμΈμœΌλ‘œ μžλ™ν™”ν•˜λ©΄ μžŠμ–΄λ²„λ¦΄ 일이 μ—†μ–΄.


🐣 영철이의 퇴근 일기

였늘 createStore() ν•˜λ‚˜λ‘œ ν…ŒμŠ€νŠΈ μ˜€μ—Ό λ¬Έμ œκ°€ μ‹Ή ν•΄κ²°λ˜λŠ” κ±Έ 보고 μ§„μ§œ κ°λ™λ°›μ•˜λ‹€.

μ²˜μŒμ— ν…ŒμŠ€νŠΈ κ°„ μƒνƒœκ°€ μ˜€μ—Όλœλ‹€λŠ” κ°œλ… 자체λ₯Ό λͺ°λžμ–΄. "혼자 μ‹€ν–‰ν•˜λ©΄ ν†΅κ³Όν•˜λŠ”λ° μ™œ 같이 μ‹€ν–‰ν•˜λ©΄ μ‹€νŒ¨ν•΄?" λΌλŠ” λ―ΈμŠ€ν„°λ¦¬κ°€ μ „μ—­ store 곡유 λ•Œλ¬Έμ΄λΌλŠ” κ±Έ μ•Œμ•˜μ„ λ•Œ "μ•„ κ·Έλž˜μ„œ ν…ŒμŠ€νŠΈ 격리가 μ€‘μš”ν•˜λ‹€κ³  ν•˜λŠ” κ±°κ΅¬λ‚˜" ν•˜λŠ” λŠλ‚Œμ΄ μ™”μ–΄.

beforeEach(() => { store = createStore() }) 이 ν•œ 쀄이 κ°€μ Έλ‹€μ£ΌλŠ” μ•ˆλ„κ°... 이제 ν…ŒμŠ€νŠΈλ§ˆλ‹€ 빈 냉μž₯κ³ μ—μ„œ μ‹œμž‘ν•˜λ‹ˆκΉŒ 이전 ν…ŒμŠ€νŠΈκ°€ 남긴 재료 걱정을 μ•ˆ 해도 λ˜κ±°λ“ .

πŸ’‘ 였늘의 κ΅ν›ˆ: "쒋은 ν…ŒμŠ€νŠΈλŠ” κ²©λ¦¬μ—μ„œ μ‹œμž‘ν•œλ‹€. createStore() λŠ” 각 ν…ŒμŠ€νŠΈμ—κ²Œ κΉ¨λ—ν•œ 냉μž₯κ³ λ₯Ό μ€€λ‹€."

그리고 DevTools μ—μ„œ atom1, atom2 λŒ€μ‹  λ‚΄κ°€ 직접 이름 뢙인 atom 이름이 λ³΄μ΄λŠ” 게 μ–Όλ§ˆλ‚˜ νŽΈν•œμ§€... debugLabel μ„Έ κΈ€μžκ°€ μ΄λ ‡κ²Œ 큰 차이λ₯Ό λ§Œλ“€λ‹€λ‹ˆ. SWC ν”ŒλŸ¬κ·ΈμΈ μ„€μ •ν•˜κ³  λ‚˜μ„œ 영호 λ‹˜μ΄ "이제 μ–΄λ–€ atom 인지 λ°”λ‘œ μ•Œκ² μ£ ?" 라고 ν•˜μ…¨μ„ λ•Œ λ„ˆλ¬΄ λΏŒλ“―ν–ˆμ–΄.

였늘 μ§„μ§œ μ„±μž₯ν•œ 것 κ°™λ‹€! ν—¬μŠ€μž₯ 찍고 μ§‘ κ°€μ•Όμ§€. 슀쿼트 100개 ν•˜λ©΄μ„œ "createStore, createStore" 볡창해야지.


πŸ”— 더 μ•Œμ•„λ³΄κΈ°