TIL

Next.js Server-Action๊ณผ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ

inz1234 2024. 7. 2. 19:54

๐Ÿšจ  TroubleShotting

๋ฌธ์ œ ๋ฐœ์ƒ์˜ ๋ฐฐ๊ฒฝ - As Is

Next.js์˜ App-Router๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์ข‹์•„์š”๋ฅผ ๊ตฌํ˜„ํ•˜๋˜ ์ค‘, ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ฒ˜์Œ์—๋Š” ์ฝ”๋“œ๋ฅผ ์ด๋ ‡๊ฒŒ ์งฐ๋‹ค.
submitQuizLike()๋ผ๋Š” ๋น„๋™๊ธฐ ํ†ต์‹  ๋กœ์ง์ด ์‹คํ–‰๋˜๊ธฐ ์ „์— setIsLiked๋กœ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ํ•ด์•ผ์ง€๋ผ๋Š” ํ—ˆ์ ‘ํ•˜๊ณ  ์•ผ๋ฌด์ง„ ์†Œ๋ง(?)๊ณผ ํ•จ๊ป˜..ใ…‹ใ…‹ใ…‹

const LikeQuiz = ({ quiz_id }: { quiz_id: string }) => {
  ...
  const [isLiked, setIsLiked] = useState(userData && quizLikeData && quizLikeData.users?.includes(userData.user_id));
  const queryClient = useQueryClient();
  
  const handleSubmitLike = async () => {
    setIsLiked((prev) => !prev);   ---------> submitQuizLike ์ „์— ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ํ•ด์•ผ์ง€~
    await submitQuizLike(quiz_id, userData?.user_id ?? '');
    queryClient.invalidateQueries({ queryKey: [QUIZLIKE_QUERY_KEY, quiz_id] });
  };

  console.log('isLiked ====>>', isLiked);
  console.log('-----------------------------------');
  return (
    <>
      <form action={handleSubmitLike}>
        <button type="submit" name="like">
          {isLiked ? <FaHeart className="text-red-500" /> : <FaRegHeart />}
        </button>
      </form>
    </>
  );
};

๊ทธ๋žฌ๋”๋‹ˆ ๋‹น์—ฐํžˆ(?) ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๊ฐ€ ์•ˆ ๋๋‹ค.

๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์ ˆ๋งํŽธ

๋ˆ„๊ฐ€๋ด๋„ ๋น„๋™๊ธฐ ํ†ต์‹ ์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ setIsLiked๊ฐ€ ์‹คํ–‰๋˜๊ณ  ์žˆ๋‹ค.
์™œ๋ƒํ•˜๋ฉด ์ƒํƒœ ์—…๋ฐ์ดํŠธ์— ์˜ํ•œ re-๋ Œ๋”๋ง์€ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ฐ์‹ธ๋Š” ํ•จ์ˆ˜ ์ „์ฒด(์—ฌ๊ธฐ์„œ๋Š” handleSubmitLike ํ•จ์ˆ˜)๊ฐ€ ์ข…๋ฃŒ๋œ ๋’ค ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
์•„๋‹ˆ ์•Œ๊ณ ๋ณด๋ฉด ๋„ˆ๋ฌด ๋‹น์—ฐํ•œ ์ด์•ผ๊ธด๋ฐ ์™œ ๋‚˜๋Š” ๋งจ๋‚  ๊นŒ๋จน๋Š”๊ฐ€?
ํ•˜๊ธด ์ด๋ ‡๊ฒŒ ์‰ฝ๊ฒŒ ๋  ๊ฒƒ์ด์—ˆ๋‹ค๋ฉด optimistic-update๋ฅผ ์œ„ํ•œ ํ›…๋“ค์ด ์—†์—ˆ๊ฒ ์ง€.
๊ด€๊ฑด์€ ์ƒํƒœ์—…๋ฐ์ดํŠธ์™€ ๋น„๋™๊ธฐ ํ†ต์‹  ๋กœ์ง์ด ํ•˜๋‚˜์˜ ํ•จ์ˆ˜ ์•ˆ์— ์žˆ๋”๋ผ๋„ ๊ตฌ๋ถ„๋˜์–ด ์‹คํ–‰๋˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. 


How(๊ณผ์ •) ?

๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์ƒ๊ฐ๋‚ฌ๋‹ค.

