π§© 13. Jotai ν μ€ν & DevTools μ€μ
π κ°μ
createStoreλ‘ isolated store λ§λ€κΈ°, atom μ΄κΈ°κ° override, React Testing Library ν΅ν©, DevTools μ€μ μ λ°°μλλ€.
π― μ΄ μΉμ μ μ½κ³ λλ©΄:
createStore()λ‘ ν μ€νΈλ§λ€ λ 립λ store λ₯Ό λ§λ€μ΄ μν μ€μΌμ μμ ν μ°¨λ¨ν μ μλ€.store.set()μΌλ‘ atom μ΄κΈ°κ°μ override ν΄μ λ€μν μλ리μ€λ₯Ό ν μ€νΈν μ μλ€.jotai-devtoolsμdebugLabelλ‘ κ°λ° κ²½ν(DX)μ ν¬κ² ν₯μμν¬ μ μλ€.
π λͺ©μ°¨
- π μ΄ λ¬Έμλ₯Ό μ½κΈ° μ μ
- π€ μ ν μ€νΈκ° μ΄λ €μ΄κ°?
- π§ createStore λ‘ isolated store λ§λ€κΈ°
- ποΈ renderWithStore β ν μ€νΈμ© ν¬νΌ ν¨μ
- π atom μ΄κΈ°κ° override β λ€μν μλλ¦¬μ€ ν μ€νΈ
- π§ͺ React Testing Library ν΅ν© μ€μ
- π DevTools μ€μ β atom μ λμΌλ‘ 보기
- π·οΈ debugLabel β ν μ€νΈ μλ¬ λ©μμ§ κ°μ
- π₯ μλ¬ ν΄κ²° μΉ΄νλ‘κ·Έ
- π μ΄λ²μ λ°°μ΄ λ΄μ© μ΄μ 리
- π λ§λ¬΄λ¦¬ ν΄μ¦
- π£ μμ² μ΄μ ν΄κ·Ό μΌκΈ°
- π λ μμ보기
π μ΄ λ¬Έμλ₯Ό μ½κΈ° μ μ
β±οΈ μμ μ½κΈ° μκ°: 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-labelSWC νλ¬κ·ΈμΈμ μ€μ νλ κ±°μμ. λΉλ μ λͺ¨λ atom μ μλμΌλ‘ λ³μλͺ κΈ°λ° debugLabel μ μΆκ°ν΄μ€μ μλμΌλ‘ λΆμΌ νμκ° μμ΄μ."
π‘ ν΅μ¬: DevTools λ₯Ό μ λλ‘ μ°λ €λ©΄ debugLabel μ΄ νμμΌ. SWC νλ¬κ·ΈμΈμΌλ‘ μλννλ©΄ μμ΄λ²λ¦΄ μΌμ΄ μμ΄.
π£ μμ² μ΄μ ν΄κ·Ό μΌκΈ°
μ€λ createStore() νλλ‘ ν
μ€νΈ μ€μΌ λ¬Έμ κ° μΉ ν΄κ²°λλ κ±Έ λ³΄κ³ μ§μ§ κ°λλ°μλ€.
μ²μμ ν μ€νΈ κ° μνκ° μ€μΌλλ€λ κ°λ μ체λ₯Ό λͺ°λμ΄. "νΌμ μ€ννλ©΄ ν΅κ³Όνλλ° μ κ°μ΄ μ€ννλ©΄ μ€ν¨ν΄?" λΌλ λ―Έμ€ν°λ¦¬κ° μ μ store 곡μ λλ¬Έμ΄λΌλ κ±Έ μμμ λ "μ κ·Έλμ ν μ€νΈ κ²©λ¦¬κ° μ€μνλ€κ³ νλ 거ꡬλ" νλ λλμ΄ μμ΄.
beforeEach(() => { store = createStore() }) μ΄ ν μ€μ΄ κ°μ Έλ€μ£Όλ μλκ°... μ΄μ ν
μ€νΈλ§λ€ λΉ λμ₯κ³ μμ μμνλκΉ μ΄μ ν
μ€νΈκ° λ¨κΈ΄ μ¬λ£ κ±±μ μ μ ν΄λ λκ±°λ .
π‘ μ€λμ κ΅ν: "μ’μ ν μ€νΈλ 격리μμ μμνλ€. createStore() λ κ° ν μ€νΈμκ² κΉ¨λν λμ₯κ³ λ₯Ό μ€λ€."
κ·Έλ¦¬κ³ DevTools μμ atom1, atom2 λμ λ΄κ° μ§μ μ΄λ¦ λΆμΈ atom μ΄λ¦μ΄ 보μ΄λ κ² μΌλ§λ νΈνμ§... debugLabel μΈ κΈμκ° μ΄λ κ² ν° μ°¨μ΄λ₯Ό λ§λ€λ€λ. SWC νλ¬κ·ΈμΈ μ€μ νκ³ λμ μνΈ λμ΄ "μ΄μ μ΄λ€ atom μΈμ§ λ°λ‘ μκ² μ£ ?" λΌκ³ νμ ¨μ λ λ무 λΏλ―νμ΄.
μ€λ μ§μ§ μ±μ₯ν κ² κ°λ€! ν¬μ€μ₯ μ°κ³ μ§ κ°μΌμ§. μ€μΏΌνΈ 100κ° νλ©΄μ "createStore, createStore" 볡창ν΄μΌμ§.