ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 항해 99 5기 TIL_83
    항해 99 2022. 4. 3. 02:24

    ▶ Today I Learned

     

    <실전 프로젝트>

     

    [브라우저를 닫아버리는 유저의 socket정보 삭제]

     

    1. 문제상황

    프론트에서 특정 유저가 뒤로가기로 나갔을 경우 socket방에서 제대로 나가지지 않는 것을 해결하기 위해

    뒤로가기로 방을 나가자마자 즉시 확인해 로컬스토리지에 해당 유저의 특정 방정보가 남아있는 지 확인 후

    socket.on(”quit room” ⇒ {socket.leave})를 실행시킨다.

    (cf. 해당 socket id가 특정방에서 이탈하는 것과 별개로 socket id는 우리의 서비스 사이트로 부터 벗어나면

    socket.disconnect()가 발생해 사라진다. 초반에 socket id를 생성시키는 io.connection()을 서비스 메인사이트에 들어올 때 해주기 때문이다.)

     

    하지만 여기서 특정 유저가 방 안에 있다가 브라우저를 바로 닫아버린다면?

    socket.leave가 실행되지 않아 여전히 특정 유저의 socketid가 특정 방에 속해있는 것 처럼 보인다.

     

    2. 해결과정

    First Trial: 따라서 이를 해결하기 위해 유저가 우리의 사이트로 부터 벗어나면, 즉, 브라우저를 닫으면 자동 실행되는 socket의 disconnecting이라는 이벤트 또한 사용하자는 의견이 나왔다.

     socket.disconnecting 부분에

    특정 소켓방에서 유저의 socket id를 제외시키기 위한 socket.leave를 추가,

    콘솔로 확인하기 위해 io.sockets.adapter.sids(socket의 서버에 들어있는 socket아이디와 그 아이디가 속해있는 방 정보)도 활용해주었다.

     

    코드는 아래와 같다.

     

    const app = require("./app");
    const server = require("http").createServer(app);
    const io = require("socket.io")(server, {
      cors: {
        origin: "*",
      },
    });
    
    const {
      sockets: {
          adapter: { sids, rooms },
      },
    } = io;
    
    //controller
    const RoomController = require("./controllers/roomController");
    
    const users = {};
    const socketToRoom = {};
    const socketToNickname = {};
    const socketToUser = {};
    
    io.on("connection", (socket) => {
      let roomId;
      let userId;
    
      let time = 0;
      let categoryId;
      let date;
    
      socket.on("join room", async (payload, done) => {
        roomId = payload.roomId;
        userId = payload.userId;
        categoryId = payload.categoryId;
        date = payload.date;
    
        if (!roomId) { socket.emit("no data") };
    
        if (users[roomId]) { // 기존 참가자 있음
          if (users[roomId].length >= 4) { // 참가자 풀방
            return done();
          };
    
          users[roomId].push(socket.id);
        } else {
          // 첫 참가자
          users[roomId] = [socket.id];
        };
       
        socket.join(roomId);
    
        socketToRoom[socket.id] = roomId; // 각각의 소켓아이디가 어떤 룸에 들어가는지
        socketToNickname[socket.id] = payload.nickname;
        socketToUser[socket.id] = {
          roomId: payload.roomId,
          id: payload.userId,
          nickname: payload.nickname,
          profileImg: payload.profileImg,
          masterBadge: payload.masterBadge,
          statusMsg: payload.statusMsg,
        };
    
        let others = users[roomId].filter((socketId) => socketId !== socket.id);
    
        const otherSockets = others.map((socketId) => {
          return {
            socketId,
            nickname: socketToNickname[socket.id],
          };
        });
        const otherUsers = others.map((socketId) => {
          return socketToUser[socketId]
        });
        
        socket.emit("send users", { otherSockets, otherUsers });
      });
    
      socket.on("send signal", (payload) => {
        const callerNickname = socketToNickname[payload.callerId];
        const userInfo = socketToUser[socket.id];
    
        io.to(payload.userToSignal).emit("user joined", {
          signal: payload.signal,
          callerId: payload.callerId,
          callerNickname,
          userInfo,
        });
      });
    
      socket.on("returning signal", (payload) => {
        io.to(payload.callerId).emit("receive returned signal", {
          signal: payload.signal,
          id: socket.id,
        });
      });
    
      socket.on("send_message", (payload) => {
        socket.broadcast.to(payload.roomId).emit('receive_message', payload);
      });
      socket.on("send_emoji", (payload) => {
        socket.broadcast.to(payload.roomId).emit('receive_emoji', payload);
      });
    
      socket.on("save_time", (payload) => {
        time = payload; // data received for every 1500 seconds
      });
    
      // 타이머
      socket.on("start_timer", (payload) => {
        socket.broadcast.to(payload.roomId).emit("start_receive", payload);
      });
    
      socket.on("stop_time", (roomId) => {
        socket.broadcast.to(roomId).emit("stop_receive");
      });
    
      socket.on("reset_time", (roomId) => {
        socket.broadcast.to(roomId).emit("reset_receive");
      });
    
      socket.on("check absence", () => {
        socket.emit("resend check absence", { socketId: socket.id, roomId: socketToRoom[socket.id] });
      });
    
      socket.on("quit room", async () => {
        const data = {
          roomId,
          userId,
          time: time,
          categoryId: categoryId,
          date: date,
        };
    
        await RoomController.delete.participant(data);
    
        if(users[roomId]) {
          users[roomId] = users[roomId].filter((id) => id !== socket.id);
        };
        const userInfo = socketToUser[socket.id];
    
        socket.broadcast.to(roomId).emit("user left", {
          socketId: socket.id,
          userInfo,
        });
    
        socket.leave(roomId);
    
        delete socketToNickname[socket.id];
        delete socketToUser[socket.id];
        delete socketToRoom[socket.id];
      });
    
    
    
    
    
    
    =============================================================================
    // 현재 글에서 가장 핵심은 이 부분!
    // 현재 글에서 가장 핵심은 이 부분!
    // 현재 글에서 가장 핵심은 이 부분!
    
      socket.on("disconnecting", async () => {
        
        // 방 정보 남아있으면은 방 나가기 처리하도록
    
        console.log("if(users[roomId].includes(socket.id))이 트루인지 펄스인지 궁금하다 조건문 실행 before", users[roomId].includes(socket.id)? true: false)
        console.log("disconnecting 할 때 조건문 before",sids );
        if(users[roomId].includes(socket.id)) {
          
          const data = {
            roomId,
            userId,
            time: time,
            categoryId: categoryId,
            date: date,
          };
      
          await RoomController.delete.participant(data);
      
          if(users[roomId]) {
            users[roomId] = users[roomId].filter((id) => id !== socket.id);
          };
          const userInfo = socketToUser[socket.id];
      
          socket.broadcast.to(roomId).emit("user left", {
            socketId: socket.id,
            userInfo,
          });
      
          socket.leave(roomId);
      
          delete socketToNickname[socket.id];
          delete socketToUser[socket.id];
          delete socketToRoom[socket.id];
    
    
          console.log(socket.id, socketToNickname[socket.id], "님의 연결이 끊겼어요.");
        };
        console.log("조건문 실행 after", users[roomId].includes(socket.id)? true: false)
        console.log("disconnecting 할 때 조건문 after",sids );
      });
    });
    
    
    module.exports = { server };

     

    콘솔로 확인한 결과

     

    if(users[roomId].includes(socket.id))이 트루인지 펄스인지 궁금하다 조건문 실행 before true
    disconnecting 할 때 조건문 before Map(1) {
      'YmYn_PffrCKmWswNAAAB' => Set(2) { 'YmYn_PffrCKmWswNAAAB', '153' }
    }
    data { roomId: '153', userId: 12, time: 1, categoryId: 2, date: 2 }
    
    ...
    
    YmYn_PffrCKmWswNAAAB undefined 님의 연결이 끊겼어요.
    조건문 실행 after false  // 우리가 만든 user 리스트에서 해당 유저가 없어졌다.
    disconnecting 할 때 조건문 after Map(0) {}
    // 특정 방으로부터 유저의 socket.id도 사라졌고 socket.id 자체도 삭제되었다.

     

    그 결과,

    방만들기 후 브라우저에서 닫았을 시 방 정보 삭제 -> 완료, 콘솔 찍었을 때 socket 서버에서도 해당방으로 부터 빠져나오게 되었다.

     

    문제 해결!

     

     

     

    [태그 및 카테고리도 검색할 수 있게 하기]

     

    현재 검색은 아래 코드를 이용하여 제목만 검색할 수 있도록 되어있다.

    //// 원래 코드
    
              // 검색어로 검색하는 경우 => 비슷한 방 제목 목록 가져오기
              rooms = await Room.findAll({
                where: {
                  [Op.or]: [
                  {title: { [Op.like]: `%${query}%` }},
                  // { "$Category.name$": {[Op.like]: `%${query}%`} },
                  // { "$Tag.name$": {[Op.like]: `%${query}%`} },
                  ],
                },
                offset: offset,
                limit: roomSearchingLimit,
                attributes: [
                  "id",
                  "title",
                  "isSecret",
                  "createdAt",
                  "likeCnt",
                  "participantCnt",
                ],
                include: [
                  {
                    model: Category,
                    attributes: ["id", "name"],
                  },
                  {
                    model: Tag,
                    as: "Tags",
                    attributes: ["id", "name"],
                    through: { attributes: [] },
                    // 참고 링크: https://github.com/sequelize/sequelize/issues/9170
                    // 구글링 결과 해당 구문은 through 구문을 배제하기 위한 용도라고 나옴
                  },
                ],
                order: [["createdAt", "desc"]],
              });
              break;
          };
    
          return res.status(200).json({
            isSuccess: true,
            data: rooms,
          });
        }),

     

    이를 해결하기 위해 다음의 코드를 짜보았다.

    // 첫번째 시도, 한 요소씩 찾아서 별도의 배열에 집어넣고 시간순으로 나열하여 반환하려했음,
    // 시간 복잡도가 너무 클 것으로 예상 ex) 정렬을 위해 또다시 map, filter, includes같은 함수들을 써야할 것 같음
            const searchingCategories = await Category.findAll({
              where: {
                name: { [Op.like]: `%${query}%` }},
            })
    
            // 예를 들어 '뷰티 운동' 이라고 입력하면 두 카테고리 모두 나와야 하는 것이 아닌지?
    
            const searchingTags = await Tag.findAll({
              where: {
                name: { [Op.like]: `%${query}%` }},
            },
    
            )
            // 관련된 모든 태그 모두 출동하려면?
    
            // 해당되는 모든 방제목 검색
    
            const searchingTitles = await Room.findAll({
              where: {
                title: { [Op.like]: `%${query}%` }},
            })
            // findAll 은 리스트형, 즉, 배열을 반환
    
    
              let searchingTitle
              let searchingTag
              let searchingTitle
              
    
              let rooms = []
    
            // 제목이 들어맞는 방
              searchingRoomTitle.map( v => 
               searchingRoom = await Room.findOne({
                  where: {
                    title: `${v.title}`},
                  // offset: offset,
                  // limit: roomSearchingLimit,
                  attributes: [
                    "id",
                    "title",
                    "isSecret",
                    "createdAt",
                    "likeCnt",
                    "participantCnt",
                  ],
                  include: [
                    {
                      model: Category,
                      attributes: ["id", "name"],
                    },
                    {
                      model: Tag,
                      as: "Tags",
                      attributes: ["id", "name"],
                      through: { attributes: [] },
                    },
                  ],
                  // order: [["createdAt", "desc"]],
                }),
                rooms. push(searchingRoom) // 찾은 결괏 값을 하나씩 여기에 넣어준다.
    
                )

     

    하지만 주석에 달아 놓은 이유 때문에 다음으로는 Op.or을 하나의 find 구문에서 같이 실행되도록 해보았다.

     

    그 코드는 다음과 같다.

     rooms = await Room.findAll({
                  where: {
                    [Op.or]: [
                    {title: { [Op.like]: `%${query}%` }},
                    // { "$Category.name$": {[Op.like]: `%${query}%`} },
                    // { "$Tag.name$": {[Op.like]: `%${query}%`} },
                    ],
                  },
                  offset: offset,
                  limit: roomSearchingLimit,
                  attributes: [
                    "id",
                    "title",
                    "isSecret",
                    "createdAt",
                    "likeCnt",
                    "participantCnt",
                  ],
                  include: [
                    {
                      model: Category,
                      attributes: ["id", "name"],
                      where: {
                        [Op.or]: [ 
                          {name: {[Op.like]: `%${query}%` }
                        }
                      ]}
                    },
                    {
                      model: Tag,
                      as: "Tags",
                      attributes: ["id", "name"],
                      through: { attributes: [] },
                      where: {
                        [Op.or]: [ 
                          {name: {[Op.like]: `%${query}%` }
                        }
                      ]}
                    },
                  ],
                  order: [["createdAt", "desc"]],
                });
    
                console.log("rooms를 콘솔에 찍으면 이렇게 나온다.", rooms, "rooms를 콘솔에 찍으면 이렇게 나온다.")

    하지만 해당 코드는 작동하지 않았다.

    공식문서와 다른 사람들의 사용 예시를 살펴보았을 때 위와 같이 사용된 바가 없었지만

    혹시나 하는 마음으로 시도해보았다.

    하지만 Op.or은 어디까지나 똑같은 where 구문 의 요소들에게만 적용될 뿐, 다른 테이블의 요소에도 공통적으로 적용되지는 않았다.

    즉, '한 테이블의 한 where구문 내 조건들 중 하나를 충족시키는 것'만이지 '다른 테이블들의 모든 where 구문 조건 중 하나라도 충족시키면 된다'는 아닌 것이다.

     

    그렇다면 어떻게 다른 방식으로 풀어나갈 지 다시 고민해보아야 겠다.

     

     

    +


    그 외에도 아래와 같은 단순한 문제도 해결했다.

    방제목 roomname이 몽고 디비에 저장되지 않음 -> 몽고DB 스키마 상에선 roomName, 그러나 new Log() 할 때는 roomTitle이라는이름으로 들어가 서로 이름이 맞지 않음 -> roomName: roomTitle 으로 변경한 후 테스트 결과 정상적으로 DB 저장

     

    ▶ 느낀 점

     

    해결한 문제 외에도 고민하고있는 문제에서 시간을 꽤나 많이 소비했다.

    하지만 서비스도 배포가 이루어졌고 팀원들간 코드의 리뷰는 1차적으로 마친 상태이며 오늘 또 다른 큰 버그가 고쳐졌기에

    조금 더 여유로워진 상황이다. (물론 도커나 traivs CI, https에 대한 공부는 한 적이 없지만 이는 항해가 끝나고 따로 공부하면서 적용해봐야 할 것 같다.)

    앞으로 다른 더 중요한 게 없다면 문제를 해결하기 위해 조금 더 깊이 고민해봐도 좋을 것 같다.

     

    ▶ 공부 시 참고 링크들

    switch 문에서 default의 역할

    https://www.codeit.kr/community/threads/2158

     

    코딩이 처음이라면, 코드잇

    월 3만원대로 Python, JavaScript, HTML/CSS, Java 등 2,600개 이상 프로그래밍 강의를 무제한 수강하세요

    www.codeit.kr

     

    https://sequelize.org/v5/manual/querying.html

     

    Manual | Sequelize

    Querying Attributes To select only some attributes, you can use the attributes option. Most often, you pass an array: Model.findAll({ attributes: ['foo', 'bar'] }); SELECT foo, bar ... Attributes can be renamed using a nested array: Model.findAll({ attribu

    sequelize.org

     

    https://ram-t.tistory.com/67

     

    200120(월) Devlog. Sequelize.Op를 이용한 DB검색

    1. Sequelize and, or, like, not 검색기능 구현 20일에 블로깅하겠다고 적어둔 내용이다. 정리하고 올려야지 했었는데 이렇게 뒀다간 한참동안 안올릴 것 같아서 그냥 올린다. 검색기능구현을 통해 Sequel

    ram-t.tistory.com

     

    '항해 99' 카테고리의 다른 글

    항해 99 5기 WIL_12  (0) 2022.04.03
    항해 99 5기 TIL_84  (0) 2022.04.03
    항해 99 5기 TIL_82  (0) 2022.04.02
    항해 99 5기 TIL_81  (0) 2022.04.01
    항해 99 5기 TIL_80  (0) 2022.03.31
Designed by Tistory.