ํฌ์—์ด์—๋Š” ๋ณ‘์› ๋ชฉ๋ก API์—์„œ ํšŒ์›์ด ํ•ด๋‹น ๋ณ‘์›์„ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ์กด์žฌํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ํ˜„์žฌ ๊ตฌํ˜„๋˜์–ด์žˆ๋Š” ๋กœ์ง์˜ ๋ฌธ์ œ์ ์ด ์žˆ๋‹ค.

  1. ์ฆ๊ฒจ์ฐพ๊ธฐ๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ, API๋ฅผ ๋ณด๋‚ด๊ณ , ๊ทธ API๊ฐ€ ์„ฑ๊ณต์œผ๋กœ Promise๊ฐ€ ๋Œ์•„์™”์„ ๋•Œ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ฒฐ๊ตญ ๋‹ค์‹œ ๋ณ‘์› ๋ชฉ๋ก์„ fetchํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ฆ๊ฒจ์ฐพ๊ธฐ๊ฐ€ ์ œ๋Œ€๋กœ ์„ฑ๊ณตํ–ˆ๋Š”์ง€๋Š” ๋‹ค์‹œ ๋‚˜๊ฐ”๋‹ค๊ฐ€ ๋“ค์–ด์™€์„œ fetch๊ฐ€ ์ด๋ฃจ์–ด์งˆ ๋•Œ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค
  2. ์ฆ๊ฒจ์ฐพ๊ธฐ 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๋กœ ๋ฐ”๊ฟˆ์œผ๋กœ์จ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.