개발새발 로그

React, Socket.Io - 웹 소켓을 이용한 채팅 구현 시 흐름 파악하기 본문

React

React, Socket.Io - 웹 소켓을 이용한 채팅 구현 시 흐름 파악하기

이즈흐 2024. 5. 17. 18:55

개발하면서 웹소켓을 사용할 일이 없다보니 이 기능이 어떻게 구현되는지 모르고 있었다.
이번 기회에 웹소켓 로직을 보통 어떻게 작성하고 기본적인 실시간 채팅기능을 어떻게 만드는지 알아보려고 한다.

 

먼저 웹소켓과 우리가 쓸 Socket.io에 대해서 궁금한 점을 작성해봤다.

📖http와 웹소켓의 차이

웹소켓(WebSocket)은 클라이언트와 서버(브라우저와 서버)를 연결하고 실시간으로 통신이 가능하도록 하는 첨단 통신 프로토콜이다. 웹소켓은 하나의 TCP 접속에 전이중(duplex) 통신 채널을 제공한다.

 

쉽게 말해, 웹소켓은 Socket Connection을 유지한 채로 실시간으로 양방향 통신 혹은 데이터 전송이 가능한 프로토콜이다.

 

기존 HTTP는 단방향 통신이였다. 클라이언트에서 서버로 Request를 보내면 서버는 클라이언트로 Response를 보내는 방식으로 동작했다. 또한, HTTP는 기본적으로 무상태(Stateless)이므로 상태를 저장하지 않는다.

 

하지만 웹소켓은 양방향 통신으로 연결이 이루어지면 클라이언트가 요청하지 않아도 데이터가 저절로 서버로부터 올 수 있다. HTTP처럼 별도의 요청을 보내지 않아도 데이터를 수신할 수 있다는 것이다.

 

📖 Socket.io와 WebSocket의 차이

Web Socket 은 웹 페이지의 한계에서 벗어나 실시간으로 상호작용하는 웹 서비스를 만드는 표준 기술이다

 

Socket.io 는 브라우저의 종류에 상관없이 다양한 방식의 웹 기술을 실시간으로 구현할 수 있도록 한 기술이다

다양한 방식의 실시간 웹 기술을 손쉽게 사용할 수 있는 모듈이라 이해하면 된다

 

🤔차이점은?

웹 소켓은 HTML5 표준의 일부로 실시간으로 상호작용하는 웹 서비스이다. 그러나 웹 소켓 프로토콜은 아직 확정된 상태가 아니기 때문에 브라우저 별로 지원하는 WebSocket 버전이 다르다. 또한 예전 브라우저 중 웹 소켓을 지원하지 않는 경우도 많다. 따라서 미래의 기술이지, 아직 인터넷 기업에서 시범적으로 써 볼 수 있는 기술이라 볼 수 없다.

 

이를 보완하기 위해 Socket.io라는 모듈이 개발되었는데, 이는 다양한 방식의 실시간 웹 기술을 손쉽게 사용할 수 있도록 해주는 모듈이다.

즉, WebSocket, FlashSocket, AJAX Long Polling, AJAX Multi part Streaming, IFrame, JSONP Polling 등, 다양한 방법을 하나의 API로 추상화한 것이다.

즉 Socket.io는 Javascript를 이용하여 브라우저 종류에 관계없이 실시간 웹을 구현할 수 있도록 해주는 기술이다.

이로 인해 개발자는 Socket.io로 개발을 한다면 클라이언트로 푸쉬 메시지를 보내기만 한다면 WebSocket을 지원하지 않는 브라우저에서도 브라우저 모델과 버전에 따라 다양한 방법으로 내부적으로 푸쉬 메시지를 보낼 수 있도록 설정이 되어있다.

즉 어느 브라우저라도 일관된 모듈로 메시지를 보낼 수 있으며, 또한 지원하는 여러 내부 기능들이 따로 있기 때문에 더 간편하고 효율적이다.

 

 

 

그럼 본격적으로 실제 채팅 기능을 만들어보면서 어떤 형식으로 실시간 채팅기능이 만들어지는지 알아보자

👨‍💻백엔드 & 프론트 엔드 초기 세팅

