Next.js + Socket.ioチャット機能を実現1編


Next.jsベースのパーソナルプロジェクトでsocket.ioを用いてチャット機能を実現した.想像以上に簡単なコード実装と応用で驚きました!👀
実装に先立ち,チャットルームUI,DBに格納されたChat構造などを考慮したが,実装後も引き続き修正を行う.第1話では,1パスに接続されたユーザがチャットで情報を送受信できる程度のコンテンツを実現する.
第2編では、MongoDBに基づいてチャットルームを作成し、参加してみます.

Socket.io


JSライブラリは、すべてのプラットフォームの双方向および低遅延通信をサポートします.Webクライアントとサーバ間のリアルタイム双方向通信を可能にする.
ブラウザで実行されるクライアントライブラリとノード.jsには2つのサーバ側リポジトリがあり、2つのコンポーネントのAPIはほぼ同じです.

Server


サーバ側の構成は大きく3つに分けられます.
  • 応答タイプ設定
  • socket.io接続
  • ルーティング接続
  • 1.応答タイプの設定


    Next.jsにおけるrequestおよびresponoeのタイプは、主にNextApiRequestおよびNextApiResponseを用いる.ここはソケットioリクエストに対してresonseタイプを個別に設定できます.

    types/chat.d.ts

    import { Server as NetServer, Socket } from "net";
    import { NextApiResponse } from "next";
    import { Server as SocketIOServer } from "socket.io";
    
    export type NextApiResponseServerIO = NextApiResponse & {
      socket: Socket & {
        server: NetServer & {
          io: SocketIOServer;
        };
      };
    };
    

    2. socket.接続io


    現在のプロジェクトのサーバとsocket.ioを接続します.

    /pages/api/chat/socketio.ts

    import { NextApiRequest } from "next";
    import { NextApiResponseServerIO } from "../../../types/chat";
    import { Server as ServerIO } from "socket.io";
    import { Server as NetServer } from "http";
    
    export const config = {
      api: {
        bodyParser: false,
      },
    };
    
    export default async (req: NextApiRequest, res: NextApiResponseServerIO) => {
      if (!res.socket.server.io) {
        console.log("New Socket.io server...✅");
    
        const httpServer: NetServer = res.socket.server as any;
        const io = new ServerIO(httpServer, {
          path: "/api/chat/socketio",
        });
    
        res.socket.server.io = io;
      }
    
      res.end();
    };
    

    3.ルーティング接続


    これで、/chatルーティングから接続されたユーザーにチャット情報を送信する応答コードを作成できます.

    /pages/api/chat/index.ts

    import { NextApiRequest } from "next";
    import { NextApiResponseServerIO } from "../../../types/chat";
    
    export default async (req: NextApiRequest, res: NextApiResponseServerIO) => {
      if (req.method === "POST") {
        const message = req.body;
        res.socket.server.io.emit("message", message);
    
        res.status(201).json(message);
      }
    };
    
    サーバー側の構成が完了しました.クライアントsocketです.io構成は/api/chat/socketioパスに接続され、クライアントルーティングで発生したsocketイベントを応答として受信し、出力する.

    Client

    /chatパス出力の<Chatting.tsx>ファイルを生成し、コードを記述する.

    components/views/chat/Chatting.tsx

    import React, { useState, useRef, useEffect, useCallback } from "react";
    import { useSelector } from "../../../store";
    import axios from "../../../lib/api";
    
    // * Socket.io
    import SocketIOClient from "socket.io-client";
    
    // * MUI
    import { Stack, TextField, Alert, Button, Paper } from "@mui/material";
    import SendIcon from "@mui/icons-material/Send";
    
    interface IMessage {
      user: string;
      message: string;
    }
    
    const Chatting: React.FC = () => {
      const [sendMessage, setSendMessage] = useState<string>("");
      const [connected, setConnected] = useState<boolean>(false);
      const [chat, setChat] = useState<IMessage[]>([]);
    
      const username = useSelector((state) => state.user.name);
    
      useEffect((): any => {
        // connect to socket server
        const socket = SocketIOClient.connect(process.env.NEXT_PUBLIC_API_URL, {
          path: "/api/chat/socketio",
        });
    
        // log socket connection
        socket.on("connect", () => {
          console.log("SOCKET CONNECTED!", socket.id);
          setConnected(true);
        });
    
        // update chat on new message dispatched
        socket.on("message", (message: IMessage) => {
          chat.push(message);
          setChat([...chat]);
        });
    
        // socket disconnect on component unmount if exists
        if (socket) return () => socket.disconnect();
      }, []);
    
      const sendMessageHandler = useCallback(
        (event: React.ChangeEvent<HTMLInputElement>) => {
          setSendMessage(event.target.value);
        },
        [sendMessage]
      );
    
      const enterKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
        if (event.key === "Enter" && !event.shiftKey) {
          // send message
          event.preventDefault();
          submitSendMessage(event);
        }
      };
    
      const submitSendMessage = async (
        event: React.FormEvent<HTMLButtonElement>
      ) => {
        event.preventDefault();
        if (sendMessage) {
          const message: IMessage = {
            user: username,
            message: sendMessage,
          };
    
          const response = await axios.post("/api/chat", message);
          setSendMessage("");
        }
      };
    
      return (
        <>
          <Stack spacing={2} direction="column">
            <Alert severity="info">
              채팅 기능은 로그인된 유저에게만 제공됩니다.
            </Alert>
            {/* 채팅 메시지 출력 영역 */}
            <Stack spacing={2} direction="column">
              <Paper variant="outlined" sx={{ minHeight: "300px" }}>
                {chat.length ? (
                  chat.map((chat, index) => (
                    <div className="chat-message" key={index}>
                      {chat.user === username ? "Me" : chat.user} : {chat.message}
                    </div>
                  ))
                ) : (
                  <div className="alert-message">No Chat Messages</div>
                )}
              </Paper>
            </Stack>
            {/* 채팅 메시지 입력 영역 */}
            <Stack spacing={1} direction="row">
              <TextField
                id="chat-message-input"
                label="enter your message"
                variant="outlined"
                value={sendMessage}
                onChange={sendMessageHandler}
                margin="normal"
                autoFocus
                multiline
                rows={2}
                fullWidth
                onKeyPress={enterKeyPress}
                placeholder={connected ? "enter your message" : "Connecting...🕐"}
              />
              <Button
                type="submit"
                variant="contained"
                color="primary"
                endIcon={<SendIcon />}
                onClick={submitSendMessage}
              >
                Send
              </Button>
            </Stack>
          </Stack>
        </>
      );
    };
    
    export default Chatting;
    
    useEffectを介してクライアントが/chatルーティングにアクセスしたときにsocketに接続する.生成されたメッセージがある場合、サーバは応答として送信し、受信したメッセージ情報はuseStateを介して保存され、チャットウィンドウに更新された情報が出力されます.
    ユーザが/chatルーティングを離れると、cleanup関数によってsocket接続が無効になります.
    次のjsを起動し、ブラウザでテストします.

    左側のChromeブラウザではTester 1ユーザーが接続されています.右側のSafariブラウザではTester 2ユーザーが接続されています.
    チャットメッセージ出力UIコンポーネントは構成されていないため、ユーザー名とメッセージのみが出力されます.

    整理する


    socket.io構成サーバとクライアントコードによってパスが接続され、このパスに接続されたルーティングは、ユーザが作成および送信したコンテンツをリアルタイムで受信することができる.
    ソケットサーバに接続し、メッセージを送信するには、socketが提供するonemitの方法を使用します.
    第2編では、MongoDBでチャットドキュメントを作成し、チャットルーム別にチャットコンテンツを格納し、チャットコンテンツのロードと出力を試みます.👋