Next.js と Go で WebSocket 通信を行おう【接続編】

# はじめに
1. [Next.js と Go で WebSocket 通信を行おう【環境構築編】](https://qiita.com/1129-tame/items/39101ed81039542198dc) 
1. [Next.js と Go で WebSocket 通信を行おう【接続編】](https://qiita.com/1129-tame/items/2036d89915b3a3d026be) <- 今回の記事

Next.js と Go を利用して WebSocket 通信を行なってみます。
環境構築は前回記事を参照してください。
今回は実際に WebSocket 接続を行い、ローカル環境で双方向通信を実現させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180763/9cedec58-9ea5-bf74-87b6-3e8bc055f1e8.png)

このように form にデータを入力し、サーバーサイドでデータを加工して受け取ることをゴールとします。

https://github.com/1129-tame/next-go-websocket

また、今回実装したコードは上記リポジトリに公開しています。

# 対象読者
- WebSocket 通信を用いて双方向通信を行いたい人

# WebSocket とは？
ウェブ上で双方向通信を行うためのプロトコルの一種です。
ウェブ上では通常 HTTP プロトコルを用いますが、それですと一回のやり取りで一つのレスポンスしか返せない、サーバーサイド自らが通信できない等制約があります。

そうした制約を回避するために WebSocket 通信が生まれました。
WebSocket を用いることでリアルタイムで双方向通信を手軽に実現することができます。

# WebSocket を用いて接続させる
それでは早速実装を行なっていきます。
以下の様な手順で WebSocket 通信を実現させます。

1. クライアンドとサーバーを接続
    1. クライアントサイド実装
    1. サーバーサイド実装
    1. 疎通確認 
1. データを送信して双方向通信を行う
    1. form 作成
    1. view に表示させる 

## クライアンドとサーバーを接続
https://qiita.com/1129-tame/items/39101ed81039542198dc

上記記事で環境構築を行うと `localhost:3000` でクライアンドサイドのサーバーが。`localhost:8080` でサーバーサイドが立ち上がる状態になっているはずです。ただ、それだけだとお互い独立してサーバーが立ち上がっているだけですので、`localhpst:3000` と `localhost:8080` を明示的に接続する必要があります。

ですのでまずこの両者の接続を行います。

### クライアンドサイド実装
1. まずは next.js デフォルトの画面を消して、 `index.tsx` をすっきりさせます。

      ```typescript:index.tsx
      import type { NextPage } from 'next'
      
      const Home: NextPage = () => {
        return (
          <h1>WebSocket</h1>
        )
      }
    
      export default Home
      ```

1. JS 標準の APIである [WebSocket](https://developer.mozilla.org/ja/docs/Web/API/WebSocket) を使ってサーバーサイドと接続します。
    - socket.io ライブラリも当初使ってみましたが、Go server 側とうまく接続できなかったので標準を使用します。

        ```typescript
        new WebSocket('<接続する相手側のpath>')
        ```

    - このようにインスタンス化させると指定した path と WebSocket 通信を試みます。 
    - 今回はサーバー側のポート番号を8080にして `/socket` で接続できる様にします。

      ```typescript:index.tsx
      import type { NextPage } from 'next'

      const Home: NextPage = () => {
        const socket = new WebSocket('ws://localhost:8080/socket')
        return (
          <h1>WebSocket</h1>
        )
      }

      export default Home
      ```

1. `useRef` を用いて値を保持できる様にします。
    - ただ上記のように単純に定数に入れてもうまくいきません。なぜなら `new WebSocket` した瞬間に WebSocket 通信が始まるため、この様に定義するとレンダリングされるたびに WebSocket 通信が初期化されてしまいます。

    - なので `useRef` を使います。 useRef を使用すればコンポーネントが再レンダリングされても値を保持してくれます。

      ```diff_typescript:index.tsx
      const Home: NextPage = () => {
      + const socketRef = useRef<WebSocket>()
      + const [isConnected, setIsConnected] = useState(false)
      
      + useEffect(() => {
      +   socketRef.current = new WebSocket('ws://localhost:8080/socket')
      +   console.log(socketRef)
      +   socketRef.current.onopen = function () {
      +     setIsConnected(true)
      +     console.log('Connected')
      +   }
      +
      +   socketRef.current.onclose = function () {
      +     console.log('closed')
      +     setIsConnected(false)
      +   }
      + }, [])

        return (
          <h1>WebSocket is connected : {`${isConnected}`}</h1>
        )
      }
      ```

    - ついでに useState で接続できているかどうかの値を入れて確認できる様にしておきましょう。
    - 当然現状では server 側では何もしていないので接続もできません。

1. `useEffect` の中に unmount 時の処理も追加します。
    - socketRef.current は undefined の可能性もあるので`?`も入れておきます。
      ```typescript:index.tsx
      return () => {
        socketRef.current?.close()
      }
      ```

最終的なコードは以下のようになります。
```typescript:index.tsx
import type { NextPage } from 'next'
import { useEffect, useRef, useState } from 'react'

const Home: NextPage = () => {
  const socketRef = useRef<WebSocket>()
  const [isConnected, setIsConnected] = useState(false)

  useEffect(() => {
    socketRef.current = new WebSocket('ws://localhost:8080/socket')
    socketRef.current.onopen = function () {
      setIsConnected(true)
      console.log('Connected')
    }

    socketRef.current.onclose = function () {
      console.log('closed')
      setIsConnected(false)
    }

    return () => {
      if (socketRef.current == null) {
        return
      }
      socketRef.current.close()
    }
  }, [])

  return (
    <>
      <h1>WebSocket is connected : {`${isConnected}`}</h1>
    </>
  )
}

export default Home

```

これでクライアントサイドの実装は終わりです。これからサーバーサイドを実装し `isConnected` に `true` が入る様にします。

### サーバーサイド実装
それでは go server 側の処理を書いていきます。
1. path の追加を行います。

      ```go:serve.go
      func main() {
      ・・・省略・・・

        e.GET("/socket", handleWebSocket)
      }
      ```

    - この様に追加することで `/socket` に接続された時に `handleWebSocket` メソッドが発火する様にします。

1. `handleWebSocket` メソッドの中身を記述します。

      ```go:serve.go
      import (
	      "fmt"
	      "log"
	      "net/http"
      
	      "github.com/labstack/echo/v4"
	      "github.com/labstack/echo/v4/middleware"
	      "golang.org/x/net/websocket"
      )
      
      func handleWebSocket(c echo.Context) error {
	      log.Println("Serving at localhost:8000...")
	      websocket.Handler(func(ws *websocket.Conn) {
		      defer ws.Close()

		      // 初回のメッセージを送信
		      err := websocket.Message.Send(ws, "Server: Hello, Next.js!")
		      if err != nil {
			      c.Logger().Error(err)
		      }

		      for {
			      // Client からのメッセージを読み込む
			      msg := ""
			      err := websocket.Message.Receive(ws, &msg)
			      if err != nil {
				      log.Println(fmt.Errorf("read %s", err))
				      c.Logger().Error(err)
			      }
      
			      // Client からのメッセージを元に返すメッセージを作成し送信する
			      err = websocket.Message.Send(ws, fmt.Sprintf("Server: \"%s\" received!", msg))
			      if err != nil {
				      c.Logger().Error(err)
			      }
		      }
	      }).ServeHTTP(c.Response(), c.Request())
	      return nil
      }
      ```
   - `handleWebSocket` メソッドの中身はこの様に記述します。受け取った msg をもとにメッセージを送信するようにします。

1. EOF error が返ってきた時にループ処理を抜ける様にします。

    ```
    2022/07/23 11:46:41 
    2022/07/23 11:46:41 read EOF
    {"time":"2022-07- 23T11:46:41.0074853Z","level":"ERROR","prefix":"echo","file":"serve.go","line":"31","message":"EOF"}
    ```

上記の実装だとこのようなエラーが出るので EOF が返ってくるときはループ文を抜ける様にします。

```diff_go
if err != nil {
+   if err.Error() == "EOF" {
+   log.Println(fmt.Errorf("read %s", err))
+   break
+   }
    log.Println(fmt.Errorf("read %s", err))
    c.Logger().Error(err)
}
```

これでサーバーサイドの実装が完了しました。続けて実際に接続できるか確認してみましょう。


### 疎通確認 
1. サーバーを起動して接続確認をします。
      ```bash
      $ cd backend
      $ go run serve.go
      ```
1. 別ターミナルを開き、クライアントサイドのサーバーも起動します。

      ```bash
      $ cd front
      $ yarn dev 
      ```

1. http://localhost:3000 にアクセスします。

![スクリーンショット 2022-07-24 8.53.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180763/4bce4091-01cb-ccd3-31cf-aaa84bb18ccb.png)

この様な画面が表示されていれば server 側と WebSocket 通信ができています。

## データを送信して双方向通信を行う
接続ができたので実際にデータを送って双方向通信ができているか試してみましょう。

### form の作成

```typescript
socketRef.current?.send('送りたいデータ')
```

このように `send` メソッドを用いると server 側にデータを送ることができます。
1. form を作って入力した値をボタンを押すことで送信できるようにしましょう。

      ```html
      <form onSubmit={sendData}>
        <input type="text" name="socketData" />
        <button type="submit">Server に送信</button>
      </form>
      ```

1. submit 時に `sendData` メソッドを発火させます。

      ```typescript
      const sendData = (event: any) => {
        event.preventDefault()
        socketRef.current?.send(event.target[0].value)
      } 
      ```

1. 分かりやすいように state に値を保存して view に表示させる様にします。

```typescript
const [formMessage, setFormMessage] = useState('')
const [sentMessage, setSentMessage] = useState('')
```

最終的なコードは以下になります。

```typescript:index.tsx
import type { NextPage } from 'next'
import { useEffect, useRef, useState } from 'react'

const Home: NextPage = () => {
  const socketRef = useRef<WebSocket>()
  const [isConnected, setIsConnected] = useState(false)
  const [formMessage, setFormMessage] = useState('')
  const [sentMessage, setSentMessage] = useState('')

  const sendData = (event: any) => {
    event.preventDefault()
    setFormMessage(event.target[0].value)
    socketRef.current?.send(event.target[0].value)
  }

  useEffect(() => {
    socketRef.current = new WebSocket('ws://localhost:8080/socket')
    socketRef.current.onopen = function () {
      setIsConnected(true)
      console.log('Connected')
    }

    socketRef.current.onclose = function () {
      console.log('closed')
      setIsConnected(false)
    }

    // server 側から送られてきたデータを受け取る
    socketRef.current.onmessage = function (event) {
      setSentMessage(event.data)
    }

    return () => {
      if (socketRef.current == null) {
        return
      }
      socketRef.current.close()
    }
  }, [])

  return (
    <>
      <h1>WebSocket is connected : {`${isConnected}`}</h1>
      <form onSubmit={sendData}>
        <input type="text" name="socketData" />
        <button type="submit">Server に送信</button>
      </form>
      <h3>form message: {formMessage}</h3>
      <h3>sent message: {sentMessage}</h3>
    </>
  )
}

export default Home
```

### view に表示
再びサーバーを立ち上げて通信ができるか確認してみます。
form の input に適当な値を入力して　Server に送信してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180763/e9bd49f6-5002-8745-2505-a5d320c899ce.png)

`socket data` というデータを送ると、サーバー側でデータを加工して返ってきます。
sent message 欄に返ってきたデータが表示されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180763/e46dd9bf-70f4-3249-fae5-caefea2da344.png)

このような画面が表示されていれば WebSocket 通信を利用して双方向にデータをやり取りすることができる様になっています。

## まとめ
Next.js と Go で WebSocket 通信ができる様になりました。この技術を使えばチャットアプリのような双方向通信を利用するサービスを生み出すことができます。
今回は localhost 同士の通信を行いましたが、 path を変更すればデプロイされたサーバー同士で通信もできる様になるはずです。

この記事を読んで少しでも WebSocket 通信について参考になれば幸いです。

## 参考文献
https://zenn.dev/empenguin/articles/bcf95c19451020

https://developer.mozilla.org/ja/docs/Web/API/WebSocket