🛠️필요 라이브러리 설치

npm init

npm i express mongoose cors dotenv http
//express로 데이터베이스 서버를 올려놓을 예정
//mongoose로 몽고DB를 쉽게 사용
//cors로 프론트엔드와 백엔드가 연결에 문제가 없도록
//dotenv로 환경변수
//http 서버를 만드는데 웹소켓을 올려놓을 예정

npm socket.io

 

🛠️데이터 베이스 세팅

유저정보채팅메세지 정보를 저장해야한다

 

user.js - 유저정보

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
	name:{
    	type: String,
        required:[true, "User must type name"],
        unique: true,
    },
    token : {
    	type: String,
    },
    online : {
    	type: boolean,
        default: false
    }
})

module.exports = mongoose.model("User",userSchema);

유저 정보에 대한 스키마를 만들었다.

name, token, online 필드를 받는다.

token은 소켓의 연결 id 정보를 담는다.

 

chat.js - 채팅메세지 정보

const mongoose = require("mongoose");

const chatSchema = new mongoose.Schema(
    {
        chat: String, //메세지 내용
        user: {
            id: {
                type: mongoose.Schema.ObjectId,
                ref: "User",
            },
            name: String,
        },
    },
    {timestamp : true }
);


module.exports = mongoose.model("Chat",chatSchema);

 

 

🛠️데이터 베이스 연결하기

app.js

const express = require("express");
const mongoose = require("mongoose");
require("dotenv").config();
const cors = require("cors");
const app = express();

app.use(cors()); //어떤 주소로도 접근을 모두 허용한다.

