• 개인 프로젝트 - 카카오톡 채팅 만들기(Express, Socket.io)

    2024. 8. 20.

    by. 서카츄

     Express와 Socket.io 통신을 곁들인 카카오톡 실시간 채팅을 만들어 보았다.
    팀 프로젝트에서 Express와 Socket.io를 사용했는데 한번 더 연습 겸

    실시간 채팅을 만들어 보기로 했다!
     
     
     

    ✨ 결과 미리보기 

    처음에 닉네임을 입력하고 입장할 수 있다.
    닉네임이 빈 공백시 닉네임을 입력해 주세요. 라는 알림창이 뜬다.
     
     
     
     
     
     
     
     

     
     
    나가면 disconnect을 사용하여 소켓에 있는 내용을 꺼버린다.
     
     
     
     
     
     
     
     
     

     
     
    실시간 broadcast로 접속한 유저에게 전체 방송을 뿌려준다.
    전체메세지와 사용자가 나갔을 때, 사용자가 입장할 때 방송해준다.
     
     

     

     

    1. server부터 세팅해보자!

     

    const dev = process.env.NODE_ENV !== "production";
    const nextApp = next({ dev }); // Next.js 앱 초기화
    const handle = nextApp.getRequestHandler(); // Next.js 요청 핸들러

     
    dev 변수는 개발환경인지, 프로덕션 환경인지 확인해준다.
    next.js를 사용했기 때문에 next.js를 애플리케이션을 초기해주고
    handle 함수는 모든 HTTP요청을 next.js가 처리하도록 설정해준다.
     
     
     
     

    const app = express();
    const server = http.createServer(app);
    const PORT = 4000;

     
    Express앱을 생성하고, HTTP서버를 만든다
    서버 포트는 4000으로 요청을 수신하도록 설정했다. (이건 내 마음대로 설정해도 됨 ㅎㅎ)
     
     
     
     
     

    const io = new Server(server, {
      cors: {
        origin: "*",
      },
    });

     
    socket.io 서버를 설정했고, 
    cors는 모든 도메인에서 접속하도록 *를 두었다.
    프로젝트 할 때는 사용하는 도메인을 넣어주기..! 나는 연습겸 *를 넣어주었다.
     
     
     
     

    nextApp.prepare().then(() => {
      // Next.js 요청 핸들러 설정
      app.all("*", (req, res) => {
        return handle(req, res);
      });

     
    next.js가 준비되면 모든 HTTP요청을 next.js가 처리하도록 설정하는 내용이다.
     
     
     
     
     
     

      server.listen(PORT, () => {
        console.log(`서버와 연결되었습니다. 포트: ${PORT}`);
      });

     
    서버가 연결되면 4000포트에서 실행된다는 설명.
    말그대로 서버를 듣는다.라는 뜻이다. 



     
     
     

     그림으로 정리하면 이런느낌이다






     

      io.on("connection", (client) => {
        const connectedClientUserName = client.handshake.query.username;
        console.log(`사용자가 들어왔습니다. ${connectedClientUserName}`);
    
        client.broadcast.emit("message", {
          username: "",
          message: `${connectedClientUserName} 님이 들어왔습니다.`,
        });
    
    
        client.on("disconnect", () => {
          io.emit("message", {
            username: "",
            message: `${connectedClientUserName} 님이 나갔습니다.`,
          });
        });
    
        client.on("message", (msg) => {
          console.log(`보낸 사용자. ${connectedClientUserName}`);
          console.log(msg);
    
          io.emit("message", {
            username: msg.username,
            message: msg.message,
          });
        });
      });

     
    socket.io에 연결되면 connect이벤트가 발생하고
    client객체를 통해 클라이언트의 정보를 가져온다.
    client.handshake.query.username 을 통해 서버에서 console.log로 찍어보니 닉네임이 있다는 것을 확인했고
    클라이언트에서 전달한 사용자 닉네임을 가져온다.
    broadcast는 클라이언트가 연결되면 모든 클라이언트들에게 해당 사용자가 들어왔다는 메시지를 보낸다.
    그리고 클라이언트가 연결을 끊으면, 모든 클라이언트에게 해당 사용자가 나갔다는 메시지를 보낸다.
    클라이언트가 서버로 메시지를 보내면 message에 있는 이벤트를 받고, 서버는 그 메시지를 모든 클라이언트에게 다시 전송한다.
     
     
     
     

    2. 클라이언트 세팅하기

      const [socket, setSocket] = useState(null);
      const [userName, setUserName] = useState("");
      const [isConnected, setIsConnected] = useState(false);
      const [userInput, setUserInput] = useState("");
      const [messages, setMessages] = useState([]);

     
    클라이언트에서 받을 상태값을 만들어준다.
     
    socket : 클라이언트와 서버간 소켓 연결
    userName : 사용자 이름(닉네임)
    isConnected : 서버 접속 여부 확인
    userInput : 사용자가 입력한 메시지
    messages : 수신된 모든 메시지 목록 저장하고, 뿌려줌
     
     
     
     

    function connectToChatServer() {
      const _socket = io("서버주소", {
        autoConnect: false,
        query: {
          userName,
        },
      });
      _socket.connect();
      setSocket(_socket);
    }

     
    _socket 변수에 서버주소를 받아오고 
    사용자 이름을 쿼리로 전달한다. 
    전달한 후 , 서버에 연결되면 socket 상태가 업데이트 된다.
     
     
     
     
     

    function disConnectToChatServer() {
      console.log("사용자가 나갔습니다.");
      socket?.disconnect();
    }

     
    사용자가 나가면 현재 소켓 연결을 종료하는 함수이다.
    socket는 처음에 null로 설정했기 때문에, 옵셔널 체이닝 문법을 사용하여
    메서드가 존재하는지 확인하고 null이나 undefined가 아닐 경우에는 호출하게 해두었다. (타입에러 방지)
     
     
     
     
     

    function onConnected() {
      console.log("프론트 들어왔다");
      setIsConnected(true);
    }
    
    function onDisConnected() {
      console.log("프론트 들어왔다");
      setIsConnected(false);
    }

     
    서버에 성공적으로 연결되면 connected로 접속상태를 true로,
    반대로 서버 연결이 끊기면 접속상태를 false로 만들어 주었다.
     
     
     
     
     
     

    function sendMessageToChatServer() {
      socket?.emit("new message", { userName, message: userInput }, (msg) => {
        console.log(msg);
      });
    }

     
    사용자가 입력한 메시지를 서버로 전송한다.
    서버로부터 응답이 오면 현재 메시지를 콘솔에 출력한다.
     
     
     
     
     

    function onMessageReceived(msg) {
      console.log("프론트 - onMessageReceived");
      console.log("msg", msg);
      setMessages((prev) => [...prev, msg]);
    }

     
    서버로부터 메시지를 수신해서 받아오면, messages를 배열에 추가한다.
     
     
     
     
     
     

    useEffect(() => {
      console.log("프론트 연결 socket useEffect");
      socket?.on("connect", onConnected);
      socket?.on("disconnect", onDisConnected);
      socket?.on("new message", onMessageReceived);
    
      return () => {
        console.log("socket useEffect clean up");
        socket?.off("connect", onConnected);
        socket?.off("disconnect", onDisConnected);
        socket?.off("new message", onMessageReceived);
      };
    }, [socket]);

     
    useEffect훅으로 socket에서 변경이 일어나면, 
    메시지를 수신할때 이벤트를 처리하는 함수들이다.
    clean up 함수에는 컴포넌트가 언마운트 되거나 socket이 변경될 때 이벤트 리스너를 정리한다.
     
     
     
     
     
     

    const messageList = messages.map((msg, index) => (
      <li key={index}>
        {msg.username} : {msg.message}
      </li>
    ));

     
    수신된 모든 메시지는 리스트 아이템으로 뿌려준다.
    닉네임과 사용자가 쓴 내용을 같이 보여준다.
    utils 함수로 빼두면서 닉네임과 메시지를 받아와서 뿌려준다.
     
     
     
     
     
     

    const renderMessageList = (messages: Messages[], currentUser: string) => {
      return messages.map((msg, index) => {
        const isCurrentUser = msg.username === currentUser;
    
        if (msg.username === "") {
          return (
            <li className={S.enterUserNickname} key={index}>
              {msg.message}
            </li>
          );
        } else {
          return (
            <li
              className={`${S.messageItem} ${isCurrentUser ? S.right : S.left}`}
              key={index}
            >
              <p className={S.nickname}>{msg.username}</p>
              <p className={S.message}> {msg.message}</p>
            </li>
          );
        }
      });
    };

     
    카카오톡 처럼 내가 보낸 메시지는 오른쪽에,
    다른 사용자가 보낸 메시지는 왼쪽에 배치하기 위해 조건부 랜더링을 사용하였다. 
    그리고 isCurrentUser 변수에 받아오는 메시지가 현재 유저와 같으면 list를 오른쪽에 두고
    아니면 왼쪽으로 배치하게 만들었다.
     
     
     
     
     

    클라이언트 전체 코드

    const Main = () => {
      //NOTE - 접속중, 미접속 표시
      const onConnected = useCallback(() => {
        console.log("프론트 들어왔습니다.");
        setIsConnect(true);
      }, [setIsConnect]);
    
      const onDisConnected = useCallback(() => {
        console.log("프론트 나갔습니다");
        setIsConnect(false);
      }, [setIsConnect]);
    
      //NOTE - 실시간 message 리스트 보여주기
      const onMessageReceived = useCallback((msg: Messages) => {
        console.log("msg", msg);
        setMessages((prev) => [...prev, msg]);
      }, []);
    
      //NOTE - 메세지 전송
      const sendMessageToChatServer = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (!userInput.trim()) {
          toast.error("내용을 입력해 주세요.");
          return;
        } else {
          socket?.emit(
            "message",
            { username: nickname, message: userInput },
            (msg: Messages) => {
              console.log(msg);
            }
          );
          setUserInput("");
        }
      };
    
      //NOTE - socket 정보 받아오기
      useEffect(() => {
        socket?.on("connect", onConnected);
        socket?.on("disconnect", onDisConnected);
        socket?.on("message", onMessageReceived);
    
        return () => {
          socket?.off("connect", onConnected);
          socket?.off("disconnect", onDisConnected);
          socket?.off("message", onMessageReceived);
        };
      }, [socket, onConnected, onDisConnected, onMessageReceived]);
    
      useEffect(() => {
        window.scrollTo({
          top: document.body.scrollHeight,
          left: 0,
          behavior: "smooth",
        });
      }, [messages]);
    
      return (
     	"생략"
      );
    };
    
    export default Main;

     
    빌드를 진행해보니, 경고 메시지가 떠서 useEffect 안에 함수들을 의존성배열에 넣었고
    useCallback으로 함수가 불필요하게 재생성 되는것을 막기 위해
    useCallback훅으로 감싸서 메모이제이션을 진행했더니 경고문이 사라졌다.
     
     
     

    useCallback을 사용한 이유?

    onConnected, onDisConnected, onMessageReceived 함수는 useEffect 안에 의존성 배열에 포함되어있는데
    불필요하게 자주 실행 되는 것을 방지하기 위함이다.
     
    그래서 useCallback훅으로 메모이제이션을 하면 값이 변경되지 않는 한
    동일한 함수 참조를 유지하고,
    useEffect훅이 불필요하게 자주 실행되는것을 방지할 수 있다.
     
     
     
     
     

    직접 만들어 본 후기?

    직접 만들어보니 역시 공부에는 끝이 없다는 것을 알게 되었음을 (ㅠㅠ)
    처음 socket.io를 접했으면 무슨말인지 몰라서 어려웠을 텐데,
    이미 팀 프로젝트에서 socket.io를 사용했어서 빠르게 구현할 수 있었던 것 같다.
    대충 흐름이 이렇게 진행된다는것만 알고 있었는데,
    이번 기회에 서버도 간단하게 직접 만들어보면서 정리해볼 수 있는 계기가 되었다.
     
     
     
     
     
     
     

    참고한 Docs

     

    Introduction | Socket.IO

    If you are new to Socket.IO, we recommend checking out our tutorial.

    socket.io

     

    socket.io-client

    Realtime application framework client. Latest version: 4.7.5, last published: 5 months ago. Start using socket.io-client in your project by running `npm i socket.io-client`. There are 8877 other projects in the npm registry using socket.io-client.

    www.npmjs.com

     

    socket.io

    node.js realtime framework server. Latest version: 4.7.5, last published: 5 months ago. Start using socket.io in your project by running `npm i socket.io`. There are 10490 other projects in the npm registry using socket.io.

    www.npmjs.com

     

    express

    Fast, unopinionated, minimalist web framework. Latest version: 4.19.2, last published: 5 months ago. Start using express in your project by running `npm i express`. There are 94560 other projects in the npm registry using express.

    www.npmjs.com

     
     
     
     
     

    배포 링크!

     

    seokachu's chat

    livechat-cd66.onrender.com

     
     

    댓글