๐Ÿงก Projects/๐Ÿงก Projects: Web

[ ๋ฌด๋ฌผ ] ๊ทผ์ฒ˜ ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ #3 : ๋‚˜์™€ ๋™์ผํ•œ ํ”„๋กœํ•„ ์œ ํ˜• ํด๋ฆญํ•˜๋ฉด ์Šคํƒฌํ”„ ์ ๋ฆฝ ๋ฐ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์• ๋‹ˆ๋ฉ”์ด์…˜

eyes from es 2025. 6. 30. 02:29
728x90
๋ฐ˜์‘ํ˜•
๋‚ด ์ฃผ๋ณ€ ์ฐพ๊ธฐ๋ฅผ ํ†ตํ•ด ๊ฐ์ง€๋œ ์‚ฌ์šฉ์ž ์ค‘ ํ”„๋กœํ•„ ์œ ํ˜•(Chat, Youtube,SNS, Books, Savings, Call) ์ค‘ ๋™์ผํ•œ ์œ ํ˜•์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ๋‚˜์˜ stamp ๊ฐ€ ์ฆ‰๊ฐ์ ์œผ๋กœ +1 ์ด ๋˜๊ณ , ๋˜ ๋‚ด๊ฐ€ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž B๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ B์˜ ํ™”๋ฉด์— ์•Œ๋ฆผ์ด ๊ฐ€๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

๐Ÿงญ ์š”๊ตฌ ์‚ฌํ•ญ

 

  • ์‚ฌ์šฉ์ž A๊ฐ€ B๋ฅผ ํด๋ฆญํ•˜๋ฉด B์˜ ํ™”๋ฉด์— A์˜ ์•„๋ฐ”ํƒ€ ์œ„์— ๐Ÿ’› ํ•˜ํŠธ ์ด๋ชจ์ง€ ํ‘œ์‹œ
  • A๋Š” ํด๋ฆญํ•œ ์œ„์น˜์— empty_stamp ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ณด์—ฌ์ฃผ๊ณ  ํ•ด๋‹น ์œ„์น˜๋ฅผ ์œ ์ง€
  • ์ด๋ฏธ ํด๋ฆญํ•œ ์‚ฌ์šฉ์ž๋Š” ๋‹ค์‹œ ํด๋ฆญํ•ด๋„ ๋ฐ˜์‘ ์—†์Œ
  • A์™€ B๊ฐ€ ๊ฐ™์€ ํ”„๋กœํ•„ ์œ ํ˜•์ผ ๊ฒฝ์šฐ A์˜ invited_count๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฆ๊ฐ€
  • invited_count์— ๋”ฐ๋ผ A์˜ ์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€ ๋ฐ boosterDisplay ์—…๋ฐ์ดํŠธ
  • ๋“ฑ์žฅ ์‹œ ๋ฐ”์šด์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”
    • boosterDisplay ๋ฐ ์•„๋ฐ”ํƒ€๊ฐ€ ๋กœ๋”ฉ ์ค‘์— default ์ด๋ฏธ์ง€๋กœ ๋จผ์ € ๋œจ๋Š” ํ˜„์ƒ์„ ํ•ด๊ฒฐ

 

 

์ด์ „ ๊ธ€์—์„œ ์‹ค์‹œ๊ฐ„ ์‚ฌ์šฉ์ž๋ฅผ ๊ฐ์ง€ํ•˜๊ณ , userId๋กœ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์œ ํ˜•์„ ๊ฐ€์ ธ์™”์œผ๋ฏ€๋กœ, ์ด์ œ๋Š” ํด๋ฆญํ–ˆ์„ ๋•Œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค. 

๊ธฐ๋Šฅ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€ ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”๊นŒ์ง€ ๊ณ ๋ คํ•œ ๊ณผ์ •์ž…๋‹ˆ๋‹ค.

 


 

๐Ÿงญ ๊ธฐ๋Šฅ ํ๋ฆ„ ์š”์•ฝ

  participant A as ์‚ฌ์šฉ์ž A
  participant B as ์‚ฌ์šฉ์ž B
  participant Server as WebSocket ์„œ๋ฒ„
  participant DB as Prisma API

  A->>Server: user_click ์ด๋ฒคํŠธ ์ „์†ก (A → B)
  Server-->>B: click_notice ์ด๋ฒคํŠธ ์ˆ˜์‹  (A๊ฐ€ ๋‹น์‹ ์„ ํด๋ฆญํ•จ)
  B->>ํ™”๋ฉด: ๐Ÿ’› ํ•˜ํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ‘œ์‹œ

  A->>ํ™”๋ฉด: empty_stamp ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ‘œ์‹œ
  A->>DB: B์˜ type ํ™•์ธ → A์™€ ๊ฐ™์œผ๋ฉด invited_count +1
  DB-->>A: count ๋ฐ˜ํ™˜ → UI ์—…๋ฐ์ดํŠธ

 


 

