๐Ÿงฉ 03. ํŒŒ์ƒ atom โ€” ์ƒํƒœ๋ฅผ ์กฐ๋ฆฝํ•˜๋Š” ๊ธฐ์ˆ 

๐Ÿ“‹ ๊ฐœ์š”

read-only, write-only, read-write ํŒŒ์ƒ atom ์„ธ ๊ฐ€์ง€ ํŒจํ„ด์„ ์™„์ „ํžˆ ์ตํžˆ๊ณ , ์˜์กด์„ฑ ์ถ”์  ์›๋ฆฌ์™€ ์…€๋ ‰ํ„ฐ ํŒจํ„ด์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

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

  • read-only, write-only, read-write ์„ธ ๊ฐ€์ง€ ํŒŒ์ƒ atom ํŒจํ„ด์„ ๋งŒ๋“ค๊ณ  ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • get ์˜ ์˜์กด์„ฑ ์ถ”์ ์ด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํŒŒ์ƒ atom ์œผ๋กœ ๋ณต์žกํ•œ ์ƒํƒœ ์กฐํ•ฉ ๋กœ์ง์„ ์ปดํฌ๋„ŒํŠธ ๋ฐ–์œผ๋กœ ์˜ฎ๊ธธ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


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

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

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

[ํŒŒ์ƒ atom ์˜ ํ•„์š”์„ฑ] โ†’ [read-only (์…€๋ ‰ํ„ฐ)] โ†’ [write-only (์•ก์…˜)] โ†’ [read-write (์–‘๋ฐฉํ–ฅ)] โ†’ [์˜์กด์„ฑ ์ถ”์  ์›๋ฆฌ]

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

์˜์ˆ˜๋„ค ์•ฑ์— ํ•„ํ„ฐ + ๊ฒ€์ƒ‰ + ์ •๋ ฌ ๊ธฐ๋Šฅ์„ ํ•จ๊ป˜ ์“ฐ๋Š” ์Šคํ„ฐ๋”” ๋ชฉ๋ก ํŽ˜์ด์ง€๊ฐ€ ์ƒ๊ฒผ์–ด.

๐Ÿฃ ์˜์ฒ : (์ปดํฌ๋„ŒํŠธ ์•ˆ์— 50์ค„์งœ๋ฆฌ ๋กœ์ง์„ ์งœ๋ฉฐ) "ํ•„ํ„ฐ๋œ ์Šคํ„ฐ๋”” ๋ชฉ๋ก... ๊ฒ€์ƒ‰์–ด ์ ์šฉ... ์ •๋ ฌ... ์ด๊ฑธ ๋‹ค ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ ๊ณ„์‚ฐํ•˜๋ฉด ๋˜๊ฒ ๋‹ค!"

๐Ÿฆ ์˜ํ˜ธ ๋‹˜: "์˜์ฒ  ๋‹˜, ๊ทธ ๊ณ„์‚ฐ ๋กœ์ง ์ปดํฌ๋„ŒํŠธ์— ๋‹ค ๋„ฃ์œผ๋ฉด ๋‚˜์ค‘์— ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—์„œ ๊ฐ™์€ ๋กœ์ง ์“ธ ๋•Œ ์–ด๋–ป๊ฒŒ ํ•˜๋ ค๊ณ ์š”? ํŒŒ์ƒ atom ์œผ๋กœ ๋นผ๋‘๋ฉด ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ , ์–ด๋””์„œ๋‚˜ ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ์–ด์š”."

๐Ÿฃ ์˜์ฒ : "ํŒŒ์ƒ atom์ด ๋ญ”๊ฐ€์š”...?"


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

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

  • ์ปดํฌ๋„ŒํŠธ ์•ˆ์— ์ƒํƒœ ๊ณ„์‚ฐ ๋กœ์ง์„ ๋‘๋Š” ๊ฒŒ ์™œ ๋ฌธ์ œ์ธ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ํŒŒ์ƒ atom ์ด ์–ด๋–ป๊ฒŒ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š”์ง€ ์ดํ•ดํ•œ๋‹ค

์ปดํฌ๋„ŒํŠธ ์•ˆ์— ๋ชจ๋“  ๊ณ„์‚ฐ์„ ๋„ฃ์œผ๋ฉด?

// โŒ ๐Ÿฃ ์˜์ฒ ์˜ ์ฝ”๋“œ โ€” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋„ˆ๋ฌด ๋˜‘๋˜‘ํ•ด
const StudyListPage = () => {
  const studyList = useAtomValue(studyListAtom)
  const keyword = useAtomValue(searchKeywordAtom)
  const filter = useAtomValue(studyFiltersAtom)
 
  // ๊ณ„์‚ฐ ๋กœ์ง์ด ์ปดํฌ๋„ŒํŠธ ์•ˆ์— ์žˆ์Œ
  const filtered = studyList
    .filter((s) => s.category === filter.category || filter.category === 'all')
    .filter((s) => s.title.includes(keyword))
    .sort((a, b) => filter.sortBy === 'popular' ? b.likeCount - a.likeCount : 0)
 
  return <div>{filtered.map((s) => <StudyCard key={s.id} study={s} />)}</div>
}

