-
16.Socket 연결MERN Stack ChatApp 2024. 8. 21. 23:30
https://socket.io/how-to/use-with-nextjs
socket setup 이벤트를 챗룸을 클릭했을 때가 아니라, 그냥 접속하면 바로 달아줘야함.
https://corner-ds.tistory.com/71
소켓미들웨어는 안될것 같고, (protect의 작동방식때문에) zustand가 처음 렌더링될때부터 상태를 어떻게 가져올 수 있는지를 알아봐야할 듯 하다. (useUser를 사용하기 위해
https://zustand.docs.pmnd.rs/integrations/persisting-store-data#how-can-i-rehydrate-on-storage-event
https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5?comments_sort=top
https://stackoverflow.com/questions/32674391/io-emit-vs-socket-emit
-- 여러가지 봤던 자료들. 모두 쓰이지는 않았다.
https://www.youtube.com/watch?v=Auh7clwIngE&t=693s
를 보며 하고 있다. 그런데 동영상이 오래되서인지 잘 안되는 게 많아서 개빡쳤다. 새로 자료조사 다하면서 만듬
-----
우선 가장 기억에 남는 건 메세지 수신이 잘 안되서 진짜 개빡쳤다.
동영상에서 socket.in()인가 뭐시긴가에 user._id를 넣어서 했는데, socket.in()이라는 게 있기는 하지만 잘 동작하지 않는 것 같고,
socket.to()를 사용하는 게 낫다.
그리고 이벤트끼리의 이름을 잘 맞춰주는 건 너무나 당연하다. (socket.emit("event1",message), socket.on("event1", message))
그런데 join을 할 때, room name이 여러개 들어갈 수 있는데, 이걸 잘 해줘야한다.
무슨일인고 하니, 이 프로젝트에서는 noti가 있다. 같은 방에 들어가있는 유저가 메세지를 보냈을 때는 noti가 안오지만,
다른방에 들어가있는 유저가 메세지를 보내면 noti가 간다. 그러기 위해선 noti와 채팅방 둘다 socket을 연결해줘야 한다.
이를 구현하기 위해 처음 유저가 로그인을 하고 들어가면 setup이라는 이벤트로 로그인한 유저의 id를 socket에 join하고,
채팅방에 들어갔을 때는 채팅방의 id를 join시킨다.
그런데 문제가 join은 여러 room에 접속할 수는 있지만, 이 join을 한번에 해줘야지 여러번에 나눠서 하게되면 처음 join만 유효하고 다음 join들은 무시되었다. 그러니깐
socket.on("setup", (userData) => { socket.join(userData._id); }); socket.on("join chat", (room) => { socket.join(room); });
이렇게 됬을 때, 처음 접속을 해서 setup이벤트를 통해 userData._id를 join시키고 나면 join chat 이벤트를 통해 채팅방에 들어갔을 때 room 으로 join이 안되는 현상이 발생했다.
이를 해결하기 위해,
// front socket.emit("join chat", [chat._id, loginUser?._id]); // back socket.on("setup", (userData) => { socket.join(userData._id); // console.log("userData : ", userData); console.log("@@@ setup - userData._id : ", userData._id); socket.emit("connected one on one"); }); socket.on("join chat", (info) => { // socket.join(room); const [room, userId] = info; socket.join(info); console.log("User Joined room : ", room, " userId : ", userId); console.log("socket.rooms : ", socket.rooms); });
이렇게 프론트에서 join chat이벤트를 보낼 때, 로그인 되어있는 유저의 id와 들어가려는 채팅방의 id를 같이 보내고, 백에서 join을 할 때, 이 두개를 한꺼번에 join시키는 방법을 사용했다.
이걸 알아내는 데 시간이 꽤 오래걸렸다. 그래도 되서 너무너무 좋다.
또, zustand에서 상태를 불러올 때, 페이지가 렌더링되고 나서 상태를 다 불러온다. 이경우 위의 setup처럼 유저가 접속하자마자 socket과 연결하고 user의 아이디를 보내줘야 하는 경우, null이나 undefined를 보내버릴 수 있다. 그렇다면 연결된 user가 없게되니 noti기능이 제대로 작동하지 않을 수 있다. 이를 해결하기 위해 렌더링 될 때 zustand의 state를 먼저 불러오고 난 다음 렌더링을 하도록 여러 검색과 시도를 해봤지만 잘 되지 않았고, zustand의 기능중 처음 기본값을 넣어놓는 기능도 있었지만 이 프로젝트에서는 유저의 정보를 localStorage에서 불러와야 했기에 맞지않아 이건 쓰지 않았다.
쨌든 그래서 고안한 방법이 useEffect의 dependency array에서 상태를 감지해서, user의 상태가 들어오면 "setup"을 emit하도록 해줬다.
이 경우 문제점은 localStorage에도 user가 없을 때 인데, 이를 어떻게 해줘야 할지 고민중이다. user가 없다고 바로 로그인 화면으로 보내버리면은 위에서 말한것 처럼 state를 불러오는 시간이 더 늦기때문에 처음에 렌더링 될 때 user는 null이고, 이럼 user를 불러오기도 전에 바로 로그인 화면으로 보내버리는 것이다. 서버액션의 경우 cookie에서 불러와서 cookie가 없을경우 로그인화면으로 리다이렉트를 시키고 있는데, 서버 요청이 있을 때 검사를 하고 보내기에 이는 별 문제가 되지 않는데, 현재 프론트 화면의 경우는 렌더링이 될 때 해야하는 거라서 어떻게 할지 고민중이다. 중간에 로딩화면 같은거를 하나 끼워서 그 화면에서 유저정보를 받으면 그걸 다음화면으로 넘기면서 props로 넘겨줘야 하는 걸까?? 이 화면은 localStorage에 없으면 로그인 화면으로 넘기고... 이것도 괜찮은 것 같다. 그런데 나중에 props drilling이 심해지지 않을지... 아닌가, 처음 setup 이벤트를 위해 서버에 user의 id만 넘겨주면 되니 상관없나. 고민좀 해봐야겠다.
잠깐 셌는데, 아래처럼 useEffect의 dependency array에 user를 넣어주고, user가 들어오면 setup을 emit하도록 해줬다.
const user = useUser((state) => state.user); const onConnect = () => { setIsConnected(true); setTransport(socket.io.engine.transport.name); socket.io.engine.on("upgrade", (transport) => { setTransport(transport.name); }); console.log("client socket connected"); console.log("onConnect - user : ", user); if (user) { console.log("setup - user"); socket.emit("setup", user); } else { // router.push("/"); } }; useEffect(() => { // socket.on("disconnect", onDisconnect); console.log("@@@ user : ", user); socket.on("connect", onConnect); return () => { socket.off("connect", onConnect); // socket.off("disconnect", onDisconnect); // socket.disconnect(); }; }, [user]);
유저가 같은 채팅방에 들어와있으면 바로 채팅이 나오고, 채팅방에 안들어가있거나 다른 채팅방에 들어가있으면 noti가 올라가는 코드는 다음과 같다.
// server socket.on("newMessage", (newMessageReceived) => { let chat = newMessageReceived.chat; if (!chat.users) { console.log("chat.users not defined"); } chat.users.forEach((user) => { if (user._id == newMessageReceived.sender._id) { return; } socket .to(user._id) .emit("socket-user-messageReceived", newMessageReceived); }); socket.broadcast .to(chat._id) .emit("socket-chat-messageReceived", newMessageReceived); }); // front useEffect(() => { socket.on("socket-user-messageReceived", (newMessageReceived) => { console.log("socket-user-messageReceived : ", newMessageReceived); if (!selectedChat || selectedChat._id !== newMessageReceived.chat._id) { addNoti({ message: newMessageReceived, isChecked: false }); } else { } }); socket.on("socket-chat-messageReceived", (newMessageReceived) => { console.log("socket-chat-messageReceived : ", newMessageReceived); // setMessage console.log("selectedChat : ", selectedChat); if (!selectedChat || selectedChat._id !== newMessageReceived.chat._id) { // addNoti({ message: newMessageReceived, isChecked: false }); } else { addMessage(newMessageReceived); } }); return () => { socket.off("socket-user-messageReceived"); socket.off("socket-chat-messageReceived"); }; }, [selectedChat]);
메세지를 받은 서버는 두개의 이벤트를 emit한다.
하나는 이 메세지를 보낸 채팅방에 emit. (밑의 socket.broadcast.to(chat._id).emit)
또 하나는 이 채팅방에 속해있는 유저들에게 보내는 emit (chat.users.forEach로 걸러주면서 socket.to(user._id)로 보내주는 emit)
그리고 프론트에서는 두 이벤트를 수신하는데, 선택된 챗방이랑 비교해서 (zustand로 선택된 챗방 상태를 관리해주고 있다.) 서버에서 보내온 메세지에 있는 챗방과 현재 유저가 선택한 챗방이 아이디가 같을 경우 메세지 추가, 다를 경우 noti에 추가한다. 이 때, 물론 채팅방에 속해있는 유저들에게 보내는 것만 해도 챗방아이디랑 비교해서 분기가 가능할 것 같긴 하지만, 쓰임새가 명확히 다르기에 나줘주는 편이 나을 것 같아 나눠줬다.
하면서 너무 오래걸려서 진짜 내가 뭐하고있나,, 포기할까 생각도 했지만 되서 너무 좋다.
기능적인 거는 처음 생각했던 게 어느정도 다 된것 같고, 더러운 코드와 ui를 고쳐야 하겠지만...
언젠가는 할수도 있겠지만 당장은 아닐 것 같고. 나중에 할수있으면 하겠다.
너무 좋다!!!
해결해야할 과제 : localStorage에 user가 없을 때, 어떻게 로그인화면으로 보낼 것인가
github : https://github.com/Wunhyeon/ChatApp-MERNStack/tree/16.websocket
'MERN Stack ChatApp' 카테고리의 다른 글
15. 채팅방에서 메세지 불러오기, 메세지 보내기 화면 (0) 2024.08.16 14. Message API (0) 2024.08.14 13. 채팅방 프론트 - 그룹챗 만들기 (0) 2024.08.13 12. 채팅방 프론트. 로그인보완 (서버사이트 상태저장) (0) 2024.08.06 11. 1:1 채팅방 API, 모든 내 대화방 가져오기, 그룹채팅방 만들기, 채팅방 이름 바꾸기 (0) 2024.08.05