โœ… ๊ธฐ๋Šฅ1 : A๊ฐ€ B๋ฅผ ํด๋ฆญํ•˜๋ฉด B ํ™”๋ฉด์— ํ•˜ํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋„์šฐ๊ธฐ

์‚ฌ์šฉ์ž A๊ฐ€ B๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ, B์˜ ํ™”๋ฉด์— A ์•„๋ฐ”ํƒ€ ์œ„์— ํ•˜ํŠธ ์ด๋ชจ์ง€๊ฐ€ 2์ดˆ๊ฐ„ ๋– ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
์‹ค์‹œ๊ฐ„์œผ๋กœ ๋™์ž‘ํ•ด์•ผ ํ•˜๋ฉฐ, ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ์žˆ์„ ๊ฒฝ์šฐ์—๋„ ์ •ํ™•ํ•œ ๋Œ€์ƒ์—๊ฒŒ๋งŒ ๋ณด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์‚ฌ์šฉ์ž A๊ฐ€ B๋ฅผ ํด๋ฆญ
   ↓
A ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„์— user_click ๋ฉ”์‹œ์ง€ ์ „์†ก
   ↓
์„œ๋ฒ„๋Š” toUserId(B)์—๊ฒŒ๋งŒ click_notice ์ด๋ฒคํŠธ ์ „์†ก
   ↓
B ํด๋ผ์ด์–ธํŠธ๋Š” fromUserId(A)์˜ ์•„๋ฐ”ํƒ€์— ํ•˜ํŠธ ํ‘œ์‹œ
   ↓
2์ดˆ ํ›„ ํ•˜ํŠธ ์‚ฌ๋ผ์ง

 

1๏ธโƒฃ ํด๋ผ์ด์–ธํŠธ → ์„œ๋ฒ„: ํด๋ฆญ ๋ฉ”์‹œ์ง€ ์ „์†ก

// ๐Ÿ“ NearbyUserAvatar.tsx
const handleClick = () => {
  if (isClicked || !wsRef.current) return;

  wsRef.current.send(
    JSON.stringify({
      type: "user_click",
      fromUserId: myUserId,
      toUserId: userId,
      fromUserName: myName, // ๐Ÿ’ก ์ด๋ฆ„๋„ ํ•จ๊ป˜ ์ „์†ก
    })
  );

  setClicked(true); // ํ•œ ๋ฒˆ ํด๋ฆญ ํ›„ ์žฌํด๋ฆญ ๋ฐฉ์ง€
};

 

 

  • ์ค‘๋ณต ํด๋ฆญ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด isClicked ์ƒํƒœ๋กœ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.
  • ์„œ๋ฒ„๋กœ๋Š” fromUserId, toUserId, ๊ทธ๋ฆฌ๊ณ  ๋ฐœ์‹ ์ž์˜ ์ด๋ฆ„๋„ ํ•จ๊ป˜ ์ „์†กํ•˜์—ฌ, ์ƒ๋Œ€๋ฐฉ์ด ์•Œ๋ฆผ์„ ๋ฐ›์„ ๋•Œ ๋ˆ„๊ฐ€ ํด๋ฆญํ–ˆ๋Š”์ง€ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

2๏ธโƒฃ ์„œ๋ฒ„์—์„œ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ํ›„ → ํด๋ฆญ๋œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ ์ „์†ก

else if (data.type === "user_click" && data.fromUserId && data.toUserId) {
  const senderName = data.fromUserName || data.fromUserId;

  for (const [targetWs, info] of clients.entries()) {
    if (info.userId === data.toUserId && targetWs.readyState === WebSocket.OPEN) {
      targetWs.send(
        JSON.stringify({
          type: "click_notice",
          fromUserId: data.fromUserId,
          fromUserName: senderName,
          toUserId: data.toUserId,
        })
      );
    }
  }
}

 

 

  • ์›น ์†Œ์ผ“์— user_click ํƒ€์ž…์˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋“ค์–ด์˜ค๋ฉด,
  • ์ˆ˜์‹ ์ž์˜ userId์™€ ์ผ์น˜ํ•˜๋Š” WebSocket์„ ์ฐพ์•„ ํ•ด๋‹น ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ click_notice ํƒ€์ž…์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
  • ์ด๋ฆ„์€ ์ต๋ช… ๋งˆ์Šคํ‚น ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ„๋„๋กœ ํฌํ•จํ•ด ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

 

3๏ธโƒฃ ์ˆ˜์‹  ํด๋ผ์ด์–ธํŠธ: ํ•˜ํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ‘œ์‹œ

