ํฌ์์ด์๋ ๋ณ์ ๋ชฉ๋ก API์์ ํ์์ด ํด๋น ๋ณ์์ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ ์ ์๋ ๊ธฐ๋ฅ์ด ์กด์ฌํ๋ค.
ํ์ง๋ง ํ์ฌ ๊ตฌํ๋์ด์๋ ๋ก์ง์ ๋ฌธ์ ์ ์ด ์๋ค.
- ์ฆ๊ฒจ์ฐพ๊ธฐ๋ฅผ ๋๋ ์ ๋, API๋ฅผ ๋ณด๋ด๊ณ , ๊ทธ API๊ฐ ์ฑ๊ณต์ผ๋ก Promise๊ฐ ๋์์์ ๋ ์๋ฌด๊ฒ๋ ํ์ง ์๋๋ค. ๊ฒฐ๊ตญ ๋ค์ ๋ณ์ ๋ชฉ๋ก์ fetchํ์ง ์๊ธฐ ๋๋ฌธ์ ์ฆ๊ฒจ์ฐพ๊ธฐ๊ฐ ์ ๋๋ก ์ฑ๊ณตํ๋์ง๋ ๋ค์ ๋๊ฐ๋ค๊ฐ ๋ค์ด์์ fetch๊ฐ ์ด๋ฃจ์ด์ง ๋ ํ์ธํด์ผ ํ๋ค
- ์ฆ๊ฒจ์ฐพ๊ธฐ api๊ฐ ์๋ํ์ ๋ ๋ค์
invalidateQueries๋ก ๊ธฐ์กด์ ๋ณ์ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๊ฒ ๋๋ฉด ๋คํธ์ํฌ ๋น์ฉ์ด ์ฆ๊ฐํ๋ค. ์์ฒญ์ด ์ฑ๊ณตํ๋ค๋ ๊ฒ์ ๊ณง ํด๋น ์ฆ๊ฒจ์ฐพ๊ธฐ ํ๋์ ๊ฐ๋ง ๋ณํ์ ๋ฟ์ธ๋ฐ ์ด๋ฅผ ์ํด ๋ค์๊ธ api๋ฅผ ํธ์ถํ๋ ํ์๋ ๋นํจ์จ์ ์ด๋ผ๊ณ ์๊ฐํ๋ค ์ด์ ๊ฐ์ ๋ฌธ์ ์๊ธฐ ๋๋ฌธ์ Optimistic Update, ์ฆ ๋๊ด์ ์ ๋ฐ์ดํธ๋ฅผ ํตํ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋ฅ ์ ์ด๋ฅผ ํ๋ ค๊ณ ํ๋ค.
์ด๋ฌํ Optimistic Update์ ๊ฒฝ์ฐ tanstack query์์๋ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด๋ค. tanstack query์ useMutation์ ์ด์ฉํด์ ์ด๋ค ๋ฐฉ์์ผ๋ก ๊ธฐ์กด์ ๊ฐ์ ๊ฐ์ ธ์์ ์ฆ์ ์ ๋ฐ์ดํธํ๊ณ , ๋ง์ฝ์ ์ ๋ฐ์ดํธ๊ฐ ์คํจํ๋ฉด ์ด๋ป๊ฒ ํ ์ง ๋ฑ์ ๋ฐฉ์์ ์ ๊ณตํ๋ค.
useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
// Snapshot the previous value
const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
// Optimistically update to the new value
queryClient.setQueryData(['todos', newTodo.id], newTodo)
// Return a context with the previous and new todo
return { previousTodo, newTodo }
},
// If the mutation fails, use the context we returned above
onError: (err, newTodo, context) => {
queryClient.setQueryData(
['todos', context.newTodo.id],
context.previousTodo,
)
},
// Always refetch after error or success:
onSettled: (newTodo) => {
queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
},
})์ฌ๊ธฐ์์ ์ด๋ฃจ์ด์ง๋ ๊ณผ์ ์
- mutate๊ฐ ํธ์ถ๋์๋ง์
- ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ๊ฐ ์คํ ์ค์ด๋ผ๋ฉด, ๋จผ์ ์ทจ์(
cancelQueries)ํ์ฌ ๋๊ด์ ์ ๋ฐ์ดํธ๋ฅผ ๋ฐฉํดํ์ง ์๋๋ก ํ๊ธฐ - ์ด์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ์ฌ, ์๋ฌ ์์ ๋ณต๊ตฌํ ์ ์๋๋ก ๋ณ์์ ํ ๋น
- ๋๊ด์ ์ ๋ฐ์ดํธ ์ํ
- context ๋ฐํ(ํ์ error๊ฐ ์์ ๋ ์ฐ์)
- ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ๊ฐ ์คํ ์ค์ด๋ผ๋ฉด, ๋จผ์ ์ทจ์(
- ์๋ฒ ์์ฒญ์ด ์คํจํ์ ๊ฒฝ์ฐ
- context๋ก ๋๊ฒจ์คฌ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ ๊ธฐ์กด ๋ฐ์ดํฐ๋ก ๋ณต๊ตฌ
- ์๋ฒ ์์ฒญ์ด ์คํจํ๊ฑฐ๋ ์ฑ๊ณตํ์ ๋ ํญ์ ์ํ
- ์๋กญ๊ฒ ๋ฐ์ดํฐ ํ์นญ ์ ๊ณผ์ ์ ๊ฑฐ์น๋ค.
const { mutate } = useMutation({
mutationFn: () =>
postBookmark({
hospitalId: hospital.hospitalId,
bookmark: !hospital.isBookmarked,
}),
onMutate: async () => {
await queryClient.cancelQueries({
queryKey: [
'hospitalList',
location?.latitude,
location?.longitude,
],
});
const previousHospitalList = queryClient.getQueryData<{
pageParams: number[];
pages: { hospitalList: Hospital[]; paging: any }[];
}>(['hospitalList', location?.latitude, location?.longitude]);
if (previousHospitalList) {
queryClient.setQueryData(
['hospitalList', location?.latitude, location?.longitude],
{
...previousHospitalList,
pages: previousHospitalList.pages.map((page) => ({
...page,
hospitalList: page.hospitalList.map((h) =>
h.hospitalId === hospital.hospitalId
? { ...h, isBookmarked: !h.isBookmarked }
: h,
),
})),
},
);
}
return { previousHospitalList };
},
onError: (err, variables, context) => {
if (context?.previousHospitalList) {
queryClient.setQueryData(
['hospitalList', location?.latitude, location?.longitude],
context.previousHospitalList,
);
}
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [
'hospitalList',
location?.latitude,
location?.longitude,
],
});
},
});mutation ํ๋๋ง ํด๋ ์ค์ด ์์ฒญ๋๋คโฆ
์ด์ ๋ ์ข์์๋ฅผ ๋๋ฅด๊ฑฐ๋ ์คํฌ๋ฉ์ ํ๋ ๋ฑ mutation์ ์คํํ์๋ง์ ํด๋น ์์ฒญ์ด ์ ์์ ์ผ๋ก ์คํ๋จ์ ๊ฐ์ ํ๊ณ ui๊ฐ ์
๋ฐ์ดํธ๋๋ฉฐ, ๋ง์ฝ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๊ฒฝ์ฐ์๋ ๋ค์๊ธ ์
๋ฐ์ดํธ ์์ผ๋จ๋ ui๋ฅผ ์ด์ ui๋ก ๋ฐ๊ฟ์ผ๋ก์จ ๋๊ด์ ์
๋ฐ์ดํธ๋ฅผ ๊ตฌํํ์๋ค.