【NestJS】WebSocketGateway を使ってチャットアプリを開発する（前編）

# はじめに

こんにちは、梅雨です。

今回は、NestJS の WebSocketGateway を用いてチャットアプリを開発する手順について解説しようと思います。

# WebSocketGateway とは？

https://docs.nestjs.com/websockets/gateways

> In Nest, a gateway is simply a class annotated with @WebSocketGateway() decorator. Technically, gateways are platform-agnostic which makes them compatible with any WebSockets library once an adapter is created. There are two WS platforms supported out-of-the-box: socket.io and ws. You can choose the one that best suits your needs. Also, you can build your own adapter by following this guide.

WebSocketGateway とは、NestJS が提供する WebSocket 用のアダプタ（デコレータ）です。

`@WebSocketGateway()` デコレータでアノテートされたクラスはゲートウェイとして、socket.io などのドライバを用いてクライアントと WebSocket 通信を実現することができます。

# WebSocketGateWay を使ってみる

それでは、実際に WebSocketGateWay を用いて通信を行ってみましょう。

## 環境構築

今回はバックエンドに NestJS、フロントエンドには Next.js を採用します。

```bash
$ npx nest new server
$ npx create-next-app@latest client
```

```bash
nestjs-websocket-demo
├── client
│   ├── next-env.d.ts
│   ├── next.config.ts
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   └── app
│   │       ├── layout.tsx
│   │       └── page.tsx
│   └── tsconfig.json
└── server
    ├── nest-cli.json
    ├── package-lock.json
    ├── package.json
    ├── src
    │   ├── app.controller.ts
    │   ├── app.module.ts
    │   ├── app.service.ts
    │   └── main.ts
    ├── tsconfig.build.json
    └── tsconfig.json
```

NestJS は 8000番ポート、Next.js は3000番ポートで起動します。

## ゲートウェイの作成（NestJS）

まずは `@nestjs/websockets` パッケージと `@nestjs/platform-socket.io` パッケージをインストールします。

```bash
$ npm i @nestjs/websockets @nestjs/platform-socket.io
```

続いて、ゲートウェイを作成しましょう。

```bash
$ npx nest g gateway events
```

すると、以下のようなファイルが生成されます。

`@SubscribeMessage("message")` の `message` の部分は通信の合言葉のようなものとなっており、クライアントからメッセージを送信する際に指定することでこの `handleMessage` ハンドラが実行されます。

```ts:server/src/events/events.gateway.ts
import { SubscribeMessage, WebSocketGateway } from "@nestjs/websockets";

@WebSocketGateway()
export class EventsGateway {
  @SubscribeMessage("message")
  handleMessage(client: any, payload: any): string {
    return "Hello world!";
  }
}
```

3000 番ポートからアクセスできるようにするため、CORS の設定を行いましょう。

```diff_typescript:server/src/events/events.gateway.ts
import { SubscribeMessage, WebSocketGateway } from "@nestjs/websockets";

- @WebSocketGateway()
+ @WebSocketGateway({
+   cors: {
+     origin: "http://localhost:3000",
+   },
+ })
export class EventsGateway {
  @SubscribeMessage("message")
  handleMessage(client: any, payload: any): string {
    return "Hello world!";
  }
}
```

メッセージの内容は、`@MessageBody()` デコレータを使うとより簡潔に書けます。

```diff_typescript:server/src/events/events.gateway.ts
- import { SubscribeMessage, WebSocketGateway } from "@nestjs/websockets";
+ import {
+   MessageBody,
+   SubscribeMessage,
+   WebSocketGateway,
+ } from "@nestjs/websockets";

@WebSocketGateway({
  cors: {
    origin: "http://localhost:3000",
  },
})
export class EventsGateway {
  @SubscribeMessage("message")
-   handleMessage(client: any, payload: any): string {
+   handleMessage(@MessageBody() body: string): string {
+     console.log(body);
    return "Hello world!";
  }
}
```

## クライアントからサーバにメッセージを送信

先程作った WebSocket のゲートウェイに対し、メッセージを送信するクライアントを作成します。

まずは `socket.io-client` パッケージをインストールしましょう。