// ๐Ÿ“ NearbyContent.tsx
socket.onmessage = (event) => {
  const message = JSON.parse(event.data);

  if (message.type === "click_notice") {
    const { fromUserId } = message;
    setHeartAnimation((prev) => ({
      ...prev,
      [fromUserId]: true, // ๐Ÿ’› ํ•˜ํŠธ ํ‘œ์‹œ ์‹œ์ž‘
    }));

    // 2์ดˆ ๋’ค ์ž๋™ ์ œ๊ฑฐ
    setTimeout(() => {
      setHeartAnimation((prev) => ({
        ...prev,
        [fromUserId]: false,
      }));
    }, 2000);
  }
};

 

 

  • click_notice ํƒ€์ž… ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ์ˆ˜์‹ ์ž ํ™”๋ฉด์—์„œ๋งŒ ํ•˜ํŠธ ํ‘œ์‹œ๋ฅผ ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค.
  • fromUserId๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•˜ํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‚ฌ์šฉ์ž ๋ณ„๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ์ฒด ํ˜•ํƒœ๋กœ setHeartAnimation์„ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
  • 2์ดˆ ๋’ค ์ž๋™์œผ๋กœ ์‚ฌ๋ผ์ง€๋„๋ก setTimeout ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

 

4๏ธโƒฃ ์‹ค์ œ ํ•˜ํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ Œ๋”๋ง

๐Ÿงฉ ์œ„์น˜: NearbyUserAvatar.tsx

<AnimatePresence>
  {showHeart && (
    <motion.div
      initial={{ opacity: 0, y: -10 }}
      animate={{ opacity: 1, y: -30 }}
      exit={{ opacity: 0, y: -50 }}
      transition={{ duration: 1 }}
      className="absolute -top-6 text-2xl"
    >
      ๐Ÿ’›
    </motion.div>
  )}
</AnimatePresence>

 

  • AnimatePresence๋Š” framer-motion์—์„œ ์š”์†Œ์˜ ์ง„์ž…/ํ‡ด์žฅ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • ํ•˜ํŠธ๋Š” ์œ„์ชฝ์œผ๋กœ ์‚ด์ง ๋– ์˜ค๋ฅด๋ฉฐ ๋‚˜ํƒ€๋‚ฌ๋‹ค๊ฐ€ ์‚ฌ๋ผ์ง€๋„๋ก Y์ถ• ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ํ•œ๋ˆˆ์— ๋“ค์–ด์˜ค๋Š” ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ๊ธฐ ์œ„ํ•ด ํ”„๋กœ์ ํŠธ ๋ฉ”์ธ ์ปฌ๋Ÿฌ์ธ ๋…ธ๋ž‘์ƒ‰ ๐Ÿ’› ์ด๋ชจ์ง€๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

 


 

 

โœ… ๊ธฐ๋Šฅ 2 :  A๊ฐ€ ํด๋ฆญํ•œ ์œ„์น˜์— empty_stamp ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ‘œ์‹œ ๋ฐ ๊ณ ์ •

์‚ฌ์šฉ์ž A๊ฐ€ B๋ฅผ ํด๋ฆญํ•˜๋ฉด, B์˜ ์•„๋ฐ”ํƒ€ ์œ„์น˜์— empty_stamp ์ด๋ฏธ์ง€๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•˜๋ฉฐ,
์ดํ›„ ๋ Œ๋”๋ง์—์„œ๋„ ๋™์ผํ•œ ์œ„์น˜์— ๊ณ ์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
์ด๋ฏธ ํด๋ฆญํ•œ ์‚ฌ์šฉ์ž๋Š” ๋‹ค์‹œ ํด๋ฆญํ•ด๋„ ์•„๋ฌด๋Ÿฐ ๋ฐ˜์‘์ด ์—†์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
์‚ฌ์šฉ์ž A๊ฐ€ B๋ฅผ ํด๋ฆญ
↓
A ํด๋ผ์ด์–ธํŠธ๋Š” ํด๋ฆญ ๋‹น์‹œ B์˜ ์œ„์น˜(angle, distance)๋ฅผ ์ €์žฅ
↓
ํ•ด๋‹น ์œ„์น˜์— empty_stamp ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ Œ๋”๋ง
↓
๋‹ค์‹œ ๋ Œ๋”๋งํ•  ๋•Œ๋„ ์ €์žฅ๋œ ์œ„์น˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ empty_stamp ์œ ์ง€
↓
์ด๋ฏธ ํด๋ฆญํ•œ B๋Š” ๋‹ค์‹œ ํด๋ฆญ๋˜์ง€ ์•Š์Œ

 

๐Ÿ˜“ ๋ฌธ์ œ ๋ฐœ์ƒ

 

์ฒ˜์Œ์—๋Š” positionCache๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํด๋ฆญ๋œ ์‚ฌ์šฉ์ž์—๊ฒŒ empty_stamp๋ฅผ ๋ Œ๋”๋งํ–ˆ์Šต๋‹ˆ๋‹ค.