1. ์ €๋ฒˆ์— ํ•œ๋ฒˆ ํฌ์ŠคํŒ… ํ–ˆ๋˜ Next.js ๊ณต์‹๋ฌธ์„œ์—์„œ ์ œ์•ˆํ•˜๋Š” useOptimistic() ์ด๋ผ๋Š” canary ๋ฒ„์ „์˜ react-hook

2. ๋Š๋‚Œ์ƒ useTransition()์œผ๋กœ๋„ ๊ฐ€๋Šฅํ•  ๊ฒƒ ๊ฐ™์•˜๋‹ค.


์‹œํ–‰์ฐฉ์˜ค 1

useOptimistic()

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#optimistic-updates

 

Data Fetching: Server Actions and Mutations | Next.js

Learn how to handle form submissions and data mutations with Next.js.

nextjs.org

๋จผ์ € useOptimistic() ๋ถ€ํ„ฐ ์‹œ๋„ํ•˜๋˜ ์ค‘ 
๋ถ„๋ช… ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ๋ˆŒ๋ €๋Š”๋ฐ, ๋‹ค์‹œ ์ข‹์•„์š”๊ฐ€ ๋œ ์ƒํƒœ๋กœ ๋Œ์•„๊ฐ€๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

=> console ์ฐฝ์— ๋ณด์ด๋Š” ๋ฐ”์™€ ๊ฐ™์ด optimisticLike๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ์งํ›„์—๋Š” false๊ฐ€ ๋˜์ง€๋งŒ, ์ด๋‚ด ๋‹ค์‹œ true๊ฐ€ ๋œ๋‹ค.

์ฝ”๋“œ๋Š” ์ด๋Ÿฌํ–ˆ๋‹ค.

const LikeQuiz = ({ quiz_id }: { quiz_id: string }) => {
  ...
  const [isLiked, setIsLiked] = useState(userData && quizLikeData && quizLikeData.users?.includes(userData.user_id));
  const queryClient = useQueryClient();
    // useOptimistic
  const [optimisticLike, updateOptimisticLike] = useOptimistic(isLiked, (state) => !state);

  const handleSubmitLike = async () => {
    updateOptimisticLike(isLiked); -------------> Optimistic Update
    await submitQuizLike(quiz_id, userData?.user_id ?? '');
    queryClient.invalidateQueries({ queryKey: [QUIZLIKE_QUERY_KEY, quiz_id] });
  };
  
  console.log('isLiked ====>>', isLiked);
  console.log('optimisticLike =>', optimisticLike);
  console.log('-----------------------------------');

  return (
    <>
      <form action={handleSubmitLike}>
        <button type="submit" name="like">
          {optimisticLike ? <FaHeart className="text-red-500" /> : <FaRegHeart />}
        </button>
      </form>
    </>
  );
};

optimisticLike์™€ useState๋กœ ์ •์˜ํ•œ isLiked ์‚ฌ์ด์— ๋ณ„๋„์˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์ง€ ์•Š์•„๋„,
์ฆ‰ ๋‘ ๊ฐœ์˜ ์ƒํƒœ๊ฐ€ ๋ณ„๊ฐœ๊ฐ€ ์•„๋‹ˆ๊ณ 


๊ฒฐ๊ตญ optimisticLike์˜ ์ƒํƒœ๋Š” isLiked์˜ ์ƒํƒœ๋ฅผ ๋”ฐ๋ผ๊ฐ€๋‚˜?

ํ•˜๋Š” ์˜๋ฌธ์ด ๋“ค์—ˆ๋‹ค.
updateOptimistic์œผ๋กœ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ํ•ด๋„ ๊ฒฐ๊ตญ ์›๋ž˜ isLiked ์ƒํƒœ๋กœ ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ ๊ฐ™์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋Ÿฌ๋ฉด handleSubmitLike ํ•จ์ˆ˜์— optimisticLike๋งŒ ์—…๋ฐ์ดํŠธํ•  ๊ฒƒ์ด ์•„๋‹ˆ๋ผ setIsLiked() ๋กœ์ง๋„ ์ถ”๊ฐ€ํ•ด์•ผ๊ฒ ๊ตฌ๋‚˜!
์ถ”๊ฐ€๋กœ, ๋น„๋™๊ธฐ ํ†ต์‹ ์ด ์„ฑ๊ณตํ•  ์ˆ˜๋„ ์žˆ๊ณ  ์‹คํŒจํ•  ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ useEffect ๋‚ด์—์„œ queryClient.invalidateQueires๋ฅผ ํ•œ ๊ฒฐ๊ณผ๋กœ setIsLiked๋ฅผ ํ™•์‹คํžˆ ์žฌํ™•์ธ ํ•ด์•ผ๊ฒ ๋‹ค ์‹ถ์—ˆ๋‹ค.

