๐Ÿงฉ 12. TanStack Query + Jotai ์กฐํ•ฉ ์•„ํ‚คํ…์ฒ˜

๐Ÿ“‹ ๊ฐœ์š”

์„œ๋ฒ„ ์ƒํƒœ๋Š” TanStack Query, ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋Š” Jotai๋กœ ์—ญํ•  ๋ถ„๋ฆฌํ•˜๋Š” ์‹ค์ „ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋ฐฐ์›๋‹ˆ๋‹ค.

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

  • ์„œ๋ฒ„ ์ƒํƒœ์™€ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ์˜ ๋ณธ์งˆ์ ์ธ ์ฐจ์ด๋ฅผ ์„ค๋ช…ํ•˜๊ณ , ๊ฐ๊ฐ์— ๋งž๋Š” ๋„๊ตฌ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Jotai ๋กœ ํ•„ํ„ฐ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  TanStack Query ์— ์ฃผ์ž…ํ•˜๋Š” ์‹ค์ „ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • "์ด ์ƒํƒœ๋Š” Jotai ๋กœ ํ• ๊นŒ, Query ๋กœ ํ• ๊นŒ?" ๋ฅผ ๋น ๋ฅด๊ฒŒ ํŒ๋‹จํ•˜๋Š” ๊ธฐ์ค€์„ ๊ฐ–๋Š”๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


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

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

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: ์˜์ฒ ์ด์˜ ํ‘์—ญ์‚ฌ

์Šคํ”„๋ฆฐํŠธ 2์ฃผ์ฐจ. ์˜์ฒ ์ด๋Š” ์ž์‹ ๊ฐ์ด ๋„˜์ณค์–ด. Jotai ๋ฅผ ์™„์ „ํžˆ ์ดํ•ดํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ฑฐ๋“ :

  • ๐Ÿฃ ์˜์ฒ : "์˜ํ˜ธ ๋‹˜! ์Šคํ„ฐ๋”” ๋ชฉ๋ก ๋ฐ์ดํ„ฐ๋„ Jotai atom ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ์•ˆ ๋ผ์š”? TanStack Query ์—†์ด๋„ ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„์š”!"
  • ๐Ÿฆ ์˜ํ˜ธ ๋‹˜: (๋ฌดํ‘œ์ •์œผ๋กœ ์ž ์‹œ ๋ฐ”๋ผ๋ณด๋‹ค๊ฐ€) "ํ•ด๋ด์š”."
  • ๐Ÿฃ ์˜์ฒ : (3์ผ ํ›„) "์˜ํ˜ธ ๋‹˜... API ์‘๋‹ต ์บ์‹ฑ ์–ด๋–ป๊ฒŒ ํ•ด์š”? refetch, staleTime, ์—๋Ÿฌ ์žฌ์‹œ๋„, background refetch... ๋‹ค ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•ด์•ผ ๋ผ์š”?"
  • ๐Ÿฆ ์˜ํ˜ธ ๋‹˜: "๊ทธ๊ฒŒ ๋‹ค TanStack Query ๊ฐ€ ํ•ด์ฃผ๋Š” ๊ฑฐ์˜ˆ์š”. ์˜์ฒ  ๋‹˜์ด 3์ผ ๋™์•ˆ ๋ฐ”ํ€ด๋ฅผ ์žฌ๋ฐœ๋ช…ํ•˜๊ณ  ์žˆ์—ˆ์–ด์š”."
  • ๐Ÿฃ ์˜์ฒ : "...๋‹ค์‹œ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค."
  • ๐Ÿ‘” ์˜์ˆ˜ ๋‹˜: "๊ทธ 3์ผ์ด ์Šคํ”„๋ฆฐํŠธ์—์„œ..."
  • ๐Ÿฃ ์˜์ฒ : (์กฐ์šฉํžˆ) "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค."

๐Ÿค” ์™œ ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ™์ด ์“ฐ๋Š”๊ฐ€? ๐ŸŸข

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

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

๐Ÿค” ์ž ๊น, ๋จผ์ € ์ƒ๊ฐํ•ด๋ด
"์Šคํ„ฐ๋”” ๋ชฉ๋ก"๊ณผ "๊ฒ€์ƒ‰ ํ•„ํ„ฐ UI ์ƒํƒœ"์˜ ์ฐจ์ด๋Š” ๋ฌด์—‡์ผ๊นŒ?
์Šคํ„ฐ๋”” ๋ชฉ๋ก์€ ์–ด๋””์— ์‚ด๊ณ  ์žˆ๊ณ , ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋Š” ์–ด๋””์— ์‚ด๊ณ  ์žˆ์„๊นŒ?

์„œ๋ฒ„ ์ƒํƒœ (Server State):
  - ์„œ๋ฒ„ DB ์— ์‹ค์ œ๋กœ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ
  - ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Œ
  - ์ตœ์‹  ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ ค๋ฉด ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐ€์ ธ์™€์•ผ ํ•จ
  - ์บ์‹ฑ, ๋™๊ธฐํ™”, ์žฌ์‹œ๋„, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์—…๋ฐ์ดํŠธ ํ•„์š”
  - ์˜ˆ: ์Šคํ„ฐ๋”” ๋ชฉ๋ก, ๋Œ“๊ธ€, ์œ ์ € ํ”„๋กœํ•„, ์•Œ๋ฆผ ์ˆ˜

ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ (Client State):
  - ์˜ค์ง ์ด ๋ธŒ๋ผ์šฐ์ €/ํƒญ์—๋งŒ ์กด์žฌ
  - ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์™€ ๋ฌด๊ด€
  - ์„œ๋ฒ„์™€ ๋™๊ธฐํ™” ํ•„์š” ์—†์Œ
  - ์˜ˆ: ๊ฒ€์ƒ‰ ํ•„ํ„ฐ, ๋ชจ๋‹ฌ ์—ด๋ฆผ ์—ฌ๋ถ€, ์„ ํƒ๋œ ํƒญ, ์ž„์‹œ ํผ ์ž…๋ ฅ๊ฐ’