// NearbyContent.tsx
const position = positionCache.current.get(user.userId);

 

ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๋ฉด ๋ Œ๋”๋ง ํƒ€์ด๋ฐ์ด๋‚˜ ์œ„์น˜ ๊ณ„์‚ฐ ์ˆœ์„œ์— ๋”ฐ๋ผ
ํด๋ฆญ ๋‹น์‹œ ์œ„์น˜๊ฐ€ ์•„๋‹Œ ์ƒˆ๋กœ์šด ์œ„์น˜์— empty_stamp๊ฐ€ ์ฐํžˆ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์‹ค์ œ๋กœ๋Š” B๊ฐ€ angle 45°, distance 100px์— ์žˆ์—ˆ์ง€๋งŒ
  • ๋ Œ๋”๋ง๋  ๋•Œ๋Š” ๋‹ค๋ฅธ ์œ ์ €๋“ค๊ณผ ๊ฒน์น˜์ง€ ์•Š๊ฒŒ ์ƒˆ๋กญ๊ฒŒ ํ• ๋‹น๋œ ์œ„์น˜๋กœ ํ‘œ์‹œ๋จ
    empty_stamp๊ฐ€ '๋œฌ๊ธˆ์—†๋Š” ๊ณณ'์— ์ฐํž˜...!

 

๐Ÿงจ ๋ฌธ์ œ ์ฝ”๋“œ

let position = wasClicked
  ? positionCache.current.get(user.userId) // โŒ ํด๋ฆญ ๋‹น์‹œ ์œ„์น˜๊ฐ€ ์•„๋‹˜
  : positionCache.current.get(user.userId);
  • positionCache์— ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์œ„์น˜๊ฐ€ ์—†์„ ๊ฒฝ์šฐ,
    • ์ฃผ๋ณ€ ์œ ์ €๋“ค๊ณผ ๊ฒน์น˜์ง€ ์•Š๋„๋ก
    • ๋žœ๋คํ•œ angle, distance ์กฐํ•ฉ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์‹œ๋„ํ•˜์—ฌ ์œ„์น˜๋ฅผ ์ •ํ•ฉ๋‹ˆ๋‹ค.
    ์ฆ‰, positionCache์˜ ์œ„์น˜๋Š” ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค "๋‹ค์‹œ ๊ณ„์‚ฐ๋  ๊ฐ€๋Šฅ์„ฑ"์ด ์žˆ์—ˆ๊ณ ,
    ํŠนํžˆ ์ƒˆ๋กœ ๋“ฑ์žฅํ•œ ์‚ฌ์šฉ์ž๋‚˜ positionCache๊ฐ€ ์ดˆ๊ธฐํ™”๋œ ๊ฒฝ์šฐ์—” ํ•ญ์ƒ ์ƒˆ ์œ„์น˜์— icon์ด ์ƒ์„ฑ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

โœ… ๊ทธ๋ž˜์„œ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋‚˜?

1๏ธโƒฃ A ํด๋ผ์ด์–ธํŠธ์—์„œ ํด๋ฆญ ๋‹น์‹œ ์œ„์น˜ ๋ณ„๋„ ์ €์žฅ

// NearbyContent.tsx
if (clickedType === myType) {
  const pos = positionCache.current.get(targetId);
  if (pos) clickedUserPositions.current.set(targetId, pos); // ํด๋ฆญ ์‹œ ์œ„์น˜ ์ €์žฅ

  setInteractedUserIds((prev) => new Set(prev).add(targetId));
  ...
}

 

2๏ธโƒฃ ๋ Œ๋”๋ง ์‹œ์—๋Š” clickedUserPositions๋ฅผ ์šฐ์„  ์‚ฌ์šฉ

let position = wasClicked
  ? clickedUserPositions.current.get(user.userId) // โœ… ํด๋ฆญ ๋‹น์‹œ ์œ„์น˜
  : positionCache.current.get(user.userId);

 

ํด๋ฆญ ์‹œ์ ์˜ ์œ„์น˜๋ฅผ ๋”ฐ๋กœ ์ €์žฅํ•˜๋Š” clickedUserPositions ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๊ณ , empty_stamp ๋ Œ๋”๋ง์€ ํ•ญ์ƒ ์ด ์ €์žฅ๋œ ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค

  • ํด๋ฆญ ๋‹น์‹œ์˜ ์œ„์น˜(angle, distance)๋ฅผ clickedUserPositions์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฐ™์€ ์‚ฌ์šฉ์ž๋ฅผ ๋‘ ๋ฒˆ ํด๋ฆญํ•˜์ง€ ์•Š๋„๋ก ์ฒ˜์Œ์— ์กฐ๊ฑด๋ฌธ์œผ๋กœ ๊ฑธ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค.

 

 

 


 

โœ… ๊ธฐ๋Šฅ 3 : invited_count์— ๋”ฐ๋ผ boosterDisplay ๋ฐ ์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€ ์‹ค์‹œ๊ฐ„ ๋ณ€๊ฒฝ