๋ฌธ์ œ์ :

  1. ์žฌ์‚ฌ์šฉ ๋ถˆ๊ฐ€ โ€” ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—์„œ ๊ฐ™์€ ํ•„ํ„ฐ ๋กœ์ง์„ ์“ฐ๋ ค๋ฉด ๋ณต๋ถ™
  2. ํ…Œ์ŠคํŠธ ์–ด๋ ค์›€ โ€” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งˆ์šดํŠธํ•ด์•ผ๋งŒ ๋กœ์ง์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Œ
  3. ๋ถˆํ•„์š”ํ•œ ๋ณต์žก๋„ โ€” ์ปดํฌ๋„ŒํŠธ๊ฐ€ "ํ‘œ์‹œ" ์™€ "๊ณ„์‚ฐ" ๋‘ ๊ฐ€์ง€ ์ฑ…์ž„์„ ๊ฐ€์ง

ํŒŒ์ƒ atom ์œผ๋กœ ๊ฐœ์„ 

// โœ… ๐Ÿฆ ์˜ํ˜ธ์˜ ์ฝ”๋“œ โ€” ๊ณ„์‚ฐ ๋กœ์ง์„ ํŒŒ์ƒ atom ์œผ๋กœ ๋ถ„๋ฆฌ
const filteredStudyListAtom = atom((get) => {
  const studyList = get(studyListAtom)
  const keyword = get(searchKeywordAtom)
  const filter = get(studyFiltersAtom)
 
  return studyList
    .filter((s) => filter.category === 'all' || s.category === filter.category)
    .filter((s) => s.title.toLowerCase().includes(keyword.toLowerCase()))
    .sort((a, b) => filter.sortBy === 'popular' ? b.likeCount - a.likeCount : 0)
})
 
// ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹จ์ˆœํ•ด์ง
const StudyListPage = () => {
  const filtered = useAtomValue(filteredStudyListAtom) // ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋งŒ ๊ตฌ๋…
  return <div>{filtered.map((s) => <StudyCard key={s.id} study={s} />)}</div>
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
ํŒŒ์ƒ atom ์€ "์—‘์…€ ์ˆ˜์‹ ์…€" ์ด์•ผ. ์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ˆ˜์‹ ์…€๋„ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ๋ผ.
์ง์ ‘ ๊ฐ’์„ ์ €์žฅํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ๋‹ค๋ฅธ atom ์„ ์กฐํ•ฉํ•ด์„œ ์ƒˆ ๊ฐ’์„ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๊ฑฐ์•ผ.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ ๐ŸŸข

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?
๋งˆํŠธ์— ์‚ฌ๊ณผ(์›๋ณธ atom)๊ฐ€ ์žˆ์–ด. ํŒŒ์ƒ atom ์€ "์‚ฌ๊ณผ๋ฅผ ์ž˜๋ผ์„œ ์ค˜" ๋ผ๋Š” ๋ฐฉ๋ฒ• ์„ ๊ธฐ์–ตํ•ด๋‘๋Š” ๊ฑฐ์•ผ.
์‚ฌ๊ณผ๊ฐ€ ์ƒˆ ๊ฒƒ์œผ๋กœ ๋ฐ”๋€Œ๋ฉด ๋‹ค์Œ์— ๋‹ฌ๋ผ๊ณ  ํ•  ๋•Œ ์•Œ์•„์„œ ์ƒˆ ์‚ฌ๊ณผ๋กœ ์ž˜๋ผ์ค˜. ์กฐ๊ฐ์„ ๋ฏธ๋ฆฌ ์ €์žฅํ•ด๋‘๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์ž๋ฅด๋Š” ๋ฐฉ๋ฒ•์„ ๊ธฐ์–ตํ•ด๋‘๋Š” ๊ฑฐ์ง€.

์„ธ ๊ฐ€์ง€ ํŒŒ์ƒ ํŒจํ„ด ๋ฏธ๋ฆฌ ๋ณด๊ธฐ

// 1. Read-only โ€” ์ฝ๊ธฐ๋งŒ ๊ฐ€๋Šฅํ•œ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ
const fullNameAtom = atom((get) =>
  `${get(firstNameAtom)} ${get(lastNameAtom)}`
)
 
// 2. Write-only โ€” ์“ฐ๊ธฐ๋งŒ ๊ฐ€๋Šฅํ•œ ์•ก์…˜
const resetFiltersAtom = atom(null, (get, set) => {
  set(searchKeywordAtom, '')
  set(studyFiltersAtom, { category: 'all', tags: [], sortBy: 'latest' })
})
 
// 3. Read-Write โ€” ์ฝ๊ธฐ + ์“ฐ๊ธฐ ์–‘๋ฐฉํ–ฅ
const celsiusAtom = atom(
  (get) => (get(fahrenheitAtom) - 32) * 5 / 9,   // ์ฝ๊ธฐ: ํ™”์”จ โ†’ ์„ญ์”จ
  (get, set, celsius: number) => {                 // ์“ฐ๊ธฐ: ์„ญ์”จ โ†’ ํ™”์”จ
    set(fahrenheitAtom, celsius * 9 / 5 + 32)
  }
)

๐Ÿ“– Read-only ํŒŒ์ƒ atom โ€” ์…€๋ ‰ํ„ฐ ํŒจํ„ด ๐ŸŸข

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

  • read-only ํŒŒ์ƒ atom ์„ ๋งŒ๋“ค๊ณ , ์—ฌ๋Ÿฌ atom ์„ ์กฐํ•ฉํ•˜๋Š” ์…€๋ ‰ํ„ฐ ํŒจํ„ด์„ ์“ธ ์ˆ˜ ์žˆ๋‹ค

๊ธฐ๋ณธ ๊ตฌ์กฐ

// read ํ•จ์ˆ˜(์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜)๋งŒ ์žˆ์œผ๋ฉด read-only atom
const derivedAtom = atom((get) => {
  const value = get(anotherAtom) // ๋‹ค๋ฅธ atom ์˜ ๊ฐ’์„ ์ฝ๊ธฐ
  return value * 2               // ์ƒˆ๋กœ์šด ๊ฐ’ ๋ฐ˜ํ™˜
})

์‹ค๋ฌด ์˜ˆ์‹œ โ€” ์˜์ˆ˜๋„ค ์Šคํ„ฐ๋”” ๊ฒ€์ƒ‰/ํ•„ํ„ฐ

// atoms/study.ts
 
// ์›๋ณธ atom
export const allStudyListAtom = atom<Study[]>([])
export const searchKeywordAtom = atom('')
export const studyFiltersAtom = atom<StudyFilter>({
  category: 'all',
  tags: [],
  sortBy: 'latest',
})
 
// ํŒŒ์ƒ atom โ€” ํ•„ํ„ฐ + ๊ฒ€์ƒ‰ + ์ •๋ ฌ ์กฐํ•ฉ
export const filteredStudyListAtom = atom((get) => {
  const all = get(allStudyListAtom)
  const keyword = get(searchKeywordAtom)
  const { category, tags, sortBy } = get(studyFiltersAtom)
 
  return all
    .filter((s) => category === 'all' || s.category === category)
    .filter((s) => tags.length === 0 || tags.every((t) => s.tags.includes(t)))
    .filter((s) => s.title.toLowerCase().includes(keyword.toLowerCase()))
    .sort((a, b) => {
      if (sortBy === 'popular') return b.likeCount - a.likeCount
      return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
    })
})
 
// ํŒŒ์ƒ atom โ€” ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ฐœ์ˆ˜
export const studyCountByCategoryAtom = atom((get) => {
  const all = get(allStudyListAtom)
  return all.reduce<Record<string, number>>((acc, study) => {
    acc[study.category] = (acc[study.category] ?? 0) + 1
    return acc
  }, {})
})

์—ฌ๋Ÿฌ atom ์„ ์กฐํ•ฉํ•˜๋Š” ํŒจํ„ด

// ๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์ข‹์•„์š”ํ•œ ์Šคํ„ฐ๋””๋งŒ ํ•„ํ„ฐ๋ง
export const myLikedStudiesAtom = atom((get) => {
  const allStudies = get(allStudyListAtom)
  const user = get(userAtom)
  if (!user) return []
  return allStudies.filter((s) => user.likedStudyIds.includes(s.id))
})
 
// ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ โ€” ๋”ฑ ํ•œ ์ค„
const MyLikedStudies = () => {
  const myStudies = useAtomValue(myLikedStudiesAtom)
  return <StudyList studies={myStudies} />
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
Read-only ํŒŒ์ƒ atom ์˜ get ์€ "์–ด๋–ค atom ๋“ค์˜ ์กฐํ•ฉ์œผ๋กœ ์ด ๊ฐ’์ด ๋งŒ๋“ค์–ด์ง€๋Š”๊ฐ€" ๋ฅผ ์„ ์–ธํ•˜๋Š” ๊ฑฐ์•ผ.
์˜์กดํ•˜๋Š” atom ์ด ๋ณ€ํ•˜๋ฉด ํŒŒ์ƒ atom ๋„ ์ž๋™ ์žฌ๊ณ„์‚ฐ๋ผ.


โœ๏ธ Write-only ํŒŒ์ƒ atom โ€” ์•ก์…˜ ํŒจํ„ด ๐ŸŸก

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

  • write-only ํŒŒ์ƒ atom ์œผ๋กœ ์—ฌ๋Ÿฌ atom ์„ ํ•œ ๋ฒˆ์— ์ˆ˜์ •ํ•˜๋Š” ์•ก์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค

๊ธฐ๋ณธ ๊ตฌ์กฐ

// read ์ž๋ฆฌ์— null, write ํ•จ์ˆ˜๋งŒ ์žˆ์œผ๋ฉด write-only atom
const actionAtom = atom(
  null,                                    // read: null (๊ฐ’ ์—†์Œ)
  (get, set, payload: SomeType) => {       // write: ์‹ค์ œ ์•ก์…˜ ๋กœ์ง
    set(atomA, payload.a)
    set(atomB, (prev) => prev + payload.b)
  }
)

์‹ค๋ฌด ์˜ˆ์‹œ โ€” ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” ์•ก์…˜

// ๐Ÿฆ ์˜ํ˜ธ: "์—ฌ๋Ÿฌ atom ์„ ํ•œ ๋ฒˆ์— ์ดˆ๊ธฐํ™”ํ•ด์•ผ ํ•  ๋•Œ write-only atom ์ด ๋”ฑ์ด์—์š”"
export const resetAllFiltersAtom = atom(
  null,
  (_get, set) => {
    set(searchKeywordAtom, '')
    set(studyFiltersAtom, { category: 'all', tags: [], sortBy: 'latest' })
  }
)
 
// ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ
const FilterResetButton = () => {
  const resetFilters = useSetAtom(resetAllFiltersAtom)
  return <button onClick={() => resetFilters()}>ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”</button>
}

๋น„๋™๊ธฐ ์•ก์…˜ โ€” ์Šคํ„ฐ๋”” ์ข‹์•„์š” ํ† ๊ธ€

// API ํ˜ธ์ถœ + ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋‚˜์˜ ์•ก์…˜์œผ๋กœ
export const toggleLikeAtom = atom(
  null,
  async (_get, set, studyId: string) => {
    // ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ (API ์‘๋‹ต ์ „์— UI ๋จผ์ € ๋ณ€๊ฒฝ)
    set(allStudyListAtom, (prev) =>
      prev.map((s) =>
        s.id === studyId
          ? { ...s, likeCount: s.likeCount + 1, isLiked: true }
          : s
      )
    )
 
    try {
      await api.toggleLike(studyId)
    } catch {
      // ์‹คํŒจ ์‹œ ๋กค๋ฐฑ
      set(allStudyListAtom, (prev) =>
        prev.map((s) =>
          s.id === studyId
            ? { ...s, likeCount: s.likeCount - 1, isLiked: false }
            : s
        )
      )
    }
  }
)
 
const LikeButton = ({ studyId }: { studyId: string }) => {
  const toggleLike = useSetAtom(toggleLikeAtom)
  return (
    <button onClick={() => toggleLike(studyId)}>โค๏ธ</button>
  )
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
Write-only atom ์€ "Redux ์˜ ์•ก์…˜ + ๋ฆฌ๋“€์„œ๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์นœ ๊ฒƒ" ์ด์•ผ.
์—ฌ๋Ÿฌ atom ์„ ํ•œ ๋ฒˆ์— ์กฐ์ž‘ํ•ด์•ผ ํ•  ๋•Œ ๊น”๋”ํ•˜๊ฒŒ ์บก์Аํ™”ํ•ด์ค˜.


๐Ÿ”„ Read-Write ํŒŒ์ƒ atom โ€” ์–‘๋ฐฉํ–ฅ ๋ณ€ํ™˜ ๐ŸŸก

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

  • read-write ํŒŒ์ƒ atom ์œผ๋กœ ์–‘๋ฐฉํ–ฅ ๋ณ€ํ™˜ ๋กœ์ง์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค
  • ์ด ํŒจํ„ด์ด ์–ธ์ œ ์œ ์šฉํ•œ์ง€ ์‹ค๋ฌด ์ผ€์ด์Šค๋ฅผ ํ†ตํ•ด ์ดํ•ดํ•œ๋‹ค

๊ธฐ๋ณธ ๊ตฌ์กฐ

const derivedAtom = atom(
  (get) => transformRead(get(sourceAtom)),           // ์ฝ๊ธฐ: ๋ณ€ํ™˜ ํ›„ ๋ฐ˜ํ™˜
  (get, set, newValue: OutputType) => {              // ์“ฐ๊ธฐ: ์—ญ๋ณ€ํ™˜ ํ›„ ์›๋ณธ ์—…๋ฐ์ดํŠธ
    set(sourceAtom, transformWrite(newValue))
  }
)

์‹ค๋ฌด ์˜ˆ์‹œ 1 โ€” URL ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ๋™๊ธฐํ™”๋˜๋Š” ํ•„ํ„ฐ

// URL ์ฟผ๋ฆฌ์ŠคํŠธ๋ง โ†” ํ•„ํ„ฐ atom ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™”
export const urlSyncedCategoryAtom = atom(
  (get) => get(studyFiltersAtom).category,             // ์ฝ๊ธฐ: ํ•„ํ„ฐ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ถ”์ถœ
  (get, set, newCategory: string) => {                 // ์“ฐ๊ธฐ: ํ•„ํ„ฐ ์—…๋ฐ์ดํŠธ
    set(studyFiltersAtom, (prev) => ({
      ...prev,
      category: newCategory,
    }))
    // URL ๋„ ๋™์‹œ ์—…๋ฐ์ดํŠธ (Next.js router)
    const params = new URLSearchParams(window.location.search)
    params.set('category', newCategory)
    window.history.replaceState(null, '', `?${params.toString()}`)
  }
)
 
// ์‚ฌ์šฉ โ€” ๋งˆ์น˜ ์ผ๋ฐ˜ atom ์ฒ˜๋Ÿผ
const CategoryFilter = () => {
  const [category, setCategory] = useAtom(urlSyncedCategoryAtom)
  return (
    <select value={category} onChange={(e) => setCategory(e.target.value)}>
      <option value="all">์ „์ฒด</option>
      <option value="frontend">ํ”„๋ก ํŠธ์—”๋“œ</option>
      <option value="backend">๋ฐฑ์—”๋“œ</option>
    </select>
  )
}

์‹ค๋ฌด ์˜ˆ์‹œ 2 โ€” ๋‹ค์ค‘ ์„ ํƒ ์ฒดํฌ๋ฐ•์Šค

// ํŠน์ • ํƒœ๊ทธ๊ฐ€ ์„ ํƒ๋œ ์ƒํƒœ โ†” ์ „์ฒด ํƒœ๊ทธ ๋ชฉ๋ก
const makeTagSelectedAtom = (tag: string) =>
  atom(
    (get) => get(studyFiltersAtom).tags.includes(tag),  // ์ฝ๊ธฐ: ํฌํ•จ ์—ฌ๋ถ€
    (get, set, isSelected: boolean) => {                 // ์“ฐ๊ธฐ: ์ถ”๊ฐ€/์ œ๊ฑฐ
      set(studyFiltersAtom, (prev) => ({
        ...prev,
        tags: isSelected
          ? [...prev.tags, tag]
          : prev.tags.filter((t) => t !== tag),
      }))
    }
  )
 
// ์ฒดํฌ๋ฐ•์Šค ์ปดํฌ๋„ŒํŠธ
const TagCheckbox = ({ tag }: { tag: string }) => {
  const tagAtom = useMemo(() => makeTagSelectedAtom(tag), [tag])
  const [isSelected, setIsSelected] = useAtom(tagAtom)
  return (
    <label>
      <input
        type="checkbox"
        checked={isSelected}
        onChange={(e) => setIsSelected(e.target.checked)}
      />
      {tag}
    </label>
  )
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
Read-write ํŒŒ์ƒ atom ์€ "์–ด๋Œ‘ํ„ฐ(Adapter)" ์•ผ. ์›๋ณธ atom ์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ ,
๋‹ค๋ฅธ ํ˜•ํƒœ๋กœ ์ฝ๊ณ  ์“ธ ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด์ค˜.


๐Ÿงฌ ์˜์กด์„ฑ ์ถ”์  ์›๋ฆฌ ๐ŸŸก

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

  • get ์ด ์˜์กด์„ฑ์„ ์–ด๋–ป๊ฒŒ ์ถ”์ ํ•˜๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์กฐ๊ฑด๋ถ€ ์˜์กด์„ฑ์ด ์™œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•œ์ง€ ์ดํ•ดํ•œ๋‹ค

์˜์กด์„ฑ์€ ๋™์ ์œผ๋กœ ์ถ”์ ๋œ๋‹ค

// ์˜์กด์„ฑ์ด ๋งค ์‹คํ–‰๋งˆ๋‹ค ๋‹ฌ๋ผ์ง€๋Š” ๊ฒฝ์šฐ
const dynamicAtom = atom((get) => {
  const isLoggedIn = get(isLoggedInAtom)
 
  if (isLoggedIn) {
    // ๋กœ๊ทธ์ธ ์ƒํƒœ์ผ ๋•Œ๋งŒ userAtom ์„ ๊ตฌ๋…
    return get(userAtom)?.name ?? '๋กœ๊ทธ์ธ ์œ ์ €'
  }
 
  // ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ์ผ ๋•Œ๋Š” userAtom ์„ ๊ตฌ๋…ํ•˜์ง€ ์•Š์Œ
  return '๊ฒŒ์ŠคํŠธ'
})
 
// ๋™์ž‘:
// - isLoggedIn = false โ†’ userAtom ๋น„๊ตฌ๋…, isLoggedInAtom ๋งŒ ๊ตฌ๋…
// - isLoggedIn = true  โ†’ userAtom + isLoggedInAtom ๋‘˜ ๋‹ค ๊ตฌ๋…

์˜์กด์„ฑ ์ถ”์  ํ๋ฆ„

filteredStudyListAtom ์ด ๊ตฌ๋…๋จ
  โ†“
read ํ•จ์ˆ˜ ์‹คํ–‰
  โ†“
์‹คํ–‰ ์ค‘ get(allStudyListAtom), get(searchKeywordAtom), get(studyFiltersAtom) ํ˜ธ์ถœ
  โ†“
Jotai store: "filteredStudyListAtom ์€ ์„ธ atom ์— ์˜์กดํ•จ" ์œผ๋กœ ๋“ฑ๋ก
  โ†“
์„ธ atom ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋ณ€๊ฒฝ โ†’ filteredStudyListAtom ์žฌ๊ณ„์‚ฐ โ†’ ๊ตฌ๋… ์ปดํฌ๋„ŒํŠธ ๋ฆฌ๋ Œ๋”

โš ๏ธ ์ฃผ์˜: get ์„ ์“ฐ์ง€ ์•Š์œผ๋ฉด ์˜์กด์„ฑ ์—†์Œ

// โŒ ๋ฌธ์ œ: get ์—†์ด ์ง์ ‘ ๋ชจ๋“ˆ ๋ณ€์ˆ˜๋ฅผ ์ฐธ์กฐํ•˜๋ฉด ๋ฐ˜์‘์„ฑ ์—†์Œ
let externalValue = 0
const brokenAtom = atom(() => externalValue * 2)
// externalValue ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ brokenAtom ์€ ์žฌ๊ณ„์‚ฐ ์•ˆ ๋จ!
 
// โœ… ๋ฐ˜๋“œ์‹œ get ์œผ๋กœ ์ฝ์–ด์•ผ ์˜์กด์„ฑ ๋“ฑ๋ก
const correctAtom = atom((get) => get(sourceAtom) * 2)

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
ํŒŒ์ƒ atom ์˜ get ์œผ๋กœ ์ฝ์€ atom ๋งŒ ์˜์กด์„ฑ์ด ์ƒ๊ฒจ. get ์„ ์•ˆ ์“ฐ๋ฉด ๋ฐ˜์‘์„ฑ ์—†์–ด.


๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ


โŒ ํŒŒ์ƒ atom ์ด ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Œ

์ฆ์ƒ: ์›๋ณธ atom ์„ ์—…๋ฐ์ดํŠธํ–ˆ๋Š”๋ฐ ํŒŒ์ƒ atom ์ด ์˜›๋‚  ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•จ

์›์ธ: ํŒŒ์ƒ atom ์˜ read ํ•จ์ˆ˜์—์„œ get ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ ๊ฐ’์„ ์ฐธ์กฐ

// โŒ ์ž˜๋ชป๋œ ์˜ˆ
const baseAtom = atom(0)
let externalRef = 0
 
const derivedAtom = atom(() => externalRef * 2) // get ์—†์Œ โ†’ ์˜์กด์„ฑ ์—†์Œ!
 
// โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ
const derivedAtom = atom((get) => get(baseAtom) * 2) // get ์œผ๋กœ ์˜์กด์„ฑ ๋“ฑ๋ก

โŒ write-only atom ์—์„œ TypeScript ์˜ค๋ฅ˜: Argument of type 'null' is not assignable

์›์ธ: write-only atom ์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜(read) ์— null ์„ ์“ธ ๋•Œ ํƒ€์ž… ์˜ค๋ฅ˜

// โŒ ํƒ€์ž… ์˜ค๋ฅ˜
const actionAtom = atom(null, (get, set) => { ... })
 
// โœ… ํƒ€์ž… ๋ช…์‹œ
const actionAtom = atom<null, [payload: string], void>(
  null,
  (_get, set, payload) => {
    set(someAtom, payload)
  }
)

๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๐Ÿ“‹ ์„ธ ๊ฐ€์ง€ ํŒŒ์ƒ atom ํŒจํ„ด ์š”์•ฝ

ํŒจํ„ด๊ตฌ์กฐ์šฉ๋„
Read-onlyatom((get) => ...)๊ณ„์‚ฐ๋œ ๊ฐ’, ์…€๋ ‰ํ„ฐ, ํ•„ํ„ฐ/์ •๋ ฌ ๊ฒฐ๊ณผ
Write-onlyatom(null, (get, set, payload) => ...)๋ณต์ˆ˜ atom ๋™์‹œ ์—…๋ฐ์ดํŠธ, ๋น„๋™๊ธฐ ์•ก์…˜
Read-Writeatom((get) => ..., (get, set, val) => ...)์–‘๋ฐฉํ–ฅ ๋ณ€ํ™˜, URL ๋™๊ธฐํ™”, ์ฒดํฌ๋ฐ•์Šค

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

์ƒํ™ฉโŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ
ํŒŒ์ƒ atom ์—์„œ ์™ธ๋ถ€ ๋ณ€์ˆ˜ ์ฐธ์กฐatom(() => externalVar)atom((get) => get(sourceAtom))
์ปดํฌ๋„ŒํŠธ์— ๊ณ„์‚ฐ ๋กœ์ง ์ง‘์–ด๋„ฃ๊ธฐ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ filter/sortํŒŒ์ƒ atom ์œผ๋กœ ๋ถ„๋ฆฌ
write-only atom ์— read ๊ฐ’atom(someValue, ...)atom(null, ...)

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. ๐Ÿ› ๏ธ ์‹ค๋ฌด ๋”œ๋ ˆ๋งˆ (์˜์ฒ ์˜ ์„ ํƒ)

์˜์ˆ˜ ๋‹˜์ด ์š”์ฒญํ–ˆ์–ด: "์Šคํ„ฐ๋”” ๋ชฉ๋ก์—์„œ ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์œ ์ €๊ฐ€ ์‹ ์ฒญํ•œ ์Šคํ„ฐ๋””๋งŒ ํ•„ํ„ฐ๋งํ•ด์„œ ๋ณด์—ฌ์ค˜."
์–ด๋–ค ํŒŒ์ƒ atom ํŒจํ„ด์„ ์“ฐ๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ ์ ˆํ• ๊นŒ?

  • A) write-only atom โ€” atom(null, (get, set) => ...)
  • B) read-only atom โ€” atom((get) => get(studyListAtom).filter(...))
  • C) primitive atom ์— useEffect ๋กœ ๊ฐ’ ์„ธํŒ…
  • D) ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ useMemo ๋กœ ๊ณ„์‚ฐ

