๐ก 05. TypeScript ๋ก ๋ง๋๋ ํ์ ์์ ํ atom
๐ ๊ฐ์
PrimitiveAtom<T>, Atom<T>, WritableAtom<V,A,R> ํ์ ์๊ทธ๋์ฒ์ atom factory ํจํด, discriminated union atom ์ค๊ณ๋ฅผ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
PrimitiveAtom<T>,Atom<T>,WritableAtom<V, A, R>์ ์ฐจ์ด๋ฅผ ์ค๋ช ํ๊ณ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ ์ ์๋ค.- atom factory ํจ์๋ฅผ TypeScript ์ ๋ค๋ฆญ์ผ๋ก ํ์ดํํ๋ ๋ฒ์ ์๋ค.
- discriminated union ์ผ๋ก ๋น๋๊ธฐ ์ํ๋ฅผ ํ์ ๋ ๋ฒจ์์ ๋ชจ๋ธ๋งํ ์ ์๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ atom ํ์ดํ์ด ์ค์ํ๊ฐ?
- ๐ Jotai ํต์ฌ ํ์ ์๊ทธ๋์ฒ
- ๐๏ธ ์ ๋ค๋ฆญ์ผ๋ก atom ์ ์ธํ๊ธฐ
- ๐ญ atom factory ํจ์ ํ์ดํ
- ๐ discriminated union atom
- โ๏ธ write-only atom ํ์ ๋ช ์
- ๐ useAtom ๋ฐํ ํ์ ์ถ๋ก
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 12๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[atom<any> ์ง์ฅ] โ [ํต์ฌ ํ์ 3๊ฐ์ง] โ [์ ๋ค๋ฆญ ์ ์ธ ํจํด] โ [factory ํจ์] โ [discriminated union] โ [write-only ํ์ ]
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
์์ฒ ์ด๊ฐ ์คํฐ๋ ์ ์ฒญ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค๊ฐ ๋ฐํ์ ์๋ฌ๋ฅผ ํฐํธ๋ฆฐ ๋ ์ด์ผ.
๐ฃ ์์ฒ : (๋นํฉํด์) "์ํธ ๋, ์ด์ํด์. ์ปดํ์ผ ์๋ฌ๋ ์์๋๋ฐ ๋ฐํ์์์
Cannot read properties of undefined๊ฐ ๋ฌ์ด์..."๐ฆ ์ํธ ๋: (์ฝ๋๋ฅผ ์ด์ด๋ณด๋ฉฐ) "์์ฒ ๋, ์ฌ๊ธฐ
atom\<any\>์ฐ์ จ๋ค์. TypeScript ๊ฐany๋ ๊ฒ์ฌ๋ฅผ ํฌ๊ธฐํด๋ฒ๋ ค์. ์ปดํ์ผ ์๋ฌ๊ฐ ์ ๋๋ ๊ฒ ๋น์ฐํ์ฃ ."๐ฃ ์์ฒ : "์
any์ฐ๋ฉด ํธํ๋๊น์... ํ์ ์ฐ๊ธฐ ๊ท์ฐฎ์์์."๐ฆ ์ํธ ๋: "ํธํ ๊ฒ ๋ง์์. ์ค๋ ๋ฐฐํฌ ์ ์๋์. ๊ทผ๋ฐ ์ด ๋ฐํ์ ์๋ฌ ์ก๋ ๋ฐ ์ผ๋ง๋ ๊ฑธ๋ ธ์ด์?"
๐ฃ ์์ฒ : (ํ์ด ์ฃฝ์ด์) "...ํ ์๊ฐ์ด์."
๐ฆ ์ํธ ๋: "TypeScript ๋ก ์ ๋๋ก ํ์ดํํ์ผ๋ฉด ์๋ํฐ์์ ๋นจ๊ฐ ์ค์ด ๋ฐ๋ก ๋ณด์์ ๊ฑฐ์์. ํ ์๊ฐ์ด 30์ด๋ก ์ค์ด๋๋ ๊ฑฐ์์."
๐ค ์ atom ํ์ดํ์ด ์ค์ํ๊ฐ? ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
atom\<any\>๊ฐ ์ "์ํ ํญํ" ์ธ์ง ์ค๋ช ํ ์ ์๋ค- TypeScript ์ ํ์ ์ถ๋ก ์ด ์ด๋๊น์ง ์๋์ผ๋ก ๋๊ณ , ์ด๋์ ๋ช ์๊ฐ ํ์ํ์ง ์ดํดํ๋ค
atom<any> โ TypeScript ์ ๋ฐฉ์ด๋ง์ ๋ซ๋ ์ฝ๋
// โ ๐ฃ ์์ฒ : "any ์ฐ๋ฉด ์ผ๋จ ์๋ฌ ์์ผ๋๊น ํธํด์!"
const studyAtom = atom\<any\>(null)
// ์ด๋ค ํ์
์ด๋ ๋ฃ์ ์ ์์ด โ TypeScript ๊ฐ ๊ฒ์ฌ๋ฅผ ํฌ๊ธฐ
const Component = () => {
const [study, setStudy] = useAtom(studyAtom)
// study ๊ฐ null ์ธ๋ฐ๋ ํ๋กํผํฐ ์ ๊ทผ โ ์ปดํ์ผ ์๋ฌ ์์!
console.log(study.title) // ๐ฃ ๋ฐํ์ ์๋ฌ: Cannot read properties of null
// ๐ฃ ์์ฒ : "์ ์๋ฌ๊ฐ ๋์ง? TypeScript ๊ฐ ์ก์์ค์ผ ํ๋ ๊ฑฐ ์๋๊ฐ์?"
}// โ
๐ฆ ์ํธ: "์ ๋ค๋ฆญ์ผ๋ก ํ์
์ ๋ช
์ํ๋ฉด ์ปดํ์ผ ํ์์ ์ก์์ค์"
const studyAtom = atom<Study | null>(null)
const Component = () => {
const [study, setStudy] = useAtom(studyAtom)
// study ๊ฐ Study | null ์ด๋ฏ๋ก null ์ฒดํฌ ์์ด ํ๋กํผํฐ ์ ๊ทผ ๋ถ๊ฐ
console.log(study.title) // โ
์ปดํ์ผ ์๋ฌ: 'study' is possibly 'null'
// ์๋ํฐ์์ ๋นจ๊ฐ ์ค โ ๋ฐฐํฌ ์ ์ ์กํ!
if (study) {
console.log(study.title) // ์์ ํ๊ฒ ์ ๊ทผ
}
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
any๋ TypeScript ์๊ฒ "์ด ๋ณ์๋ ์ ๊ฒฝ ๊บผ์ค" ๋ผ๊ณ ํ๋ ๊ฒ๊ณผ ๊ฐ์. ์ปดํ์ผ๋ฌ๊ฐ ๊บผ๋ฒ๋ฆฌ๋ฉด ๋ฐํ์์์ ์ง์ ํฐ์ ธ. ๊ทธ ํญํ์ ํด์ ํ๋ ์๊ฐ์ด ํ ์๊ฐ์ด ๋๊ธฐ๋ ํด.
๐ Jotai ํต์ฌ ํ์ ์๊ทธ๋์ฒ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
PrimitiveAtom,Atom,WritableAtom์ ์ฐจ์ด๋ฅผ ํ์ ๋ ๋ฒจ์์ ์ดํดํ๋ค- ์ด๋ค atom ์ ์ด๋ค ํ์ ์ ๋ถ์ฌ์ผ ํ๋์ง ํ๋จํ ์ ์๋ค
Jotai ์๋ 3๊ฐ์ง ํต์ฌ ํ์ ์ด ์์ด. ์ด ์ธ ๊ฐ์ง๋ฅผ ๊ตฌ๋ถํ๋ ๊ฒ ํ์ดํ์ ํต์ฌ์ด์ผ.
1. PrimitiveAtom<T> โ ์ฝ๊ณ ์ธ ์ ์๋ ๊ธฐ๋ณธ atom
import type { PrimitiveAtom } from 'jotai'
// PrimitiveAtom<T> ์ ์ค์ ์ ์ (Jotai ๋ด๋ถ)
// type PrimitiveAtom<T> = WritableAtom<T, [SetStateAction<T>], void>
// ๐ฆ ์ํธ: "atom() ๋ก ๋ง๋ ๊ธฐ๋ณธ atom ์ด ๋ฐ๋ก PrimitiveAtom ์ด์์"
const searchKeywordAtom: PrimitiveAtom<string> = atom('')
const isModalOpenAtom: PrimitiveAtom<boolean> = atom(false)
const userAtom: PrimitiveAtom<User | null> = atom<User | null>(null)
// PrimitiveAtom ์ ์ฝ๊ธฐ์ ์ฐ๊ธฐ ๋ชจ๋ ๊ฐ๋ฅ
const Component = () => {
const [keyword, setKeyword] = useAtom(searchKeywordAtom) // โ
์ฝ๊ธฐ + ์ฐ๊ธฐ ๋ชจ๋
}2. Atom<T> โ ์ฝ๊ธฐ ์ ์ฉ atom (derived atom)
import type { Atom } from 'jotai'
// ๐ฆ ์ํธ: "read ํจ์๋ง ์๋ derived atom ์ Atom<T> ํ์
์ด์์. ์ธ ์ ์์ด์."
const filteredStudyListAtom: Atom<Study[]> = atom((get) => {
const keyword = get(searchKeywordAtom)
return get(studyListAtom).filter((s) => s.title.includes(keyword))
})
// โ Atom<T> ๋ ์ฝ๊ธฐ ์ ์ฉ โ setFilteredStudyList ๊ฐ ์์ด
const Component = () => {
const [studies, setStudies] = useAtom(filteredStudyListAtom)
// TypeScript ์๋ฌ: Atom<Study[]> ๋ WritableAtom ์ด ์๋์์
setStudies([]) // โ ์ปดํ์ผ ์๋ฌ
}
// โ
์ฝ๊ธฐ๋ง ํ์ํ๋ฉด useAtomValue
const Component = () => {
const studies = useAtomValue(filteredStudyListAtom) // โ
์ฝ๊ธฐ ์ ์ฉ
}3. WritableAtom<Value, Args, Result> โ ์์ ํ ์ปค์คํ ํ์ดํ
import type { WritableAtom } from 'jotai'
// WritableAtom<Value, Args, Result>
// Value: atom ์ด ๋ฐํํ๋ ๊ฐ์ ํ์
// Args: write ํจ์๊ฐ ๋ฐ๋ ์ธ์ ํ์
(๋ฐฐ์ด ํํ)
// Result: write ํจ์์ ๋ฐํ ํ์
// ๐ฆ ์ํธ: "write-only atom ์ด๋ ์ปค์คํ
write ๋ก์ง์ ๊ฐ์ง atom ์ ์ด ํ์
์ ์จ์"
const asyncSubmitAtom: WritableAtom<null, [StudyFormData], Promise<void>> = atom(
null, // read ๊ฐ ์์ (write-only)
async (_get, set, formData: StudyFormData) => {
await fetch('/api/studies', {
method: 'POST',
body: JSON.stringify(formData),
})
set(studyListAtom, (prev) => [...prev, formData])
}
)ํ์ 3ํ์ ๋น๊ตํ
| ํ์ | ์ฝ๊ธฐ | ์ฐ๊ธฐ | ์ฌ์ฉ ์์ |
|---|---|---|---|
PrimitiveAtom<T> | โ | โ | atom(''), atom(false) |
Atom<T> | โ | โ | atom((get) => ...) (read-only derived) |
WritableAtom<V, A, R> | โ | โ (์ปค์คํ ) | atom(read, write) |
๐๏ธ ์ ๋ค๋ฆญ์ผ๋ก atom ์ ์ธํ๊ธฐ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ค๋ฌด์์ ์์ฃผ ์ฐ๋ atom ํ์ดํ ํจํด๋ค์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ค
๊ธฐ๋ณธ ํจํด๋ค
import { atom } from 'jotai'
import type { Study, User, FilterType } from '@/types'
// ๐ข ์์๊ฐ โ TypeScript ๊ฐ ์๋ ์ถ๋ก (๋ช
์ ์๋ต ๊ฐ๋ฅ)
const likeCountAtom = atom(0) // PrimitiveAtom<number>
const searchKeywordAtom = atom('') // PrimitiveAtom<string>
const isModalOpenAtom = atom(false) // PrimitiveAtom<boolean>
// ๐ก nullable ๊ฐ โ ๋ฐ๋์ ๋ช
์! null ์ด๊ธฐ๊ฐ๋ง์ผ๋ก๋ ํ์
์ถ๋ก ๋ถ๊ฐ
const userAtom = atom<User | null>(null)
// ๐ฃ ์์ฒ : "atom(null) ํ๋ฉด ์ ๋ผ์?"
// ๐ฆ ์ํธ: "atom(null) ์ PrimitiveAtom<null> ์ด ๋ผ์. User ๋ฅผ ๋ฃ์ ์๊ฐ ์์ด์."
// ๐ก ๋ฐฐ์ด โ ๋ฐ๋์ ๋ช
์! [] ์ด๊ธฐ๊ฐ๋ง์ผ๋ก๋ ์์ ํ์
์ถ๋ก ๋ถ๊ฐ
const studyListAtom = atom<Study[]>([])
// ๐ฃ ์์ฒ : "atom([]) ํ๋ฉด PrimitiveAtom<never[]> ๊ฐ ๋๋ค๊ณ ์?"
// ๐ฆ ์ํธ: "๋ง์์. never[] ์๋ ์๋ฌด๊ฒ๋ ๋ชป ๋ฃ์ด์."
// ๐ก ์ ๋์ธ ํ์
const activeFilterAtom = atom<FilterType>('all') // FilterType = 'all' | 'active' | 'closed'
// ๐ด ๋ณต์กํ ๊ฐ์ฒด โ ๋ช
์ ๊ฐ๋ ฅ ๊ถ์ฅ
interface StudyFilter {
keyword: string
tags: string[]
category: string | null
sortBy: 'latest' | 'popular'
}
const studyFilterAtom = atom<StudyFilter>({
keyword: '',
tags: [],
category: null,
sortBy: 'latest',
})Before / After ๋น๊ต
// โ Before โ ์์ฒ ์ any ๋จ๋ฐ
// ๐ฃ ์์ฒ : "์ผ๋จ any ๋ก ๋ค ๋ซ๊ณ ๋์ค์ ํ์
๋ฃ๊ฒ ์ต๋๋ค"
const userAtom = atom\<any\>(null) // any ์ง์ฅ ์์
const studyListAtom = atom\<any\>([]) // any ์ ์ผ
const filterAtom = atom\<any\>({}) // any ํ์ฐ
// ์ปดํฌ๋ํธ์์ ์๋์์ฑ ์์, ์คํ ์ก๊ธฐ ๋ถ๊ฐ, ๋ฐํ์ ํญํ ๋ด์ฌ
const Component = () => {
const [user] = useAtom(userAtom)
return <div>{user.name}</div> // null ์ผ ๋ ๋ฐํ์ ํฐ์ง
}// โ
After โ ์ํธ๊ฐ ๋ฆฌํฉํ ๋งํ ๋ฒ์
// ๐ฆ ์ํธ: "ํ์
์ ๋ช
์ํ๋ฉด IDE ๊ฐ ์๋์์ฑ + ์๋ฌ ๊ฐ์ง๋ฅผ ํด์ค์"
const userAtom = atom<User | null>(null)
const studyListAtom = atom<Study[]>([])
const filterAtom = atom<StudyFilter>({
keyword: '',
tags: [],
category: null,
sortBy: 'latest',
})
// ์ปดํฌ๋ํธ์์ ์๋์์ฑ ์๋ฒฝ ์๋, ์ปดํ์ผ ํ์์ ์๋ฌ ๊ฐ์ง
const Component = () => {
const user = useAtomValue(userAtom)
if (!user) return <LoginPrompt /> // null ๊ฐ๋ ๊ฐ์
return <div>{user.name}</div> // ์๋์์ฑ์ผ๋ก .name ์ถ์ฒ
}๐ญ atom factory ํจ์ ํ์ดํ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋์ ์ผ๋ก atom ์ ์์ฑํ๋ factory ํจ์๋ฅผ TypeScript ๋ก ์ฌ๋ฐ๋ฅด๊ฒ ํ์ดํํ ์ ์๋ค
๊ธฐ๋ณธ factory ํจํด
์์๋ค ํ์์๋ ์คํฐ๋๋ง๋ค ๊ฐ๋ณ ์ํ๊ฐ ํ์ํ ๊ฒฝ์ฐ๊ฐ ์์ด. atom factory ๋ก ๊ฐ ์คํฐ๋์ ์ํ๋ฅผ ์บก์ํํ์ด:
import { atom } from 'jotai'
import type { PrimitiveAtom } from 'jotai'
import type { Study } from '@/types'
// ๐ฆ ์ํธ: "๋ฐํ ํ์
์ PrimitiveAtom<T> ๋ก ๋ช
์ํ๋ ๊ฒ ์ค๋ฌด ๊ด๋ก์์"
function createStudyDetailAtom(initialStudy: Study): PrimitiveAtom<Study> {
return atom(initialStudy)
}
// ์ฌ์ฉ ์ โ ํ์
์ด ์๋์ผ๋ก Study ๋ก ์ขํ์ง
const reactStudyAtom = createStudyDetailAtom({
id: '001',
title: 'React ์ฌํ',
members: [],
isActive: true,
})
// ๐ฃ ์์ฒ : "์ค, ์๋์์ฑ์ด Study ํ๋๋ฅผ ๋ค ๋ณด์ฌ์ค์!"
const Component = () => {
const [study, setStudy] = useAtom(reactStudyAtom)
return (
<div>
{study.title} {/* โ
์๋์์ฑ ์๋ฒฝ ์๋ */}
<button onClick={() => setStudy((prev) => ({ ...prev, isActive: false }))}>
์คํฐ๋ ์ข
๋ฃ
</button>
</div>
)
}์ ๋ค๋ฆญ factory โ ์ฌ๋ฌ ํ์ ์ ์ฌ์ฌ์ฉ
// ๐ฆ ์ํธ: "์ ๋ค๋ฆญ factory ๋ฅผ ๋ง๋ค๋ฉด ํ์
๋ณ๋ก ์ฌ์ฌ์ฉํ ์ ์์ด์"
function createOptionalAtom<T>(initialValue: T | null = null): PrimitiveAtom<T | null> {
return atom<T | null>(initialValue)
}
// ํ์
๋ณ atom ์์ฑ
const selectedStudyAtom = createOptionalAtom<Study>()
// โ PrimitiveAtom<Study | null>
const hoveredCommentAtom = createOptionalAtom<Comment>()
// โ PrimitiveAtom<Comment | null>
// ๐ฆ ์ํธ: "์ด ํจํด์ผ๋ก '์ ํ๋ ํญ๋ชฉ' atom ์ ์ผ๊ด๋๊ฒ ๋ง๋ค ์ ์์ด์"factory ์ ์ฅ์ โ useMemo ์์ด ์ปดํฌ๋ํธ ์ธ๋ถ์์ ์์ฑ
// โ
๋ชจ๋ ๋ ๋ฒจ์์ ๋ฏธ๋ฆฌ ์์ฑ โ ๋ ๋๋ง๊ณผ ๋ฌด๊ด
const studyAtoms = {
react: createStudyDetailAtom(reactStudy),
typescript: createStudyDetailAtom(typescriptStudy),
nextjs: createStudyDetailAtom(nextjsStudy),
}
// ์ปดํฌ๋ํธ์์๋ ๊บผ๋ด ์ฐ๊ธฐ๋ง
const ReactStudyCard = () => {
const [study] = useAtom(studyAtoms.react)
return <StudyCard study={study} />
}๐ ์ฐ๊ฒฐ ๊ณ ๋ฆฌ
ID ๋ณ๋ก ๋์ ์ผ๋ก atom ์ ์์ฑํด์ผ ํ๋ ๊ฒฝ์ฐ(๋ฌดํ ์คํฌ๋กค ๋ฑ)๋ 06. atomFamily ๊ฐ ๋ ์ ํฉํด.
๐ discriminated union atom ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- discriminated union ์ผ๋ก "๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ" ์ ํ์ ๋ ๋ฒจ์์ ๋ฐฉ์งํ ์ ์๋ค
- ๋น๋๊ธฐ ์ํ ๋ชจ๋ธ๋ง์ ํ์ค ํจํด์ ์ค๋ฌด์ ์ ์ฉํ ์ ์๋ค
๋ฌธ์ : ๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ
// โ ๐ฃ ์์ฒ ์ ๋ฐฉ์ โ 3๊ฐ์ ๋
๋ฆฝ atom
const isLoadingAtom = atom(false)
const isErrorAtom = atom(false)
const dataAtom = atom<Study[] | null>(null)
// ๐ฃ ์์ฒ : "๋ญ๊ฐ ๋ฌธ์ ์ฃ ? ๊ฐ๊ฐ ๊ด๋ฆฌํ๋ฉด ๋์์์."
// ๐ฆ ์ํธ: "isLoading=true, isError=true, data=[...] ๊ฐ ๋์์ ๊ฐ๋ฅํด์.
// ๋
ผ๋ฆฌ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ์ด ํ์
๋ ๋ฒจ์์ ํ์ฉ๋๋ ๊ฑฐ์์."
// ์ค์ ๋ก ์ด๋ฐ ์ฝ๋๊ฐ ์๊ฒจ:
const setBothTrueByMistake = () => {
setIsLoading(true) // ๋ก๋ฉ ์์
setIsError(true) // ๐ค ๋ก๋ฉ ์ค์ธ๋ฐ ์๋ฌ?
setData([]) // ๐ค ์๋ฌ์ธ๋ฐ ๋ฐ์ดํฐ?
}
// TypeScript ๋ ์ด๊ฑธ ์๋ฌ๋ก ๋ณด์ง ์์!ํด๊ฒฐ: discriminated union ์ผ๋ก ์ํ ๊ณต๊ฐ ๋ชจ๋ธ๋ง
// โ
๐ฆ ์ํธ: "discriminated union ์ผ๋ก '๊ฐ๋ฅํ ์ํ' ๋ง ํ์
์ผ๋ก ํ์ฉํด์"
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
// 'idle' ์ผ ๋๋ data ์ error ํ๋กํผํฐ๊ฐ ์์
// 'loading' ์ผ ๋๋ data ์ error ํ๋กํผํฐ๊ฐ ์์
// 'success' ์ผ ๋๋ ๋ฐ๋์ data ๊ฐ ์์
// 'error' ์ผ ๋๋ ๋ฐ๋์ error ๊ฐ ์์
const studyFetchStateAtom = atom<FetchState<Study[]>>({ status: 'idle' })
// ์ฌ์ฉํ ๋ โ TypeScript ๊ฐ status ์ ๋ฐ๋ฅธ ํ์
์ ์๋์ผ๋ก ์ขํ์ค
const StudyList = () => {
const [fetchState, setFetchState] = useAtom(studyFetchStateAtom)
const loadStudies = async () => {
setFetchState({ status: 'loading' })
// ๐ฃ ์์ฒ : "loading ์ํ์์ data ๋ฅผ ๊ฐ์ด ๋ฃ์ ์ ์์ด์?"
// setFetchState({ status: 'loading', data: [] }) // โ ์ปดํ์ผ ์๋ฌ!
try {
const data = await fetchStudies()
setFetchState({ status: 'success', data })
// success ์ํ์ ๋ฐ๋์ data ๊ฐ ์์ด์ผ ํจ โ ํ์
์ด ๊ฐ์
} catch (error) {
setFetchState({ status: 'error', error: error as Error })
}
}
// status ๋ก ์ขํ๋ฉด TypeScript ๊ฐ data/error ์กด์ฌ๋ฅผ ๋ณด์ฅํด์ค
if (fetchState.status === 'idle') {
return <button onClick={loadStudies}>์คํฐ๋ ๋ถ๋ฌ์ค๊ธฐ</button>
}
if (fetchState.status === 'loading') {
return <Spinner />
}
if (fetchState.status === 'error') {
return <ErrorMessage error={fetchState.error} />
// fetchState.error โ TypeScript ๊ฐ ์ฌ๊ธฐ์ error ๊ฐ ์๋ค๊ณ ๋ณด์ฅ
}
// status === 'success' ์ด๋ฉด data ๊ฐ ๋ฐ๋์ ์์
return <ul>{fetchState.data.map((s) => <StudyCard key={s.id} study={s} />)}</ul>
// โ ์๋์์ฑ ์๋ฒฝ
}์์๋ค ํ์ ์ค์ FetchState ์ ํธ
// atoms/utils.ts โ ํ ์ ์ฒด๊ฐ ๊ณต์ ํ๋ FetchState ํ์
์ ํธ
export type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
// FetchState ๋ฅผ ์ฝ๊ฒ ๋ง๋๋ ํฌํผ (ํ์
์์ )
export const FetchStates = {
idle: (): FetchState<never> => ({ status: 'idle' }),
loading: (): FetchState<never> => ({ status: 'loading' }),
success: <T>(data: T): FetchState<T> => ({ status: 'success', data }),
error: (error: Error): FetchState<never> => ({ status: 'error', error }),
}
// ์ฌ์ฉ:
// setFetchState(FetchStates.success(data)) โ ํ์
์์ ํ๊ฒ ์ํ ์ ํ๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
discriminated union ์ "์ด ์ํ์์๋ ์ด ๋ฐ์ดํฐ๋ง ์์ด์ผ ํด" ๋ฅผ ํ์ ์ผ๋ก ํํํ๋ ๊ฑฐ์ผ. "๋ก๋ฉ ์ค์ด๋ฉด์ ๋์์ ์๋ฌ" ๊ฐ์ ๋ถ๊ฐ๋ฅํ ์กฐํฉ์ ์ปดํ์ผ๋ฌ๊ฐ ๋ง์์ค.
โ๏ธ write-only atom ํ์ ๋ช ์ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- write-only atom ์ ํ์ ์๊ทธ๋์ฒ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์์ฑํ ์ ์๋ค
WritableAtom<null, [Args], Result>ํจํด์ ์ค๋ฌด์ ์ ์ฉํ ์ ์๋ค
write-only atom ๊ธฐ๋ณธ ํ์ดํ
import type { WritableAtom } from 'jotai'
// ๐ฆ ์ํธ: "write-only atom ์ read ๊ฐ์ด null ์ด๊ณ , Args ์ Result ๋ฅผ ๋ช
์ํด์"
// WritableAtom<null, [์ธ์ํ์
], ๋ฐํํ์
>
// ์คํฐ๋ ์ข์์ ํ ๊ธ action atom
const toggleStudyLikeAtom: WritableAtom<null, [studyId: string], void> = atom(
null, // read ๋ null โ ์ด atom ์ ๊ฐ์ ์ฝ์ ํ์ ์์
(_get, set, studyId: string) => {
set(studyLikeMapAtom, (prev) => {
const next = new Map(prev)
next.set(studyId, !prev.get(studyId))
return next
})
}
)
// ๋น๋๊ธฐ write-only atom
const submitStudyApplicationAtom: WritableAtom<null, [StudyFormData], Promise<void>> = atom(
null,
async (_get, set, formData: StudyFormData) => {
await fetch('/api/applications', {
method: 'POST',
body: JSON.stringify(formData),
})
// ์ฑ๊ณต ํ ๋ชฉ๋ก ๊ฐฑ์ atom ํธ๋ฆฌ๊ฑฐ
set(studyListRefreshAtom, (prev) => prev + 1)
}
)
// ์ฌ์ฉ โ useSetAtom ์ผ๋ก setter ๋ง ๊ฐ์ ธ์ด
const ApplyButton = ({ studyId }: { studyId: string }) => {
const toggleLike = useSetAtom(toggleStudyLikeAtom)
return (
<button onClick={() => toggleLike(studyId)}>
์ข์์
</button>
)
}์ฌ๋ฌ ์ธ์๋ฅผ ๋ฐ๋ write atom
// ๐ฆ ์ํธ: "Args ๋ ๋ฐฐ์ด์ด์์. ์ฌ๋ฌ ์ธ์๋ฅผ ๋๊ธธ ๋ ์ด๋ ๊ฒ ํด์"
const updateStudyMemberAtom: WritableAtom<null, [studyId: string, userId: string, role: 'member' | 'leader'], void> = atom(
null,
(_get, set, studyId, userId, role) => {
set(studyMembersAtom, (prev) => ({
...prev,
[studyId]: prev[studyId].map((m) =>
m.id === userId ? { ...m, role } : m
),
}))
}
)
// ์ฌ์ฉ
const MemberActions = ({ studyId, userId }: { studyId: string; userId: string }) => {
const updateRole = useSetAtom(updateStudyMemberAtom)
return (
<button onClick={() => updateRole(studyId, userId, 'leader')}>
๋ฆฌ๋ ์๋ช
</button>
)
}๐ useAtom ๋ฐํ ํ์ ์ถ๋ก ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- TypeScript ๊ฐ ์๋ ์ถ๋ก ํ๋ ๊ฒฝ์ฐ์ ๋ช ์๊ฐ ํ์ํ ๊ฒฝ์ฐ๋ฅผ ๊ตฌ๋ถํ ์ ์๋ค
ExtractAtomValue์ ํธ ํ์ ์ ์ฌ์ฉํ๋ ๋ฒ์ ์๋ค
์๋ ์ถ๋ก vs ๋ช ์ ํ์
// โ
TypeScript ๊ฐ ์๋ ์ถ๋ก ํ๋ ๊ฒฝ์ฐ (๋๋ถ๋ถ์ ๊ฒฝ์ฐ)
const countAtom = atom(0)
const [count, setCount] = useAtom(countAtom)
// count: number, setCount: SetAtom<[SetStateAction<number>], void>
// โ ๋ช
์ ๋ถํ์
const userAtom = atom<User | null>(null)
const user = useAtomValue(userAtom)
// user: User | null โ ๋ช
์ ๋ถํ์
// ๐ด ๋ช
์๊ฐ ํ์ํ ๊ฒฝ์ฐ โ ํ์
๊ฐ๋ ์์ด ์ขํ๊ณ ์ถ์ ๋
const [user] = useAtom(userAtom)
// user ํ์
์ด User | null ์ธ๋ฐ, ํน์ ์ปจํ
์คํธ์์ User ์์ด ํ์คํ ๋
const assertedUser = user as User // ํ์
๋จ์ธ (์ฃผ์: null ๊ฐ๋ ํ์๋ง)ExtractAtomValue โ atom ์ ๊ฐ ํ์ ์ถ์ถ
import type { ExtractAtomValue } from 'jotai'
// ๐ฆ ์ํธ: "atom ์ ๊ฐ ํ์
์ ์ถ์ถํด์ผ ํ ๋ ExtractAtomValue ๋ฅผ ์จ์"
const userAtom = atom<User | null>(null)
type UserAtomValue = ExtractAtomValue<typeof userAtom>
// โ User | null
// ํจ์ ์ธ์ ํ์
์ผ๋ก ํ์ฉ
function processUser(user: ExtractAtomValue<typeof userAtom>) {
if (!user) return null
return user.name.toUpperCase()
}// ๐ฆ ์ํธ: "์ค๋ฌด์์ ์์ฃผ ์ฐ๋ ํจํด์ด์์"
import type { ExtractAtomValue, useAtomValue } from 'jotai'
import { userAtom } from '@/atoms/auth'
import { useQuery } from '@tanstack/react-query'
// atom ์ ํ์
์ ์ง์ import ํ์ง ์๊ณ ExtractAtomValue ๋ก ์ถ์ถ
function useUserProfile(user: ExtractAtomValue<typeof userAtom>) {
return useQuery({
queryKey: ['user', user?.id],
queryFn: () => fetch(`/api/users/${user?.id}`).then((r) => r.json()),
enabled: !!user,
})
}๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ Type 'null' is not assignable to type 'Study'
์์ธ:
const studyAtom = atom<Study>(null) // โ null ์ Study ๊ฐ ์๋ํด๊ฒฐ์ฑ :
const studyAtom = atom<Study | null>(null) // โ
nullable ๋ช
์โ Property does not exist on type 'never[]'
์์ธ:
const studyListAtom = atom([]) // PrimitiveAtom<never[]>
const [studies] = useAtom(studyListAtom)
studies.push({ id: '1' }) // โ Type '{ id: string }' is not assignable to type 'never'ํด๊ฒฐ์ฑ :
const studyListAtom = atom<Study[]>([]) // โ
์์ ํ์
๋ช
์โ WritableAtom ํ์ ์๋ฌ โ Args ๊ฐ ๋ฐฐ์ด์ด ์๋ ๊ฒฝ์ฐ
์์ธ:
// โ Args ๋ ๋ฐ๋์ ๋ฐฐ์ด(tuple) ํํ์ฌ์ผ ํด
const badAtom: WritableAtom<null, string, void> = atom(null, (_g, _s, id: string) => {})
// โ ๋ฐฐ์ด์ด ์๋!ํด๊ฒฐ์ฑ :
// โ
Args ๋ฅผ ๋ฐฐ์ด(tuple) ๋ก ๊ฐ์ธ์ผ ํจ
const goodAtom: WritableAtom<null, [string], void> = atom(null, (_g, _s, id: string) => {})
// โ [string] โ 1๊ฐ ์ธ์๋ฅผ ๋ฐฐ์ด๋ก๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ Jotai ํ์ 3ํ์ ์์ฝ
| ํ์ | ์ค๋ช | ์์ |
|---|---|---|
PrimitiveAtom<T> | ์ฝ๊ณ ์ธ ์ ์๋ ๊ธฐ๋ณธ atom | atom(''), atom<User|null>(null) |
Atom<T> | ์ฝ๊ธฐ ์ ์ฉ derived atom | atom((get) => ...) |
WritableAtom<V, A, R> | ์ปค์คํ read/write atom | atom(null, (_, set, arg) => ...) |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| nullable ์ด๊ธฐ๊ฐ | atom(null) โ PrimitiveAtom<null> | atom<User|null>(null) |
| ๋น ๋ฐฐ์ด ์ด๊ธฐ๊ฐ | atom([]) โ PrimitiveAtom<never[]> | atom<Study[]>([]) |
| ๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ | isLoading + isError ๋
๋ฆฝ atom | discriminated union atom |
| write-only Args | WritableAtom<null, string, void> | WritableAtom<null, [string], void> |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ๐ด ์๋์ด ๋ฉด์ ์ง๋ฌธ (์์ฒ ์ ๋ฉด์ ๋์ )
"Jotai ์
PrimitiveAtom<T>์Atom<T>์ ์ฐจ์ด๋ ๋ฌด์์ธ๊ฐ์? ์ค๋ฌด์์ ๊ฐ๊ฐ ์ธ์ ์ฌ์ฉํด์ผ ํ๋์?"
- A) ๋ ๋ค ๋์ผํ๊ณ , ์ทจํฅ์ ๋ฐ๋ผ ๊ณจ๋ผ ์ฐ๋ฉด ๋๋ค
- B)
PrimitiveAtom<T>๋ ์ฝ๊ธฐ์ ์ฐ๊ธฐ๊ฐ ๋ชจ๋ ๊ฐ๋ฅํ๊ณ ,Atom<T>๋ ์ฝ๊ธฐ ์ ์ฉ์ด๋ค.useAtom์ ๋ ๋ค ์ฌ์ฉ ๊ฐ๋ฅํ์ง๋ง,Atom<T>์์๋ setter ๊ฐ ์๋ค - C)
Atom<T>๋ ๋น๋๊ธฐ atom ์์๋ง ์ฌ์ฉํ๋ ํ์ ์ด๋ค - D)
PrimitiveAtom<T>๋ ์ซ์/๋ฌธ์์ด์๋ง,Atom<T>๋ ๊ฐ์ฒด์๋ง ์ฌ์ฉํ๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
:
PrimitiveAtom<T>๋ ๋ด๋ถ์ ์ผ๋กWritableAtom<T, [SetStateAction<T>], void>์ผ. ์ฝ๊ธฐ์ ์ฐ๊ธฐ ๋ชจ๋ ๊ฐ๋ฅํ ๊ธฐ๋ณธ atom ์ด์ผ.Atom<T>๋ read ํจ์๋ง ์๋ derived atom ์ ํ์ ์ด๊ณ ,useAtom์ผ๋ก ๊ตฌ๋ ํ๋ฉด setter ๊ฐ ์์ด. - ์ค๋ต ํผ๋๋ฐฑ: A ๋ ํ๋ ธ์ด โ ๋ ํ์
์ ์ฐ๊ธฐ ๊ฐ๋ฅ ์ฌ๋ถ๊ฐ ์์ ํ ๋ฌ๋ผ. C ๋ ํ๋ ธ์ด โ
Atom<T>๋ ๋น๋๊ธฐ์ ๋ฌด๊ดํ๊ณ read-only derived atom ์ ํ์ ์ด์ผ. D ๋ ํ๋ ธ์ด โ ๊ฐ์ ์์ ์ฌ๋ถ์ ๋ฌด๊ดํด. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "Primitive ๋ ๊ธฐ๋ณธ๊ฐ์ ์ฝ๊ณ ์ฐ๋ ์ง์ง atom. Atom ์ ๊ณ์ฐ๋ ๊ฐ์ ์ฝ๊ธฐ๋ง ํ๋ ํ์ atom."
Q2. ๐ฅ ๊ธด๊ธ ๋๋ฒ๊น (์์์ ํธํต)
๋ฐฐํฌ ์ง์ ์์ ๋์ด ์ฌ๋์: "๋ฐฉ๊ธ ์คํฐ๋ ์ ์ฒญ ๋ฒํผ ๋๋ ๋๋ฐ 'Cannot read properties of undefined' ์๋ฌ ๋ฌ์ด์."
์์ฒ ์ด๊ฐ ์ด์ด๋ณธ ์ฝ๋:
const selectedStudyAtom = atom<any>(null)
const ApplyPage = () => {
const [study] = useAtom(selectedStudyAtom)
return <h1>{study.title}</h1> // ์๋ฌ ๋ฐ์ ์ง์
}๊ฐ์ฅ ๋น ๋ฅด๊ฒ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์?
- A)
study.title์study?.title๋ก ๋ฐ๊พผ๋ค - B)
atom<any>๋ฅผatom<Study | null>(null)๋ก ๋ฐ๊พธ๊ณ null ๊ฐ๋๋ฅผ ์ถ๊ฐํ๋ค - C)
try/catch๋ก ๊ฐ์ผ๋ค - D)
selectedStudyAtom์ ์ด๊ธฐ๊ฐ์{}๋ก ๋ฐ๊พผ๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: A ๋ ์ฆ์์ ์์ ์ฒ๋ฐฉํ๋ ๊ฒ์ ๋ถ๊ณผํด.
atom<Study | null>๋ก ํ์ ์ ๋ช ์ํ๋ฉด TypeScript ๊ฐstudy.title์ ๊ทผ ์ ์ ์ปดํ์ผ ์๋ฌ๋ฅผ ๋์ ธ์ null ๊ฐ๋(if (!study) return ...)๋ฅผ ๊ฐ์ ํด. ๊ทผ๋ณธ ์์ธ(any ํ์ )์ ๊ณ ์น๋ ๊ฒ ์ฌ๋ฐ๋ฅธ ์ ๊ทผ์ด์ผ. - ์ค๋ต ํผ๋๋ฐฑ: A ๋ null ์ผ ๋
undefined๋ฅผ ๋ ๋๋งํ๋ ๊ฑฐ๋ผ UX ๊ฐ ๋ง๊ฐ์ง ์ ์์ด. D ๋ ๋น ๊ฐ์ฒด๊ฐ Study ํ์ ์ ๋ง์กฑํ์ง ์์ผ๋ ๋ค๋ฅธ ์๋ฌ๊ฐ ์๊ฒจ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "any ๋ ๋ฌธ์ ๋ฅผ ์จ๊ธฐ๊ณ , ํ์ ๋ช ์๋ ๋ฌธ์ ๋ฅผ ๋๋ฌ๋ด. ๋๋ฌ๋ ๋ฌธ์ ๋ ๊ณ ์น ์ ์์ด."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
discriminated union atom ์ด ๋ ๋ฆฝ๋
isLoading/isError/dataatom 3๊ฐ๋ณด๋ค ๋์ ์ด์ ๋ฅผ ๊ฐ๋ฐ์ ์น๊ตฌ์๊ฒ ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"๋ ๋ฆฝ atom 3๊ฐ๋ '๋ก๋ฉ ์ค์ด๋ฉด์ ๋์์ ์๋ฌ'์ฒ๋ผ ๋ ผ๋ฆฌ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํ ์ํ ์กฐํฉ์ ๋ง์ ๋ฐฉ๋ฒ์ด ์์ด. discriminated union ์
{ status: 'loading' }๋๋{ status: 'error', error }์ฒ๋ผ ๋์์ ํ๋์ ์ํ๋ง ๊ฐ๋ฅํ๊ฒ ๊ฐ์ ํด. TypeScript ๊ฐstatus๋ฅผ ๋ณด๊ณ ์ด๋ค ํ๋กํผํฐ๋ฅผ ์ ๊ทผํ ์ ์๋์ง ์๋์ผ๋ก ์ขํ์ค์ null ์ฒดํฌ ๋น ํธ๋ฆฌ๋ ์ค์๋ ์์ด์ ธ."
๐ก ์ด ๋น์ ๋ฅผ ์ง์ ๋ง๋ค์๋ค๋ฉด: discriminated union ์ ํต์ฌ ๊ฐ์น๋ฅผ ์ดํดํ ๊ฑฐ์ผ. ๋ค์ ๊ฐ์ด๋๋ก ๋์ด๊ฐ๋ ์ถฉ๋ถํด!
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋ ์ง์ง ์์ํ๋ค๊ฐ ๋์ค์ ๋ฟ๋ฏํ ํ๋ฃจ์์ด.
์ค์ ์ atom<any> ๋ก ๋๋ฐฐ๋ ์ฝ๋ ๋๋ฌธ์ ๋ฐํ์ ์๋ฌ ์ก๋๋ผ ํ ์๊ฐ์ ๋ ๋ ธ๋๋ฐ, ์ํธ ๋์ด "ํ์
์ ๋๋ก ์ฐ๋ฉด 30์ด์ ์กํ์ ๊ฑฐ์์" ๋ผ๊ณ ํ์ ๋ ๋๋ฌผ์ด ๋ ๋ปํ๋ค. ์์งํ ๊ท์ฐฎ์์ any ์ด ๊ฑฐ๋ผ์ ๋ณ๋ช
๋ ๋ชป ํ์ด.
๊ทธ ๋ค์์ discriminated union ์ผ๋ก isLoading/isError/data ์ธ ๊ฐ๋ฅผ ํ๋๋ก ํฉ์ณค๋๋ฐ, ๊ทธ๋ฌ๊ณ ๋์ ์ปดํฌ๋ํธ ์ฝ๋๊ฐ ๊ฐ์๊ธฐ ๋๋ฌด ๊น๋ํด์ง ๊ฑฐ์ผ. status === 'success' ์ฒดํฌ ํ์ TypeScript ๊ฐ data ๊ฐ ์๋ค๊ณ ์๋์ผ๋ก ์์์ฃผ๋๊น ๋ถํ์ํ ! ๋จ์ธ๋ ์์ด์ง๊ณ , IDE ์๋์์ฑ์ด ์๋ฒฝํ๊ฒ ๋์ํ๋๋ผ๊ณ .
๐ก ์ค๋์ ๊ตํ: "any ๋ ์ค๋์ ํธํจ์ด๊ณ , ํ์ ๋ช ์๋ ๋ฏธ๋์ ๋ ์์ ์ ํฅํ ๋ฐฐ๋ ค๋ค."
ํด๊ทผํ๋ฉด์ ์ข์ํ๋ ํ์บ์คํธ ๋ค์ผ๋ฉด์ ์ง ๊ทผ์ฒ ํธ์์ ์์ ์ผ๊ฐ๊น๋ฐฅ์ด๋ผ๋ ์ฌ์ผ์ง. ์ค๋์ ์นผ๋ก๋ฆฌ ๊ณ์ฐ ํจ์ค. ๋ญ๊ฐ ์์ํ ๋ณด์์ด ํ์ํด.