์‚ฌ์šฉ์ž A๊ฐ€ ๋™์ผํ•œ ์œ ํ˜•์˜ ์‚ฌ์šฉ์ž B๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ, A์˜ invited_count๊ฐ€ 1 ์ฆ๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
์ด ์ˆ˜์น˜๋Š” ๋ฐ”๋กœ UI์— ๋ฐ˜์˜๋˜์–ด์•ผ ํ•˜๋ฉฐ, ๋ ˆ๋ฒจ ์กฐ๊ฑด(5/10)์„ ๋งŒ์กฑํ•˜๋ฉด ์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€๋„ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.
boosterDisplay ๋ฌธ๊ตฌ("๋ ˆ๋ฒจ์—…๊นŒ์ง€ N๋ช…!" ๋˜๋Š” "๋งŒ๋ ™!")๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

A๊ฐ€ B๋ฅผ ํด๋ฆญ
↓
A์™€ B์˜ ์„ฑํ–ฅ์„ ๋น„๊ต
↓
๋™์ผํ•˜๋ฉด /api/invite/increase ํ˜ธ์ถœ
↓
A์˜ ์ดˆ๋Œ€ ์ˆ˜(invited_count) +1
↓
์ฟผ๋ฆฌ ๊ฐฑ์‹  → boosterDisplay ๋ฐ ์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ

 

 

 

1๏ธโƒฃ ์ดˆ๋Œ€ ์ˆ˜ ์ฆ๊ฐ€ API

// ๐Ÿ“ /api/invite/increase.ts
export async function POST(req: NextRequest) {
  const { inviterId } = await req.json();

  const updated = await prisma.user.update({
    where: { id: inviterId },
    data: {
      invited_count: {
        increment: 1,
      },
    },
  });

  return NextResponse.json({ invited_count: updated.invited_count });
}
  • inviterId๋งŒ ์ „๋‹ฌํ•˜๋ฉด ์ดˆ๋Œ€ ์ˆ˜๋ฅผ 1 ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค.
  • ์˜ˆ์™ธ ์ƒํ™ฉ(์—†๋Š” ์œ ์ € ID ๋“ฑ)์— ๋Œ€ํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” status code๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

 

2๏ธโƒฃ invited_count ์ฆ๊ฐ€ ์š”์ฒญ - ์ปค์Šคํ…€ ํ›… useIncreaseInvitedCount

import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";

export function useIncreaseInvitedCount(userId?: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async () => {
      const res = await axios.post("/api/invite/increase", {
        inviterId: userId,
      });
      return res.data;
    },
    onSuccess: () => {
      // ๐Ÿ” ์บ์‹œ ์—…๋ฐ์ดํŠธ → ์‹ค์‹œ๊ฐ„ UI ๋ฐ˜์˜
      queryClient.invalidateQueries({
        queryKey: ["userInfo", userId],
      });
    },
  });
}

 

  • ๋‹จ๋ฐœ์„ฑ POST ์š”์ฒญ์„ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ์„ฑ๊ณต ์‹œ์—๋งŒ UI๋ฅผ ๊ฐฑ์‹ ํ•˜๊ธฐ ์œ„ํ•ด useMutation์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์šฐ์„  ์ดˆ๋Œ€ ์ˆ˜ ์ฆ๊ฐ€ ๋กœ์ง์€ ํด๋ฆญ ์‹œ ํ•œ๋ฒˆ๋งŒ ์„œ๋ฒ„์— POST ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ์ž‘์—…์ด๋ฏ€๋กœ ์ฝ”๋“œ ํ•œ์ค„์˜ ํ˜ธ์ถœ๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

  • ๋˜, ์š”์ฒญ ์„ฑ๊ณต ์‹œ์ธ onSuccess์—๋งŒ ์ฟผ๋ฆฌ๋ฅผ invalidateํ•˜์—ฌ ๊ฐฑ์‹ ํ•˜๋„๋ก ๊ตฌ์„ฑํ•จ์œผ๋กœ์จ, "DB์— ์ •๋ง ๋ฐ˜์˜๋œ ๊ฒฝ์šฐ์—๋งŒ" ํ”„๋ก ํŠธ์— ๋ฐ˜์˜๋  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ณ , userInfo ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๊ฒŒ ๋งŒ๋“ค์–ด UI์— ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.

 

3๏ธโƒฃ ํด๋ฆญํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์ผ ์œ ํ˜•์ผ ๋•Œ mutation.mutate() ํ˜ธ์ถœ

// NearbyContent.tsx
if (clickedType === myCharacterType && clickedUserId !== myUserId) {
  increaseInvitedCountMutation.mutate(); // โœ… ์‹ค์‹œ๊ฐ„ invited_count ์ฆ๊ฐ€
}

 

  • ํƒ€์ž…์ด ๋™์ผํ•œ ๊ฒฝ์šฐ์—๋งŒ ์นด์šดํŠธ๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค.
  • clickedUserId !== myUserId ์กฐ๊ฑด์œผ๋กœ ์ž๊ธฐ ์ž์‹ ์„ ํด๋ฆญํ•ด๋„ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š๋„๋ก ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

 

 