๋„๊ตฌ ์„ ํƒ:
  ์„œ๋ฒ„ ์ƒํƒœ โ†’ TanStack Query
    โœ… ์บ์‹ฑ (staleTime, gcTime)
    โœ… ์ž๋™ ์žฌ์‹œ๋„ (retry)
    โœ… ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŽ˜์น˜ (refetchOnWindowFocus)
    โœ… ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ (optimistic update)
    โœ… ์„œ๋ฒ„ ์ƒํƒœ ๋™๊ธฐํ™” (invalidateQueries)

  ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ โ†’ Jotai
    โœ… ์ตœ์†Œ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ (useState ์ฒ˜๋Ÿผ ๋‹จ์ˆœ)
    โœ… ์„ ํƒ์  ๋ฆฌ๋ Œ๋”๋ง (atom ๋‹จ์œ„ ๊ตฌ๋…)
    โœ… ํŒŒ์ƒ ์ƒํƒœ (derived atom)
    โœ… Provider ์Šค์ฝ”ํ”„ ๊ฒฉ๋ฆฌ

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
TanStack Query ๋Š” "์„œ๋ฒ„์˜ ์ฐฝ๊ณ  ๊ด€๋ฆฌ์ž". Jotai ๋Š” "๋‚ด ์ฑ…์ƒ ์œ„ ๋ฉ”๋ชจ์ง€". ์ฐฝ๊ณ  ๊ด€๋ฆฌ๋ฅผ ๋ฉ”๋ชจ์ง€์— ํ•˜๋ฉด ๋งํ•˜๊ณ , ์ฑ…์ƒ ๋ฉ”๋ชจ๋ฅผ ์ฐฝ๊ณ ์— ๋„ฃ์œผ๋ฉด ๋ณต์žกํ•ด์ ธ.


๐Ÿ“š ์—ญํ•  ๋ถ„๋ฆฌ์˜ ์›์น™ ๐ŸŸก

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

  • ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์—ญํ•  ๊ฒฝ๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ๊ทธ์„ ์ˆ˜ ์žˆ๋‹ค
  • ์—ญํ• ์ด ์„ž์ด๋ฉด ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค

TanStack Query ์˜ ์—ญํ•  (์„œ๋ฒ„ ์ƒํƒœ)

// โœ… TanStack Query ๊ฐ€ ๋‹ด๋‹นํ•˜๋Š” ๊ฒƒ๋“ค
const { data: studies, isLoading, refetch } = useQuery({
  queryKey: ['studies', filters],          // ์บ์‹œ ํ‚ค
  queryFn: () => fetchStudies(filters),    // ๋ฐ์ดํ„ฐ fetching
  staleTime: 1000 * 60 * 5,               // 5๋ถ„๊ฐ„ ์‹ ์„ ํ•œ ๋ฐ์ดํ„ฐ๋กœ ์ทจ๊ธ‰
  refetchOnWindowFocus: true,              // ํƒญ ํฌ์ปค์Šค ์‹œ ์ž๋™ ์žฌ์š”์ฒญ
  retry: 3,                               // ์‹คํŒจ ์‹œ 3๋ฒˆ ์žฌ์‹œ๋„
})
// โ†’ ์˜์ฒ ์ด๊ฐ€ 3์ผ ๋™์•ˆ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ ค ํ–ˆ๋˜ ๋ชจ๋“  ๊ฒƒ๋“ค

Jotai ์˜ ์—ญํ•  (ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ)

// โœ… Jotai ๊ฐ€ ๋‹ด๋‹นํ•˜๋Š” ๊ฒƒ๋“ค
const studyFiltersAtom = atom({
  category: 'all',
  sortBy: 'latest' as const,
  tags: [] as string[],
})
 
const isFilterDrawerOpenAtom = atom(false)
const selectedStudyIdAtom = atom<string | null>(null)
// โ†’ ์„œ๋ฒ„์™€ ๋ฌด๊ด€ํ•œ ์ˆœ์ˆ˜ UI ์ƒํƒœ๋“ค

๊ฒฝ๊ณ„๊ฐ€ ํ๋ฆฟํ•ด์ง€๋ฉด ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ

// โŒ Jotai ๋กœ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋ ค ํ–ˆ์„ ๋•Œ (์˜์ฒ ์ด์˜ ํ‘์—ญ์‚ฌ)
const studiesAtom = atom<Study[]>([])
const isLoadingAtom = atom(false)
const errorAtom = atom<Error | null>(null)
 
// fetchStudyListAtom โ€” ๋น„๋™๊ธฐ write atom
const fetchStudyListAtom = atom(null, async (get, set) => {
  set(isLoadingAtom, true)
  try {
    const data = await fetchStudies()
    set(studiesAtom, data)
  } catch (e) {
    set(errorAtom, e as Error)
  } finally {
    set(isLoadingAtom, false)
  }
})
 
// ๋ฌธ์ œ๋“ค:
// 1. ์บ์‹ฑ ์—†์Œ โ†’ ๋งค ๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ fetch
// 2. ์žฌ์‹œ๋„ ๋กœ์ง ์—†์Œ โ†’ ํ•œ ๋ฒˆ ์‹คํŒจํ•˜๋ฉด ์—๋Ÿฌ ์ƒํƒœ๋กœ ๊ณ ์ฐฉ
// 3. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฐฑ์‹  ์—†์Œ โ†’ ํƒญ ์ „ํ™˜ ํ›„ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ค๋ž˜๋œ ์ƒํƒœ
// 4. ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ํ•„์š”ํ•˜๋ฉด โ†’ ์ค‘๋ณต fetch
// 5. invalidation ๋กœ์ง ์—†์Œ โ†’ mutation ํ›„ ๋ชฉ๋ก์ด ์•ˆ ๊ฐฑ์‹ ๋จ
// ๐Ÿฃ ์˜์ฒ : "์ด๊ฑฐ ๋‹ค ์ œ๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ–ˆ๋˜ ๊ฑฐ์˜ˆ์š”..."

๐Ÿ”ง ํ•„ํ„ฐ UI ์ƒํƒœ๋ฅผ Jotai ๋กœ, Query ์— ์ฃผ์ž…ํ•˜๋Š” ํŒจํ„ด ๐ŸŸก

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

  • Jotai atom ์˜ ๊ฐ’์„ TanStack Query ์˜ queryKey ์™€ queryFn ์— ์ฃผ์ž…ํ•˜๋Š” ํŒจํ„ด์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ํ•„ํ„ฐ๊ฐ€ ๋ฐ”๋€” ๋•Œ ์ž๋™์œผ๋กœ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ๋ฆ„์„ ์ดํ•ดํ•œ๋‹ค
