미새문지

웹 소켓(Web Socket) 채팅 앱 만들기 - 메세지 전송 본문

웹 프론트엔드

웹 소켓(Web Socket) 채팅 앱 만들기 - 메세지 전송

문미새 2024. 4. 29. 00:01
728x90

코딩알려주는누나 메세지전송 강의 : https://www.youtube.com/watch?v=pRGOEtGjI-k&t=0s

 

이전 학습내용:

https://moonmisae-cdpt.tistory.com/211

 

웹 소켓(Web Socket) 채팅 앱 만들기 - 유저 로그인

코딩알려주는누나 웹소켓 강의 : https://www.youtube.com/watch?v=oFiw5VvgRFg&t=0s 이전 학습 내용:https://moonmisae-cdpt.tistory.com/209 웹 소켓(Web Socket) 채팅 앱 만들기 - 클라이언트 서버 연결유튜브의 코딩알려

moonmisae-cdpt.tistory.com


이전 강의에선 유저 이름을 넘겨 서버에서 유저 정보를 응답해주고 useState를 이용해 값을 받아놓기까지 했다.

이번 강의는 메세지를 전송하는 내용인데 현재 화면이 기본 카카오톡 배경밖에 없다.

빈 배경화면

이제 이 배경에 채팅을 입력할 컴포넌트를 채워야 하는데 기존 프론트엔드 폴더에 컴포넌트가 이미 들어가있다.

컴포넌트 구성

 

먼저 입력창 컴포넌트를 보자

import React from 'react'
import { Input } from "@mui/base/Input";
import { Button } from "@mui/base/Button";
import './InputField.css'

/* 메세지 값이 담기는 message,
  메세지가 변경되었을 때 사용하는 setMessage,
  메세지를 서버로 보낼 때 사용하는 메소드 이름 */
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

onChange로 입력값이 바뀔 때마다 value값이 수정되게 작성되어 있고 메세지를 입력하지 않으면 버튼이 비활성화되는 로직으로 되어있다.

 

이 컴포넌트를 app.js div내부에 넣어보자

import "./App.css";
import InputField from "./components/InputField/InputField";
import socket from "./server";
import {useEffect, useState} from "react";

function App() { 
  const [user, setUser] = useState(null);
  // 추가된 부분
  const [message, setMessage] = useState('');
  
  useEffect(() => {
    askUserName();
  }, []);

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

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

  // 추가된 부분
  const sendMessage = (e) => {
    e.preventDefault();

    socket.emit("sendMessage", message, (res) => {
      console.log("sendMessage res", res);
    });
  }

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

export default App;

className이 App인 div안에 입력 컴포넌트를 넣었다. 아까 컴포넌트 코드에서 인자로 받아올 값이 3개였기 때문에 그 값을 보내줘야 해서 작성해준다. 그리고 메세지 값을 상태관리해줘야 하기 때문에 useState를 사용해 메세지 값을 관리해준다. 마지막으로 메세지를 보내는 함수를 만들고 sendMessage라는 값을 서버에서 받게 작성해서 인자로 넘겨준다.

 

화면은 이렇게 작성하고 서버 폴더의 io,js에서 sendMessage가 올 때의 로직을 만들어주는데, 유저 정보처럼 메세지에 따른 컨트롤러도 필요하기 때문에 먼저 컨트롤러 폴더에 메세지 컨트롤러를 작성해준다.

컨트롤러 폴더

const chatController = {};

chatController = () => {
    
}

module.exports = chatController;

이 후 코드를 작성하기 위해 외부에서 사용할 수 있게 뼈대 코드만 작성해주고 io.js에서도 받는 로직을 작성해준다.

 

socket.on("sendMessage", async (message, cb) => {
    // socket id로 유저 찾기
            
    // 메세지 저장

})

io.js에 sendMessage를 받을 코드를 작성해준다. 먼저 socket id로 유저를 찾아야 하는데 유저 정보는 userController에 있기 때문에 userController.js에서 socket id를 찾는 로직을 작성해줘야 한다.

 

// 소켓 아이디(유저 아이디)를 찾는다.
user.Controller.checkUser = async (sid) => {
	// 입력된 socket id와 같은 유저를 찾는다.
    const user = await User.findOne({token: sid});

	// 만약 user가 없으면 에러메세지를 전달한다.
    if(!user) throw new Error("user not found");
    return user;
}

작성해줬으면 io.js에서 작성한 컨트롤러를 가져와보자

 

socket.on("sendMessage", async (message, cb) => {
    // socket id로 유저 찾기
    const user = await userController.checkUser(socket.id);
    // 메세지 저장
    const newMessage = await chatController.saveChat(message, user);
})

userController에서 socket id를 찾는 코드를 가져오고 이 후 chatController에서 작성할 메세지 저장 코드를 미리 작성해준다.

 

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;

 

_id는 mongoDB에서 데이터가 생성될 때 고유id를 부여해주는데 그 값이다. 작성하면 다시 io,js로 와서 sendMessage 로직에 콜백 데이터를 작성해주는데 유저이름을 받을 때처럼 작성하지 않는다.

왜냐하면 채팅은 단순히 서버와 클라이언트의 통신이 아니라 클라이언트가 둘 이상일 때의 상황으로 생각해야 하기 때문에 a가 입력한 메세지 통신을 b도 알아야 한다. 그러므로 클라이언트에서 요청이 오면 응답을 클라이언트 전체에 전달해줘야 한다.

 

socket.on("sendMessage", async (message, cb) => {
            try {
                // socket id로 유저 찾기
                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});
            }
        })

