๐Ÿ–Œ๏ธFrontend

[Tanstack Query] ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ์ด์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ UX ๊ฐœ์„ ๊ธฐ

bananackck 2025. 7. 1. 18:15

์ด ๊ธ€์€ Tanstack Query์™€ SSE๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋กœ์ ํŠธ์—์„œ ์ƒํƒœ ๋™๊ธฐํ™” ๋ฌธ์ œ๋ฅผ ๊ฒฝํ—˜ํ•œ ๋ถ„๋“ค์„ ์œ„ํ•œ ์‚ฌ๋ก€ ๊ณต์œ ์ž…๋‹ˆ๋‹ค.

 

์ปค๋ฆฌ์–ด๋น„์—์„œ ์•Œ๋ฆผ ๋„๋ฉ”์ธ์„ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ์ •๋ง ๋งŽ์€ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…์ด ์žˆ์—ˆ๋‹ค.

์˜ค๋Š˜์€ ๊ทธ ์ค‘ ํ•˜๋‚˜์ธ “์•Œ๋ฆผ ์•„์ด์ฝ˜ ์ฝ์Œ ์ฒ˜๋ฆฌ ์—…๋ฐ์ดํŠธ” ์— ๋Œ€ํ•ด์„œ ์ด์•ผ๊ธฐํ•ด๋ณด๊ฒ ๋‹ค.

๋ฌธ์ œ ์ƒํ™ฉ

๋จผ์ € ์ปค๋ฆฌ์–ด๋น„์—์„œ๋Š” SSE๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด ์•Œ๋ฆผ์ด ์ƒ๊ธฐ๋ฉด ๊ทธ ์—ฌ๋ถ€๋ฅผ boolean์œผ๋กœ ๋ฐ›์•„์˜จ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ด๋ฅผ header์˜ ์•Œ๋ฆผ ์•„์ด์ฝ˜์— ์—…๋ฐ์ดํŠธ ํ•ด์ค€๋‹ค.

 

์ด์Šˆ๋Š” ์—ฌ๊ธฐ์—์„œ ๋ฐœ์ƒํ•œ๋‹ค.

์‚ฌ์šฉ์ž๋Š” ์•Œ๋ฆผ์„ ๋ณด๊ธฐ ์œ„ํ•ด ์•Œ๋ฆผ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•œ๋‹ค.

ํด๋ฆญ ์‹œ /notification ์œผ๋กœ ์ด๋™ํ•œ๋‹ค.

์ด๋™ ํ›„, header์˜ ์•Œ๋ฆผ ์•„์ด์ฝ˜์ด ์ผ๋ฐ˜ ์•„์ด์ฝ˜์œผ๋กœ ๋˜๋Œ์•„๊ฐ€์•ผ ํ•œ๋‹ค.

ํฌ์ธํŠธ๋Š” ๋ฌด์‹œํ•˜์ž…

ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„์˜ ๊ด€์  ์ฐจ์ด

์—ฌ๊ธฐ์„œ ๋ฐฑ์—”๋“œ์™€์˜ ๋กœ์ง์ด ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์ด ์ด์Šˆ์˜ ์‹œ๋ฐœ์ ์ด์—ˆ๋‹ค.

์šฐ์„  ์ปค๋ฆฌ์–ด๋น„์—์„œ๋Š” “์•Œ๋ฆผ ํŽ˜์ด์ง€์— ์ง„์ž… ํ•จ” = “์•Œ๋ฆผ์„ ์ฝ์Œ” ์œผ๋กœ ๊ฐ„์ฃผํ•˜์˜€๋‹ค.

๐ŸŽจ ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ

  • ์•Œ๋ฆผ ํŽ˜์ด์ง€์— ์ง„์ž… ์‹œ ๋ฐ”๋กœ header์˜ ์•Œ๋ฆผ ์•„์ด์ฝ˜์ด ์ •์ƒ์œผ๋กœ ๋Œ์•„์™€์•ผ ํ•œ๋‹ค.