// atoms/studyFilters.ts โ€” ํ•„ํ„ฐ ์ƒํƒœ atom
import { atom } from 'jotai'
 
export interface StudyFilters {
  category: string
  sortBy: 'latest' | 'popular' | 'deadline'
  tags: string[]
  searchKeyword: string
}
 
export const studyFiltersAtom = atom<StudyFilters>({
  category: 'all',
  sortBy: 'latest',
  tags: [],
  searchKeyword: '',
})
// components/StudyListContainer.tsx
'use client'
 
import { useAtomValue } from 'jotai'
import { useQuery } from '@tanstack/react-query'
import { studyFiltersAtom } from '@/atoms/studyFilters'
 
async function fetchStudies(filters: StudyFilters): Promise<Study[]> {
  const params = new URLSearchParams({
    category: filters.category,
    sortBy: filters.sortBy,
    keyword: filters.searchKeyword,
    tags: filters.tags.join(','),
  })
  const res = await fetch(`/api/studies?${params}`)
  return res.json()
}
 
export function StudyListContainer() {
  // ๐Ÿฆ ์˜ํ˜ธ: "Jotai atom ์—์„œ ํ•„ํ„ฐ๋ฅผ ์ฝ์–ด์„œ Query ์— ๋„˜๊ฒจ์š”.
  //          ํ•„ํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด queryKey ๊ฐ€ ๋ฐ”๋€Œ๊ณ , TanStack Query ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์š”."
  const filters = useAtomValue(studyFiltersAtom)
 
  const { data: studies, isLoading, isError } = useQuery({
    // queryKey ์— filters ํฌํ•จ โ€” ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ƒˆ ์ฟผ๋ฆฌ ์‹คํ–‰
    queryKey: ['studies', filters],
    queryFn: () => fetchStudies(filters),
    staleTime: 1000 * 60 * 3, // 3๋ถ„
  })
 
  if (isLoading) return <StudyListSkeleton />
  if (isError) return <ErrorMessage />
 
  return <StudyList studies={studies ?? []} />
}
// components/StudyFilterBar.tsx
'use client'
 
import { useAtom } from 'jotai'
import { studyFiltersAtom } from '@/atoms/studyFilters'
 
export function StudyFilterBar() {
  // ๐Ÿฃ ์˜์ฒ : "ํ•„ํ„ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ atom ์„ ๋ฐ”๊พธ๋ฉด, StudyListContainer ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋„ค์š”!"
  const [filters, setFilters] = useAtom(studyFiltersAtom)
 
  return (
    <div>
      <select
        value={filters.category}
        onChange={(e) => setFilters((prev) => ({ ...prev, category: e.target.value }))}
      >
        <option value="all">์ „์ฒด</option>
        <option value="frontend">ํ”„๋ก ํŠธ์—”๋“œ</option>
        <option value="backend">๋ฐฑ์—”๋“œ</option>
        <option value="devops">DevOps</option>
      </select>
 
      <select
        value={filters.sortBy}
        onChange={(e) =>
          setFilters((prev) => ({
            ...prev,
            sortBy: e.target.value as StudyFilters['sortBy'],
          }))
        }
      >
        <option value="latest">์ตœ์‹ ์ˆœ</option>
        <option value="popular">์ธ๊ธฐ์ˆœ</option>
        <option value="deadline">๋งˆ๊ฐ ์ž„๋ฐ•์ˆœ</option>
      </select>
 
      <input
        value={filters.searchKeyword}
        onChange={(e) => setFilters((prev) => ({ ...prev, searchKeyword: e.target.value }))}
        placeholder="์Šคํ„ฐ๋”” ๊ฒ€์ƒ‰..."
      />
    </div>
  )
}

๋ฐ์ดํ„ฐ ํ๋ฆ„ ๋‹ค์ด์–ด๊ทธ๋žจ

์‚ฌ์šฉ์ž๊ฐ€ ํ•„ํ„ฐ ๋ณ€๊ฒฝ
  โ†“
studyFiltersAtom ์—…๋ฐ์ดํŠธ (Jotai)
  โ†“
StudyListContainer ๋ฆฌ๋ Œ๋” (atom ๊ตฌ๋…)
  โ†“
useQuery({ queryKey: ['studies', newFilters] }) ์‹คํ–‰
  โ†“
์บ์‹œ์— ['studies', newFilters] ์—†์œผ๋ฉด โ†’ fetch ์‹คํ–‰
์บ์‹œ์— ์žˆ์œผ๋ฉด โ†’ ์ฆ‰์‹œ ์บ์‹œ ๋ฐ˜ํ™˜ (staleTime ๋‚ด)
  โ†“
UI ์—…๋ฐ์ดํŠธ

โšก Mutation ํ›„ Query Invalidate ํŠธ๋ฆฌ๊ฑฐ ํŒจํ„ด ๐ŸŸก

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

  • useMutation ๊ณผ queryClient.invalidateQueries ๋ฅผ ์กฐํ•ฉํ•˜๋Š” ์‹ค์ „ ํŒจํ„ด์„ ์•ˆ๋‹ค
  • Jotai ์™€ TanStack Query ๊ฐ€ mutation ์—์„œ ์–ด๋–ป๊ฒŒ ํ˜‘๋ ฅํ•˜๋Š”์ง€ ์ดํ•ดํ•œ๋‹ค
// ์Šคํ„ฐ๋”” ์ข‹์•„์š” ๋ฎคํ…Œ์ด์…˜ โ€” TanStack Query + Jotai ํ˜‘์—…
'use client'
 
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { studyFiltersAtom } from '@/atoms/studyFilters'
 
async function likeStudy(studyId: string): Promise<void> {
  await fetch(`/api/studies/${studyId}/like`, { method: 'POST' })
}
 
