7839

雑草魂エンジニアブログ

【Next】Next.js + SocketIO で簡単なチャットアプリを作る

今回は、SocketIO を用いた簡単なチャットアプリを作ってみたので、紹介する。

f:id:serip39:20200907223728g:plain

なお、実装したサンプルコードは GitHub に公開している。 Docker でコンテナを起動するだけで、サンプルプログラムの動作確認ができるようにしているので、どうぞ。

github.com

Next.js + Socket通信の実装

サーバーサイド

今回は、Next.js をベースとしている。Next.js のカスタムサーバーとして、Express を用いており、サーバー側で注意すべきは以下の部分ではないかと思う。

const httpServer = server.listen(port, (err?: any) => {
  if (err) throw err
  console.log(`> Ready on localhost:${port} - env ${process.env.NODE_ENV}`)
})

const io = socketio.listen(httpServer)

SocketサーバーをExpressサーバーに接続する際には、server.listen(httpServer[, options]) メソッドを用いて、 httpServer を引数として渡してあげる必要がある。そして、httpServer は、Express の app.listen([port[, host[, backlog]]][, callback]) の返り値で得ることができる。

app.listen = function () {
  var server = http.createServer(this)
  return server.listen.apply(server, arguments)
}

ここさえできてしまえば、残りは通常通り Socket サーバーを実装するだけである。

メッセージを送信する場合は、次のパターンがある。(今回は、ルームの概念がない場合のみで説明する。)

  • io.emit('eventName', 'message')
    • 全員に送信する(送信者含む)
  • socket.emit('eventName', 'message')
    • eventNameのクライアントにのみ、送信する(socket.on('eventName', callback)待っているクライアントにのみメッセージを送信する。)
  • socket.broadcast.emit('eventName', 'message')
    • 全員に送信する(送信者を除く)

また、今回のサンプルでは、設定していないが、以下のイベントハンドラの設定はした方がいいのではないかと思っている。

  • socket.on('Event', callback)
    • disconnect / error / disconnecting が発生した場合のエラーハンドリング

今回は、チャットということで、以下のように POST /chat でデータが送られてきた場合に、全員に向けてデータを転送するようにした。(イベント名を 'update-data' としている。)

server.post('/chat', (req: Request, res: Response) => {
  postIO(req.body)
  res.status(200).json({ message: 'success' })
})

const postIO = (data) => {
  io.emit('update-data', data)
}

フロントエンド

フロントエンドに関しては、socket.io-client を用いて実装した。まずは、完成したコードを先に紹介する。

const [socket, _] = useState(() => io())
const [newChat, setNewChat] = useState<ChatType>({})
const [chats, setChats] = useState<ChatType[]>()
useEffect(() => {
  socket.on('connect', () => {
    console.log('socket connected!!')
  })
  socket.on('disconnect', () => {
    console.log('socket disconnected!!')
  })
  socket.on('update-data', (newData: ChatType) => {
    console.log('Get Updated Data', newData)
 setNewChat(newData)
  })
  return () => {
    socket.close()
  }
}, [])
useEffect(() => {
  if (newChat.message) {
    setChats([ ...chats, newChat])
  }
}, [newChat])

useEffect(() => {}, []) の第二引数に空配列を渡して、初回のマウント時に、イベントハンドラをそれぞれ設定している。 'update-data' でデータが送られてきた際に、本来は以下のように実装したかった。

socket.on('update-data', newData => {
  console.log('Get Updated Data', newData)
  setChats([ ...chats, newChat])
})

Server側から送られてきたデータを、そのままchats配列に追加したかったが、chatsに対して、1つは追加されるが、1つ以上追加できずに、常にチャットを上書きしてしまう状態になってしまった。原因は、setChats() で追加したchats配列が毎回更新されずにいたことであった。setChats() は動作して、チャットが上書きはされていたので、chats配列が参照渡しではなく、イベントハンドラ設定時のみしか値を読み込んでいないように思えた。

そこで、今回は、新規データのみを格納する変数 newChat を作り、その変数が変更された場合に、setChats() をすることで、一応 Socket で送られたデータをリアルタイムに反映することができた。

Material-UI で簡単にUI構成

今回、UIに関しては、Material-UI を利用してみた。

f:id:serip39:20200908001418p:plain

Slack風のUIにしてみた。すごく簡単に実装できたので、今後も使っていきたいと思えた。

まとめ

今回は、Next.js + SocketIO で簡単にチャットのサンプルアプリを実装してみた。Socket の接続がまだ不安定だったりする部分があるが、そこはエラーハンドラをしっかり設定するなどして、改善すべき事項であると思っている。

本来は、IoT のセンサデータをリアルタイムに可視化するために、以下のようなアプリを開発したいと思っている。これから、また色々と試行錯誤していきたい。

f:id:serip39:20200903021257j:plain

それでは、ステキな開発ライフを。