4๏ธโƒฃ ์‹ค์‹œ๊ฐ„ UI ๋ฐ˜์˜ - boosterDisplay, ์•„๋ฐ”ํƒ€ ์—…๋ฐ์ดํŠธ

// NearbyUserAvatar.tsx
const invitedCount = userInfo?.invited_count ?? 0;
const level = invitedCount >= 10 ? 3 : invitedCount >= 5 ? 2 : 1;

const imageSrc =
  characterType === "default"
    ? "/assets/moono/default-moono.png"
    : `/assets/moono/${level === 1 ? "" : `lv${level}/`}${characterType}-moono.png`;

 

  • ์ดˆ๋Œ€ ์ˆ˜(invitedCount)์— ๋”ฐ๋ผ ๋ ˆ๋ฒจ์„ ๊ณ„์‚ฐํ•˜๊ณ , ํ•ด๋‹น ๋ ˆ๋ฒจ๋ณ„ ํด๋”์—์„œ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ๋™์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
{invitedCount >= 10 ? (
  <span className="font-bold text-green-600">๋งŒ๋ ™!</span>
) : (
  <span className="font-bold text-yellow-600">
    ๋ ˆ๋ฒจ์—…๊นŒ์ง€ {5 - (invitedCount % 5)}๋ช… ๋‚จ์Œ!
  </span>
)}

 

 

  • invited_count๊ฐ€ 10 ์ด์ƒ์ด๋ฉด "๋งŒ๋ ™!", ์•„๋‹ˆ๋ฉด "๋ ˆ๋ฒจ์—…๊นŒ์ง€ N๋ช… ๋‚จ์Œ"์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.
  • useMutation์˜ invalidateQueries → useQuery → ์บ์‹œ ๊ฐฑ์‹  ํ๋ฆ„์ด ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํด๋ฆญ ์‹œ์— ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค.

 

 


 

 

โœ… ๊ธฐ๋Šฅ 4: ๋กœ๋”ฉ ์„ฑ๋Šฅ ์ตœ์ ํ™”

boosterDisplay ๋ฐ ์•„๋ฐ”ํƒ€๊ฐ€ ๋กœ๋”ฉ๋  ๋•Œ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€๋กœ ๊นœ๋นก์ด๋Š” ํ˜„์ƒ ์—†์ด, ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ์ด ๋๋‚œ ํ›„์— ๋ Œ๋”๋ง๋˜๋„๋ก ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.
์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง๊ณผ ๋กœ๋”ฉ ์ค‘ UI ๊นœ๋นก์ž„์„ ์ตœ์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค.

 

 

โ— ๋ฌธ์ œ ์ƒํ™ฉ: ๋กœ๋”ฉ ์ค‘ ๊นœ๋นก์ž„ (default ์ด๋ฏธ์ง€ / ์ž˜๋ชป๋œ booster ๋ฌธ๊ตฌ)

 

๐Ÿ” ์ฆ์ƒ 1. boosterDisplay์—์„œ "0๋ช…" ๋ฌธ๊ตฌ๊ฐ€ ์ž ๊น ๋‚˜ํƒ€๋‚จ

  • ์‚ฌ์šฉ์ž invited_count๋Š” useGetUserInfo(userId)๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ๋กœ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ,
  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋จผ์ € ๋ Œ๋”๋ง๋˜๋ฉฐ undefined ๋˜๋Š” 0์„ ๊ธฐ์ค€์œผ๋กœ ์ž˜๋ชป๋œ booster ๋ฌธ๊ตฌ๊ฐ€ ๋จผ์ € ๋œจ๋Š” ๋ฌธ์ œ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ ์ฝ”๋“œ

const invitedCount = userInfo?.invited_count ?? 0;

const boosterText = invitedCount >= 10
  ? "๋งŒ๋ ™!"
  : `๋ ˆ๋ฒจ์—…๊นŒ์ง€ ${5 - (invitedCount % 5)}๋ช…!`; // ์ž˜๋ชป๋œ ๋ฌธ๊ตฌ ์ถœ๋ ฅ ๊ฐ€๋Šฅ

 

 

  • userInfo๊ฐ€ undefined์ผ ๋•Œ 0์œผ๋กœ fallback๋˜๋ฉฐ "๋ ˆ๋ฒจ์—…๊นŒ์ง€ 5๋ช…!"์ด ๋จผ์ € ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
  • ์ดํ›„ ์„œ๋ฒ„ ์‘๋‹ต ๋„์ฐฉ ํ›„ "๋ ˆ๋ฒจ์—…๊นŒ์ง€ 2๋ช…!" ๋“ฑ์˜ ์ง„์งœ ๊ฐ’์œผ๋กœ ๊ฐฑ์‹ ๋˜์ง€๋งŒ, ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋Š” ๊นœ๋นก์ธ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์˜€์Šต๋‹ˆ๋‹ค.

 

 

 

 

