[ ๋ฌด๋ฌผ ] ๊ทผ์ฒ ์ฌ์ฉ์ ์ฐพ๊ธฐ #3 : ๋์ ๋์ผํ ํ๋กํ ์ ํ ํด๋ฆญํ๋ฉด ์คํฌํ ์ ๋ฆฝ ๋ฐ ์ค์๊ฐ ์๋ฆผ ์ ๋๋ฉ์ด์
๋ด ์ฃผ๋ณ ์ฐพ๊ธฐ๋ฅผ ํตํด ๊ฐ์ง๋ ์ฌ์ฉ์ ์ค ํ๋กํ ์ ํ(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๊ฐ ์ด๊ธฐํ๋ ๊ฒฝ์ฐ์ ํญ์ ์ ์์น์ 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