【個人開発】独学が捗る！オンライン自習室サービスを作りました【Rails ✕ Nuxt】

![ogp](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/f179f0ef-1916-cdf3-f5c1-96198160c3df.png)

## はじめに
閲覧ありがとうございます！
現在、独学をしながらエンジニアを目指しているたかしょー（[@takasho1024](https://twitter.com/takasho1024)）と申します。

独学をしている人なら分かると思いますが、独学するモチベーションを保つのって結構大変ですよね。
そこでもくもく会の出番なのですが、出先だと自宅の作業環境を再現するのが難しいですし、なにより時間もお金もかかります（郊外住みの私には厳しい）。
一方オンラインもくもく会では、上記のデメリットはないのですが早朝だけ開催など時間的な縛りがあります（生活リズムが乱れがちな私にはこれまた厳しい 笑）。
また、カメラとマイクを繋げるのが少し仰々しく感じて参加までに至らないという人が私以外にもいるのではないでしょうか。
もっとカジュアルに、いつでも、どこでも、仲間ともくもくできる場所があればいいのに・・・
そんな独学者たちの悩みを解決するべく、オンライン自習室サービス **MokuTube（もくつべ）** を作りました！

▼サービスURL
※現在はサービスを停止しています。

~~※パソコンからのみ利用可能です。~~
~~※現在、平日の9:00〜21:00の間だけ稼働しています。~~
~~https://mokutube.net~~

▼GitHubURL

https://github.com/usho1024/MokuTube

## サービス概要
MokuTubeはYouTube上にある作業用BGMを流しながらゆるっと気軽に参加できるオンライン自習室サービスです。
**“もくもく会をもっとカジュアルに“** がコンセプトです。
基本的には登録不要ですべての機能を使うことができます（ゲストは24時間だけ有効）。
自習室内では常にWebSocketによるリアルタイム通信が行われるので、家にいながら本当の自習室にいるかのような没入感が味わえます。

### 使い方
1. ゲストログイン OR ログイン
1. ルーム一覧からルームを選んで入室
1. 着席する
基本的にこれだけです。
最短3クリックで使えるようになっています。

### ターゲット層
勉強のモチベーションを上げたい独学者（プログラミングに限らず）

## 主な機能

### ルーム内
主に4つの機能があります。

![自習室内機能.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/a4ca1642-a4ab-8dc0-b378-f7b8b6f95e7c.png)

#### ①リアルタイムな座席情報の共有
画像上の座席をクリックすると席を確保することができます。
※座席数はルームの種類により異なります。

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/71abb6c2-387f-8f15-7c3b-4724ba718424.gif)

#### ②リアルタイムチャット
簡単なコミュニケーションツールとしてルーム内のユーザー同士でチャットができます。
※着席していなくても使えます。

![ezgif.com-video-to-gif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/02a48c9e-05df-f8a3-ae5b-3733204b045d.gif)

#### ③BGMの視聴
リラックスして作業に集中できるように各ルームに設定されたBGMを視聴することができます。
付随して以下の機能も使えます。

- スライダーによる音量調節
- ボリュームアイコンクリックでミュートの切替（デフォルトではミュート）

#### ④滞在時間の計測（ストップウォッチ）
入室から退室までの時間を計測して **勉強時間の見える化** ができます。
付随して以下の機能も使えます。

- ボタンクリックで計測の一時停止と再開の切替
- 退室時に自動で滞在時間を各ユーザーの総利用時間に加算

#### ⑤その他
座席上のユーザーアイコンクリックで簡単なプロフィールを閲覧することができます。

### ルーム一覧
ここから好きなルームを選んで入室することができます。

![FireShot Capture 117 - ルーム一覧 - MokuTube - mokutube.net.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/3d68d120-b9ac-ded7-836b-e5701d925068.png)

- ルームのソート（公式ルーム、ユーザーが多い順、作成日時が新しい順）
- 動的なページネーション
- 現在の着席ユーザー人数の表示

### ルーム作成
自分でルームを作ることもできます。

![ezgif.com-video-to-gif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/396b207e-e9e9-57cb-cfa7-b1166a77aa6d.gif)