export function StudyCard({ study }: { study: Study }) {
  const queryClient = useQueryClient()
  // ๐Ÿฆ ์˜ํ˜ธ: "ํ˜„์žฌ ํ•„ํ„ฐ๋ฅผ ์•Œ์•„์•ผ ์˜ฌ๋ฐ”๋ฅธ ์ฟผ๋ฆฌ๋ฅผ invalidate ํ•  ์ˆ˜ ์žˆ์–ด์š”"
  const filters = useAtomValue(studyFiltersAtom)
 
  const likeMutation = useMutation({
    mutationFn: likeStudy,
    // ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ โ€” ์„œ๋ฒ„ ์‘๋‹ต ์ „์— UI ๋จผ์ € ์—…๋ฐ์ดํŠธ
    onMutate: async (studyId) => {
      // ์ง„ํ–‰ ์ค‘์ธ ๋ฆฌํŽ˜์น˜ ์ทจ์†Œ (๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ์™€ ์ถฉ๋Œ ๋ฐฉ์ง€)
      await queryClient.cancelQueries({ queryKey: ['studies', filters] })
 
      // ํ˜„์žฌ ์บ์‹œ ์Šค๋ƒ…์ƒท ์ €์žฅ
      const previousStudies = queryClient.getQueryData<Study[]>(['studies', filters])
 
      // ๋‚™๊ด€์ ์œผ๋กœ UI ์—…๋ฐ์ดํŠธ
      queryClient.setQueryData<Study[]>(['studies', filters], (old) =>
        old?.map((s) =>
          s.id === studyId ? { ...s, likeCount: s.likeCount + 1 } : s
        )
      )
 
      return { previousStudies }
    },
    // ์—๋Ÿฌ ์‹œ ๋กค๋ฐฑ
    onError: (err, studyId, context) => {
      queryClient.setQueryData(['studies', filters], context?.previousStudies)
    },
    // ์„ฑ๊ณต ๋˜๋Š” ์‹คํŒจ ํ›„ ํ•ญ์ƒ ์„œ๋ฒ„์—์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ์žฌ์š”์ฒญ
    onSettled: () => {
      // ๐Ÿฃ ์˜์ฒ : "onSettled ์—์„œ invalidate ํ•˜๋ฉด ์„ฑ๊ณต์ด๋“  ์‹คํŒจ๋“  ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋„ค์š”!"
      queryClient.invalidateQueries({ queryKey: ['studies'] })
    },
  })
 
  return (
    <div>
      <h3>{study.title}</h3>
      <button
        onClick={() => likeMutation.mutate(study.id)}
        disabled={likeMutation.isPending}
      >
        {likeMutation.isPending ? '์ฒ˜๋ฆฌ ์ค‘...' : `โค๏ธ ${study.likeCount}`}
      </button>
    </div>
  )
}

์Šคํ„ฐ๋”” ์‹ ์ฒญ โ€” ๋ณต์žกํ•œ Mutation ํŒจํ„ด

// ์Šคํ„ฐ๋”” ์‹ ์ฒญ โ€” Jotai ๋กœ ํผ ์ƒํƒœ, Query ๋กœ mutation
'use client'
 
import { atom, useAtom } from 'jotai'
import { useMutation, useQueryClient } from '@tanstack/react-query'
 
// ์‹ ์ฒญ ํผ ์ƒํƒœ๋Š” Jotai (ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ)
const applicationFormAtom = atom({
  motivation: '',
  availableTime: '',
  experience: '',
})
 
export function StudyApplicationForm({ studyId }: { studyId: string }) {
  const [form, setForm] = useAtom(applicationFormAtom)
  const queryClient = useQueryClient()
 
  const applyMutation = useMutation({
    mutationFn: (formData: typeof form) =>
      fetch(`/api/studies/${studyId}/apply`, {
        method: 'POST',
        body: JSON.stringify(formData),
      }).then((r) => r.json()),
    onSuccess: () => {
      // ์‹ ์ฒญ ์„ฑ๊ณต โ†’ ๋‚ด ์‹ ์ฒญ ๋ชฉ๋ก ๊ฐฑ์‹  + ์Šคํ„ฐ๋”” ๋ฉค๋ฒ„ ์ˆ˜ ๊ฐฑ์‹ 
      queryClient.invalidateQueries({ queryKey: ['my-applications'] })
      queryClient.invalidateQueries({ queryKey: ['studies', studyId] })
      // ํผ ์ดˆ๊ธฐํ™”
      setForm({ motivation: '', availableTime: '', experience: '' })
    },
  })
 
  return (
    <form onSubmit={(e) => { e.preventDefault(); applyMutation.mutate(form) }}>
      <textarea
        value={form.motivation}
        onChange={(e) => setForm((prev) => ({ ...prev, motivation: e.target.value }))}
        placeholder="์ง€์› ๋™๊ธฐ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
      />
      <textarea
        value={form.availableTime}
        onChange={(e) => setForm((prev) => ({ ...prev, availableTime: e.target.value }))}
        placeholder="์ฐธ์—ฌ ๊ฐ€๋Šฅ ์‹œ๊ฐ„์„ ์ž…๋ ฅํ•˜์„ธ์š”"
      />
      <button type="submit" disabled={applyMutation.isPending}>
        {applyMutation.isPending ? '์‹ ์ฒญ ์ค‘...' : '์Šคํ„ฐ๋”” ์‹ ์ฒญํ•˜๊ธฐ'}
      </button>
      {applyMutation.isError && (
        <p className="error">์‹ ์ฒญ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.</p>
      )}
    </form>
  )
}

๐ŸŽฏ "Jotai ๋กœ ํ• ๊นŒ, Query ๋กœ ํ• ๊นŒ?" ํŒ๋‹จ ๊ธฐ์ค€ํ‘œ ๐ŸŸก

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

  • ์ƒˆ๋กœ์šด ์ƒํƒœ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ ์–ด๋–ค ๋„๊ตฌ๋กœ ๊ด€๋ฆฌํ• ์ง€ ์ฆ‰์‹œ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค
ํŒ๋‹จ ์งˆ๋ฌธYes โ†’No โ†’
์„œ๋ฒ„ DB ์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ์ธ๊ฐ€?TanStack Query๋‹ค์Œ ์งˆ๋ฌธ
๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€?TanStack Query๋‹ค์Œ ์งˆ๋ฌธ
์„œ๋ฒ„์™€ ๋™๊ธฐํ™”(์บ์‹ฑ, refetch)๊ฐ€ ํ•„์š”ํ•œ๊ฐ€?TanStack Query๋‹ค์Œ ์งˆ๋ฌธ
ํƒญ์„ ๋‹ซ์œผ๋ฉด ์‚ฌ๋ผ์ ธ๋„ ๋˜๋Š”๊ฐ€?Jotai๋‹ค์Œ ์งˆ๋ฌธ
์˜ค์ง ์ด ์‚ฌ์šฉ์ž์˜ ์ด ํƒญ์—๋งŒ ์กด์žฌํ•˜๋Š”๊ฐ€?Jotai๊ณ ๋ฏผ ํ•„์š”

