-
항해 99 5기 TIL_83항해 99 2022. 4. 3. 02:24
▶ Today I Learned
<실전 프로젝트>
[브라우저를 닫아버리는 유저의 socket정보 삭제]
- 문제상황
프론트에서 특정 유저가 뒤로가기로 나갔을 경우 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
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