Next.jsでWebSocketアプリケーションを作成する(サーバー編)

## WebSocket

なにかしらのWebアプリケーションを作る際に、ユーザー間でリアルタイムに情報交換が必要な場面があるかと思います。
そんな時にはWebSocketというプロトコルを使う事が定番です。JavaScript界隈では、WebSocketクライアントの機能は既にWebブラウザに組み込まれています。WebSocketサーバーを作成するためには`Socket.io`というパッケージを使う事が定番です。

## Next.jsとWebSocket

Next.jsでは、サーバーサイドを作成することができますが、そこにWebSocketサーバーの機能を持たせたいなと思うのはごく自然な流れだと思います。
ところがNext.jsの新しい作り方である`App Router`では現在のところWebSocketサーバーを組み込むことができません。筆者もそのやり方をいろいろと探しましたが、今のところそれは出来ないようです。

そこでNext.jsの昔の作り方である`Pages Router`ではどうなのかと目を向けてみると、こちらの方はWebSocketサーバーを組み込むノウハウがいろいろとあるようです。幸いにも`Pages Router`と`App Router`は一つのNext.jsアプリケーション内で共存できますので、今回はWebSocketサーバーを`Pages Router`で作成し、残りの機能(クライアント側含む）は`App Router`で作成することにしましょう。

## API Routes

Next.jsの`Pages Router`でサーバーサイドを作成するためには`API Routes`を利用します。`API Routes`の基本のコードは下記のようになります。

```TypeScript: 
import type { NextApiRequest, NextApiResponse } from 'next'
 
type ResponseData = {
  message: string
}
 
export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<ResponseData>
) {
  res.status(200).json({ message: 'Hello from Next.js!' })
}
```

出展：https://nextjs.org/docs/pages/building-your-application/routing/api-routes

このコードが書かれたファイルを`pages/api`フォルダ内に設置すると、`/api/ファイル名`というURLでアクセスできるサーバーサイドが完成します。
今回の作戦に従い、ここにWebSocketサーバーを組み込みます。

## Next.jsアプリケーションの雛形生成

Next.jsアプリケーションを準備しましょう。下記のコマンドを実行し、尋ねられる選択肢も下記のように選択してください。

```
> npx create-next-app@latest

✔ What is your project named? … next-rtc-app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No
```

途中の質問で「App Routerを使うか(Would you like to use App Router?)」と尋ねられますがこれをどう答えたとしても、所定のフォルダ(`app`フォルダもしくは`pages`フォルダ)さえ自作すれば、`Pages Router`と`App Router`のどちらも利用できるようです。

## Socket.ioのインストール

Socket.ioを利用するためのサーバー用とクライアント用のパッケージを同時にインストールしておきます。

```
> next-rtc-app
> npm install socket.io
> npm install socket.io-client
> npm install cors
```

## WebSocketサーバーのコーディング

WebSocketサーバーのコードを完成させましょう。比較的、短いコードです。機能的には接続してきたWebSocketクライアントを受け入れ、接続状態を維持し、いずれかのWebSocketクライアントが送信してきたメッセージを、そのまま他のWebSocketクライアントに転送します。この際、メッセージの中身は特に見ていません。来たものをそのままムーディに受け流しているだけです。

下記のコードを作成する場所に注意してください。`src`フォルダ内に、`pages`フォルダ、そしてその中に`api`フォルダを作成してください。これらのフォルダの名前や階層構造は`API Routes`でサーバーサイドを作成するために設定されていますので、変更はできません。
そのフォルダの中に`sockets.ts`というファイルを作成します。こちらは任意の名前で構いませんが、クライアントからアクセスする際のURLに影響します。

ここまでで次のようにフォルダ構成が完成していればOKです。
```
src
└ pages
  └ api
     └ sockets.ts
```

下記のコードを書いてください。

```src/pages/api/sockets.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import cors from 'cors';

import type { Socket as NetSocket } from 'net';
import type { Server as HttpServer } from 'http';
import { Server as SocketServer } from 'socket.io';

// Next.jsの型定義を拡張してSocket.IOの型定義を追加
type ReseponseWebSocket = NextApiResponse & {
  socket: NetSocket & { server: HttpServer & { io?: SocketServer } };
};

const corsMiddleware = cors();

// Next.jsのAPIルーティングの入り口となる関数
export default function SocketHandler(req: NextApiRequest, res: ReseponseWebSocket) {
  if (req.method !== 'POST') {
    return res.status(405).end();
  }
  if (res.socket.server.io) {
    return res.send('already-set-up');
  }
  // Socket.IOのサーバーを作成する
  const io = new SocketServer(res.socket.server, {
    addTrailingSlash: false,
  });

  // クライアントが接続してきたら、コネクションを確立する
  io.on('connection', (socket) => {
    const clientId = socket.id;
    console.log(`A client connected. ID: ${clientId}`);

    // メッセージを受信したら、全クライアントに送信する
    socket.on('message', (data) => {
      io.emit('message', data);
      console.log('Received message:', data);
    });

    // クライアントが切断した場合の処理
    socket.on('disconnect', () => {
      console.log('A client disconnected.');
    });
  });

  // CORS対策を一応、有効にした上でサーバーを設定する
  corsMiddleware(req, res, () => {
    res.socket.server.io = io;
    res.end();
  });
}
```

WebSocketは、比較的単純なプロトコルで、`connection`イベントで接続が確立し、`message`イベントでメッセージが届きます。切断した場合は`disconnect`イベントが発生します。

`API Routes`の仕組みとSocket.ioの仕組みをつなげる必要があります。`API Routes`からのレスポンスを担当する`NextApiResponse`型の`res`引数に、Socket.ioの接続情報を持つオブジェクトを`socket.server.io`という名前のプロパティを組み込む事で実現します。

やっかいなのが、TypeScript的に型の整合性を持たせることです。`NextApiResponse`型に、`socket.server.io`という元々存在しないプロパティを勝手に増やそうとしても、TypeScriptはコンパイルエラーを出します。それはそうですね。

そこでTypeScriptの交差型 (Intersection Types)を利用して`NextApiResponse`型に`socket.server.io`プロパティを結合します。
```TypeScript: 
// Next.jsの型定義を拡張してSocket.IOの型定義を追加
type ReseponseWebSocket = NextApiResponse & {
  socket: NetSocket & { server: HttpServer & { io?: SocketServer } };
};
```
交差型は`&`で複数の型を結合できます。今回は、ioプロパティを持つオブジェクト型にserverプロパティを持つオブジェクト型を結合し、それをさらにsocketプロパティを持つオブジェクト型と結合し、最後にNextApiResponseと結合しています。（書いててわけわからんですね）

参考：https://qiita.com/suema0331/items/a145909db0bcbcc3f949

WebSocketを使って、メッセージを送信したい場合は、`emit`メソッドを利用します。何を送るかは自由ですが、今回は（前述のとおり）クライアントから来たメッセージをそのまま送信しなおしているだけです。
```TypeScript: 
// メッセージを受信したら、全クライアントに送信する
socket.on('message', (data) => {
  io.emit('message', data);
  console.log('Received message:', data);
});
```

最後に今回のコードでは、CORS(オリジン間リソース共有)を行うためのミドルウェア(リクエストとレスポンスの間で動作するフィルター的なもの)を利用しています。これはSocket.ioのバージョンによっては必要なので入れてあります。ただし現時点で最新版の`Socket.io 4.7.2`では不要なようです。

以上でコードの説明は完了です。

## WebSocketサーバーの起動

WebSocketサーバーの起動は、Next.jsアプリケーションを起動するためのコマンドと同じです。これでサーバーとクライアントの双方を起動できます。

```
npm run dev
```

## クライアント側の作成

次の記事で説明します：https://qiita.com/ochiochi/items/102d14649396d351ab80


## 参考情報

当記事は下記の記事を参考にしています。すごく助かりました。ありがとうございました。
https://www.zenryoku-kun.com/new-post/nextjs-socketio