[メモ] Socket.IO使って無難にチャットアプリ作ってみるよ

# Socket.IOとは
サーバーとクライアントのリアルタイムな双方向通信を行うことができるライブラリです。
双方向通信の確立する際に様々なプロトコルを使用しサーバーとの通信を確立します。
Socket.IOを使うことでプロトコルの詳細を理解する必要なく双方向通信を実現することが可能です。
また、ルーム（特定のクライアント同士で通信できる部屋）などを作成する場合も簡単に実装することができます。

# 開発環境
今回はNextJSでSocket.IOを使用した双方向通信をざっくり実装してみたいと思います。
`Pages Router`の場合のみ、`API Routes`を使用することでサーバーサイドの実装も可能となっていましたが、今回ご紹介する方法では`Pages Router`、`App Router`ともに実装可能です。


# インストール
```shell
npm install socket.io
```
```shell
npm install socket.io-client
```

# サーバー側
プロジェクトルートにserver.jsを作成し以下を記述。
```server.js
import { createServer } from "node:http";
import next from "next";
import { Server } from "socket.io";

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handler = app.getRequestHandler();

app.prepare().then(() => {
  const httpServer = createServer(handler);

  const io = new Server(httpServer);

  io.on("connection", (socket) => {
    console.log('接続しました。')
  });

  httpServer
    .once("error", (err) => {
      console.error(err);
      process.exit(1);
    })
    .listen(port, () => {
      console.log(`> Ready on http://${hostname}:${port}`);
    });
});
```

package.jsonを編集し、実行時にWebSocketサーバを起動させる。
```pachage.json
{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js",
    "lint": "next lint"
   }
}
```

# クライアント側
## サーバーへの接続
srcフォルダ配下にsocket.jsを作成し、以下を記述。
```src/socket.js
"use client";

import { io } from "socket.io-client";

export const socket = io();
```
**`io()`について**
- `io()`を呼び出すことにより現在のホストに対して接続を行います
- `io()`に引数（URL）を渡すことで別ドメインのサーバーに接続することもできます
- 戻り値として`socket`を受け取り以降記述するクライアントの処理でサーバーに対してイベントの送受信を行うことが可能です

## 接続状態を確認
```page.tsx
"use client";

import { useEffect, useState } from "react";
import { socket } from "../socket";

const Home = () => {
  const [isConnected, setIsConnected] = useState(false);
  const [transport, setTransport] = useState("N/A");

  const onConnect = () => {
    setIsConnected(true);
    setTransport(socket.io.engine.transport.name);

    socket.io.engine.on("upgrade", (transport) => {
      setTransport(transport.name);
    });
  };

  const onDisconnect = () => {
    setIsConnected(false);
    setTransport("N/A");
  };

  useEffect(() => {
    if (socket.connected) {
      onConnect();
    }

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);

    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
    };
  }, []);

  return (
    <div>
      <p>Status: {isConnected ? "接続済み" : "接続中..."}</p>
      <p>Transport: {transport}</p>
    </div>
  );
};

export default Home;
```
接続後の流れ
- 最初は`poling`で通信を行う
    - クライアント側から一方通行の通信
- クライアントとサーバー側でwebsocket通信をサポートしている場合、`websocket`のプロトコルへと切り替える
    - 双方向通信になる

**`onConnect()`について**
- サーバーからupgradeイベントを受け取りwebsocketでの通信へと昇華する

実行してみると接続状態が`N/A`→`poling`→`websocket`の順に変わることが確認できると思います。
```shell
npm run dev
```

# メッセージ送信
## サーバー側でメッセージ受信
```server.ts
app.prepare().then(() => {
  ...
  io.on("connection", (socket) => {
    socket.on('send-message', (message) => {
      io.emit('receive-message', message)
    })
  });
  ...
}
```
`socket.on()`の`send-message`イベントでクライアント側からメッセージを受け取る。
`io.emit()`の`receive-message`で接続されている全てのクライアントへメッセージを送信。

## クライアント側でメッセージ送信
```page.tsx
const Home = () => {
  const [message, setMessage] = useState("");
  const [receivedMessages, setReceivedMessages] = useState<string[]>([]);
  ...
  // メッセージの送信
  const sendMessage = () => {
    if (message.trim() === "") return;
    socket.emit("send-message", message);
    setMessage("");
  };

  useEffect(() => {
    // メッセージの受信
    socket.on("receive-message", (receiveMessage) => {
      setReceivedMessages((currentReceivedMessages) => [
        ...currentReceivedMessages,
        receiveMessage,
      ]);
    });

    return () => {
      socket.off("receive-message");
    }
  }

  return (
    <>
      <div>
        <ul>
          {receivedMessages.map((receivedMessage, i) => (
            <li key={i} className="border-b border-dashed mb-2">
              <p className="whitespace-break-spaces">{receivedMessage}</p>
            </li>
          ))}
        </ul>
      </div>
      <div className="flex align-center">
          <textarea
            rows={1}
            value={message}
            onChange={(event) => setMessage(event.target.value)}
            className="grow text-stone-950 p-2 rounded-l-lg resize-none outline-none field-sizing-content"
            style={{ fieldSizing: "content" }}
          />
          <button
            onClick={() => sendMessage()}
            className="bg-blue-500 px-2 rounded-r-lg outline-none"
          >
            送信
          </button>
        </div>
    </>
  )
}
```
`socket.emit()`の`send-message`によりサーバーにメッセージ送信。
`socket.on()`の`"receive-message"`によりメッセエージを受信。

# まとめ
- リアルタイム通信はやっぱりいいね！別タブ開いて送信した内容が反映されてるの見るだけでテンション上がる
- VercelだとNextJSのカスタムサーバーが非対応らしいので公開する時は注意が必要かも
- 色々調べているうちにAWSのAPI GatewayでWebsocket API作れるらしいので気が向いたら見てみようかな
    - https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-websocket-api-overview.html


# 参考
- https://socket.io/how-to/use-with-nextjs
- https://socket.io/docs/v4/