โœ… ์ •๋‹ต: B

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์›๋ฆฌ ์„ค๋ช…: ๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ์‹ ์ฒญํ•œ ์Šคํ„ฐ๋”” ๋ชฉ๋ก์€ "์›๋ณธ ๋ฐ์ดํ„ฐ์—์„œ ๊ณ„์‚ฐ๋œ ์ฝ๊ธฐ ์ „์šฉ ๊ฐ’" ์ด์•ผ. ์ฝ๊ธฐ๋งŒ ํ•„์š”ํ•˜๊ณ , ์ด ๊ฐ’ ์ž์ฒด๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•  ์ผ์ด ์—†์œผ๋‹ˆ read-only ํŒŒ์ƒ atom ์ด ๋”ฑ ๋งž์•„. studyListAtom ๊ณผ userAtom ๋‘˜ ๋‹ค ์˜์กดํ•˜๋ฉด ๋˜๊ณ , ์–ด๋А ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ž๋™์œผ๋กœ ์žฌ๊ณ„์‚ฐ๋ผ.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: C ์™€ D ๋Š” ์žฌ์‚ฌ์šฉ์ด ์–ด๋ ค์›Œ. ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•  ๋•Œ ์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ์ƒ๊ฒจ. A ๋Š” ์“ฐ๊ธฐ ์ „์šฉ์ด๋ผ ๊ฐ’์„ ์ฝ๋Š” ์šฉ๋„์—” ๋งž์ง€ ์•Š์•„.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "๊ณ„์‚ฐ๋œ ๊ฐ’ = read-only, ์—ฌ๋Ÿฌ atom ์ˆ˜์ • = write-only, ์–‘๋ฐฉํ–ฅ = read-write."

