[DevCamp] ๐Ÿš€ React์—์„œ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ

๋™๊ฑดยท2025๋…„ 4์›” 23์ผ

DevCamp

๋ชฉ๋ก ๋ณด๊ธฐ
54/85

โšก React์—์„œ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ(Optimistic Update) ๊ตฌํ˜„ํ•˜๊ธฐ


๐Ÿง  ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ž€?

๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋Š” ์„œ๋ฒ„ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ ,
์š”์ฒญ์ด ์„ฑ๊ณตํ•  ๊ฑฐ๋ผ๊ณ  ๊ฐ€์ •ํ•˜๊ณ  UI๋ฅผ ๋จผ์ € ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ์‹
์ด๋‹ค.

์ฆ‰, ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ์•ก์…˜์„ ํ–ˆ์„ ๋•Œ, ๋ฐ”๋กœ ํ™”๋ฉด์ด ๋ฐ”๋€Œ๊ณ  ์ดํ›„ ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค. ์„ฑ๊ณตํ•˜๋ฉด ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๊ณ , ์‹คํŒจํ•˜๋ฉด ๋กค๋ฐฑํ•˜๋Š” ์ „๋žต์ด๋‹ค.


๐Ÿ™‹โ€โ™€๏ธ ์™œ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ?

  • ๋น ๋ฅธ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ
  • ๋А๋ฆฐ ๋„คํŠธ์›Œํฌ์—์„œ๋„ ๋ถ€๋“œ๋Ÿฌ์šด UX
  • ์†Œ์…œ ๋ฏธ๋””์–ด, ๋Œ“๊ธ€, ์ข‹์•„์š”, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋“ฑ์— ์ž์ฃผ ์‚ฌ์šฉ๋จ

๐Ÿ’ก ๊ธฐ๋ณธ ๊ตฌํ˜„ ํ๋ฆ„

  1. UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ (์„œ๋ฒ„ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Œ)
  2. ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋ƒ„
  3. ์„ฑ๊ณต โ†’ ๊ทธ๋Œ€๋กœ ์œ ์ง€
  4. ์‹คํŒจ โ†’ ์ด์ „ ์ƒํƒœ๋กœ ๋กค๋ฐฑ

๐Ÿ“ฆ ์˜ˆ์ œ: ๊ฒŒ์‹œ๊ธ€ ์ข‹์•„์š” ๋ฒ„ํŠผ

export const useBook = (bookId: string | undefined) => {
  const [book, setBook] = useState<BookDetail | null>(null);
  const { isloggedIn } = useAuthStore();
  const showAlert = useAlert();

  const likeToggle = () => {
    if (!isloggedIn) {
      showAlert('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
      return;
    }

    if (!book) return;

    if (book.liked) {
      // ๐Ÿ”„ ๋‚™๊ด€์ ์œผ๋กœ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
      setBook({
        ...book,
        liked: false,
        likes: book.likes - 1,
      });

      // ๐Ÿ“ก ์„œ๋ฒ„ ์š”์ฒญ
      unLikeBook(book.id).catch(() => {
        // โŒ ์‹คํŒจ ์‹œ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ ํ•„์š” (ํ˜„์žฌ๋Š” ์ƒ๋žต๋˜์–ด ์žˆ์Œ)
      });

    } else {
      setBook({
        ...book,
        liked: true,
        likes: book.likes + 1,
      });

      likeBook(book.id).catch(() => {
        // โŒ ์‹คํŒจ ์‹œ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ ํ•„์š”
      });
    }
  };

  useEffect(() => {
    if (!bookId) return;

    fetchBook(bookId).then(setBook);
  }, [bookId]);

  return { book, likeToggle };
};

---

## ๐Ÿ› ๏ธ Tip: ์บ์‹œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ•จ๊ป˜ ์“ฐ๊ธฐ

`React Query`๋‚˜ `SWR` ๊ฐ™์€ ์บ์‹œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

### ์˜ˆ: React Query์—์„œ์˜ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ

```tsx
const queryClient = useQueryClient();

const mutation = useMutation(updateTodo, {
  // 1. ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries(['todo', newTodo.id]);

    const previousTodo = queryClient.getQueryData(['todo', newTodo.id]);

    queryClient.setQueryData(['todo', newTodo.id], newTodo);

    return { previousTodo };
  },
  // 2. ์‹คํŒจ ์‹œ ๋กค๋ฐฑ
  onError: (err, newTodo, context) => {
    if (context?.previousTodo) {
      queryClient.setQueryData(['todo', newTodo.id], context.previousTodo);
    }
  },
  // 3. ์„ฑ๊ณต ๋˜๋Š” ์‹คํŒจ ํ›„ refetch
  onSettled: (newTodo) => {
    queryClient.invalidateQueries(['todo', newTodo.id]);
  },
});

โš ๏ธ ์œ ์˜์‚ฌํ•ญ

  • ์„œ๋ฒ„ ์š”์ฒญ์ด ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ „์ œ ํ•˜์— ๋กค๋ฐฑ ๋กœ์ง์ด ๊ผญ ํ•„์š”ํ•˜๋‹ค.
  • ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋Š” ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ์™€ ์„œ๋ฒ„ ์ƒํƒœ๊ฐ€ ๋ถˆ์ผ์น˜ํ•  ์ˆ˜ ์žˆ์Œ์— ์œ ์˜ํ•ด์•ผ ํ•œ๋‹ค.
  • ์บ์‹œ ์ƒํƒœ์™€ ์‹ค์ œ UI๊ฐ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ผญ ์„œ๋ฒ„ ์ •ํ•ฉ์„ฑ ์œ ์ง€๋„ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

๐Ÿ”จ TIL

  • ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋น ๋ฅธ ์‘๋‹ต์„ ์ฃผ๋Š” ๊ธฐ์ˆ ์ด๋‹ค.
  • UI๋ฅผ ๋จผ์ € ๋ณ€๊ฒฝํ•˜๊ณ  ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉฐ, ์‹คํŒจ ์‹œ ๋กค๋ฐฑ์ด ํ•ต์‹ฌ ํฌ์ธํŠธ๋‹ค.
  • ์ง์ ‘ ๊ตฌํ˜„ํ•  ์ˆ˜๋„ ์žˆ๊ณ , React Query ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด ๋” ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์‹ค๋ฌด์—์„œ๋Š” ๋Œ“๊ธ€, ์ข‹์•„์š”, ํˆฌํ‘œ ๋“ฑ ์ž์ฃผ ์“ฐ์ด๋Š” ํŒจํ„ด์ด๋‹ˆ ์ตํ˜€๋‘์ž!
profile
๋ฐฐ๊ณ ํ”ˆ ๊ฐœ๋ฐœ์ž

0๊ฐœ์˜ ๋Œ“๊ธ€