๐Ÿ’ก 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 ์œผ๋กœ ๋น„๋™๊ธฐ ์ƒํƒœ๋ฅผ ํƒ€์ž… ๋ ˆ๋ฒจ์—์„œ ๋ชจ๋ธ๋งํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


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

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 20๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 12๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„

[atom&lt;any&gt; ์ง€์˜ฅ] โ†’ [ํ•ต์‹ฌ ํƒ€์ž… 3๊ฐ€์ง€] โ†’ [์ œ๋„ค๋ฆญ ์„ ์–ธ ํŒจํ„ด] โ†’ [factory ํ•จ์ˆ˜] โ†’ [discriminated union] โ†’ [write-only ํƒ€์ž…]

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

์˜์ฒ ์ด๊ฐ€ ์Šคํ„ฐ๋”” ์‹ ์ฒญ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋‹ค๊ฐ€ ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๋ฅผ ํ„ฐํŠธ๋ฆฐ ๋‚ ์ด์•ผ.

๐Ÿฃ ์˜์ฒ : (๋‹นํ™ฉํ•ด์„œ) "์˜ํ˜ธ ๋‹˜, ์ด์ƒํ•ด์š”. ์ปดํŒŒ์ผ ์—๋Ÿฌ๋Š” ์—†์—ˆ๋Š”๋ฐ ๋Ÿฐํƒ€์ž„์—์„œ Cannot read properties of undefined ๊ฐ€ ๋‚ฌ์–ด์š”..."

๐Ÿฆ ์˜ํ˜ธ ๋‹˜: (์ฝ”๋“œ๋ฅผ ์—ด์–ด๋ณด๋ฉฐ) "์˜์ฒ  ๋‹˜, ์—ฌ๊ธฐ atom\&lt;any\&gt; ์“ฐ์…จ๋„ค์š”. TypeScript ๊ฐ€ any ๋Š” ๊ฒ€์‚ฌ๋ฅผ ํฌ๊ธฐํ•ด๋ฒ„๋ ค์š”. ์ปดํŒŒ์ผ ์—๋Ÿฌ๊ฐ€ ์•ˆ ๋‚˜๋Š” ๊ฒŒ ๋‹น์—ฐํ•˜์ฃ ."

๐Ÿฃ ์˜์ฒ : "์•„ any ์“ฐ๋ฉด ํŽธํ•˜๋‹ˆ๊นŒ์š”... ํƒ€์ž… ์“ฐ๊ธฐ ๊ท€์ฐฎ์ž–์•„์š”."

๐Ÿฆ ์˜ํ˜ธ ๋‹˜: "ํŽธํ•œ ๊ฒŒ ๋งž์•„์š”. ์˜ค๋Š˜ ๋ฐฐํฌ ์ „์—๋Š”์š”. ๊ทผ๋ฐ ์ด ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ ์žก๋Š” ๋ฐ ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ ธ์–ด์š”?"

๐Ÿฃ ์˜์ฒ : (ํ’€์ด ์ฃฝ์–ด์„œ) "...ํ•œ ์‹œ๊ฐ„์ด์š”."

๐Ÿฆ ์˜ํ˜ธ ๋‹˜: "TypeScript ๋กœ ์ œ๋Œ€๋กœ ํƒ€์ดํ•‘ํ–ˆ์œผ๋ฉด ์—๋””ํ„ฐ์—์„œ ๋นจ๊ฐ„ ์ค„์ด ๋ฐ”๋กœ ๋ณด์˜€์„ ๊ฑฐ์˜ˆ์š”. ํ•œ ์‹œ๊ฐ„์ด 30์ดˆ๋กœ ์ค„์–ด๋“œ๋Š” ๊ฑฐ์˜ˆ์š”."


๐Ÿค” ์™œ atom ํƒ€์ดํ•‘์ด ์ค‘์š”ํ•œ๊ฐ€? ๐ŸŸข

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

  • atom\&lt;any\&gt; ๊ฐ€ ์™œ "์‹œํ•œ ํญํƒ„" ์ธ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • TypeScript ์˜ ํƒ€์ž… ์ถ”๋ก ์ด ์–ด๋””๊นŒ์ง€ ์ž๋™์œผ๋กœ ๋˜๊ณ , ์–ด๋””์„œ ๋ช…์‹œ๊ฐ€ ํ•„์š”ํ•œ์ง€ ์ดํ•ดํ•œ๋‹ค

atom&lt;any&gt; โ€” TypeScript ์˜ ๋ฐฉ์–ด๋ง‰์„ ๋šซ๋Š” ์ฝ”๋“œ

// โŒ ๐Ÿฃ ์˜์ฒ : "any ์“ฐ๋ฉด ์ผ๋‹จ ์—๋Ÿฌ ์—†์œผ๋‹ˆ๊นŒ ํŽธํ•ด์š”!"
const studyAtom = atom\&lt;any\&gt;(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\&lt;any\&gt;(null)        // any ์ง€์˜ฅ ์‹œ์ž‘
const studyListAtom = atom\&lt;any\&gt;([])     // any ์ „์—ผ
const filterAtom = atom\&lt;any\&gt;({})        // 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>์ฝ๊ณ  ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ณธ atomatom(''), atom<User|null>(null)
Atom<T>์ฝ๊ธฐ ์ „์šฉ derived atomatom((get) => ...)
WritableAtom<V, A, R>์ปค์Šคํ…€ read/write atomatom(null, (_, set, arg) => ...)

โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ

์ƒํ™ฉโŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ
nullable ์ดˆ๊ธฐ๊ฐ’atom(null) โ†’ PrimitiveAtom<null>atom<User|null>(null)
๋นˆ ๋ฐฐ์—ด ์ดˆ๊ธฐ๊ฐ’atom([]) โ†’ PrimitiveAtom<never[]>atom<Study[]>([])
๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ์กฐํ•ฉisLoading + isError ๋…๋ฆฝ atomdiscriminated union atom
write-only ArgsWritableAtom<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/data atom 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 ๋Š” ์˜ค๋Š˜์˜ ํŽธํ•จ์ด๊ณ , ํƒ€์ž… ๋ช…์‹œ๋Š” ๋ฏธ๋ž˜์˜ ๋‚˜ ์ž์‹ ์„ ํ–ฅํ•œ ๋ฐฐ๋ ค๋‹ค."

ํ‡ด๊ทผํ•˜๋ฉด์„œ ์ข‹์•„ํ•˜๋Š” ํŒŸ์บ์ŠคํŠธ ๋“ค์œผ๋ฉด์„œ ์ง‘ ๊ทผ์ฒ˜ ํŽธ์˜์ ์—์„œ ์‚ผ๊ฐ๊น€๋ฐฅ์ด๋ผ๋„ ์‚ฌ์•ผ์ง€. ์˜ค๋Š˜์€ ์นผ๋กœ๋ฆฌ ๊ณ„์‚ฐ ํŒจ์Šค. ๋ญ”๊ฐ€ ์†Œ์†Œํ•œ ๋ณด์ƒ์ด ํ•„์š”ํ•ด.


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