Q2. ๋นˆ์นธ ์ฑ„์šฐ๊ธฐ

์•„๋ž˜ write-only atom ์˜ ๋นˆ์นธ์„ ์ฑ„์›Œ๋ด. ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•˜๋Š” ์•ก์…˜์ด์•ผ:

const unlikeStudyAtom = atom(
  ______,                                          // (1)
  (_get, set, studyId: string) => {
    set(allStudyListAtom, (prev) =>
      prev.______(                                 // (2)
        (s) => s.id === studyId
          ? { ...s, likeCount: s.likeCount - 1, isLiked: false }
          : s
      )
    )
  }
)

โœ… ์ •๋‹ต: (1) null, (2) map

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • (1) โ€” write-only atom ์€ read ํ•จ์ˆ˜ ์ž๋ฆฌ์— null ์„ ๋„ฃ์–ด. ์ด atom ์˜ ๊ฐ’์„ ์ฝ์„ ์ผ์ด ์—†๋‹ค๋Š” ๋œป์ด์•ผ. useSetAtom ์œผ๋กœ๋งŒ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ผ.
  • (2) โ€” ๊ธฐ์กด ๋ฐฐ์—ด์„ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ์ƒˆ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋ฏ€๋กœ map ์„ ์จ. filter ๋Š” ํŠน์ • ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ•  ๋•Œ ์“ฐ๊ณ , map ์€ ํ•ญ๋ชฉ์„ ๋ณ€ํ™˜ํ•  ๋•Œ ์จ.