๋„๋ฉ”์ธ๋ณ„ ๋ถ„๋ฅ˜ ์˜ˆ์‹œ

TanStack Query ๋กœ ๊ด€๋ฆฌ:
  โœ… ์Šคํ„ฐ๋”” ๋ชฉ๋ก (์„œ๋ฒ„ DB ๋ฐ์ดํ„ฐ)
  โœ… ์Šคํ„ฐ๋”” ์ƒ์„ธ (์„œ๋ฒ„ DB ๋ฐ์ดํ„ฐ)
  โœ… ์œ ์ € ํ”„๋กœํ•„ (์„œ๋ฒ„ DB ๋ฐ์ดํ„ฐ)
  โœ… ๋Œ“๊ธ€ ๋ชฉ๋ก (์„œ๋ฒ„ DB ๋ฐ์ดํ„ฐ)
  โœ… ์•Œ๋ฆผ ๋ชฉ๋ก (์„œ๋ฒ„ DB ๋ฐ์ดํ„ฐ)
  โœ… ๋‚ด ์‹ ์ฒญ ํ˜„ํ™ฉ (์„œ๋ฒ„ DB ๋ฐ์ดํ„ฐ)

Jotai ๋กœ ๊ด€๋ฆฌ:
  โœ… ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ƒํƒœ (UI ์ƒํƒœ)
  โœ… ๋ชจ๋‹ฌ ์—ด๋ฆผ/๋‹ซํž˜ (UI ์ƒํƒœ)
  โœ… ์„ ํƒ๋œ ์Šคํ„ฐ๋”” ID (UI ์ƒํƒœ)
  โœ… ์‚ฌ์ด๋“œ๋ฐ” ํŽผ์นจ/์ ‘ํž˜ (UI ์ƒํƒœ)
  โœ… ํผ ์ž…๋ ฅ ์ค‘์ธ ํ…์ŠคํŠธ (UI ์ƒํƒœ)
  โœ… ๋‹คํฌ๋ชจ๋“œ ์„ค์ • (UI ์ƒํƒœ, + atomWithStorage)
  โœ… ํ˜„์žฌ ๋กœ๊ทธ์ธ ์œ ์ € ์„ธ์…˜ (auth ์ƒํƒœ โ€” ์˜ˆ์™ธ์  ์ผ€์ด์Šค)

๐Ÿ’ก ํ—ท๊ฐˆ๋ฆฌ๋Š” ์ผ€์ด์Šค: ๋กœ๊ทธ์ธ ์œ ์ € ์ •๋ณด

  • ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•˜์ง€๋งŒ, ์„ธ์…˜ ๋™์•ˆ ์ž์ฃผ ๋ฐ”๋€Œ์ง€ ์•Š์•„
  • TanStack Query ๋กœ fetch ํ•˜๊ณ  (useQuery(['me'], fetchMe))
  • Jotai ๋กœ ์บ์‹ฑ ์—†์ด ์•ฑ ์ „์—ญ์—์„œ ์ ‘๊ทผํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ฐฉ๋ฒ•๋„ ์žˆ์–ด
  • ํŒ€๋งˆ๋‹ค ์ทจํ–ฅ ์ฐจ์ด๊ฐ€ ์žˆ์Œ โ€” ์ค‘์š”ํ•œ ๊ฑด ์ผ๊ด€์„ฑ ์ด์•ผ

๐Ÿ“ฆ Before/After โ€” ๋ชจ๋“  ์ƒํƒœ๋ฅผ Jotai ๋กœ โ†’ ์—ญํ•  ๋ถ„๋ฆฌ ๐ŸŸก

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

  • ์—ญํ• ์ด ์„ž์ธ ์ฝ”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฆฌํŒฉํ† ๋ง ๊ณผ์ •์„ ์ดํ•ดํ•œ๋‹ค
// โŒ Before โ€” ์˜์ฒ ์ด์˜ ํ‘์—ญ์‚ฌ ์ฝ”๋“œ (๋ชจ๋“  ๊ฑธ Jotai ๋กœ)
 
// ๐Ÿฃ ์˜์ฒ : "๋‹ค atom ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ๋˜๊ฒ ์ง€!"
const studiesAtom = atom<Study[]>([])         // ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ
const isLoadingAtom = atom(false)             // ๋กœ๋”ฉ ์ƒํƒœ
const errorAtom = atom<Error | null>(null)    // ์—๋Ÿฌ ์ƒํƒœ
const filtersAtom = atom({ category: 'all' }) // ํ•„ํ„ฐ (์ด๊ฑด ๋งž๋Š”๋ฐ ์œ„์˜ ๊ฒƒ๋“ค๊ณผ ์„ž์ž„)
const lastFetchedAtom = atom<Date | null>(null) // ์ˆ˜๋™ ์บ์‹ฑ ์‹œ๋„
 
// ์ง์ ‘ ๊ตฌํ˜„ํ•œ fetch ๋กœ์ง (TanStack Query ๊ฐ€ ํ•ด์ฃผ๋Š” ๊ฑธ ์ง์ ‘ ํ•˜๋Š” ์ค‘)
const fetchStudiesAtom = atom(null, async (get, set) => {
  const lastFetched = get(lastFetchedAtom)
  const now = new Date()
 
  // ์ง์ ‘ ๊ตฌํ˜„ํ•œ staleTime ์ฒดํฌ (5๋ถ„)
  if (lastFetched && now.getTime() - lastFetched.getTime() < 5 * 60 * 1000) {
    return // ์บ์‹œ ์‚ฌ์šฉ
  }
 
  set(isLoadingAtom, true)
  set(errorAtom, null)
  try {
    const filters = get(filtersAtom)
    const data = await fetchStudiesAPI(filters)
    set(studiesAtom, data)
    set(lastFetchedAtom, now)
  } catch (e) {
    set(errorAtom, e as Error)
    // ์žฌ์‹œ๋„ ๋กœ์ง? ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•จ...
  } finally {
    set(isLoadingAtom, false)
  }
})
// ๋ฐฑ๊ทธ๋ผ์šด๋“œ refetch ์—†์Œ
// ํƒญ ํฌ์ปค์Šค ์‹œ refetch ์—†์Œ
// ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ์“ธ ๋•Œ ์ค‘๋ณต fetch ์œ„ํ—˜
// ๋“ฑ๋“ฑ... TanStack Query ๊ฐ€ ํ•ด๊ฒฐํ•˜๋Š” ๋ฌธ์ œ๋“ค์ด ์‚ฐ๋”๋ฏธ
// โœ… After โ€” ์—ญํ•  ๋ถ„๋ฆฌ ํ›„ ๊น”๋”ํ•œ ์ฝ”๋“œ
 