sendMessage 로직에 io.emit으로 메세지를 클라이언트에 알려주고 콜백데이터를 전달해준다.

작성이 끝났으면 다시 프론트엔드 app.js로 와서 useEffect에 message를 읽어와 콘솔을 출력하는 코드를 작성한다.

useEffect(() => {
    socket.on('message', (message) => {
      console.log("res", message);
    })
    askUserName();
  }, []);

작성했으면 이제 화면에서 작동이 되는지 테스트를 해보자.

 

문미새 채팅

아까 추가한 컴포넌트로 인해 하단에 채팅 입력칸이 생겼고 프롬프트에 이름을 입력해 서버와 통신을 잘 받았다.

그리고 채팅은 최소 두 명 이상이기 때문에 새 창으로 다른 프롬프트를 입력해준다.

남재희 채팅

 

여기서 문미새로 채팅을 쳤을 때 서버와 잘 연동되는 부분을 볼 수 있다.

문미새 채팅 작성

보통은 이렇게 통신하면 완료이지만, 채팅의 경우 각 클라이언트가 통신을 해야 하기 때문에 다른 사용자에게도 응답이 가야한다.

남재희 채팅 확인

남재희 채팅에서도 응답이 온 것을 볼 수 있는데 이것이 채팅의 로직이라고 생각하면 된다.

 

이제 화면에 채팅 내용을 띄워줘야 한다. 하지만 채팅을 한 개만 보내는 게 아니기 때문에 배열로 받아와야 한다.

import "./App.css";
import InputField from "./components/InputField/InputField";
import socket from "./server";
import {useEffect, useState} from "react";

function App() { 
  const [user, setUser] = useState(null);
  const [message, setMessage] = useState('');
  // 추가된 부분
  const [messageList, setMessageList] = useState([]);

  // 추가된 부분(메세지 확인용)
  console.log("message List", messageList);
  
  useEffect(() => {
    socket.on('message', (message) => {
      // 추가된 부분
      setMessageList((prevState)=> prevState.concat(message));
    })
    askUserName();
  }, []);

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

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

  const sendMessage = (e) => {
    e.preventDefault();

    socket.emit("sendMessage", message, (res) => {
      console.log("sendMessage res", res);
    });
  }

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

export default App;

 

다시 테스트를 해보자.

채팅 배열에 담기는 모습

각 유저가 입력한 채팅이 배열에 잘 담기는 것을 볼 수 있다.

이제 콘솔에는 잘 담기는걸 확인했으니 채팅을 화면에 보여줘야 한다. 그래야 채팅창이니까

 

미리 들어가있던 컴포넌트 중 MessageContainer을 넣어야 한다. 컴포넌트를 한번 살펴보면

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와 user를 받아오는 것을 볼 수 있다. 그리고 map을 이용해 해당하는 리스트 값을 전부 가져온다.

그리고 메세지 유저가 시스템이면 시스템 메세지를 보여주고 유저가 본인이라면 본인 메세지를 띄워주는데, 여기서 채팅이 본인이 친 채팅이라면 오른쪽에 보이고 본인이 친 채팅이 아니라면 왼쪽에 보이는 로직을 가지고 있다.

 

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

app.js에 MessageContainer를 추가해 테스트를 해보자.

 

채팅 테스트

채팅 유저를 둘로 테스트했을 때 채팅이 잘 이루어지는 것을 볼 수 있다.

 

현재 시스템 메세지가 없는데 이 부분은 통신에 있어서 꼭 필요하진 않지만 있으면 누가 들어왔는지 확인도 가능하고 무엇보다 멋있으니까 따라해본다.

 

시스템 메세지는 유저가 들어왔을 때 들어왔다는 시스템 메세지를 보여줘야 한다. 그러면 로그인을 했을 때 띄워줘야 하니까 io.js의 로그인 로직에 코드를 추가해준다

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});
            }
        })

유저가 소켓으로 통신을 했을 때 system이란 이름으로 해당 유저 이름을 채팅으로 띄워주는 로직을 작성해줬다.

다시 테스트를 해보면,

시스템 메세지 추가

유저가 들어왔다는 메세지가 잘 작동되는 것을 볼 수 있다.


채팅의 간격이 좀 안맞는거랑 닉네임이 없어서 누가 누군지 안보이는게 좀 아쉽지만 이 부분은 금방 수정이 가능할 것 같으니 이 후에 추가해봐야겠다.

728x90