TIL

2024.04.10 TIL #์ฑ„ํŒ… #์ฐธ์—ฌ์ค‘_๋ฉค๋ฒ„_์กฐํšŒ #Realtime_presence #์‹ค์‹œ๊ฐ„

inz1234 2024. 5. 7. 21:37

๐Ÿ“Œ Task TODOLIST

- [x] ์ฑ„ํŒ…๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ๋ฉค๋ฒ„๋“ค ํ”„๋กœํ•„ ์กฐํšŒ

- [x] ์ฑ„ํŒ…๋ฐฉ์— ์‹ค์‹œ๊ฐ„ online ๋ฉค๋ฒ„ ํ‘œ์‹œ


โœจ ๊ฐœ๋ฐœ ๋‚ด์šฉ

1. ์ฑ„ํŒ…๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ๋ฉค๋ฒ„๋“ค ํ”„๋กœํ•„ ์กฐํšŒ

 

Why(์ด์œ ) ?

ํ˜„์žฌ ์ฑ„ํŒ…๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ๋ฉค๋ฒ„๋“ค์ด ์–ด๋–ค ์‚ฌ๋žŒ๋“ค์ธ์ง€ ์•Œ ์ˆ˜ ์žˆ์–ด์•ผ ์ฑ„ํŒ…์„ ํ•  ๋•Œ

"๊ทธ๋ž˜์„œ ์ด ์‚ฌ๋žŒ์ด ์–ด๋–ค ์‚ฌ๋žŒ์ด๋”๋ผ?" ํ•  ์œ ์ €๋“ค์˜ ๋งˆ์Œ์— ๊ฐ์ •์ด์ž…์„.. 

 

How(๊ณผ์ •) ?

  (1) ํ˜„์žฌ ์ฑ„ํŒ…๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ๋žŒ๋“ค์˜ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜จ๋‹ค. 

  (2) ์ฑ„ํŒ…๋ฐฉ ํ—ค๋”์— ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ๋žŒ๋“ค์„ ๋‚˜์—ดํ•œ๋‹ค.

  (3) ๊ทธ ์ค‘์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฑ„ํŒ…๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ๋žŒ๋“ค์€ ์ปฌ๋Ÿฌ์ฒ˜๋ฆฌ, ์‹ค์‹œ๊ฐ„ ์ฐธ์—ฌ ์ค‘์ด ์•„๋‹Œ ์‚ฌ๋žŒ๋“ค์€ blur ์ฒ˜๋ฆฌํ•ด์•ผ๊ฒ ๋‹ค. 

 

what(๊ฒฐ๊ณผ) 

export const useParticipantsQuery = (roomId: string) => {
  const { data: participants } = useSuspenseQuery({
    queryKey: [PARTICIPANTS_QUERY_KEY, roomId],
    queryFn: () => fetchParticipants(roomId),
  return participants;
};

 

Supabase-realtime์˜ presence ๊ธฐ๋Šฅ์œผ๋กœ ํ˜„์žฌ ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ๋žŒ๋“ค์˜ user_id๋ฅผ ์–ป์–ด๋‚ด๊ณ  ๊ทธ๊ฑธ onlineUsers ๋ผ๋Š” state๋กœ Zustand-Store์— ์ €์žฅํ–ˆ๋‹ค.

const ChatPresence = () => {
  const { data: user } = useGetUserDataQuery();
  const { chatRoomId, onlineUsers, setOnlineUsers } = chatStore((state) => state);

  useEffect(() => {
    if (chatRoomId) {
      const channel = clientSupabase.channel(chatRoomId);
      channel
        .on('presence', { event: 'sync' }, () => {
          const nowUsers = [];
          for (const id in channel.presenceState()) {
            // @ts-ignore
            nowUsers.push(channel.presenceState()[id][0].user_id);
          }
          setOnlineUsers([...nowUsers]);
        })
        .subscribe(async (status) => {
          if (status === 'SUBSCRIBED') {
            await channel.track({ online_at: new Date().toISOString(), user_id: user?.user_id });
          }
        });
    }
  }, [user, chatRoomId]);

  return (
    <>
      <div className="flex gap-2">
        <div className="h-4  w-4 bg-mainColor rounded-full animate-pulse my-auto text-base"></div>
        {chatRoomId && <h1>{onlineUsers.length} online</h1>}
      </div>
    </>
  );
};

export default ChatPresence;

 

 

๊ทธ๋Ÿฐ ๋‹ค์Œ, ์ฑ„ํŒ…๋ฐฉ ํ—ค๋”์—์„œ ์ด ๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ๋žŒ๋“ค์˜ Avatar๋“ค์„ ๋‚˜์—ดํ•˜๊ณ 

์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ๋žŒ๋“ค(์ดํ•˜ onlineUsers)์€ ์ปฌ๋Ÿฌ๋กœ,

๊ทธ๋ ‡์ง€ ์•Š์€ ์‚ฌ๋žŒ๋“ค์€ opacity-30์œผ๋กœ ๋ธ”๋Ÿฌ์ฒ˜๋ฆฌ ํ–ˆ๋‹ค(Avatar์˜ Disabled ์†์„ฑ๋„ ์žˆ์—ˆ์ง€๋งŒ ์ด๊ฒŒ ๋” ๋šœ๋ ทํ•˜์—ฌ ์ด ๋ฐฉ๋ฒ• ํƒ.)

const ChatHeader = () => {
return (
<div className="flex gap-5 items-center">
            <ChatPresence />
            <AvatarGroup isBordered max={8}>
              {participants?.map((person) => (
                <Popover key={person.user_id} showArrow placement="bottom">
                  <PopoverTrigger>
                    <Avatar
                      as="button"
                      src={person.users.avatar as string}
                      className={`w-[32px] h-[32px] ${
                        !onlineUsers.find((id) => id === person.user_id) ? 'bg-black opacity-30' : ''
                      }`}
                    />
                  </PopoverTrigger>
                  <PopoverContent>
                    <ShowChatMember person={person.users} />
                  </PopoverContent>
                </Popover>
              ))}
            </AvatarGroup>
          </div>
)
}

export default ChatHeader;

๐Ÿšจ  TroubleShotting

๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท

-> ์‹ค์‹œ๊ฐ„ ์ฐธ์—ฌ ์ค‘์ธ ๋ฉค๋ฒ„๋งŒ ์ปฌ๋Ÿฌ์ฒ˜๋ฆฌ -> ์ด ์ฑ„ํŒ…๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์‚ฌ๋žŒ๋“ค์˜(ํ˜„์žฌ 2๋ช…) ํ”„๋กœํ•„ ์กฐํšŒ๊ฐ€๋Šฅ

๐Ÿ“š ๋ ˆํผ๋Ÿฐ์Šค

supabase-realtime-presence

Sync and track state

https://supabase.com/docs/guides/realtime/presence#sync-and-track-state