๐Ÿ” ์ฆ์ƒ 2. ์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€๊ฐ€ default-moono.png๋กœ ๊นœ๋นก์˜€๋‹ค๊ฐ€ ์‹ค์ œ ์ด๋ฏธ์ง€๋กœ ๋ฐ”๋€œ

const characterType = profile?.type?.toLowerCase() ?? "default";
const imageSrc = `/assets/moono/${characterType}-moono.png`;

return (
  <Image src={imageSrc} alt="user-avatar" />
);

 

  • useGetUserCharacterProfile(userId)์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์•„์ง ๋ฐ›์•„์˜ค๊ธฐ ์ „์—๋Š” undefined๋กœ ๊ฐ„์ฃผ๋˜์–ด ๊ธฐ๋ณธ๊ฐ’ "default"๋ฅผ ๋จผ์ € ๋ Œ๋”๋ง
  • ์‹ค์ œ ์ด๋ฏธ์ง€ ํƒ€์ž…์ด ๋„์ฐฉํ•˜๋ฉด ๋‹ค์‹œ lv1/xxx-moono.png๋กœ ๋ฐ”๋€Œ๋ฉด์„œ ๋‘ ๋ฒˆ ๋ Œ๋”๋ง
  • ์ด ๊ณผ์ •์—์„œ default ์ด๋ฏธ์ง€๊ฐ€ ์ž ๊น ๊นœ๋นก์˜€๋‹ค๊ฐ€ ๊ต์ฒด๋จ → UX ์ €ํ•˜

 

๐Ÿ”จ ๊ฐœ์„  ์ฝ”๋“œ 1 : ์•„๋ฐ”ํƒ€ ๋ Œ๋”๋ง ์ตœ์ ํ™”

if (loading) return null; → ๋กœ๋”ฉ ์™„๋ฃŒ ํ›„์—๋งŒ ๋ Œ๋”๋ง ์ˆ˜ํ–‰
  • ์•„์˜ˆ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ฐฉํ•˜๊ธฐ ์ „์—๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ์œผ๋กœ์จ, ์‚ฌ์šฉ์ž๊ฐ€ ์ฒ˜์Œ ๋ณด๋Š” ํ™”๋ฉด์ด ์™„์„ฑ๋œ ์ •๋ณด ๊ธฐ๋ฐ˜์œผ๋กœ ์•ˆ์ •์ ์œผ๋กœ ๋ณด์ด๋„๋ก ๊ฐœ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

 

๐Ÿ”จ ๊ฐœ์„  ์ฝ”๋“œ 2 : boosterDisplay ๊นœ๋นก์ž„ ๋ฐฉ์ง€

const { data: userInfo, isLoading } = useGetUserInfo(userId);

if (isLoading || !userInfo) return null;

const invitedCount = userInfo.invited_count;

const boosterText = invitedCount >= 10
  ? "๋งŒ๋ ™!"
  : `๋ ˆ๋ฒจ์—…๊นŒ์ง€ ${5 - (invitedCount % 5)}๋ช…!`;

 

 

  • isLoading ๋˜๋Š” userInfo๊ฐ€ undefined์ผ ๋•Œ boosterText๋ฅผ ๋ Œ๋”๋งํ•˜์ง€ ์•Š๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • fallback ๊ฐ’(0๋ช…)์„ ๋„ฃ๋Š” ๋Œ€์‹  ์ •ํ™•ํ•œ invited_count๊ฐ€ ๋„์ฐฉํ•œ ๋’ค์—๋งŒ booster ๋ฌธ๊ตฌ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

 



๊ฒฐ๊ณผ ํ™”๋ฉด

 


 

๋งˆ์น˜๋ฉฐ... ์ถ”๊ฐ€๋กœ ์„ฑ๋Šฅ ๊ฐœ์„ ํ•  ์ ์ด ์—†์—ˆ์„๊นŒ?

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

 

์šฐ์„  ์ง€๊ธˆ ์ƒ๊ฐ๋‚˜๋Š” ๊ฐœ์„  ๋ฐฉํ–ฅ์€, 

 

โœ… ์œ„์น˜ ์บ์‹ฑ ๋กœ์ง ํ†ตํ•ฉํ•˜๊ธฐ

์ค‘๋ณต๋˜๋Š” ๋ถ€๋ถ„

 

