NestJS+Reactで簡易チャットアプリを作ってみる

# はじめに
前回記事ではPostman経由でWebsocket通信の動作を確認してきました。
今回は、実際に画面上の操作でWebsocket通信の挙動を確認していきたいと思います。
サーバーサイドはNestJS、フロントエンドはReactの構成となります。
※サーバーサイドの実装は前回記事のソースを使用する前提で話を進めていきます。

**前回記事**

https://qiita.com/tamakiiii/items/35a8bfd8bf75b1746eaf

# 実装

## サーバーサイド
まずは、サーバーサイドの実装になります。
といっても、前回記事で9割完成しており、あとは部分的な修正で完了です。
該当の修正箇所は２箇所です。

```main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // await app.listen(3000); ←React側のポート番号と重複しないようにコメントアウト
  await app.listen(9000); // ←追加
}
```

```gateway.ts
// 修正前
@WebSocketGateway()

// 修正後
@WebSocketGateway({
  cors: {
    origin: ['http://localhost:3000'],
  },
})
```
CORS許可の設定を追加しています。

## フロントエンド

#### プロジェクト作成
```
npx create-react-app react-nest-websocket --template typescript
```

#### パッケージをインストール
```
npm add socket.io-client
```

#### 実装
ソケット通信の設定部分を実装
```src/contexts/WebsocketContext.tsx
import { createContext } from "react";
import { io, Socket } from "socket.io-client";

export const socket = io('http://localhost:9000');
export const WebsocketContext = createContext<Socket>(socket);
export const WebsocketProvider = WebsocketContext.Provider;
```

チャット画面の実装
```src/components/Websocket.tsx
import { useContext, useEffect, useState } from "react"
import { WebsocketContext } from "../contexts/WebsocketContext"

type MessagePayload = {
  content: string;
  msg: string;
};

export const Websocket = () => {
  const [value, setValue] = useState('');
  const [messages, setMessages] = useState<MessagePayload[]>([]);
  const socket = useContext(WebsocketContext);

  useEffect(() => {
    socket.on('onMessage', (newMessage: MessagePayload) => {
      setMessages((prev) => [...prev, newMessage]);
    });

    return () => {
      socket.off('connect');
      socket.off('onMessage');
    }
  }, );

  const onSubmit = () => {
    socket.emit('newMessage', value);
    setValue('');
  }

  return (
    <div>
      <div>
        <h1>チャットアプリ</h1>
        <div>
          {messages.length === 0 ? <div>まだメッセージがありません。</div> : <div>
            {messages.map((msg, idx) => <div key={idx}>
              <p>{msg.content}</p>
            </div>)}</div>}
        </div>
        <div>
          <input
            type="text"
            value={value}
            onChange={(e) => setValue(e.target.value)}
          />
          <button onClick={onSubmit}>Submit</button>
        </div>
      </div>
    </div>
  )
}
```

上記追加したファイルをインポートと登録
```src/App.tsx
import './App.css';
import { Websocket } from './components/Websocket';
import { socket, WebsocketProvider } from './contexts/WebsocketContext';

function App() {
  return (
    <WebsocketProvider value={socket}>
      <Websocket />
    </WebsocketProvider>
  );
}

export default App;
```

# 完成

利用者Aさんの画面（既にメッセージ入力済み）
![スクリーンショット 2023-02-13 17.19.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554111/30f69c33-3aac-d404-d97f-0f4889e5c188.png)

利用者Bさんの画面（未入力の状態）
![スクリーンショット 2023-02-13 17.19.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554111/603f7048-fe03-723d-088f-0b5f3bcacd02.png)


このあと利用者Bさんが「はじめまして〜利用者Bです」と入力した時のAさんとBさんの画面結果
利用者Aさん
![スクリーンショット 2023-02-13 17.22.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554111/bd87d722-3d48-c376-54b5-39bd79924ae0.png)


利用者Bさん
![スクリーンショット 2023-02-13 17.22.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554111/91941ea7-a56f-6b45-9f9e-84d879194a37.png)