# 채팅방 만들기 (by 코딩알려주는 누나❤) *해당 자료는 무단 도용과 배포를 금지합니다.* ' ![](https://c.tenor.com/UNTqMDwqh1gAAAAM/hello-hi.gif) 안녕하세요 여러분 :) 😊 코딩알려주는 누나 채널을 통해 오신분들 환영합니다! 👍👍 **제 채팅앱 만들기 강의 잘 들으셨나요?** **(❗이 강의들을 듣고 오셔야 합니다!❗)** * 채팅앱 강의 1탄: https://youtu.be/uE9Ncr6qInQ * 채팅앱 강의 2탄: https://youtu.be/oFiw5VvgRFg * 채팅앱 강의 3탄: https://youtu.be/pRGOEtGjI-k?si=z78TSE7Mwq1astjM 이 자료는 제 강의내용에서 더 나아가 채팅방까지 만들어보는 내용입니다! **강의를 백번 보아도 스스로 해보지 않으면 그것은 나의 지식이 아닙니다.** ![](https://hackmd.io/_uploads/Bym7X3tl6.png) 마치 수영을 유투브로 배우는것과 같죠.👀 한번 **스스로 수영을 하는 연습을 해볼까요?🏊‍♀️🚣‍♂️🚣‍♂️** Let's go~ ## 🏔Milestone 1. room 모델 만들기 채팅방 정보를 저장할 새로운 모델인 room 을 만들어보자! * Models/room.js ```javascript= const mongoose = require("mongoose"); const roomSchema = new mongoose.Schema( { room: String, members: [ { type: mongoose.Schema.ObjectId, unique: true, ref: "User", }, ], }, { timestamp: true } ); module.exports = mongoose.model("Room", roomSchema); ``` room: 채팅방 이름 members: 이 방안에 들어있는 맴버들 리스트 ## 🏔Milestone 2. 유저와 메세지에도 채팅방 정보 저장 하기 이제는 유저도 내가 어떤 채팅방에 들어있는지 정보를 저장해야하고 메세지도 어느채팅방에서 전달되고있는 메세지인지 채팅방 정보를 따로 저장해줘야한다. chat.js와 user.js에 각각 room 필드를 추가하자 * user.js ```javascript= const mongoose = require("mongoose"); const userSchema = new mongoose.Schema({ ... 앞에 기타 필드들, room: { type: mongoose.Schema.ObjectId, ref: "Room", }, }); module.exports = mongoose.model("User", userSchema); ``` * chat.js ```javascript= const mongoose = require("mongoose"); const chatSchema = new mongoose.Schema( { ... 앞에 기타 필드들 room: { type: mongoose.Schema.ObjectId, ref: "Room", }, }, { timestamp: true } ); module.exports = mongoose.model("Chat", chatSchema); ``` ## 🏔Milestone 3. 채팅방 만들기 유저가 스스로 채팅방을 만들 수 있는 기능을 만들면 좋지만 이번 강의에서는 미리 서버에서 채팅방을 만들어줄 예정이다. (유저가 만들 수 있게 하는기능은 나중에 스스로 해보세요!) * app.js ```javascript= // 임의로 룸을 만들어주기 app.get("/", async (req, res) => { Room.insertMany([ { room: "자바스크립트 단톡방", members: [], }, { room: "리액트 단톡방", members: [], }, { room: "NodeJS 단톡방", members: [], }, ]) .then(() => res.send("ok")) .catch((error) => res.send(error)); }); ``` **http://localhost:5001/ 를 브라우저에서 호출**하면 mongodb compass에서 채팅방이 생성되는걸 확인할 수 있다. ![](https://hackmd.io/_uploads/HyxOMDKla.png) 브라우저에 호출 ![](https://hackmd.io/_uploads/By4YzDtgT.png) 몽고디비 컴파스 확인 ## 🏔Milestone 4. 채팅방 정보 보내주기 이제 채팅방도 데이터베이스에 생겼겠다 이 정보를 클라이언트에 소켓을 이용해 보내주면 된다. * utils/io.js ```javascript= socket.emit("rooms", await roomController.getAllRooms()); // 룸 리스트 보내기 ``` 이 코드를 `io.on("connection", async (socket) => {` 이후에 추가해주자 물론 이제 `roomController.getAllRooms` 를 이해할 수 없다는 에러가 날 것이다. 이 함수를 만들어주러 가자 * 새로운 컨트롤러인 roomController를 만든다 Controllers/room.controller.js ```javascript= const Room = require("../Models/room"); const roomController = {}; roomController.getAllRooms = async () => { const roomList = await Room.find({}); return roomList; }; module.exports = roomController; ``` * 다시 io.js 로 돌아와서 방금 만든 함수를 import 해준다 ```javascript= // 파일 상단에 추가해주기 const roomController = require("../Controllers/room.controller"); ``` ## 🏔Milestone 5. 채팅방 정보 받아오기 (클라이언트) 이젠 클라이언트에서 채팅방 정보를 받아와야한다 * App.js ```javascript= const [rooms, setRooms] = useState([]); // useEffect 안에 추가 socket.on("rooms", (res) => { setRooms(res); }); ``` 한번 rooms 에 값이 잘 들어가 지는지 console.log로 확인해보자! ## 🏔Milestone 6. 쉬어가는 코너: 리액트 라우터 세팅 이제 우리는 한가지 페이지가 아닌 두개의 페이지를 핸들해야한다 * 채팅방 리스트 페이지 * 채팅방 페이지 따라서 우리도 그에 따른 2개의페이지를 만들기위해 라우터를 세팅하고 프로젝트 구조를 살짝(?)바꾸자 ### 1. react-router-dom 설치 ``` npm install react-router-dom ``` ### 2. App.js에 라우터 셋업 기존에 return에 있던 코드들은 주석처리해주시고 이 코드를 넣어라 ```javascript= import { BrowserRouter, Routes, Route } from "react-router-dom"; function App() { // ... 기타 코드를 return ( <BrowserRouter> <Routes> <Route exact path="/" element={<RoomListPage rooms={rooms} />} /> <Route exact path="/room/:id" element={<ChatPage user={user} />} /> </Routes> </BrowserRouter> ); ``` 물론 우리에겐 RoomListPage도 ChatPage도 없다. 따라서 각각 페이지를 만들러 가보자 ### 3. pages/Chatpage/Chatpage.jsx 그전 강의에서 만든 채팅방 코드들을 일부 여기에 넣으면 된다. ```javascript= import React, { useEffect, useState } from 'react' import socket from "../../server"; import { Button } from "@mui/base/Button" import MessageContainer from "../../components/MessageContainer/MessageContainer"; import InputField from "../../components/InputField/InputField"; import './chatPageStyle.css' const ChatPage = ({user}) => { const [messageList, setMessageList] = useState([]); const [message, setMessage] = useState(""); useEffect(() => { socket.on("message", (res) => { console.log("message",res) setMessageList((prevState) => prevState.concat(res)); }); }, []); const sendMessage = (event) => { event.preventDefault(); socket.emit("sendMessage", message, (res) => { if (!res.ok) { console.log("error message", res.error); } setMessage(""); }); }; return ( <div> <div className="App"> <div> {messageList.length > 0 ? ( <MessageContainer messageList={messageList} user={user} /> ) : null} </div> <InputField message={message} setMessage={setMessage} sendMessage={sendMessage} /> </div> </div> ); } export default ChatPage ``` askUserName에 관련된 부분을 제외하고 메세지를 주고받는 부분의 코드는 다 이 파일로 옮겨왔다. * chatPageStyle.css (참고로 뒤에서 나올 새로운 컴포넌트에 대한 스타일도 들어가있다.지금은 무시해라!) ```css! .App { height:100vh; background-image: url("../../../public/background.png"); margin-left: auto; margin-right: auto; max-width: 28rem; position: relative; } nav{ display: flex; justify-content: flex-start; align-items: center; } .back-button{ height: 50px; width: 50px; background: none; border: none; font-size: 20px; font-weight: bolder; cursor: pointer; margin-right: 10px; } .nav-user{ font-size: 20px; font-weight: 500; } ``` ### 4. pages/RoomListPage/RoomListPage.jsx ```javascript! import React, { useEffect, useState } from "react"; import socket from "../../server"; import { useNavigate } from "react-router-dom"; import "./RoomListPageStyle.css"; const RoomListPage = ({rooms}) => { const navigate = useNavigate(); const moveToChat = (rid) => { navigate(`/room/${rid}`); }; return ( <div className="room-body"> <div className="room-nav">채팅 ▼</div> {rooms.length > 0 ? rooms.map((room) => ( <div className="room-list" key={room._id} onClick={() => moveToChat(room._id)} > <div className="room-title"> <img src="/profile.jpeg" /> <p>{room.room}</p> </div> <div className="member-number">{room.members.length}</div> </div> )) : null} </div> ); }; export default RoomListPage; ``` App.js에서 받아온 rooms의 정보를 prop으로 받아와 채팅방 리스트를 보여주는 코드이다 각 채팅방을 누르면 이제 navigate함수를 통해 각 대화방으로 이동을한다. * RoomListPageStyle.css ```css= .room-body { height:100vh; background-color: whitesmoke; margin-left: auto; margin-right: auto; max-width: 28rem; position: relative; } .room-nav{ padding: 20px 10px; background-color: white; } .room-list{ display: flex; align-items: center; background-color: white; padding:10px; justify-content: space-between; } .room-list:hover{ background-color: whitesmoke; } .room-list img{ width: 50px; border-radius: 20px; margin-right: 10px; } .room-title{ display: flex; } .member-number{ color: white; background-color: rgb(237, 71, 71); border-radius: 50px; padding: 3px 8px } ``` ## 🏔Milestone 7. 채팅방이 잘 보이는지 확인 서버와 클라이언트를 둘다 틀고 채팅방이 잘 나오는지 꼭 확인한다. ![](https://hackmd.io/_uploads/SJjtdwKxp.png) ## 🏔Milestone 8. 채팅방에 조인하기 이제는 다시 **서버** 로 돌아와서 채팅방을 클릭시 그 채팅방에 조인을 하는 작업을 해야한다. 채팅방에 조인을 한다는 작업을 디테일하게 나누어보면 1. room 데이터에 members 필드 리스트에 해당 유저가 추가된다. 2. user데이터에 room필드에도 유저가 조인한 room정보를 업데이트한다. 3. soket은 해당 room id로 된 채널에 조인한다. (나중에 방에 팀원들끼리 대화 주고받을때 필요.) 4. 유저가 조인했다는 메세지는 방에 있는 팀원에게만 보여준다.(다른 방은 알 필요가 없다) ![](https://hackmd.io/_uploads/B1zb9vKeT.png) 5. 다시 업데이트된 room데이터를 클라이언트들에게 보내준다 (실시간으로 조인한 맴버 수를 볼 수 있음) ![](https://hackmd.io/_uploads/BJd8cwYep.png) 이 1~5번까지 프로세스를 코드로 풀어내면 다음과 같다 * io.js ```javascript= socket.on("joinRoom", async (rid, cb) => { try { const user = await checkUser(socket.id); // 일단 유저정보들고오기 await roomController.joinRoom(rid, user); // 1~2작업 socket.join(user.room.toString());//3 작업 const welcomeMessage = { chat: `${user.name} is joined to this room`, user: { id: null, name: "system" }, }; io.to(user.room.toString()).emit("message", welcomeMessage);// 4 작업 io.emit("rooms", await roomController.getAllRooms());// 5 작업 cb({ ok: true }); } catch (error) { cb({ ok: false, error: error.message }); } }); ``` * room.controller.js ```javascript= roomController.joinRoom = async (roomId, user) => { const room = await Room.findById(roomId); if (!room) { throw new Error("해당 방이 없습니다."); } if (!room.members.includes(user._id)) { room.members.push(user._id); await room.save(); } user.room = roomId; await user.save(); }; ``` 6. 기존 Login시 조인했던 코드는 지워주기 이제는 채팅방에 들어가야 조인메세지를 보여주기 때문에 더이상 로그인시 welcomeMessage 를 보여줄 필요가 없다 ```javascript= socket.on("login", async (name, cb) => { ... 기타 코드들 const welcomeMessage = { chat: `${user.name} is joined to this room`, user: { id: null, name: "system" }, }; io.emit("message", welcomeMessage); ... } ``` 이 부분을 login에서 지워주자 ## 👀여기서 잠깐! socket.join은 뭐지? ![](https://hackmd.io/_uploads/BklmnvYxT.png) 우리가 그전 강의에서는 채팅방이 나누어져 있지 않았기 때문에 그냥 모두가 한 공간에서 말하고 한공간에서 들을 수 있었다. 따라서 그냥 아무나에게 emit하고 내가 하고싶은말을 다 보내버리면 됐다. **_하지만 채팅방이 생기면서 이제는 우리가 모두에게 emit을 할 수 없어졌다._** 따라서 socket들을 어떤 그룹으로 분리 할 수 있고 그 그룹에 들어가게 하는 함수를 join이라고한다. 우리의 코드에서 ` socket.join(user.room.toString());` 의 의미는 **이 소켓은 유저가 들어있는 방의 id를 이름으로 사용하는 어떤 그룹으로 들어가겠다는 이야기다.** 이 같은이름의 방으로 다른 소켓이 들어가면 우리는 그 다른 소켓과 프라이베잇한 대화를 할 수 있는데 ` io.to(user.room.toString()).emit("message", welcomeMessage);` 이 코드를 통해 프라이베잇하게 대화할 수 있다. **이 룸id에들어있는 사람들 에게(to) 말한다 (emit) 이 메세지를 (welcomeMessages)** ## 🏔Milestone 9. 클라이언트에서 채팅방 조인 테스트하기 1. 이 채팅방 조인이 성공적인지 확인하기위해서 해당코드를 넣어주자 * ChatPage.jsx ```javascript= import {useParams} from "react-router-dom" ... const ChatPage = ({user}) => { const {id} = useParams() // 유저가 조인한 방의 아이디를 url에서 가져온다. ... useEffect(() => { ... socket.emit("joinRoom",id,(res)=>{ if(res && res.ok){ console.log("successfully join",res) } else{ console.log("fail to join",res) } }) }, []); ``` 2. 이제 3개의 클라이언트 브라우저를 한번에 켜주자 ![](https://hackmd.io/_uploads/r1j90vtlT.png) 4. 첫번째 브라우저는 유저이름 A를, 두번째 브라우저에는 유저이름 B를 세번째는 C를 넣어주자. 5. 첫번째 브라우저에 A는 자바스크립트 단톡방에 조인한다. 6. 두번째 브라우저에 B는 리액트 단톡방에 조인한다 7. 세번째 브라우저에 C는 자바스크립트 단톡방에 조인한다. 8. A 브라우저에서 C가 조인했다는 메세지를 확인할 수 있어야한다. ![](https://hackmd.io/_uploads/HJWu1_Ygp.png) 9. B 브라우저에서는 C가 조인했다는 메세지가 없어야한다. ![](https://hackmd.io/_uploads/Sy85k_txT.png) ## 🏔Milestone 10. 채팅방 내에서만 대화하기 그전 강의에서는 모두에게 내 대화를 보내면 끝이기 때문에 그냥 단순이 emit으로 메세지를 보내면 됐다. 하지만 이제는 채팅방이 있기 때문에 채팅방에 있는 소켓 맴버들에게만 메세지를 보내야한다. 따라서 다음과같이 코드를 수정하자 * io.js ```javascript= socket.on("sendMessage", async (receivedMessage, cb) => { try { const user = await checkUser(socket.id); if (user) { const message = await chatController.saveChat(receivedMessage, user); io.to(user.room.toString()).emit("message", message); // 이부분을 그냥 emit에서 .to().emit() 으로 수정 return cb({ ok: true }); } } catch (error) { cb({ ok: false, error: error.message }); } }); ``` * chat.controller.js ```javascript= chatController.saveChat = async (message, user) => { const newChat = new Chat({ chat: message, user: { id: user._id, name: user.name, }, room: user.room, // 메세지에 채팅방 정보도 저장하는 부분 추가! }); await newChat.save(); return newChat; }; ``` **이제 채팅방내에 대화가 다른 채팅방에서는 안보이는지 확인해보자!** ## 🏔Milestone 11. 채팅방 나가기 (클라이언트) 채팅방에 들어왔다면 나갈 수도 있어야 한다. 채팅방을 나가는 버튼을 채팅방 상단에 만들자. ![](https://hackmd.io/_uploads/Sy85k_txT.png) * ChatPage.jsx ```javascript= const ChatPage = ({user}) => { return ( <div> <div className="App"> {/* nav 이부분 추가 */} <nav> <Button onClick={leaveRoom}className='back-button'>←</Button> <div className='nav-user'>{user.name}</div> </nav> <div> {messageList.length > 0 ? ( <MessageContainer messageList={messageList} user={user} /> ) : null} </div> <InputField message={message} setMessage={setMessage} sendMessage={sendMessage} /> </div> </div> ); } export default ChatPage ``` 뒤로가기 버튼을 눌렀을때 실행될 leaveRoom함수도 정의해주자 ```javascript= import {useNavigate} from "react-router-dom" ... const ChatPage = ({user}) => { const navigate = useNavigate() ... const leaveRoom=()=>{ socket.emit("leaveRoom",user,(res)=>{ if(res.ok) navigate("/") // 다시 채팅방 리스트 페이지로 돌아감 }) } ... } ``` ## 🏔Milestone 12. 채팅방 나가기 서버단에서 처리하기 * io.js ```javascript! socket.on("leaveRoom", async (_, cb) => { try { const user = await checkUser(socket.id); await roomController.leaveRoom(user); const leaveMessage = { chat: `${user.name} left this room`, user: { id: null, name: "system" }, }; socket.broadcast.to(user.room.toString()).emit("message", leaveMessage); // socket.broadcast의 경우 io.to()와 달리,나를 제외한 채팅방에 모든 맴버에게 메세지를 보낸다 io.emit("rooms", await roomController.getAllRooms()); socket.leave(user.room.toString()); // join했던 방을 떠남 cb({ ok: true }); } catch (error) { cb({ ok: false, message: error.message }); } }); ``` * room.controller.js ```javascript= roomController.leaveRoom = async (user) => { const room = await Room.findById(user.room); if (!room) { throw new Error("Room not found"); } room.members.remove(user._id); await room.save(); }; ``` **이제 내가 나간 후에 남아있는 채팅방 맴버들에게 나갔다는 메세지가 뜨는지 확인하자** ![](https://hackmd.io/_uploads/HyLYxhKg6.png) ## 마지막 인사👨🏿‍🤝‍👨🏾 ![](https://hackmd.io/_uploads/ry-UZnYgp.png) 여기까지 오느라 정말 수고하셨습니다!😍 쉽지않은 내용이지만 여러번 복습하시면서 실시간 연결이라는게, 채팅이라는게, 어떻게 동작하는지 개념을 조금이나마 그릴 수 있으시다면 저는 정말 만족합니다! 😁 여러분들이 여기에서 더 나아가서 다양한 서비스를 스스로 개발해보셨으면 좋겠습니다!🦾 * 온라인인 유저들과 오프라인인 유저들을 보여주는 기능 * 채팅방 UI 개선 (채팅창이 길어진다면?) * 채팅방 조인시 기존채팅 가져오기 등등.. 한번 스스로 도전해보시길 바랍니다! 제 강의와 학습 컨텐츠가 좋았다면 **당신은 제 강의를 사랑하실겁니다!❤** 제 **웹개발자 풀스택 강의도 확인해주세요!** 🔥 https://codingnoona.thinkific.com/ 이상!