NearbyContent์—์„œ ์•„๋ฐ”ํƒ€ ๋“ฑ์žฅ ์œ„์น˜๊ฐ€ ์„œ๋กœ ๊ฒน์น˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์บ์‹ฑํ•˜๋Š”๋ฐ, ์ด ๋ถ€๋ถ„์ด NearbyContent ์—์„œ๋„ ๋ณ„๋„๋กœ ์บ์‹ฑํ•˜๊ณ  ์žˆ๊ณ , NearbyUserAvataer์—์„œ๋„ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋กœ์ง์„ ๋ถ„๋ฆฌํ•ด์„œ ๋ณ„๋„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋งŒ๋“ค๋ฉด ๋” ๊น”๋”ํ•˜๊ณ  ์ค‘๋ณต ๊ณ„์‚ฐ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์—ˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 


 

โœ… boosterDisplay๋ฅผ  useMemo๋กœ ์บ์‹ฑํ•˜์—ฌ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ์žฌ๊ณ„์‚ฐํ•˜๊ธฐ + key

 

ํ˜„์žฌ useMutation์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋Š” ๋ฐ˜์˜ํ–ˆ์ง€๋งŒ boosterDisplay ์ž์ฒด๊ฐ€ ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ณ„์† ์žฌ๊ณ„์‚ฐ๋˜๋Š” ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์•„์ง ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์ „์— ๋ Œ๋”๋ง๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜์—ฌ ํ…์ŠคํŠธ๊ฐ€ ๊นœ๋นก์ด๋Š” ๋ฌธ์ œ๋Š” ๋ฐฉ์ง€ํ•˜์˜€์ง€๋งŒ, ํŠนํžˆ AnimatedCount ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ์ถ”๊ฐ€ ์ ์šฉํ•˜์—ฌ ์ˆซ์ž๊ฐ€ ์Šคํฌ๋กค๋˜๋ฉฐ ์˜ฌ๋ผ๊ฐ€๋Š” ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋Š”๋ฐ, ์ด ๋‚ด๋ถ€ ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ์žฌ์‹œ์ž‘๋˜๋Š”... ์•„์ฃผ ๋ถˆํ•„์š”ํ•œ ๊ณ„์‚ฐ์ด ๋“ค์–ด๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ useMemo๋ฅผ ํ†ตํ•ด ์บ์‹ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์ถ”๊ฐ€๋กœ ๋– ์˜ฌ๋ž์Šต๋‹ˆ๋‹ค (๋ฌผ๋ก  ์‹œ๊ฐ„ ์—†์–ด์„œ ์—…๋ฐ์ดํŠธ๋Š” ๋ชปํ–ˆ์ง€๋งŒ..)

const boosterDisplay = useMemo(() => {
  ...

}, [localInvitedCount, loading]);

 

๊ทธ๋Ÿฌ๋ฉด loading ์ด ์™„๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜, invitedCount ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ boosterDisplay๊ฐ€ ์žฌ๊ณ„์‚ฐ๋˜๊ณ  ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ๋งค๋ฒˆ ์žฌ๋žœ๋”๋ง๋˜์ง€ ์•Š์•„๋„ ๋˜์—ˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

์ถ”๊ฐ€๋กœ AnimatedCount ์ž์ฒด์—์„œ๋„ useMemo๋กœ ๊ฐ์‹ธ๋ฉด ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€ํ•˜๊ณ , invitedCount ๊ฐ’๋งŒ ๋ณ€๊ฒฝ๋  ๋•Œ ๋ Œ๋”๋ง์„ ๋‹ค์‹œ ํ•˜๋ฉด ๋˜๋ฏ€๋กœ key์— value๋กœ ํ•ด๋‹น ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ์žฌ๋žœ๋”๋ง์„ ์œ ๋„ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋„ค์š”.

 

 

 

https://github.com/Ureka-Middle-Team1/moo-mool/issues/216

 

[FEAT] ๊ทผ๊ฑฐ๋ฆฌ ํ†ต์‹ _๋‚˜์™€ ๋™์ผํ•œ ์œ ํ˜• ํด๋ฆญํ•˜๋ฉด ์Šคํƒฌํ”„ ์ ๋ฆฝ · Issue #216 · Ureka-Middle-Team1/moo-mool

๐Ÿ“Œ๋ชฉํ‘œ ๊ธฐ๋Šฅ ์‚ฌ์šฉ์ž A๊ฐ€ ๋™์ผํ•œ ํƒ€์ž…(Type)์˜ ์‚ฌ์šฉ์ž C๋ฅผ ํด๋ฆญ A์˜ ํ™”๋ฉด์—์„œ C ์•„์ด์ฝ˜์ด ํšŒ์ƒ‰ ์Šคํƒฌํ”„ ์•„์ด์ฝ˜์œผ๋กœ ๊ต์ฒด๋จ B์˜ ํ™”๋ฉด์—์„œ๋Š” C ์•„์ด์ฝ˜์ด ๊ทธ๋Œ€๋กœ ์œ ์ง€๋จ C๋Š” "A๊ฐ€ C๋ฅผ ํด๋ฆญํ–ˆ์Šต๋‹ˆ๋‹ค" ํŒ์—…

github.com

728x90
๋ฐ˜์‘ํ˜•