```bash
$ npm i socket.io-client
```

このパッケージの `io` モジュールを使って、8000 番ポートとの WebSocket 接続を確立します。

そして、先ほど決めた `message` という合言葉と共に、"message from client" というメッセージをサーバに送信（emit）してみましょう。

```tsx:client/src/app/page.tsx
"use client";

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

const socket = io("http://localhost:8000");

const Home = () => {
  return (
    <div>
      <button
        onClick={() => {
          socket.emit("message", "message from client");
        }}
      >
        メッセージを送信
      </button>
    </div>
  );
};

export default Home;
```

少しわかりにくですが、クライアントでボタンを押すたびにサーバ側のログに　"message from client" という文字列が出力されています。

![001.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2671591/7111ed6a-5a37-aea7-f85a-e3172ece4145.gif)

## サーバからクライアントにメッセージを送信

今度は、サーバがクライアントからメッセージを受け取ったタイミングで、クライアントにメッセージを返すということをしてみましょう。

まずはクライアント側でメッセージを受け取る準備をします。

```diff_tsx:client/src/app/page.tsx
"use client";

+ import { useEffect } from "react";
import { io } from "socket.io-client";

const socket = io("http://localhost:8000");

const Home = () => {
+   useEffect(() => {
+     socket.on("update", (message: string) => {
+       console.log(message);
+     });
+ 
+     return () => {
+       socket.off("update");
+     };
+   }, []);
+
  return (
    <div>
      <button
        onClick={() => {
          socket.emit("message", "message from client");
        }}
      >
        メッセージを送信
      </button>
    </div>
  );
};

export default Home;
```

`socket.on()` はコンポーネントがマウントされる時に一度だけ実行したいので、`useEffect()`　フックを用いています。念の為、クリーンアップ関数には `socket.off()` を記述しておきました。

ここで指定している `update` の部分は、先程と同様に合言葉となります。

続いて、サーバからクライアントにメッセージを送信します。`update` という合言葉とともに "message from server" というメッセージを emit しましょう。

```diff_typescript:server/src/events/events.gateway.ts
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
+   WebSocketServer,
} from "@nestjs/websockets";
+ import { Server } from "socket.io";

@WebSocketGateway({
  cors: {
    origin: "http://localhost:3000",
  },
})
export class EventsGateway {
+   @WebSocketServer()
+   server: Server;

  @SubscribeMessage("message")
-   handleMessage(@MessageBody() body: string): string {
+   handleMessage(@MessageBody() body: string) {
    console.log(body);
-     return "Hello world!";
+     this.server.emit("update", "message from server");
  }
}
```

これで、サーバからクライアントへのメッセージの送信ができました。

![002.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2671591/f9bc0b9c-dde8-77df-f931-ec8663d4ff26.gif)

しかし、このままだとサーバと通信が開いているクライアント全てにメッセージが送信されてしまいます。

## サーバから "特定の" クライアントにメッセージを送信

最後に、サーバから特定のクライアントにメッセージを送信する方法を見てみましょう。

### ソケットの ID を指定

`@ConnectedSocket()` デコレータを用いると、メッセージが送信されたソケットの情報を取得することができます。

`.to()` でソケットの ID を指定すると、そのソケットのみにメッセージを送信することができます。

```diff_typescript:server/src/events/events.gateway.ts
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from "@nestjs/websockets";
- import { Server } from "socket.io";
+ import { Server, Socket } from "socket.io";

@WebSocketGateway({
  cors: {
    origin: "http://localhost:3000",
  },
})
export class EventsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage("message")
-   handleMessage(@MessageBody() body: string) {
+   handleMessage(@MessageBody() body: string, @ConnectedSocket() socket: Socket) {
    console.log(body);
-     this.server.emit("update", "message from server");
+     this.server.to(socket.id).emit("update", "message from server");
  }
}
```

### ルームを指定

ソケット ID ではなく、ルームを指定することもできます。

まずはクライアント側でルームを選択できるような UI を作ってみましょう。