// Jotai: ์ˆœ์ˆ˜ ํด๋ผ์ด์–ธํŠธ UI ์ƒํƒœ๋งŒ
const studyFiltersAtom = atom({ category: 'all', sortBy: 'latest' as const })
const selectedStudyIdAtom = atom<string | null>(null)
const isFilterDrawerOpenAtom = atom(false)
 
// TanStack Query: ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ
function useStudies() {
  const filters = useAtomValue(studyFiltersAtom) // Jotai ์—์„œ ํ•„ํ„ฐ ์ฝ๊ธฐ
 
  return useQuery({
    queryKey: ['studies', filters],           // ํ•„ํ„ฐ๊ฐ€ queryKey ์˜ ์ผ๋ถ€
    queryFn: () => fetchStudiesAPI(filters),
    staleTime: 1000 * 60 * 5,               // 5๋ถ„ stale
    retry: 3,                               // ์‹คํŒจ ์‹œ 3๋ฒˆ ์žฌ์‹œ๋„
    refetchOnWindowFocus: true,             // ํƒญ ํฌ์ปค์Šค ์‹œ ์ž๋™ ๊ฐฑ์‹ 
    // โ†’ ์œ„ 3๊ฐ€์ง€๊ฐ€ ์˜์ฒ ์ด๊ฐ€ 3์ผ ๋™์•ˆ ์ง์ ‘ ์งœ๋ ค ํ–ˆ๋˜ ๊ฒƒ๋“ค
  })
}
 
// ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ง
export function StudyListContainer() {
  const { data: studies, isLoading } = useStudies()
 
  if (isLoading) return <Skeleton />
  return <StudyList studies={studies ?? []} />
}

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

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด. ๋Œ€๋ถ€๋ถ„ ์—ฌ๊ธฐ ์žˆ์–ด.


โŒ ํ•„ํ„ฐ ๋ฐ”๊ฟ”๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์•ˆ ๋ฐ”๋€œ

์›์ธ: queryKey ์— filters ๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์•„์„œ TanStack Query ๊ฐ€ ๊ฐ™์€ ์บ์‹œ๋ฅผ ๋ฐ˜ํ™˜

ํ•ด๊ฒฐ์ฑ…:

const { data } = useQuery({
  queryKey: ['studies', filters], // โœ… filters ๋ฅผ ๋ฐ˜๋“œ์‹œ ํฌํ•จ
  queryFn: () => fetchStudies(filters),
})

โŒ Mutation ํ›„ ๋ชฉ๋ก์ด ๊ฐฑ์‹ ๋˜์ง€ ์•Š์Œ

์›์ธ: onSuccess / onSettled ์—์„œ invalidateQueries ๋ฅผ ๋น ๋œจ๋ฆผ

ํ•ด๊ฒฐ์ฑ…:

const mutation = useMutation({
  mutationFn: submitStudy,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['studies'] }) // โœ… invalidate
  },
})

โŒ useQueryClient ๊ฐ€ undefined

์›์ธ: QueryClientProvider ๊ฐ€ ์—†๊ฑฐ๋‚˜, ์ปดํฌ๋„ŒํŠธ๊ฐ€ Provider ๋ฐ–์—์„œ ๋ Œ๋”๋จ

ํ•ด๊ฒฐ์ฑ…:

// app/providers.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
 
const queryClient = new QueryClient()
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <JotaiProvider>
        {children}
      </JotaiProvider>
    </QueryClientProvider>
  )
}

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

๐Ÿ“‹ ์—ญํ•  ๋ถ„๋ฆฌ ์›์น™ ์š”์•ฝ

์ƒํƒœ ์œ ํ˜•๋„๊ตฌํ•ต์‹ฌ ์ด์œ 
์„œ๋ฒ„ DB ๋ฐ์ดํ„ฐTanStack Query์บ์‹ฑ, refetch, retry, ๋™๊ธฐํ™” ์ž๋™ ์ œ๊ณต
UI ์ƒํƒœ (ํ•„ํ„ฐ, ๋ชจ๋‹ฌ ๋“ฑ)Jotai๋‹จ์ˆœํ•˜๊ณ  ์„ ํƒ์  ๋ฆฌ๋ Œ๋”
ํผ ์ž…๋ ฅ ์ƒํƒœJotai + atom๊ฐ€๋ณ๊ณ  ์ฆ‰๊ฐ์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ
๋น„๋™๊ธฐ ์„œ๋ฒ„ ์ž‘์—…TanStack Query (useMutation)๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ๋กค๋ฐฑ

โš ๏ธ ํ”ํ•œ ์‹ค์ˆ˜

์ƒํ™ฉโŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ
์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ atom ์œผ๋กœ์บ์‹ฑ/์žฌ์‹œ๋„ ์ง์ ‘ ๊ตฌํ˜„TanStack Query ์‚ฌ์šฉ
Query ๋กœ UI ์ƒํƒœ ๊ด€๋ฆฌuseQuery ๋กœ boolean ์ƒํƒœ ๊ด€๋ฆฌJotai atom ์‚ฌ์šฉ
ํ•„ํ„ฐ์™€ ์ฟผ๋ฆฌ ์—ฐ๊ฒฐํ•„ํ„ฐ๋ฅผ queryKey ์—์„œ ์ œ์™ธqueryKey: ['studies', filters]
Mutation ํ›„ ๊ฐฑ์‹  ์—†์ŒinvalidateQueries ์ƒ๋žตonSettled ์—์„œ ํ•ญ์ƒ invalidate

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

Q1. ๐Ÿ’ผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ (ํŒ€ ์ „์ฒด ํšŒ์˜)