- 10種類のアイソメトリックイラストからルームイメージを選択（[iStock](https://www.istockphoto.com/jp)の素材を使用）
- YouTube Data APIによる再生リストの自動取得
    - BGMは著作権フリーのものを使用しています
    - 提供元：[RYU ITO MUSIC 様](https://www.youtube.com/@RYUITOMUSIC)
- BGMの試聴

### 認証関係、マイページ
ユーザーはマイページで自分の情報の確認&編集ができます。

![FireShot Capture 118 - マイページ - MokuTube - mokutube.net.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/839bf005-0a37-ffc7-206e-f0760a87f510.png)

- トークンベースの認証
    - 新規会員登録
    - 一般会員ログイン
    - ゲストログイン（24時間だけ有効）
- ユーザーの詳細情報の閲覧
- ユーザーの登録情報の編集

## 工夫&苦労した点

### オンライン自習室をどのように表現するか
ググっても実装例が皆無に等しかったので、この部分は非常に難儀しました。
DB設計、フロントとバックの連携、ActionCableの使い方など考慮することが多く、ピースをハメようと試行錯誤する日々が1ヶ月くらいは続きました。
特にActionCableを用いたWebSocket通信の実装は苦労しました。
チャット機能はよくある実装例なのですが、席の移動に関しては情報が全くなく一から自分でロジックを組む必要がありました。
正直途中で実現不可能なのでは？と思ったりしたのですが、集めまくった断片的な情報をうまいことつなぎ合わせてなんとか実装に至ることができました。

参考までに作成したRails側のActionCable関連ファイルの一部を下記に記載します。

<details><summary>room_channel.rb</summary>

ルームチャンネルに関するメソッドをまとめたファイルです。

```ruby:room_channel.rb
class RoomChannel < ApplicationCable::Channel
  # ルームID毎にストリームを別ける
  def subscribed
    stream_from "room#{params[:room]}"
  end

  # 退室の際にカレントユーザーの席情報を削除する
  def unsubscribed
    return unless RoomsUser.exists?(room_id: params[:room], user_id: current_user.id)

    RoomsUser.find_by(room_id: params[:room], user_id: current_user.id).destroy
    room_users = RoomsUser.where(room_id: params[:room]).includes(:user)
    room_users.map do |room_user|
      room_user.detail = {
        avatar: room_user.user.avatar.thumb.url
      }
    end
    content = {
      type: 'getSeat',
      body: room_users
    }
    ActionCable.server.broadcast("room#{params[:room]}", content)
  end

  # チャットのメッセージを作成する
  def speak(data)
    Message.create!(
      room_id: params[:room],
      user_id: current_user.id,
      body: data['message']
    )
  end

  # ルームの席を確保する
  def get_seat(data)
    if RoomsUser.exists?(room_id: params[:room], seat_number: data['seat_number'])
      return
    elsif RoomsUser.exists?(room_id: params[:room], user_id: current_user.id)
      RoomsUser.find_by(room_id: params[:room], user_id: current_user.id).destroy
    end

    RoomsUser.create!(
      room_id: params[:room],
      user_id: current_user.id,
      seat_number: data['seat_number'],
      x_coord: data['x_coord'],
      y_coord: data['y_coord']
    )
  end
end
```
</details>

<details><summary>message_broadcast_job.rb</summary>

チャットメッセージをブロードキャストするActiveJobのファイルです。
メッセージは、DBに保存された後に該当ルームにいる全ユーザーに送信されます。

```ruby:message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    message.sender = {
      name: message.user.name,
      avatar: message.user.avatar.thumb.url
    }
    content = {
      type: 'speak',
      body: message
    }
    ActionCable.server.broadcast("room#{message.room_id}", content)
  end
end
```
</details>

<details><summary>connection.rb</summary>

WebSocketのコネクションに関するファイルです。
認証されたログインユーザーごとに接続を確立するようにしています。

```ruby:connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      uid = request.params[:uid]
      token = request.params[:token]
      client = request.params[:client]
      user = User.find_by(uid:)
      if user && user.valid_token?(token, client)
        user
      else
        reject_unauthorized_connection
      end
    end
  end
end
```
</details>

ちなみにアイデアをまとめるツールとしてNotionを活用していました。
下記は本サービスのアイデアをまとめたメモです。
自分用に作ったので少し荒削りですがよければご覧ください。

https://childlike-apology-f05.notion.site/YouTube-6bae7ac8340e471291f9b56f4ae315a3

### デプロイ関連
デプロイ後にCORSエラーが発生したのですが、中々解決できず5日間程時間を無駄にしてしまいました。
色々試していくうちに本番環境から導入したNginxが怪しいと思い以下の作業を実施。

- Railsコンテナ内で同居していたNginxを別のコンテナで動かすようにする
- ローカル環境（`docker-compose.prod.yml`）で動作を確認
- ECSコンソールを新→旧に変えて細かい設定をいじる
- CloudWatchでログを確認して発生しているエラーを一つずつ確実に潰す

工夫という程でもありませんが、これで無事CORSエラーが解消されました。
ECSへのデプロイは今回がはじめてだったのですが、

- `難しいことは同時に複数やらないこと`
- `詰まったら問題を細かく分割して一つ一つ確認すること`

が大事だということを改めて思い知りました。

特に、サポートが不十分な新コンソールが2023年からデフォだったのはかなり盲点でした。
CORSエラーの原因は未だハッキリと解明できていませんが、ここまで手間取ってしまったのは自分のECS設定に不備があったからだと考えています。

これからECSにデプロイするという初学者には、不要なトラブルを避けるため旧コンソールからデプロイ作業をすることを強くおすすめします。

![スクリーンショット 2023-03-04 0.23.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/c99d9147-e9ec-1b7f-11c5-0f766dfd5172.png)

## ER図
ルーム画像上のXY座標を保存することでルーム上のユーザーの位置を表現しています。
![ER図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/5c8bc24f-511d-bbb5-4278-44d20e97f2f6.png)

## インフラ構成図
![インフラ構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2071279/803bd2e4-2398-efa2-409b-e4c49fa26898.png)

## 使用技術一覧

### フロントエンド
- Nuxt.js 2.15.8（SPAモード）
- Vuetify
- Jest
- ESLint
- Prettier
- 主要なpackage
    - @nuxtjs/axios 
    - @nuxtjs/auth（認証関係）
    - @nuxtjs/google-gtag（Googleアナリティクス用）
    - @nuxtjs/moment（表示日時のフォーマット）
    - actioncable（RailsのActionCableとの提携）
    - vue-youtube（YouTubeIFramePlayerAPIのVue用ラッパー）
    - vuex-persistedstate（Vuexデータの永続化）

:::note info
**技術選定に関する補足**
JSフレームワークについて、Reactと迷いましたが以下の理由により今回はVue.js（Nuxt.js）を採用しました。

- Nuxt.jsを含めても学習コストがそこまで高くない
- 初学者の実装例が多いので再現性が高い
- 個人開発なので大規模ではない

また、SSRではなくSPAモードを選んだのはあまりSEOを意識する必要がなかったというのと、単純にSPAモードでの開発に慣れていたからです。
:::

### バックエンド
- Ruby 3.1.2
- Rails 6.1.7（APIモード）
- RSpec
- RuboCop
- MySQL 8.0.31
- Nginx
- 主要なGem
    - rack-cors
    - devise_token_auth（アクセストークンの発行）
    - carrierwave（画像アップロード）

### インフラ
- AWS（ECS Fargate, ECR, Route53, ACM, ALB, RDS, S3）
- CircleCI

:::note info
**技術選定に関する補足**
CI/CDについて、GitHub Actionsと迷いましたが以下の理由により今回はCircleCIを採用しました。

- お手軽&多機能なGitHub Actionsにシェアが奪われつつあるものの、依然として採用している企業が多い（私の観測範囲内）
- 過去に作成したポートフォリオでGitHub Actionsを使ったことがあったので、単純に自分の守備範囲を広げたかった
:::

### 開発環境
- VSCode
- Docker（docker-compose）
- MacBook Air (M1, 2020)

## 今後の展望

インフラ面が少し弱いのでそこを中心に改善していきたいです。
- CloudWatch Events × LambdaでECSを自動起動・自動停止できるようにする
- Terraformでインフラのコード化
- その他機能やUIのブラッシュアップ

## おわりに
最後まで読んでいただきありがとうございます！
独学はしんどいですが最後までちゃんとやりきれた場合、確かな自信と実力が身につきますしその結果大きなリターンを得ることにも繋がります。
このサービスと記事がひとりでも独学を頑張る人たちの役に立てるなら本望です。
アプリのリポジトリも公開しているので、もしアプリの作り方で悩んでいる人がいたら是非参考にしてみてください！