๐งฉ 12. TanStack Query + Jotai ์กฐํฉ ์ํคํ ์ฒ
๐ ๊ฐ์
์๋ฒ ์ํ๋ TanStack Query, ํด๋ผ์ด์ธํธ ์ํ๋ Jotai๋ก ์ญํ ๋ถ๋ฆฌํ๋ ์ค์ ์ํคํ ์ฒ๋ฅผ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์๋ฒ ์ํ์ ํด๋ผ์ด์ธํธ ์ํ์ ๋ณธ์ง์ ์ธ ์ฐจ์ด๋ฅผ ์ค๋ช ํ๊ณ , ๊ฐ๊ฐ์ ๋ง๋ ๋๊ตฌ๋ฅผ ์ ํํ ์ ์๋ค.
- Jotai ๋ก ํํฐ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ TanStack Query ์ ์ฃผ์ ํ๋ ์ค์ ํจํด์ ๊ตฌํํ ์ ์๋ค.
- "์ด ์ํ๋ Jotai ๋ก ํ ๊น, Query ๋ก ํ ๊น?" ๋ฅผ ๋น ๋ฅด๊ฒ ํ๋จํ๋ ๊ธฐ์ค์ ๊ฐ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ด ์ฐ๋๊ฐ?
- ๐ ์ญํ ๋ถ๋ฆฌ์ ์์น
- ๐ง ํํฐ UI ์ํ๋ฅผ Jotai ๋ก, Query ์ ์ฃผ์ ํ๋ ํจํด
- โก Mutation ํ Query Invalidate ํธ๋ฆฌ๊ฑฐ ํจํด
- ๐ฏ "Jotai ๋ก ํ ๊น, Query ๋ก ํ ๊น?" ํ๋จ ๊ธฐ์คํ
- ๐ฆ Before/After โ ๋ชจ๋ ์ํ๋ฅผ Jotai ๋ก โ ์ญํ ๋ถ๋ฆฌ
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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" โ ์ด๊ฑฐ ๋ฌธ์ ์ด๋ผ๋ ์๊ฒจ์ผ ํ๋.