ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 14. Presence (현재 접속자)
    사이드프로젝트 2024. 7. 25. 22:08

    https://www.youtube.com/watch?v=-xXASlyU0Ck&t=2007s

    를 보며 실습하며 정리하는 글

    ---

     

    다음으로 접속자 상태를 알 수 있는 걸 만들겠다.

     

    먼저 components/ChatPresence.tsx 파일을 만들어주고, 기존 components/ChatHeader.tsx에서 온라인 상태를 표시해주던 부분을 가지고와 대체해준다.

     

    components/chatPresence.tsx

    import React from "react";
    
    const ChatPresence = () => {
      return (
        <div className="flex items-center gap-1">
          <div className="h-4 w-4 bg-green-500 rounded-full animate-pulse"></div>
          <h1 className="text-sm text-gray-400">2 Online</h1>
        </div>
      );
    };
    
    export default ChatPresence;

     

    components/ChatHeader.tsx

    "use client";
    
    import React from "react";
    import { Button } from "./ui/button";
    import { createClient } from "@/utils/supabase/client";
    import { User } from "@supabase/supabase-js";
    import { useRouter } from "next/navigation"; // 주의할 점. next/navigation에서 임포트한다.
    import ChatPresence from "./ChatPresence";
    
    const ChatHeader = ({ user }: { user: User | null }) => {
      const router = useRouter();
    
      // props로 UserResponse를 받는다.
      const handleLoginWithGithub = () => {
        const supabase = createClient();
        supabase.auth.signInWithOAuth({
          provider: "github",
          options: {
            redirectTo: location.origin + "/auth/callback",
          },
        });
      };
    
      const handleLogout = async () => {
        const supabase = createClient();
        await supabase.auth.signOut();
        // const { error } = await supabase.auth.signOut();
        router.refresh();
      };
    
      return (
        <div className="h-20">
          <div className="p-5 border-b flex items-center justify-between h-full">
            <div>
              <h1 className="text-xl font-bold">Daily Chat</h1>
              <ChatPresence />
            </div>
            {user ? ( // user가 있으면 (로그인했으면) 로그아웃 버튼 표시, 없으면 (로그인 안했으면) 로그인 버튼 표시
              <Button onClick={handleLogout}>Logout</Button>
            ) : (
              <Button onClick={handleLoginWithGithub}>Login</Button>
            )}
          </div>
        </div>
      );
    };
    
    export default ChatHeader;

     

     

    이제 components/ChatPresence.tsx에 가서 계속해서 작업을 해주자.

    먼저

    https://supabase.com/docs/reference/javascript/subscribe

     

    Subscribe to channel | Supabase

    Creates an event handler that listens to changes.

    supabase.com

    여기서 보면 Listen to presence Sync라는 메뉴가 있는데, 말그래도 현재 몇명있나 싱크를 맞춰주는 거다. 복사해서 가지고오자. 그리고 그걸 useEffect에 넣어준다.

    접속하고, 나가는거까지 표시해주고 싶으면  아래에서 sending state까지 복사해서 잘 조합해서 붙여넣기해준다. 나는 sending state까지 해주겠다.

    https://supabase.com/docs/guides/realtime/presence

     

    Presence | Supabase Docs

    Share state between users with Realtime Presence.

    supabase.com

    (여기서 주의해야 할게, channel이름을 저네 chat-room이라고 해줬었는데, 그거랑 다르게 해줘야 presence가 제대로 작동한다. 이유는 잘 모르겠는데, 같은 채널이름으로 해도 안되고, 예전에 ListMessages.tsx에서 만든 채널에다가 on으로 붙여줘도 제대로 작동하지 않느다. supabase reddit에 질문글을 올려놨는데, 아직 답변이 오진 않았다. https://www.reddit.com/r/Supabase/comments/1eba6kd/nextjs_supabase_realtime_insertdb_and_presence/ )

     

    // // // // components/ChatPresence.tsx
    "use client";
    
    import { useUser } from "@/lib/store/user";
    import { createClient } from "@/utils/supabase/client";
    import React, { useEffect, useRef, useState } from "react";
    import { toast } from "sonner";
    
    const ChatPresence = () => {
      const user = useUser((state) => state.user);
      const supabase = createClient();
    
      useEffect(() => {
        const channel = supabase.channel("chat-room1");
    
        console.log("ChatPresence - channel : ", channel);
    
        channel
          .on("presence", { event: "sync" }, () => {
            const newState = channel.presenceState();
            console.log("sync : ", newState);
          })
          .on("presence", { event: "join" }, ({ key, newPresences }) => {
            console.log("join", key, newPresences);
          })
          .on("presence", { event: "leave" }, ({ key, leftPresences }) => {
            console.log("leave : ", key, leftPresences);
          })
          .subscribe(async (status) => {
            if (status !== "SUBSCRIBED") {
              return;
            }
            // 여기서 track등록을 해주면, sync 이벤트의 channel.presenceState 부분에서 추적할 수 있게 나온다.
            const presenceTrackStatus = await channel.track({
              online_at: new Date().toISOString(),
              trackTest: "OOOOKKKK!!!",
            });
            console.log("presenceTrackStatus : ", presenceTrackStatus);
          });
      }, []);
    
      if (!user) {
        // 로그인 하지 않았을 때는, Presence를 보여주지 않도록.
        return <div className="h-3 w-1"></div>;
      }
    
      return (
        <div className="flex items-center gap-1">
          <div className="h-4 w-4 bg-green-500 rounded-full animate-pulse"></div>
          <h1 className="text-sm text-gray-400">2 Online</h1>
        </div>
      );
    };
    
    export default ChatPresence;

    그리고 새로고침을 하고 콘솔창을 켜보면 

    이렇게 접속한 사람들의 상태가 나타난다.

    잘 나타나서 좋긴한데, 여기서 문제가 뭐냐면 새롭게 탭을 생성해서 들어갈 때마다 접속자가 늘어나는 것처럼 보인다는 것이다.

    그러니깐 한 유저아이디로 크롬 탭을 여러개 켜서 접속하거나 하면 여러명이 접속한것처럼 나온다.

     

    이 문제를 해결해주기 위해 useEffect부분을 살짝 변경해준다.

    useEffect(() => {
        const channel = supabase.channel("chat-room1");
    
        console.log("ChatPresence - channel : ", channel);
    
        channel
          .on("presence", { event: "sync" }, () => {
            const newState = channel.presenceState();
            console.log("sync : ", newState);
          })
          .on("presence", { event: "join" }, ({ key, newPresences }) => {
            console.log("join", key, newPresences);
          })
          .on("presence", { event: "leave" }, ({ key, leftPresences }) => {
            console.log("leave : ", key, leftPresences);
          })
          .subscribe(async (status) => {
            if (status !== "SUBSCRIBED") {
              return;
            }
            // 여기서 track등록을 해주면, sync 이벤트의 channel.presenceState 부분에서 추적할 수 있게 나온다.
            const presenceTrackStatus = await channel.track({
              user_id: user?.id, // user를 추적할 수 있도록.
              online_at: new Date().toISOString(),
              trackTest: "OOOOKKKK!!!",
            });
            console.log("presenceTrackStatus : ", presenceTrackStatus);
          });
      }, [user]);

     

    subscribe부분인데, 저기서 track에 등록을 해주면 channel.presenceState()로 추적을 해줄 수 있다.

    그래서 같은 아이디인 것들은 중복을 제거하고 한 사람으로 해주면 되는 것이다. 

    이를 위해 channel.track 안에 변수로 user_id를 넣어줬다.

     

    또, user를 zustand store에서 useUser로 가져와주고 있는데, 처음 렌더링 했을때는 없다가 이후에 불러올 수 있기 때문에, 변화를 감지해주기 위해 useEffect의 두번째 변수로 user를 넣어줬다.

     

    이후에 콘솔을 찍어보면

    이렇게 같은 user_id가 찍힌 것들을 볼 수 있다.

    이친구들을 하나로 보고 접속중인 유저 처리를 해주면 된다.

     

    // components/ChatPresence.tsx
    "use client";
    
    import { useUser } from "@/lib/store/user";
    import { createClient } from "@/utils/supabase/client";
    import { RealtimePresenceState } from "@supabase/supabase-js";
    import React, { useEffect, useRef, useState } from "react";
    import { toast } from "sonner";
    
    const ChatPresence = () => {
      const user = useUser((state) => state.user);
      const supabase = createClient();
      const [onlineUsers, setOnlineUsers] = useState(0);
    
      useEffect(() => {
        const channel = supabase.channel("chat-room1");
    
        console.log("ChatPresence - channel : ", channel);
    
        channel
          .on("presence", { event: "sync" }, () => {
            // channel.presenceState()의 type은 '{ presence_ref: string; }'만 되어있기 때문에, 추가로 user_id도 타입으로 설정해준다.
            const newState: RealtimePresenceState<{ user_id: string }> =
              channel.presenceState();
            console.log("sync : ", newState);
            const userIds = [];
            for (const item in newState) {
              const userId = newState[item][0].user_id;
              userIds.push(userId);
            }
    
            setOnlineUsers(new Set(userIds).size);
          })
          .on("presence", { event: "join" }, ({ key, newPresences }) => {
            console.log("join", key, newPresences);
          })
          .on("presence", { event: "leave" }, ({ key, leftPresences }) => {
            console.log("leave : ", key, leftPresences);
          })
          .subscribe(async (status) => {
            if (status !== "SUBSCRIBED") {
              return;
            }
            // 여기서 track등록을 해주면, sync 이벤트의 channel.presenceState 부분에서 추적할 수 있게 나온다.
            const presenceTrackStatus = await channel.track({
              user_id: user?.id, // user를 추적할 수 있도록.
              online_at: new Date().toISOString(),
              trackTest: "OOOOKKKK!!!",
            });
            console.log("presenceTrackStatus : ", presenceTrackStatus);
          });
      }, [user]); // user의 상태가 변경되면 리 렌더링하도록 user를 넣어준다.
    
      if (!user) {
        // 로그인 하지 않았을 때는, Presence를 보여주지 않도록.
        return <div className="h-3 w-1"></div>;
      }
    
      return (
        <div className="flex items-center gap-1">
          <div className="h-4 w-4 bg-green-500 rounded-full animate-pulse"></div>
          <h1 className="text-sm text-gray-400">{onlineUsers} Online</h1>
        </div>
      );
    };
    
    export default ChatPresence;

     

    useState로 onLineUsers, 그러니깐 온라인인 유저들의 수를 관리해준다. 

    그리고 useEffect안의 channel.on("presence"), sync 이벤트 안을 보면 된다.

    타입을 살짝 바꿔줬고, newState안의 전체 유저아이디들을 모든 다음 set으로 중복을 제거하고 나온 수를 onlineUsers에 세팅을 해줬다.

    그 결과 html부분에서 {onlineUsers}로 표시된 부분에 알맞은 숫자가 들어가게 된다.

     

     

    탭을 여러개 키고, sync 콘솔안에 중복된 아이디가 여러개 찍히지만 2Online으로 맞는 숫자가 찍히는 걸 볼 수 있다!

     

    github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/14.Presence

     

    GitHub - Wunhyeon/Next-Supabase-Chat

    Contribute to Wunhyeon/Next-Supabase-Chat development by creating an account on GitHub.

    github.com

     

     

     

    바로는아니고, 다음에 해볼 것.

    https://getstream.io/blog/collaborative-nextjs-whiteboard/

     

    Build a Real-time Collaborative Whiteboard: NextJS, Supabase, Stream Video

    Dive into real-time application development & create a whiteboard with video and live presence using NextJS, Supabase, and Stream Video React SDK.

    getstream.io

     

    '사이드프로젝트' 카테고리의 다른 글

    16. 배포  (0) 2024.07.26
    15. Logout  (0) 2024.07.26
    13. Pagination  (0) 2024.07.24
    한글 두번씩 입력되는 버그해결  (2) 2024.07.24
    12. Arrow down & notification  (4) 2024.07.24

    댓글

Designed by Tistory.