์˜์ˆ˜ ๋‹˜์ด ํšŒ์˜์—์„œ ๋ฌผ์—ˆ์–ด: "๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ ์ƒํƒœ๋Š” ์–ด๋””์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด?"
ํŒ€์›๋“ค์˜ ์ฃผ์žฅ์ด ๊ฐˆ๋ ธ์–ด. ๊ฐ€์žฅ ์˜ฌ๋ฐ”๋ฅธ ํŒ๋‹จ์€?

  • A) ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋Š” ์„œ๋ฒ„์™€ ๋™๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•˜๋‹ˆ TanStack Query ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค
  • B) ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋Š” ์ˆœ์ˆ˜ UI ์ƒํƒœ(์ด ํƒญ์—์„œ๋งŒ ์กด์žฌ, ์„œ๋ฒ„ ์ €์žฅ ์•ˆ ๋จ)์ด๋ฏ€๋กœ Jotai atom ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , queryKey ์— ํฌํ•จ์‹œ์ผœ์„œ TanStack Query ๊ฐ€ ํ‚ค์›Œ๋“œ ๋ณ€๊ฒฝ์— ๋ฐ˜์‘ํ•˜๊ฒŒ ํ•œ๋‹ค
  • C) ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋Š” useState ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค
  • D) ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋Š” URL ์˜ query string ์œผ๋กœ๋งŒ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค

โœ… ์ •๋‹ต: B

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

  • ์›๋ฆฌ ์„ค๋ช…: ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋Š” ํด๋ผ์ด์–ธํŠธ UI ์ƒํƒœ์•ผ โ€” ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒ€์ƒ‰์ฐฝ์— ์ž…๋ ฅํ•˜๋Š” ๊ฐ’์ด๊ณ , ์„œ๋ฒ„ DB ์— ์ €์žฅ๋˜์ง€ ์•Š์•„. ๊ทธ๋ž˜์„œ Jotai atom ์ด ์ ํ•ฉํ•ด. ๋™์‹œ์— ์ด ํ‚ค์›Œ๋“œ๋ฅผ TanStack Query ์˜ queryKey ์— ํฌํ•จ์‹œํ‚ค๋ฉด, ํ‚ค์›Œ๋“œ๊ฐ€ ๋ฐ”๋€” ๋•Œ Query ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒˆ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์™€. ์ด๊ฒŒ ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ˜‘๋ ฅํ•˜๋Š” ํ•ต์‹ฌ ํŒจํ„ด์ด์•ผ.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: A ๋Š” ํ‚ค์›Œ๋“œ ์ž์ฒด๊ฐ€ ์„œ๋ฒ„ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ์•ผ. C ๋Š” ๊ฐ€๋Šฅํ•˜์ง€๋งŒ atom ์„ ์“ฐ๋ฉด ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ณต์œ ๊ฐ€ ์‰ฌ์›Œ. D ๋Š” URL ๋™๊ธฐํ™”๋Š” ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์ด์ง€ ๋Œ€์ฒด์ œ๊ฐ€ ์•„๋‹ˆ์•ผ.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "Jotai atom ์—์„œ ํ•„ํ„ฐ ์ฝ๊ธฐ โ†’ queryKey ์— ํฌํ•จ โ†’ Query ๊ฐ€ ์ž๋™ ๋ฐ˜์‘"

Q2. โšก ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ

์˜์ฒ ์ด๊ฐ€ "์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅด๋ฉด ์„œ๋ฒ„ ์‘๋‹ต์ด ๋А๋ ค์„œ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋œ ์ฑ„ ๋„ˆ๋ฌด ์˜ค๋ž˜ ์žˆ์–ด์š”" ๋ผ๊ณ  ๋ถˆํ‰ํ–ˆ์–ด.
์˜ํ˜ธ ๋‹˜์ด ์ œ์•ˆํ•œ ํ•ด๊ฒฐ์ฑ…์œผ๋กœ ๊ฐ€์žฅ ์ ์ ˆํ•œ ๊ฒƒ์€?

  • A) ์„œ๋ฒ„ ์‘๋‹ต ์†๋„๋ฅผ ๋†’์ด๋„๋ก ๋ฐฑ์—”๋“œ ํŒ€์— ์š”์ฒญํ•œ๋‹ค
  • B) useMutation ์˜ onMutate ์—์„œ ๋‚™๊ด€์ ์œผ๋กœ UI ๋ฅผ ๋จผ์ € ์—…๋ฐ์ดํŠธํ•˜๊ณ , onError ์—์„œ ๋กค๋ฐฑํ•˜๊ณ , onSettled ์—์„œ invalidate ํ•œ๋‹ค
  • C) ์ข‹์•„์š” ๋ฒ„ํŠผ์˜ disabled ๋ฅผ ์ œ๊ฑฐํ•ด์„œ ์—ฌ๋Ÿฌ ๋ฒˆ ํด๋ฆญ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค
  • D) ์ข‹์•„์š” ๊ธฐ๋Šฅ์„ Jotai atom ์œผ๋กœ๋งŒ ๊ด€๋ฆฌํ•˜๊ณ  ์„œ๋ฒ„ ๋™๊ธฐํ™”๋ฅผ ํฌ๊ธฐํ•œ๋‹ค

โœ… ์ •๋‹ต: B

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

  • ์›๋ฆฌ ์„ค๋ช…: ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ(Optimistic Update)๋Š” ์„œ๋ฒ„ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์ „์— UI ๋ฅผ ๋จผ์ € ์—…๋ฐ์ดํŠธํ•˜๋Š” ํŒจํ„ด์ด์•ผ. onMutate ์—์„œ ์บ์‹œ๋ฅผ ๋‚™๊ด€์ ์œผ๋กœ ์ˆ˜์ • โ†’ ์‚ฌ์šฉ์ž๊ฐ€ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ โ†’ onError ์—์„œ ์‹คํŒจ ์‹œ ์ด์ „ ์บ์‹œ๋กœ ๋กค๋ฐฑ โ†’ onSettled ์—์„œ ํ•ญ์ƒ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋กœ ๋™๊ธฐํ™”. ์ด ํŒจํ„ด์ด TanStack Query ์˜ ๊ฐ•์ ์ด์•ผ.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: A ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•œ UX ๋ฌธ์ œ๋ฅผ ๋ฐฑ์—”๋“œ์— ์ „๊ฐ€ํ•˜๋Š” ๊ฑฐ์•ผ. C ๋Š” ์ค‘๋ณต ์š”์ฒญ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ. D ๋Š” ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ํฌ๊ธฐํ•˜๋Š” ๊ฑฐ์•ผ.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ 3๋‹จ๊ณ„: onMutate(๋จผ์ € ๋ฐ”๊พธ๊ธฐ) โ†’ onError(์‹คํŒจ๋ฉด ๋˜๋Œ๋ฆฌ๊ธฐ) โ†’ onSettled(์„œ๋ฒ„์—์„œ ํ™•์ธ)"

