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

# 前回の記事

https://qiita.com/tsuyuni/items/5dd2bc2d480d7663266e

# はじめに

こんにちは、梅雨です。

この記事では、前回に引き続き NestJS のWebSocketGateway を使ってチャットアプリを開発していきます。

# UI の作成

まずはチャットアプリの UI から作成していきましょう。（コードは記載していませんが、適宜 CSS をあてています。）

### チャット一覧

```tsx:client/src/app/page.tsx
import Link from "next/link";

const Home = () => {
  const chats = [
    { id: "cats", name: "猫チャット" },
    { id: "dogs", name: "犬チャット" },
    { id: "reptiles", name: "爬虫類チャット" },
  ];

  return (
    <main className="chats">
      <h1>チャット一覧</h1>
      <ul>
        {chats.map((chat) => (
          <li key={chat.id}>
            <Link href={`/chat/${chat.id}`}>{chat.name}</Link>
          </li>
        ))}
      </ul>
    </main>
  );
};

export default Home;
```

<img width=320 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2671591/2238a6bc-77b4-593a-4907-062b4168ba4c.png">

### チャット画面

```tsx:client/src/app/chat/[id]/page.tsx
"use client";

import { useParams } from "next/navigation";

const ChatPage = () => {
  const { id } = useParams<{ id: string }>();

  const chats = [
    { id: "cats", name: "猫チャット" },
    { id: "dogs", name: "犬チャット" },
    { id: "reptiles", name: "爬虫類チャット" },
  ];

  return (
    <main className="chat">
      <h1>{chats.find((chat) => chat.id === id)?.name}</h1>
      <ul></ul>
      <div>
        <input type="text" />
        <button>送信</button>
      </div>
    </main>
  );
};

export default ChatPage;
```

<img width=320 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2671591/765f4a5e-f778-83d0-1fe4-afa1deeb8304.png">

# メッセージの送信（クライアント → サーバ）

続いて、メッセージの送信を記述していきます。

まずはチャットルームへの入退室をサーバに知らせましょう。

```diff_tsx:client/src/app/chat/[id]/page.tsx
"use client";

import { useParams } from "next/navigation";
+ import { useEffect } from "react";
+ import { io } from "socket.io-client";

+ const socket = io("http://localhost:8000");
+ 
const ChatPage = () => {
  const { id } = useParams<{ id: string }>();

  const chats = [
    { id: "cats", name: "猫チャット" },
    { id: "dogs", name: "犬チャット" },
    { id: "reptiles", name: "爬虫類チャット" },
  ];

+   useEffect(() => {
+     socket.emit("join", id);
+ 
+     return () => {
+       socket.emit("leave", id);
+     };
+   }, []);
+ 
  return (
    <main className="chat">
      <h1>{chats.find((chat) => chat.id === id)?.name}</h1>
      <ul></ul>
      <div>
        <input type="text" />
        <button>送信</button>
      </div>
    </main>
  );
};

export default ChatPage;
```

次に、`useState()` を用いて input ボックスに入力されている文字列を制御しサーバに送信します。

```diff_tsx:client/src/app/chat/[id]/page.tsx
"use client";

import { useParams } from "next/navigation";
- import { useEffect } from "react";
+ import { useEffect, useState } from "react";
import { io } from "socket.io-client";

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

const ChatPage = () => {
  const { id } = useParams<{ id: string }>();

  const chats = [
    { id: "cats", name: "猫チャット" },
    { id: "dogs", name: "犬チャット" },
    { id: "reptiles", name: "爬虫類チャット" },
  ];

+   const [message, setMessage] = useState("");

  useEffect(() => {
    socket.emit("join", id);

    return () => {
      socket.emit("leave", id);
    };
  }, []);

  return (
    <main className="chat">
      <h1>{chats.find((chat) => chat.id === id)?.name}</h1>
      <ul></ul>
      <div>
-         <input type="text" />
-         <button>送信</button>
+         <input
+           type="text"
+           value={message}
+           onChange={(e) => {
+             setMessage(e.target.value);
+           }}
+         />
+         <button
+           onClick={() => {
+             socket.emit("message", message);
+             setMessage("");
+           }}
+         >
+           送信
+         </button>
      </div>
    </main>
  );
};

export default ChatPage;
```

# メッセージの送信（サーバ → クライアント）

サーバでは、メッセージを受信したらそのルームに対してメッセージを返します。これによって、ルームに参加している他に人にも WebSocket を通して新規のメッセージを知らせることができます。

```ts:server/src/events/events.gateway.ts
import {
  ConnectedSocket,
  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() message: string,
    @ConnectedSocket() socket: Socket
  ) {
    const [, room] = Array.from(socket.rooms);
    this.server.to(room).emit("update", message);
  }

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

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

しかしこれだけだと送信者が誰か判別することができないので、`socketId` を含めたオブジェクトとしてクライアントに送信してあげましょう。

```diff_typescript:server/src/events/events.gateway.ts
import {
  ConnectedSocket,
  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() message: string,
    @ConnectedSocket() socket: Socket
  ) {
-     const [, room] = Array.from(socket.rooms);
-     this.server.to(room).emit("update", message);
+     const [socketId, room] = Array.from(socket.rooms);
+     this.server.to(room).emit("update", { sender: socketId, message });
  }

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

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

クライアントではこのオブジェクトを受け取って UI の更新を行います。

```diff_tsx:client/src/app/chat/[id]/page.tsx
"use client";

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

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

const ChatPage = () => {
  const { id } = useParams<{ id: string }>();

  const chats = [
    { id: "cats", name: "猫チャット" },
    { id: "dogs", name: "犬チャット" },
    { id: "reptiles", name: "爬虫類チャット" },
  ];

  const [message, setMessage] = useState("");
+   const [messages, setMessages] = useState<
+     { sender: string; message: string }[]
+   >([]);

  useEffect(() => {
    socket.emit("join", id);
+     socket.on("update", (data: { sender: string; message: string }) => {
+       setMessages((prev) => [...prev, data]);
+     });

    return () => {
      socket.emit("leave", id);
+       socket.off("update");
    };
  }, []);

  return (
    <main className="chat">
      <h1>{chats.find((chat) => chat.id === id)?.name}</h1>
      <ul>
+         {messages.map((message, index) => (
+           <li
+             key={index}
+             className={`chat__item ${
+               socket.id === message.sender
+                 ? "chat__item--self"
+                 : "chat__item--others"
+             }`}
+           >
+             {message.message}
+           </li>
+         ))}
      </ul>
      <div>
        <input
          type="text"
          value={message}
          onChange={(e) => {
            setMessage(e.target.value);
          }}
        />
        <button
          onClick={() => {
            socket.emit("message", message);
            setMessage("");
          }}
        >
          送信
        </button>
      </div>
    </main>
  );
};

export default ChatPage;
```

これで WebSocket を用いたリアルタイムのチャットが完成しました。

データベースを使用していないため一度チャットを閉じると消えてしまいますが、インスタグラムの消えるモードみたいで意外と良いですね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2671591/73052756-e09f-a037-0360-c81aef2a36e1.gif)

# おわりに

以上が WebSocketGateway を使ったチャットアプリの実装例となります。

実際のアプリケーションではソケットと認証されたユーザを紐づけたり、送信されたメッセージをデータベースに保存したりする必要がありますが、基本的な部分はこの記事のように行うことができます。

リアルタイムで更新されるチャットアプリを開発したいと考えている方は是非参考にしてみてください。

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