๐Ÿ—„๏ธ ์„œ๋ฒ„ ์ž…์žฅ

  • ์ƒˆ ์•Œ๋ฆผ๋“ค์˜ ID๋ฅผ PATCH ์š”์ฒญ์„ ํ†ตํ•ด ๋ฐ›๊ณ , ํ•ด๋‹น ์•Œ๋ฆผ๋“ค์˜ ์ฝ์Œ ์—ฌ๋ถ€๋ฅผ '์•ˆ์ฝ์Œ' -> '์ฝ์Œ' ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.
  • ์•Œ๋ฆผํ…Œ์ด๋ธ”์—์„œ ๊ฐ ์œ ์ €๋“ค์˜ ์•Œ๋ฆผ๊ณผ ์ฝ์Œ ์—ฌ๋ถ€๋ฅผ ํ•จ๊ป˜ ์ €์žฅํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค

hasNewAlarm์˜ T/F ๋ณ€ํ™˜ ์‹œ์ ์— ๋Œ€ํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ์‚ฌ์ด์˜ ์ž…์žฅ ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๊ฒƒ ๋•Œ๋ฌธ์— ๋ฐฑ์—”๋“œ์—๊ฒŒ ๋ฐ์ดํ„ฐํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ผ๊ณ  ํ•  ์ˆ˜๋Š” ์—†๋Š” ๋…ธ๋ฆ‡์ด๊ธฐ์— ๋‚˜๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•ด๋ƒˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ํ›„๋ณด

[๋ฐฉ๋ฒ• 1] useEffect์—์„œ setQueryData๋กœ ์ฆ‰์‹œ false ์ฒ˜๋ฆฌ

useEffect(() => {
  queryClient.setQueryData(['userinfo'], (oldData: any) => {
    if (!oldData) return oldData;
    if (oldData.hasNewAlarm === false) return oldData;
    return {
      ...oldData,
      hasNewAlarm: false,
    };
  });
}, []);

์ด๋Ÿฐ ์‹์œผ๋กœ ๊ฐ•์ œ๋กœ ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •ํ•œ๋‹ค.

โŒ ๋‹จ์ 

  • PATCH ์š”์ฒญ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „ userInfo ์ฟผ๋ฆฌ๊ฐ€ stale๋˜์–ด refetch๋˜๋ฉด:
    • ์„œ๋ฒ„์—์„œ๋Š” ์•„์ง hasNewAlarm: true ์ƒํƒœ๋ผ ๋‹ค์‹œ true๋กœ ๋Œ์•„์˜ด
    • ์•„์ด์ฝ˜์ด ๊นœ๋นก์˜€๋‹ค ๋‹ค์‹œ ์ผœ์งˆ ์ˆ˜ ์žˆ์Œ
  • ์ผ๊ด€์„ฑ์ด ๊นจ์งˆ ๊ฐ€๋Šฅ์„ฑ

[๋ฐฉ๋ฒ• 2] PATCH ์™„๋ฃŒ ํ›„ refetchQueries

await PATCH(...);
queryClient.refetchQueries(['userInfo'])

PATCH๊ฐ€ ์„ฑ๊ณตํ•˜๋ฉด ์„œ๋ฒ„์—์„œ ๋ณ€๊ฒฝ๋œ userInfo ๋ฐ์ดํ„ฐ๋ฅผ refetch ํ•œ๋‹ค.

โŒ ๋‹จ์ 

  • POST ์‘๋‹ต์ด ๋А๋ฆฐ ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์•Œ๋ฆผ ํŽ˜์ด์ง€ ๋“ค์–ด๊ฐ”๋Š”๋ฐ ํ—ค๋” ์•Œ๋ฆผ ์•„์ด์ฝ˜์ด ํ•œ์ฐธ ํ›„์— ์‚ฌ๋ผ์ง
    • ์ฆ‰์‹œ ํ”ผ๋“œ๋ฐฑ์ด ์—†์Œ
    • ์ฒด๊ฐ์ƒ UX๊ฐ€ ๋”œ๋ ˆ์ด๋œ๋‹ค๊ณ  ๋А๋‚„ ์ˆ˜ ์žˆ์Œ
  • ๋„คํŠธ์›Œํฌ ๋ ˆ์ดํ„ด์‹œ ์˜์กด