Q3. ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

read-only ํŒŒ์ƒ atom ๊ณผ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ useMemo ์˜ ์ฐจ์ด๋ฅผ ์„ค๋ช…ํ•ด๋ด.

์˜ˆ์‹œ ๋‹ต๋ณ€:

"๋‘˜ ๋‹ค ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ์บ์‹ฑํ•ด์„œ ์„ฑ๋Šฅ์„ ์˜ฌ๋ฆฌ๋Š” ๊ฑด ๋น„์Šทํ•œ๋ฐ, ํ•ต์‹ฌ ์ฐจ์ด๋Š” '๋ˆ„๊ฐ€ ์˜์กดํ•˜๋Š”๊ฐ€'์•ผ. useMemo ๋Š” ๊ทธ ์ปดํฌ๋„ŒํŠธ ํ•˜๋‚˜๋งŒ ์จ๋จน์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ํŒŒ์ƒ atom ์€ ์•ฑ ์–ด๋””์„œ๋“  useAtomValue ๋กœ ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ์–ด. ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์—์„œ ๊ฐ™์€ ํ•„ํ„ฐ ๋กœ์ง์ด ํ•„์š”ํ•˜๋ฉด ํŒŒ์ƒ atom ์ด ํ›จ์”ฌ ์œ ๋ฆฌํ•ด."