```tsx:client/src/app/page.tsx
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { io } from "socket.io-client";

const socket = io("http://localhost:8000");

const Home = () => {
  const [selectedRoom, setSelectedRoom] = useState("room1");

  return (
    <div>
      <select
        value={selectedRoom}
        onChange={(e) => {
          setSelectedRoom(e.target.value);
        }}
      >
        <option value="room1">Room 1</option>
        <option value="room2">Room 2</option>
        <option value="room3">Room 3</option>
      </select>
      <button
        onClick={() => {
          socket.emit("message", "message from client");
        }}
      >
        送信
      </button>
    </div>
  );
};

export default Home;
```

次に、選択されているルームが変わるたびにサーバにそれを通知するようなメッセージを送信します。

```diff_tsx:client/src/app/page.tsx
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { io } from "socket.io-client";

const socket = io("http://localhost:8000");

const Home = () => {
  const [selectedRoom, setSelectedRoom] = useState("room1");

+   useEffect(() => {
+     socket.emit("join", selectedRoom);
+ 
+     return () => {
+       socket.emit("leave", selectedRoom);
+     };
+   }, [selectedRoom]);
+ 
  return (
    <div>
      <select
        value={selectedRoom}
        onChange={(e) => {
          setSelectedRoom(e.target.value);
        }}
      >
        <option value="room1">Room 1</option>
        <option value="room2">Room 2</option>
        <option value="room3">Room 3</option>
      </select>
      <button
        onClick={() => {
          socket.emit("message", "message from client");
        }}
      >
        送信
      </button>
    </div>
  );
};

export default Home;
```

サーバ側では、この `join` と `leave` のメッセージをハンドルする関数を作成します。

```diff_typescript:server/src/events/events.gateway.ts
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from "@nestjs/websockets";
import { Server, Socket } from "socket.io";

@WebSocketGateway({
  cors: {
    origin: "http://localhost:3000",
  },
})
export class EventsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage("message")
  handleMessage(@MessageBody() body: string, @ConnectedSocket() socket: Socket) {
    console.log(body);
    this.server.to(socket.id).emit("update", "message from server");
  }
+ 
+   @SubscribeMessage("join")
+   handleJoin(@MessageBody() roomId: string, @ConnectedSocket() socket: Socket) {
+     socket.join(roomId);
+   }
+ 
+   @SubscribeMessage("leave")
+   handleLeave(
+     @MessageBody() roomId: string,
+     @ConnectedSocket() socket: Socket
+   ) {
+     socket.leave(roomId);
+   }
}
```

ソケットが属しているルームは `socket.rooms` で取得できます。`socket.rooms` は JavaScript の Set で記述されています。

また、`socket.rooms` は１番目にソケットの ID、２番目以降にルーム名が含まれています。

```diff_typescript:server/src/events/events.gateway.ts
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from "@nestjs/websockets";
import { Server, Socket } from "socket.io";

@WebSocketGateway({
  cors: {
    origin: "http://localhost:3000",
  },
})
export class EventsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage("message")
-   handleMessage(@MessageBody() body: string, @ConnectedSocket() socket: Socket) {
-     console.log(body);
-     this.server.to(socket.id).emit("update", "message from server");
+   handleMessage(
+     @MessageBody() message: string,
+     @ConnectedSocket() socket: Socket
+   ) {
+     const [, room] = socket.rooms;
+     this.server.to(room).emit("update", message);
  }

  @SubscribeMessage("join")
  handleJoin(@MessageBody() roomId: string, @ConnectedSocket() socket: Socket) {
    socket.join(roomId);
  }

  @SubscribeMessage("leave")
  handleLeave(
    @MessageBody() roomId: string,
    @ConnectedSocket() socket: Socket
  ) {
    socket.leave(roomId);
  }
}
```

`.to()` でルームを指定することで、そのルームに接続している各クライアントにメッセージを送信することができました。

# おわりに

この記事では NestJS の WebSocketGateway を用いて、サーバとクライアントの間の WebSocket 通信を実現することができました。

後編の記事では、実際にクライアント間でリアルタイムにチャットができるアプリケーションの開発を行っていきます。

最後までお読みいただきありがとうございました。

# 次回の記事

https://qiita.com/tsuyuni/items/e0a4c4af44a9476c0941