UI ๊นœ๋นก์ž„์ด ์ผ์–ด๋‚  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์„ ์ •๋ฆฌํ•ด๋ณด์ž๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

  1. ๋„คํŠธ์›Œํฌ ์ƒํƒœ๊ฐ€ ๋А๋ฆฐ ํ™˜๊ฒฝ (์˜ˆ: ๋ชจ๋ฐ”์ผ 3G)
  2. ๋ฐฑ์—”๋“œ ์‘๋‹ต ์ง€์—ฐ (์˜ˆ: 1~2์ดˆ ์ด์ƒ)
  3. ์•Œ๋ฆผ์„ ์ฝ์Œ ์ฒ˜๋ฆฌํ•˜๋Š” POST ์š”์ฒญ๊ณผ, userInfo ์ฟผ๋ฆฌ๊ฐ€ ๋‘˜ ๋‹ค ํ™œ์„ฑํ™” ์ƒํƒœ๋ผ refetch ํƒ€์ด๋ฐ์ด ๊ฒน์นจ

ํ•˜์ง€๋งŒ ์ตœ๋Œ€ํ•œ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ํŒจํ„ด์„ ์ ์šฉํ•˜๋Š”๊ฒŒ UX ์ธก๋ฉด์—์„œ ์ข‹์„ ๊ฒƒ์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์•„๋ž˜์˜ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ–ˆ๋‹ค.

[๋ฐฉ๋ฒ• 3] ๋ฎคํ…Œ์ด์…˜ ์‚ฌ์šฉ

// PATCH ์š”์ฒญ ์ค‘์—๋Š” refetch ํ•˜์ง€ ์•Š๊ฒŒ
const mutation = useMutation(markNotificationsAsRead, {
  onMutate: async () => {
    // ์•Œ๋ฆผ ์ฆ‰์‹œ ๊บผ์ง€๊ธฐ
    queryClient.setQueryData(['userInfo'], (old) => ({ ...old, hasNewAlarm: false }));
    // ๊ธฐ์กด ์ฟผ๋ฆฌ ์ทจ์†Œ (์ค‘๊ฐ„ ๊ฐฑ์‹  ๋ฐฉ์ง€)
    await queryClient.cancelQueries(['userInfo']);
  },
  onSuccess: () => {
    queryClient.invalidateQueries(['userInfo']);
  },
});

100% ๋ฐ์ดํ„ฐ์˜ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ๋งž๋‹ค.

ํ•˜์ง€๋งŒ ์‹ค์‚ฌ์šฉ์—์„œ ์•Œ๋ฆผ ์ƒํƒœ ๊นœ๋นก์ž„์ด 500ms ์ดํ•˜์— ๊ทธ์ณค๊ณ , ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์—์„œ ํ˜ผ๋ž€์„ ๋А๋‚€ ์‚ฌ๋ก€๊ฐ€ ์—†์—ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๋ฎคํ…Œ์ด์…˜์€ ์˜ค๋ฒ„์ŠคํŽ™์ด๋ผ๋Š” ๊ฒฐ๋ก ์„ ์ง€์—ˆ๋‹ค.

[๋ฐฉ๋ฒ• 4] setQueryData์™€ refetchQueries ๋ชจ๋‘ ์‚ฌ์šฉ + ๋กค๋ฐฑ

๋ง ๊ทธ๋Œ€๋กœ ์œ„์˜ ๋ฐฉ๋ฒ•์„ ๋ชจ๋‘ ์ ์šฉํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ๋„ ์ถฉ๋ถ„ํžˆ ๋ณด์žฅ๋  ์ˆ˜ ์žˆ๋‹ค.

์ด์ œ ์™„๋ฒฝํ•œ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ํŒจํ„ด์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋กค๋ฐฑ ๋กœ์ง๋งŒ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.

๋กค๋ฐฑ ๋กœ์ง์€ ์•„๋ž˜์ฒ˜๋Ÿผ ์ž‘์„ฑํ•˜์˜€๋‹ค.

const previousUserInfo = queryClient.getQueryData(['userInfo']);
try {
  await PATCH(...);
  queryClient.refetchQueries({ queryKey: ['userinfo'] });
} catch (error) {
  // ๋กค๋ฐฑ
  queryClient.setQueryData(['userInfo'], previousUserInfo);
}

 

ํšŒ๊ณ 