mongoose.connect("process.env.DB", {
	useNewUrlParser: true,
    useUnifiedTopology: true,
}).then(()=>console.log("connected to database");


module.exports = app;

//터미널에서 node app.js로 실행가능
//nodemon 설치해서 사용해도 됨

 

🛠️웹 소켓 세팅하기

http를 이용해 서버를 만들고, 거기에 웹소켓과 데이터베이스를 올릴 것이다.

index.js

const { createServer } = require("http");
const app = require("./app"); // 데이터베이스 연결 로직을 담고있는 파일
const { Server } = require("socket.io");
require("dotenv").config();

const httpServer = createServer(app); // http서버에 데이터베이스 연결 부분을 올린다.
const io = new Server(httpServer, { // 웹소켓 서버위에 http서버를 올린다.
	cors:{
    	origin: "http://localhost:3000" // 지정해준 주소만 웹소켓 통신을 한다.
	},
});

require("./utils/io")(io); //만든 io를 매개변수로 넘겨서 io.js에서 사용한다.

httpServer.listen(process.env.PORT, ()=>{
	console.log("server listening on port",process.env.PORT);
});

현재 io를 만들어놓고 사용하지 않고있다.

io를 이용해서 통신하는 로직을 만들어야한다.

하지만 그에 대한 코드를 index.js에 쓰기에는 코드 양이 많기 때문에 따로 파일을 만들어서 관리한다.

 

utils/io.js

io에서는 크게 emiton을 사용한다.

emit은 송신, on은 수신으로 알고 있으면 된다.

module.exports = function (io) {
    // 'io.on' 메서드를 사용하여 'connection' 이벤트를 감지합니다. 
    // 이 'connection' 이벤트는 클라이언트가 서버에 연결될 때마다 발생합니다.
    // 'socket' 파라미터는 연결된 클라이언트의 소켓 객체입니다.
	io.on("connection", async(socket)=>{
    	console.log("client is connected",socket.id);
    })
}

 

 

🛠️프론트엔드 세팅 - 연결 테스트

프론트엔드에서도 웹소켓을 다운받아야한다 

npm install socket.io-client

이제 프론트엔드에서 서버를 만들어줘야한다.

server.js

import { io } from "socket.io-client"
const socket = io("http://localhost:5001"); //백엔드 서버를 여는 주소
export default scoket; // 소켓을 원하는 곳에서 사용

 

App.js

import "./App.css";
import socket from "./server";

function App() {
  return (
    <div>
      <div className="App"></div>
    </div>
  );
}

export default App;

위처럼 import만 해도 연결이 될 것이다.

이후 서버를 node 또는 nodemon으로 실행해주면 아래와 같이 연결 성공이 뜨게 된다.

위처럼 소켓 ID를 볼 수 있는데 이게 연결 아이디라고 생각하면 된다.

브라우저 탭을 새로 만들거나 refresh하면 프론트엔드 서버 주소로 접속할 때마다 새로운 연결아이디가 생성된다.

 

그럼 연결을 끊으려면 어떻게 해야할까?

utils/io.js

다시 백엔드로 돌아와서 아래와 같이 코드를 추가한다.

module.exports = function (io) {
	io.on("connection", async(socket)=>{
    	console.log("client is connected",socket.id);
        
        //연결이 끊기는 것을 확인할 수 있다.
        socket.on("disconnect",()=>{
        	console.log("user is disconnected");
        });
    })
}

그럼 브라우저 탭을 닫을 때 연결이 끊기는 것을 확인할 수 있다.

 


 

👨‍💻유저 로그인

🛠️프론트엔드 로그인

유저 로그인 부분은 간단하게 만든다.

App.js

import "./App.css";
import socket from "./server";

function App() {
  const askUserName = () =>{
  	  //원래 로그인 로직이 존재해야 함
      const userName= prompt("당신의 이름을 입력하세요");
      console.log("user : ",userName);
      
      //emit(대화의 제목, 보낼 내용,콜백 함수)
      //콜백함수는 emit이 잘 처리가 되면 콜백함수로 응답을 받을 수 있음
      socket.emit("login",userName,(res)=>{
      	console.log("res : ".res); //응답은 서버에서 정해짐
      });
   };  
  useEffect(()=>{
    askUserName();
  },[])

// ...

간단하게 prompt에 useName을 입력받는다. 

원래는 로그인 로직이 있거나 로그인 유무를 확인하는 로직이 있어야 한다.

이후 emit을 통해 서버에 로그인을 알려야한다.

 

🛠️백엔드 로그인

그럼 이제 백엔드에서  login을 들을 준비가 되어야한다.

utils/io.js

module.exports = function (io) {
	io.on("connection", async(socket)=>{
    	console.log("client is connected",socket.id);
        
        //login을 들을 준비
        //여기서는 프론트엔드단에서 보낸 useName(데이터)와 콜백함수를 사용할 수 있다.
        socket.on("login",async(userName,cb)=>{
        	//받은 유저정보를 저장하고, 소켓ID 정보도 저장해야함
        });
        

        socket.on("disconnect",()=>{
        	console.log("user is disconnected");
        });
    })
}

여기서 로그인 로직은 io 파일과 상관없으므로 따로 파일을 만들어준다.

 

Controller/user.controller.js

const User = require("../Models/user"); // "User" 모델을 "../Models/user" 파일에서 불러온다.
const userController = {}

userController.saveUser = async(userName,sid)=>{
	//이미 있는 유저인지 확인
    const user = await User.findOne({name: userName});
    // 없다면 새로 유저정보 만들기
    if(!user){
    	user = new User({
        	name: userName,
            token: sid,
            online: true,
        })
    }
    // 이미 있는 유저라면 연결정보 token 값만 바꿔주자.
    user.token = sid;
    user.online = true;
    
    await user.save(); // 변경된 유저 정보를 데이터베이스에 저장한다.
    return user
    
}

module.exports = userController

유저 정보를 저장하는 로직이다.

이를 io 파일에서 사용하면된다.

 

utils/io.js

const userController = require("../Controller/user.controller");

module.exports = function (io) {
	io.on("connection", async(socket)=>{
    	console.log("client is connected",socket.id);
        
        socket.on("login",async(userName,cb)=>{
        	try{
                // 유저 아이디와 socket id를 넘겨줘야한다.
                const user = await userController.saveUser(userName,socket.id);
                //클라이언트에 응답을 해준다.
                cb({ok : true, data: user});
            }
            catch(error){
            	cb({ok : false , error: error.message})
            }
        });
        
        socket.on("disconnect",()=>{
        	console.log("user is disconnected");
        });
    })
}

user정보를 DB에 저장하는 로직을 수행하고 프론트엔드단에 응답을 보내주는 것이다.

 

그럼 클라이언트단에서는 아래와 같이 응답을 받을 수 있다.

 

 

🛠️프론트엔드에서 유저 정보 저장하기

App.js

import "./App.css";
import socket from "./server";

function App() {
  const [user,setUser] = useState(null); //유저정보를 variable로 저장한다.

  const askUserName = () =>{
      const userName= prompt("당신의 이름을 입력하세요");
      console.log("user : ",userName);
      
      socket.emit("login",userName,(res)=>{
      //유저 정보를 저장한다.
      	if(res?.ok){
        	setUser(res.data);
        }
      });
   };  
   
  useEffect(()=>{
    askUserName();
  },[])

// ...

useState를 이용해서 상태에 로그인 유저 정보를 저장한다.

 

 


 

👨‍💻메세지 주고 받기

components/InputField.jsx

아래 코드는 메시지를 입력할 input이다.

const InputField = ({message,setMessage,sendMessage}) => {

  return (
    <div className="input-area">
          <div className="plus-button">+</div>
          <form onSubmit={sendMessage} className="input-container">
            <Input
              placeholder="Type in here…"
              value={message}
              onChange={(event) => setMessage(event.target.value)}
              multiline={false}
              rows={1}
            />

            <Button
              disabled={message === ""}
              type="submit"
              className="send-button"
            >
              전송
            </Button>
          </form>
        </div>
  )
}

export default InputField

 

이를 App.js에서 사용해준다.

App.js

import "./App.css";
import socket from "./server";
import InputField from "./components/InputField/InputField"

function App() {
  const [user,setUser] = useState(null);
  const [message,setMessage] = useState(""); //message 상태 생성
  
  //메시지를 보내는 함수 생성
  const sendMessage = (event)=>{
  	event.preventDefault():
    //메시지를 보낸다.
    socket.emit("sendMessage",message,(res)=>{
    	console.log("sendMessage res",res)
    })
  }

  const askUserName = () =>{
      const userName= prompt("당신의 이름을 입력하세요");
      console.log("user : ",userName);

      socket.emit("login",userName,(res)=>{
        if(res?.ok){
            setUser(res.data);
        }
      });
};  
   
  useEffect(()=>{
    askUserName();
  },[])

  return (
    <div>
      <div className="App">
      
      <InputField 
      	message={message} 
      	setMessage={setMessage} 
        sendMessage={sendMessage}/> //메시지를 보내기 위한 input
        
      </div>
    </div>
  );
}

export default App;

App.js에 메세지를 보내기위한 input을 만들고, 해당 input에 메세지 데이터를 연결해서 제어하고, 

button을 누르면 socket.emit으로 서버에 데이터를 보낸다.

 

그럼 백엔드에서는 이 "sendMessage"를 들을 수 있도록 코드를 작성해야 한다.

utils/io.js

module.exports = function (io) {
	io.on("connection", async(socket)=>{


		// ...
        
        socket.on("sendMessage", async (message,cb)=>{
            //socket Id로 유저를 찾기
            const user = await userController.checkUser(socket.id); //userController에 함수를 만들어야한다.
            //메세지 저장
            const newMessage = await chatController.saveChat(message,user); // chatController에 만들어준다.
        });

		//...
    })
}

메세지 저장하는 로직도 다른 파일에서 관리하도록 한다.

이때 메세지는 DB에 보내는 메세지 유저의 정보를 저장하기 때문에

socket의 id에 해당하는 유저의 정보를 먼저 찾아야한다.

 

Controllers/user.controller.js

먼저 유저를 찾는 함수를 만들어준다.

userController.checkUser = async (sid) => {
    const user = await User.findOne({token:sid});
    if(!user) throw new Error("user not found");
    return user;
}

Controllers/chat.controller.js

const Chat = require("../Models/chat");
const chatController = {}

//받은 유저 정보와 메세지를 저장
chatController.saveChat = async (message,user) =>{
    const newMessage = new Chat({
    	chat : message,
        user:{
        	id:user._id,
            name:user.name
        }
    await newMessage.save();
    return newMessage;
}

module.exports = chatController

DB에 메시지를 저장한다.

 

utils/io.js

다시 돌아와서 이제 아까처럼 콜백함수를 이용해 응답을 보내주면 끝인걸까? cb({ok:true,data: newMessage});

 

아니다. 만약 A 클라이언트가 메시지를 보내서 백엔드에 저장했는데 대화하는 상대가 많다면 B,C,D 클라이언트는 이 메시지가 저장되어있는 것을 모르고 있는 것이다.

즉, 모두에게 새로운 메시지가 생긴 것 알려줘야 한다.

module.exports = function (io) {
	io.on("connection", async(socket)=>{


		// ...
        
        socket.on("sendMessage", async (message,cb)=>{
            try{
                const user = await userController.checkUser(socket.id);
                const newMessage = await chatController.saveChat(message,user);
                io.emit("message",newMessage); // 모든 클라이언트들에게 알려야한다.
                cb({ok:true});
            }
            catch (error){
            	cb({ok : false, error: error.message});
            }

        });

		//...
    })
}

이렇게 서버에서 io.emit을 통해 전송하고 있다.

그러면 어떻게 해야할까?

물론 프론트엔드에서 이걸 들어야(on) 한다.

 

App.js

useEffect를 자세히 보자

useEffect내에서 첫 렌더링에 한번 socket.on을 실행한다.

그래서 만약 서버에 메세지가 저장됐다면 서버에서 emit이 실행되고 그걸 "듣는 것"이다.

import "./App.css";
import socket from "./server";
import InputField from "./components/InputField/InputField"

function App() {
  const [user,setUser] = useState(null);
  const [message,setMessage] = useState("");
  const [messageList, setMessageList] = useState([]); //모든 메시지를 갖고있는 메세지리스트
  
  const sendMessage = (event)=>{
    event.preventDefault():
    socket.emit("sendMessage",message,(res)=>{
    	console.log("sendMessage res",res)
    })
  }

  const askUserName = () =>{
      const userName= prompt("당신의 이름을 입력하세요");
      console.log("user : ",userName);

      socket.emit("login",userName,(res)=>{
        if(res?.ok){
            setUser(res.data);
        }
      });
};  
   
  useEffect(()=>{
  	//서버에 저장된 새로운 메시지에 대한 emit을 듣고있다.
    //이를 통해 만약에 다른 클라이언트에서 메시지를 보내면 서버에서는 그 메시지를 저장하고,
    //서버는 다시 emit으로 모든 클라이언트에게 알린다.
    //그럼 클라이언트는 다시 on으로 그걸 듣고 출력한다.
    socket.on("message",(message)=>{
    	//새로운 메시지가 오면 메세지 리스트에 합쳐준다.
    	setMessageList((prev)=> prev.concat(message));
    });
    askUserName();
  },[])

  return (
    <div>
      <div className="App">
      
      <InputField 
      	message={message} 
      	setMessage={setMessage} 
        sendMessage={sendMessage}/>
        
      </div>
    </div>
  );
}

export default App;

 

그럼 이제 이 messageList를 이쁘게 출력하면된다.

 

components/MessageContainer.jsx

import React, { useState } from "react";
import "./MessageContainer.css";
import { Container } from "@mui/system";

const MessageContainer = ({ messageList, user }) => {
  return (
    <div>
      {messageList.map((message, index) => {
        return (
          <Container key={message._id} className="message-container">
            {message.user.name === "system" ? (
              <div className="system-message-container">
                <p className="system-message">{message.chat}</p>
              </div>
            ) : message.user.name === user.name ? (
              <div className="my-message-container">
                <div className="my-message">{message.chat}</div>
              </div>
            ) : (
              <div className="your-message-container">
                <img
                  src="/profile.jpeg"
                  className="profile-image"
                  style={
                    (index === 0
                      ? { visibility: "visible" }
                      : messageList[index - 1].user.name === user.name) ||
                    messageList[index - 1].user.name === "system"
                      ? { visibility: "visible" }
                      : { visibility: "hidden" }
                  }
                />
                <div className="your-message">{message.chat}</div>
              </div>
            )}
          </Container>
        );
      })}
    </div>
  );
};

export default MessageContainer;

위 컴포넌트는 말 그대로 메세지 리스트를 출력하는 컴포넌트다.

이때 자신의 아이디와 messageList를 받는데 자신의 아이디와 메세지의 아이디가 같으면 자신의 메세지로 보이게끔 한 것이다. 

간단한 로직이라서 자세한 설명은 생략하겠다.

 

App.js

// ...

return (
    <div>
      <div className="App">
      
      //메세지리스토와 현재 로그인한 유저 정보 prop
      <MessageContainer messageList={messageList} user={user} /> 
      
      <InputField 
      	message={message} 
      	setMessage={setMessage} 
        sendMessage={sendMessage}/>
        
      </div>
    </div>
  );

위처럼 메시지 리스트를 App.js에 넣어준다.

 

 

 

👨‍💻입장합니다 기능

채팅방에 들어갔다는 것은 로그인했다는 것이니까 

로그인 로직에 넣어준다.

socket.on("login",async(userName,cb)=>{
    try{
        const user = await userController.saveUser(userName,socket.id);
        //welcomeMessage 생성
        const welcomeMessage = {
        	chat: `{$user.name} 님이 들어왔습니다.`
            user: { id:null, name: "system" }
        };
        //방에 들어왔음을 모든 클라이언트에게 알림
       io.emit("message",welcomeMessage);
        
       cb({ok : true, data: user});
    }
    catch(error){
        cb({ok : false , error: error.message})
    }
});

 

 

 

 

📘전체 코드

🛠️프론트엔드

App.js

import "./App.css";
import socket from "./server";
import InputField from "./components/InputField/InputField"

function App() {
  const [user,setUser] = useState(null);
  const [message,setMessage] = useState("");
  const [messageList, setMessageList] = useState([]); //모든 메시지를 갖고있는 메세지리스트
  
  const sendMessage = (event)=>{
    event.preventDefault():
    socket.emit("sendMessage",message,(res)=>{
    	console.log("sendMessage res",res)
    })
  }

  const askUserName = () =>{
      const userName= prompt("당신의 이름을 입력하세요");
      console.log("user : ",userName);

      socket.emit("login",userName,(res)=>{
        if(res?.ok){
            setUser(res.data);
        }
      });
};  
   
  useEffect(()=>{
    socket.on("message",(message)=>{
    	setMessageList((prev)=> prev.concat(message));
    });
    askUserName();
  },[])

  return (
    <div>
      <div className="App">
      <MessageContainer messageList={messageList} user={user} /> 
      <InputField 
      	message={message} 
      	setMessage={setMessage} 
        sendMessage={sendMessage}/>
        
      </div>
    </div>
  );
}

export default App;

components/MessageContainer.jsx

import React, { useState } from "react";
import "./MessageContainer.css";
import { Container } from "@mui/system";

const MessageContainer = ({ messageList, user }) => {
  return (
    <div>
      {messageList.map((message, index) => {
        return (
          <Container key={message._id} className="message-container">
            {message.user.name === "system" ? (
              <div className="system-message-container">
                <p className="system-message">{message.chat}</p>
              </div>
            ) : message.user.name === user.name ? (
              <div className="my-message-container">
                <div className="my-message">{message.chat}</div>
              </div>
            ) : (
              <div className="your-message-container">
                <img
                  src="/profile.jpeg"
                  className="profile-image"
                  style={
                    (index === 0
                      ? { visibility: "visible" }
                      : messageList[index - 1].user.name === user.name) ||
                    messageList[index - 1].user.name === "system"
                      ? { visibility: "visible" }
                      : { visibility: "hidden" }
                  }
                />
                <div className="your-message">{message.chat}</div>
              </div>
            )}
          </Container>
        );
      })}
    </div>
  );
};

export default MessageContainer;

components/InputField.jsx

const InputField = ({message,setMessage,sendMessage}) => {

  return (
    <div className="input-area">
          <div className="plus-button">+</div>
          <form onSubmit={sendMessage} className="input-container">
            <Input
              placeholder="Type in here…"
              value={message}
              onChange={(event) => setMessage(event.target.value)}
              multiline={false}
              rows={1}
            />

            <Button
              disabled={message === ""}
              type="submit"
              className="send-button"
            >
              전송
            </Button>
          </form>
        </div>
  )
}

export default InputField

server.js

import { io } from "socket.io-client"
const socket = io("http://localhost:5001");
export default scoket;

 

🛠️백엔드

 

app.js

const express = require("express");
const mongoose = require("mongoose");
require("dotenv").config();
const cors = require("cors");
const app = express();

app.use(cors());

mongoose.connect("process.env.DB", {
	useNewUrlParser: true,
    useUnifiedTopology: true,
}).then(()=>console.log("connected to database");


module.exports = app;

index.js

const { createServer } = require("http");
const app = require("./app");
const { Server } = require("socket.io");
require("dotenv").config();

const httpServer = createServer(app);
const io = new Server(httpServer, {
	cors:{
    	origin: "http://localhost:3000"
	},
});

require("./utils/io")(io);

httpServer.listen(process.env.PORT, ()=>{
	console.log("server listening on port",process.env.PORT);
});

 

utils/io.js

const userController = require("../Controller/user.controller");

module.exports = function (io) {
	io.on("connection", async(socket)=>{
    	console.log("client is connected",socket.id);
        
        socket.on("login",async(userName,cb)=>{
            try{
                const user = await userController.saveUser(userName,socket.id);
                const welcomeMessage = {
                    chat: `{$user.name} 님이 들어왔습니다.`
                    user: { id:null, name: "system" }
                };
               io.emit("message",welcomeMessage);

               cb({ok : true, data: user});
            }
            catch(error){
                cb({ok : false , error: error.message})
            }
        });
        
        socket.on("sendMessage", async (message,cb)=>{
            try{
                const user = await userController.checkUser(socket.id);
                const newMessage = await chatController.saveChat(message,user);
                io.emit("message",newMessage);
                cb({ok:true});
            }
            catch (error){
            	cb({ok : false, error: error.message});
            }

        });

        
        socket.on("disconnect",()=>{
        	console.log("user is disconnected");
        });
    })
}

Controllers/user.controller.js

const User = require("../Models/user");
const userController = {}

userController.saveUser = async(userName,sid)=>{
    const user = await User.findOne({name: userName});
    if(!user){
    	user = new User({
        	name: userName,
            token: sid,
            online: true,
        })
    }

    user.token = sid;
    user.online = true;
    
    await user.save();
    return user
    
}

userController.checkUser = async (sid) => {
    const user = await User.findOne({token:sid});
    if(!user) throw new Error("user not found");
    return user;
}

module.exports = userController

 

Controllers/chat.controller.js

const Chat = require("../Models/chat");
const chatController = {}

chatController.saveChat = async (message,user) =>{
    const newMessage = new Chat({
    	chat : message,
        user:{
        	id:user._id,
            name:user.name
        }
    await newMessage.save();
    return newMessage;
}

module.exports = chatController

🛠️데이터베이스

user.js

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
	name:{
    	type: String,
        required:[true, "User must type name"],
        unique: true,
    },
    token : {
    	type: String,
    },
    online : {
    	type: boolean,
        default: false
    }
})

module.exports = mongoose.model("User",userSchema);

chat.js

const mongoose = require("mongoose");

const chatSchema = new mongoose.Schema(
    {
        chat: String,
        user: {
            id: {
                type: mongoose.Schema.ObjectId,
                ref: "User",
            },
            name: String,
        },
    },
    {timestamp : true }
);


module.exports = mongoose.model("Chat",chatSchema);

 

 

📘마무리하며..

이렇게 실시간 채팅기능을 만들어 봤다.
만들면서 처음으로 socket.io의 흐름을 알게 됐다.

이제는 실시간 시스템을 만들 때 이 흐름을 계속해서 기억해서 개발하려고한다.

나는 아래 유튜브 영상을 보면서 공부했다.

https://www.youtube.com/watch?v=uE9Ncr6qInQ&t=0s

또 socketio를 이용해 각각의 채팅방을 만들수 있다고 한다.

https://hackmd.io/@oW_dDxdsRoSpl0M64Tfg2g/Syy1xDFep#%F0%9F%8F%94Milestone-1-room-%EB%AA%A8%EB%8D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0

이 부분도 공부해서 정리해보려고 한다.

728x90
반응형
LIST