const LikeQuiz = ({ quiz_id }: { quiz_id: string }) => {
  ...    
  const queryClient = useQueryClient();
  const [isLiked, setIsLiked] = useState(userData && quizLikeData && quizLikeData.users?.includes(userData.user_id));
  const [optimisticLike, updateOptimisticLike] = useOptimistic(isLiked, (state) => !state);

  useEffect(() => {
  // (3) ๋น„๋™๊ธฐ ํ†ต์‹  ์„ฑ๊ณต or ์‹คํŒจ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ํ™•์‹คํ•œ isLiked ์žฌ์„ค์ •
    setIsLiked(userData && quizLikeData && quizLikeData.users?.includes(userData.user_id));
  }, [quizLikeData, userData]);


  const handleSubmitLike = async () => {
    updateOptimisticLike(isLiked); // ---------> (1) ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ
    setIsLiked((prev) => !prev);   // ---------> (2) + ๊ฒฐ์ •์ ์ธ setIsLiked
    await submitQuizLike(quiz_id, userData?.user_id ?? '');
    queryClient.invalidateQueries({ queryKey: [QUIZLIKE_QUERY_KEY, quiz_id] });
  };

  console.log('isLiked ====>>', isLiked);
  console.log('optimisticLike =>', optimisticLike);
  console.log('-----------------------------------');
  
  return (
    <>
      <form action={handleSubmitLike}>
        <button type="submit" name="like">
          {optimisticLike ? <FaHeart className="text-red-500" /> : <FaRegHeart />}
        </button>
      </form>
    </>
  );
};

์„ฑ๊ณต!!

์ข‹์•„์š” ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด์ž๋งˆ์ž ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋„ ์ž˜ ๋˜๊ณ ,
-> setIsLiked๋„ ๋น„๋™๊ธฐ ํ†ต์‹  ํ›„์— ๋˜๋ฉฐ,
-> ๋งˆ์ง€๋ง‰์— useEffect๋กœ ๋‹ค์‹œ ํ•œ๋ฒˆ ๋น„๋™๊ธฐ ํ†ต์‹ ์˜ ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ํ™•์‹คํžˆ setIsLiked๊ฐ€ ๋˜๋˜,
-> ์ด์ „๊ณผ ์ƒํƒœ๊ฐ€ ๊ฐ™๋‹ค๋ฉด UI๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š์•„์„œ ๋งค๋„๋Ÿฝ๊ฒŒ ๋ณด์ด๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. 

ํ•˜์ง€๋งŒ ์ด useOptimistic()์ด๋ผ๋Š” react-hook์€ ์–ด๋””๊นŒ์ง€๋‚˜ ์•„์ง ๋ฆฌ์•กํŠธ์˜ canary-version์ด๋ผ๋Š” ์ ์ด ์ข€ ์ฐ์ฐํ–ˆ๋‹ค.
์‚ฌ์‹ค ๋‚ด ๋จธ๋ฆฟ์†์—๋Š” useTransition() hook์ด ๋จผ์ € ๋– ์˜ฌ๋ž๋‹ค. ๊ทธ๋ž˜์„œ 


์‹œํ–‰์ฐฉ์˜ค 2

useTranstion()

https://react.dev/reference/react/useTransition

 

useTransition – React

The library for web and native user interfaces

react.dev

useTransition์„ ์‚ฌ์šฉํ•ด์„œ ์ข‹์•„์š” ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋น„๋™๊ธฐ ํ†ต์‹ ์ด ์‹œ์ž‘๋˜๊ณ ,
์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ isPending ์ด๋ผ๋Š” ์ƒํƒœ๊ฐ€ true๊ฐ€ ๋œ๋‹ค.

๊ฑฐ๊พธ๋กœ ๋งํ•˜๋ฉด isPending์ด true๋ผ๋Š” ๊ฒƒ์€ ์ข‹์•„์š” ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋‹ค๋Š” ๋ง์ด ๋œ๋‹ค.

๊ทธ๋ž˜์„œ isPending์ด true๋ฉด ์ผ๋‹จ isLiked ์ƒํƒœ๋ฅผ ์ด์ „ ๊ฒƒ๊ณผ ๋ฐ˜๋Œ€๋˜๋„๋ก ๋ฐ”๊พธ๋ฉด ์–ด๋–จ๊นŒ?

๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

๊ทธ ๋’ค์— ์œ„์—์„œ์ฒ˜๋Ÿผ ๋น„๋™๊ธฐ ํ†ต์‹ ์˜ ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์— ๋”ฐ๋ผ์„œ ํ™•์‹คํžˆ ์žฌ์„ค์ • ํ•ด์ฃผ๋ฉด ๋˜์ž๋„?


what(๊ฒฐ๊ณผ) - To Be

๊ฒฐ๊ณผ๋Š” ์„ฑ๊ณต!!

์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

const LikeQuiz = ({ quiz_id }: { quiz_id: string }) => {
  ...
  const [isLiked, setIsLiked] = useState(userData && quizLikeData && quizLikeData.users?.includes(userData.user_id));
  const queryClient = useQueryClient();
  // useTransition
  const [isPending, startTransition] = useTransition();

  useEffect(() => {
  // (3) ๋น„๋™๊ธฐ ํ†ต์‹ ์˜ ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ํ™•์‹คํ•œ isLiked ์žฌ์„ค์ •
    setIsLiked(userData && quizLikeData && quizLikeData.users?.includes(userData.user_id));
  }, [quizLikeData, userData]);

  useEffect(() => {
  // (2) isPending์ด true๋ฉด ์ผ๋‹จ isLiked ์ƒํƒœ๋ฅผ ์ด์ „๊ณผ ๋ฐ˜๋Œ€๋กœ ์—…๋ฐ์ดํŠธ
    isPending && setIsLiked((prev) => !prev);
  }, [isPending]);


  const handleSubmitLike = async () => {
    startTransition(async () => { // ---------> (1) ๋น„๋™๊ธฐ ํ†ต์‹  ์‹œ์ž‘!
      await submitQuizLike(quiz_id, userData?.user_id ?? '');
      queryClient.invalidateQueries({ queryKey: [QUIZLIKE_QUERY_KEY, quiz_id] });
    });
  };

  console.log('isLiked ====>>', isLiked);
  console.log('-----------------------------------');
  
  return (
    <>
      <form action={handleSubmitLike}>
        <button type="submit" name="like">
          {isLiked ? <FaHeart className="text-red-500" /> : <FaRegHeart />}
        </button>
      </form>
    </>
  );
};

useTransition์œผ๋กœ Optimistic-Update ํฌ๋งํŽธ

๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด isPending์ด true๊ฐ€ ๋˜๊ณ 
-> isLiked ์ƒํƒœ๋Š” ๋ฌด์กฐ๊ฑด ์ด์ „๊ณผ ๋ฐ˜๋Œ€ ์ƒํƒœ๋กœ ๋ฐ”๋€Œ๊ณ 
-> ๋น„๋™๊ธฐ ํ†ต์‹ ์ด ์™„๋ฃŒ๋œ ํ›„, ๋น„๋™๊ธฐ ํ†ต์‹ ์˜ ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์— ๋”ฐ๋ผ isLiked ์ƒํƒœ๊ฐ€ ์ข€ ๋” ํ™•์‹คํžˆ ๋ฐ”๋€๋‹ค.
๊ฐœ์ธ์ ์œผ๋กœ๋Š” ์ด ๋ฐฉ๋ฒ•์ด ๋กœ์ง๋„ ๋” ๊น”๋”ํ•ด์„œ ๋งˆ์Œ์— ๋“ ๋‹ค.


๐Ÿ’ก ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ๋œ ์ 

์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ๋œ ๊ฒƒ์€ ์•„๋‹ˆ์ง€๋งŒ
์žŠ์ง€๋ง์ž! ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ์ข…๋ฃŒ๋˜์–ด์•ผ re-๋ Œ๋”๋ง์ด ๋œ๋‹ค!!


๋งŒ์•ฝ Server-Action์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด react-Query์˜ onMutate, onError, onSettled์™€
setQueryData & getQueryData๋กœ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์„ํ…๋ฐ
๋ญ”๊ฐ€ ์ •ํ•ด์ง„ ๋ฐฉ๋ฒ• ๋ง๊ณ  ์Šค์Šค๋กœ ์ƒ๊ฐํ•ด๋ดค๋‹ค๋Š” ์ ์ด ์ƒˆ๋กœ์› ๋‹ค.