์ด๋ฒˆ ๊ฒฝํ—˜์„ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋Š” ๋‹จ์ˆœํžˆ UI์— ํ‘œ์‹œ๋˜๋А๋ƒ ์˜ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ, ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ๊ณผ ์‚ฌ์šฉ์ž ์ฒด๊ฐ ์†๋„, ๊ทธ๋ฆฌ๊ณ  ๊ฐœ๋ฐœ ๋น„์šฉ๊นŒ์ง€ ํ•จ๊ป˜ ๊ณ ๋ คํ•ด์•ผ ํ•˜๋Š” ์˜์—ญ์ด๋ผ๋Š” ๊ฑธ ๋‹ค์‹œ ๋А๊ผˆ๋‹ค.

 

์ฒ˜์Œ์—๋Š” ๊ทธ๋ƒฅ ์–ด์ฐจํ”ผ ๊ธˆ๋ฐฉ ๊ฐฑ์‹ ๋  ๊ฑฐ๋‹ˆ๊นŒ refetch๋งŒ ํ•ด๋„ ๋˜๊ฒ ์ง€ ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋ง‰์ƒ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด๋‹ˆ, ๋„คํŠธ์›Œํฌ๊ฐ€ ๋А๋ฆฐ ํ™˜๊ฒฝ์ด๋‚˜ ์ €์‚ฌ์–‘ ๊ธฐ๊ธฐ์—์„œ๋Š” ์•Œ๋ฆผ ์•„์ด์ฝ˜์ด ๊นœ๋นก๊ฑฐ๋ฆฌ๋Š” ์ˆœ๊ฐ„์ด ๋ณด์˜€๋‹ค. ์ด๋Ÿฐ ์ž‘์€ ๋ถ€๋ถ„๋„ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š์„ ์ˆ˜ ์—†๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ์ค‘์š”ํ•œ ๊ฑด ์–ด๋–ค ์„ ํƒ์„ ํ•˜๋“  ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๊ฐ€ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์ด๋‹ค. ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ์“ฐ๋ฉด ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋Š” ๋น ๋ฅด๊ฒŒ ๋ฐ˜์‘ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์—ฌ์„œ ์ข‹์ง€๋งŒ, ๋งŒ์•ฝ ์„œ๋ฒ„ ์ฒ˜๋ฆฌ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ๋Š” ๋กค๋ฐฑ๊นŒ์ง€ ์ฑ…์ž„์ ธ์•ผ ํ•œ๋‹ค. ์ƒ๊ฐํ• ๊ฒŒ ๋งŽ์•„์ง€์ง€๋งŒ ์„œ๋น„์Šค๋Š” ๊ฒฌ๊ณ ํ•ด์ง„๋‹ค. 

 

์ˆ˜๋งŽ์€ ๊ธฐ์ˆ ๋“ค์ด ์žˆ์ง€๋งŒ ์–ด๋””์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•˜๋А๋ƒ๋Š” ๋‚˜์˜ ์„ ํƒํ•˜๊ณ , ๊ตฌํ˜„ ๋ฐฉ์•ˆ๋„ ์ˆ˜๋‘๋ฃฉํ•˜์ง€๋งŒ ๊ทธ์ค‘ ๋ฌด์—‡์„ ์„ ํƒํ•˜๋А๋ƒ๋Š” ๋‚˜์˜ ํŒ๋‹จ์— ์˜ํ•œ๋‹ค. ๊ทธ ๊ทผ๊ฑฐ๋“ค๋„ ์žˆ์–ด์•ผ ํ•œ๋‹ค.

 

์ด๋ฒˆ ์ด์Šˆ๋ฅผ ํ†ตํ•ด ๋‹จ์ˆœํžˆ ์•Œ๋ฆผ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ž‘์—…์„ ๋„˜์–ด, ๊ธฐ๋Šฅ์˜ ๋ณต์žก๋„๋ฅผ ์–ด๋””๊นŒ์ง€ ๊ฐ€์ ธ๊ฐˆ ๊ฒƒ์ธ์ง€, ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๊ณผ ๊ฐœ๋ฐœ ํšจ์œจ์˜ ๊ท ํ˜•์„ ์–ด๋–ป๊ฒŒ ์žก์„์ง€๋ฅผ ๊ณ ๋ฏผํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์•ž์œผ๋กœ๋„ ํŒŒ์ดํŒ…!!