๐Ÿ’ก ์ง์ ‘ ์„ค๋ช…ํ•ด๋ดค๋‹ค๋ฉด: ํŒŒ์ƒ atom ์˜ ํ•ต์‹ฌ์„ ์ดํ•ดํ•œ ๊ฑฐ์•ผ! ๐ŸŽ‰


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜ ์ง„์งœ ํฐ ๊นจ๋‹ฌ์Œ์„ ์–ป์€ ๋‚ ์ด์—ˆ๋‹ค.

์ง€๊ธˆ๊นŒ์ง€ ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ filter, sort, reduce ๋ฅผ ๋‹ค ํ–ˆ๋Š”๋ฐ... ๊ทธ๊ฒŒ ์™œ ๋ฌธ์ œ์ธ์ง€ ๋ชฐ๋ž์–ด. ๊ทผ๋ฐ ์˜ํ˜ธ ๋‹˜์ด "์ด ๋กœ์ง ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—์„œ ์“ฐ๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•  ๊ฑฐ์˜ˆ์š”?" ๋ผ๊ณ  ๋ฌผ์–ด๋ณด๋Š” ์ˆœ๊ฐ„ ๋จธ๋ฆฌ๊ฐ€ ํ•˜์–˜์กŒ๋‹ค.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "์ปดํฌ๋„ŒํŠธ๋Š” ํ™”๋ฉด์— ๋ฌด์—‡์„ ๋ณด์—ฌ์ค„์ง€๋งŒ ์•Œ๋ฉด ๋ผ. ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ฐ€๊ณตํ•˜๋Š”์ง€๋Š” ํŒŒ์ƒ atom ์ด ์•Œ์•„์„œ ํ•ด."

