社内システムをHotwire移行してみた

弊社が提供しているSaaS「Re:lation」の顧客情報や契約内容を管理する社内システムをRails7から導入されたHotwireで構築し直した話を書きます。

なぜ移行しようと思ったか?

移行する前は、Rails 6.1.7 + Bootstrap4系 + jQueryの構成でした。 全画面の共通の作りとして、ajaxでデータを取得&更新する作りとなっており、jQueryを使ってHTMLの更新を行っていました。

当時は、HTMLの構成を変更しづらかった点が効率的な開発を妨げる原因になっていました。 部分的に書かれたHTMLタグをjQueryの変数に格納。データに合わせて子要素を追加といった処理が書かれていたため、 少しBootstrapのコンポーネントを試そうと思ってもjs内にHTMLを書いて試すことが手間で、 またHTMLのパーツをうまく共通化できておらず、個々のjs内に同じ構成のHTMLを書いている現状でした。

こういった理由もありしばらく画面デザインの変更が行えておらず、要素の付け足しを続けていくことでどんどん1ページ内の要素が多くなり情報過多になっていました。

そこでjQuery脱却を目指し、フロントエンドFWを変更することをきっかけに動き出しました。

移行前の下準備

jQuery脱却に向け、まずはjQuery依存のライブラリを剥がしていきました。
Bootstrapを5系に上げ(5系からjQuery非依存)、jQuery依存のライブラリselect2やdatepickerをTomSelect, Flatpickrに置き換えました。 また、Railsを7系に上げjsのアセットパイプラインにimportmapを導入しました。

Hotwireを採用した理由

当時は jQueryの代わりに Svelte を導入しようと考えていました。
jQueryの開発しづらい点を解消できると思ったのと、単純に触ってみたかったからです。

最終的に Svelte ではなく Hotwireを選んだ理由は以下の理由です。

  1. この社内システムの開発&運用を行うのは1~2名のエンジニアなので、Railsで完結するHotwireの方が開発&運用コストが少ないと思ったから
  2. 非同期で処理させたい明確な理由がなかったから

私が担当を引き継いだ時から非同期処理が導入されていて、漠然とjQueryの代わりのフロントエンドFWを と思っていましたが、 そもそも非同期処理をしなければいけないほどユーザビリティやレスポンスが要求されるシステムではないと思い、Hotwireの導入を決めました。

Hotwireへの移行は画面毎に

社内システムとはいえ顧客情報や契約内容を扱う重要なシステムなので、まずはシステムの主要機能ではない画面から移行していきました。

Hotwireに移行した画面の共通Controllerを定義し、layoutファイルを指定します。 こうすることで移行前の画面は、applicaiton.html.erbを使ったlayoutで従来のアセットパイプラインのまま、移行後の画面はimportmapを使うよう切り分けを行いました。

移行と合わせてデザイン変更を行う

兼ねてより既存画面のデザイン変更は行いたいと思っていましたが、なかなか重い腰を上げられずにいました。 上記に上げた「HTMLの変更がしづらい」問題があったのと、開発の優先度を上げる理由がなかったからですが、 Hotwire移行が画面構成を見直す良い機会となりました。

Hotwireを使う場合、基本的にはTurbo Driveを使ってBodyタグを入れ替えます。 jQueryによる画面の部分的な置換は Turbo Frameもしくは Turbo Streamを使いますが、jQueryと同じように複数箇所の更新を Turbo Streamで置き換えることは保守性が下がると私は考えます。 それは変更を加える部分を turbo_frame_tagで囲む必要がある点やアクション後にそれら箇所に対して変更を行うことを把握しておかなければいけないからです。

明確にルールを決めたわけではありませんが、Turbo Streamによる部分的な変更は2箇所までとしています。 更新対象のデータ表示部分と、Bootstrapのトーストを使ったユーザへのメッセージを表示する箇所です。

Hotwireを使う場合、RailsのRESTのルーティングを活用して特定のリソースに対する操作が行いやすい画面構成にした方がHotwireのメリットを活かせると思います。

Hotwireならではの割り切った考え方

特定のリソースを検索する機能では検索メニューを用意しますが、検索項目が多くなると「検索条件をクリアする」を設置したくなります。 jQueryでは律儀に各検索項目を初期化していた処理をHotwireではそのページのURLを再描画することで検索メニューをクリアするようにしています。

トータル的な処理コストを考えるとjsで行った方が良いかと思いますが、こういった割り切りによって開発工数や保守工数を削減することができます。

移行前後のファイル数・ファイル行数の比較

Hotwire移行を始めて約1年。移行以外にも機能開発や機能改善を行っているため、基本的にファイル数は多くなっています。
そこで、移行前、移行後のリクエスト数を元に比較してみました。

移行前

リクエスト数:194

種別 ファイル数 総行数 行数(per file) 行数(per request)
js 101 27904 276.3 143.8
scss 58 4468 77.0 23.0
rb 179 30060 167.9 154.9
erb 97 13380 137.9 69.0

移行後

リクエスト数:501

種別 ファイル数 総行数 行数(per file) 行数(per request)
js 60 4076 67.9 8.1
scss 92 13268 144.2 26.5
rb 282 49178 174.4 98.2
erb 416 23684 56.9 47.3

差分

種別 ファイル数 総行数 行数(per file) 行数(per request)
js -41 -23828 -208.3 -135.7
scss 34 8800 67.2 3.5
rb 103 19118 6.5 -56.8
erb 319 10304 -81.0 -21.7

jsについては予想通り大幅な削減が行えました。js内に記載していたHTMLの記述が不要になったこと、StimulusのControllerとして定義したことで流用がきく構成になったことが削減理由になります。
一方erbのファイル数は増えましたが、1ファイルあたりの行数、1リクエストあたりの行数は変更前に比べ少なくなりました。 データ更新はBootstrapのModalやOffcanvasを活用し、表形式に並べたデータは行単位で更新できるようTurbo Frameを活用しています。 短い行数で対象とするデータを限定した作りとすることで、可読性は上がったと実感しています。

コミット数の比較

本来は移行期間を除いて集計したかったのですが、移行後の期間が短いため以下の期間で比較してみることにしました。

before:Hotwire移行が始まる前の1年間
after:Hotwire移行を含む1年間

それぞれの1年間のコミット数を集計すると、before: 1199、after: 1409でした。 期間中の参画エンジニアの人月で割ると、beforeは57.09、afterは82.88とコミット数が上がった結果となりました。
上でも可読性が上がったと書きましたが、こちらにも現れていると思います。

さいごに

約1年かけて社内システムの全画面をHotwireで構築し直しました。

データの流れがRailsで完結するのであっちこっち見ることなく改修を加えられるようになりましたし、 erbの共通化(partial)やStimulusのControllerを活用することでコンポーネントを活用できるようにもなりました。

逆にHotwireでの開発で留意しておくこは、リソースに合わせてControllerを用意すること、データ更新後に画面のどの部分を更新するか、それはTurbo Driveによる全体的な更新かFrameによる特定部分の変更かを決めておくことです。

個人的に、Hotwire移行による開発が行いやすくなった点と同じくらい、この移行をきっかけにデザイナーさんに協力してもらいながら全ての画面のデザインを刷新できたこと、これまで改善したかった機能が改善できたことがこの対応を行なって良かった点だと感じました。

以上です。