Q3. ๐Ÿง  ์„ค๊ณ„ ํŒ๋‹จ

์˜์ฒ ์ด๊ฐ€ "์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์ˆ˜(์˜ˆ: ํ—ค๋”์˜ ๋นจ๊ฐ„ ๋ฑƒ์ง€)"๋ฅผ ์–ด๋””์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š”์ง€ ๋ฌผ์—ˆ์–ด.
์˜ํ˜ธ ๋‹˜์€ ์–ด๋–ค ๋„๊ตฌ๋ฅผ ์ถ”์ฒœํ–ˆ์„๊นŒ? ์ด์œ ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ•˜๋Š” ์˜ˆ์‹œ ๋‹ต๋ณ€์„ ์จ๋ด.

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

"์•Œ๋ฆผ ์ˆ˜๋Š” ์„œ๋ฒ„ DB ์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๊ณ , ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž(ํ˜น์€ ๋‹ค๋ฅธ ํƒญ)์—์„œ ์•Œ๋ฆผ์ด ์˜ค๋ฉด ๊ฐ’์ด ๋ฐ”๋€” ์ˆ˜ ์žˆ์–ด. ๊ทธ๋ž˜์„œ TanStack Query ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ๋งž์•„. useQuery(['notifications', 'count'], fetchNotificationCount, { refetchInterval: 30000 }) ์ฒ˜๋Ÿผ ํด๋ง์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜, WebSocket ์œผ๋กœ ์•Œ๋ฆผ์ด ์˜ค๋ฉด invalidateQueries(['notifications']) ๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์ตœ์‹  ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด. ํ—ค๋” ๋ฑƒ์ง€ UI ๋Š” ๋ณ„๋„ atom ์œผ๋กœ ๊ด€๋ฆฌํ•  ํ•„์š” ์—†์ด, useQuery ์˜ data ๋ฅผ ์ง์ ‘ ์จ๋„ ์ถฉ๋ถ„ํ•ด."

๐Ÿ’ก ํ•ต์‹ฌ: ๋ฐ์ดํ„ฐ์˜ "์ถœ์ฒ˜"์™€ "๋ณ€๊ฒฝ ๊ฐ€๋Šฅ ์ฃผ์ฒด"๋ฅผ ๋จผ์ € ํŒŒ์•…ํ•˜๋ฉด ๋„๊ตฌ ์„ ํƒ์ด ์‰ฌ์›Œ์ ธ.


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

3์ผ ๋™์•ˆ TanStack Query ๋ฅผ ์žฌ๋ฐœ๋ช…ํ•˜๋‹ค๊ฐ€ ์‹คํŒจํ•œ ํ‘์—ญ์‚ฌ๊ฐ€ ์˜ค๋Š˜ ๋“œ๋””์–ด ์ฒญ์‚ฐ๋๋‹ค.

์ฒ˜์Œ์—๋Š” "Jotai ๋กœ ๋‹ค ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ์™œ Query ๋ฅผ ๊ฐ™์ด ์จ?" ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์–ด. ๊ทผ๋ฐ staleTime, retry, background refetch, ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ... ์ด๊ฑฐ ํ•˜๋‚˜ํ•˜๋‚˜๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ์—„์ฒญ๋‚œ ์‹œ๊ฐ„์ด ๋“œ๋Š” ๊ฑฐ์•ผ. ์˜ํ˜ธ ๋‹˜์ด "ํ•ด๋ด์š”" ๋ผ๊ณ  ํ–ˆ์„ ๋•Œ ๋ฌด์Šจ ๋œป์ธ์ง€ ์ด์ œ ์•Œ๊ฒ ์–ด. ์ง์ ‘ ๊ฒช์–ด๋ด์•ผ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ณ ํ†ต์ด์—ˆ์–ด.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "๋ฐ”ํ€ด๋ฅผ ์žฌ๋ฐœ๋ช…ํ•˜์ง€ ๋งˆ๋ผ. ์„œ๋ฒ„ ์ƒํƒœ๋Š” TanStack Query ๊ฐ€ ์ด๋ฏธ ์™„๋ฒฝํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋‘” ๋ฐ”ํ€ด๊ฐ€ ์žˆ๋‹ค. Jotai ๋Š” ๋‚ด ์ฑ…์ƒ ์œ„ ๋ฉ”๋ชจ์ง€ ์—ญํ• ์— ์ง‘์ค‘ํ•˜๋ฉด ๋œ๋‹ค."

ํ•„ํ„ฐ๋ฅผ atom ์œผ๋กœ ๋‘๊ณ  queryKey ์— ๋„ฃ๋Š” ํŒจํ„ด์ด ์ง„์งœ ์šฐ์•„ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์–ด. ํ•„ํ„ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ atom ๋งŒ ๋ฐ”๊พธ๋ฉด, Query ๊ฐ€ ์•Œ์•„์„œ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋“ . ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ˜‘๋ ฅํ•˜๋Š” ๋А๋‚Œ.

์˜ค๋Š˜ ๋ถ„์‹์ง‘ ๊ฐ€๊ณ  ์‹ถ๋‹ค. ๋–ก๋ณถ์ด ๋จน์œผ๋ฉด์„œ ์˜ค๋Š˜ ๋ฐฐ์šด ์—ญํ•  ๋ถ„๋ฆฌ ์›์น™ ๋‹ค์‹œ ๊ณฑ์”น์–ด๋ด์•ผ์ง€. "์„œ๋ฒ„ ์ƒํƒœ๋Š” Query, ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋Š” Jotai" โ€” ์ด๊ฑฐ ๋ฌธ์‹ ์ด๋ผ๋„ ์ƒˆ๊ฒจ์•ผ ํ•˜๋‚˜.


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