write-only atom ์œผ๋กœ ์—ฌ๋Ÿฌ atom ์„ ํ•œ ๋ฒˆ์— ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ฒƒ๋„ ์ฒ˜์Œ ์•Œ์•˜๋‹ค. "Redux ์•ก์…˜์ด๋ž‘ ๋ฆฌ๋“€์„œ๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์นœ ๊ฒƒ" ์ด๋ผ๋Š” ์˜ํ˜ธ ๋‹˜ ์„ค๋ช…์ด ๋”ฑ ์™€๋‹ฟ์•˜์–ด.

์˜์กด์„ฑ ์ถ”์ ๋„ ์‹ ๊ธฐํ–ˆ๋Š”๋ฐ, get ์œผ๋กœ ์ฝ์€ ๊ฒƒ๋งŒ ์˜์กด์„ฑ์œผ๋กœ ์žกํžŒ๋‹ค๋Š” ๊ฑฐ... ๋‹น์—ฐํ•œ ๊ฒƒ ๊ฐ™์œผ๋ฉด์„œ๋„ ๋ชฐ๋ž๋˜ ๋‚ด์šฉ์ด๋‹ค. ์˜ค๋Š˜ ํ•˜๋ฃจ ๋ฟŒ๋“ฏํ•˜๋‹ค. ์ง‘ ๊ฐ€๋ฉด์„œ ๋‹ญ๊ผฌ์น˜ ํ•˜๋‚˜ ์‚ฌ๋จน๊ณ  ์‹ถ๋‹ค. ๐Ÿข


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