エンジニアの中根です。
trocco®︎のフロントサイドのリプレイスを現在進行形で行なっているので、ご紹介させて頂きます。

背景

trocco®︎のサーバサイドはRuby on Railsで実装されています。
これまでインフラ基盤へのkubernetesの導入などサービスのグロースに合わせた対応は随時行ってきました。
一方でフロントサイドに関しては、テンプレートエンジン直書きのJavaScriptやjQueryによる愚直なDOM操作など開発初期のままで育ってきてしまいました。
これらが開発スピード、開発体験の低下の要因となっているという課題は、メンバー皆が認識しつつも割れ窓理論でどんどん汚くなっていく一方…。
そのような状況が続く中、2019年12月ついにモダンなフロント技術への移行に乗り出しました。

Webpackの導入

まずは、JavaScriptのバンドル環境を整えました。
Sprocketsはnpmでモジュール管理をするようになった現代のフロント開発のニーズに対応できていないため、webpackに乗り換えました。
Rails6からはwebpackをラップしたGemであるWebpackerがデフォルトにもなったようです。
しかし、今回は素のwebpackを用いてRails側に依存しない構成を取りました。
webpackはフロントに不慣れなエンジニアにとって学習コストが高いなどの意見もありますが、Webpackerでラップされていてもさして変わらず、むしろ問題が起きたときの調査コストは以下のような理由でWebpackerの方が大きいと考えています。

  • webpackのバージョンを上げる必要がある場合、Gemの依存関係を解決しなければならない。Webpacker側がwebpack最新verに対応していない場合は為す術もない。
  • 「Webpacker」で検索してもwebpackの情報が多くヒットする(笑)。その割にWebpacker側の問題であることも多い。

Railsとwebpackの連携方法に関しては、検索すればいくつも例が出てくると思いますが、今回は以下を参考にしました。

各々のプロジェクト構成に合わせてカスタマイズしていくのがいいと思いますが、核となる部分としては

  • 分割したい単位のjsのエントリファイルを置くディレクトリを切って、webpack.config.jsでその配下のファイルを全てentryとしてなめるように設定する
  • public/**/* 配下をビルドの吐き先とする
  • Webpack Manifest Pluginを使ってファイル名とハッシュダイジェスト付きパスのマッピング情報を吐き出す
  • それらを読み込むためのRailsヘルパを作成

あたりでしょうか。
結局はWebpackerで行われていることを最小限に絞ってスクラッチで実装している感じです。

また、assets:precompile 時に生成されていたgzip圧縮されたファイルがなくなるので、
RackミドルウェアにDeflaterを追加しました。
ただ、前者は事前に圧縮される一方で後者はリクエストのたびに圧縮処理が行われるため、負荷が大きい場合は注意が必要です。

Rails.application.config.middleware.insert_before ActionDispatch::Static, Rack::Deflater

Reactの導入

jQueryによるDOM操作から脱却するためにReactを入れます。
Rails + Reactのパターンはいくつも方法があるかと思いますが、ページ数が既にそこそこあり、常に開発が進行している状況下では、Rails側はAPIに徹しビューはReact側が全て受け持つようなフルSPAは非現実的でした。
そこで、ルーティングは従来通りRailsで制御し、ページごとにルートコンポネントをマウントする構成を選択しました。

以下、簡単な実装例です。

  • ルートコンポネントをマウントするdiv要素のHTMLのdata属性にReact側へ渡したい情報を埋め込む。ヘルパを定義しておいてテンプレートエンジン側で呼び出す。
  • JavaScript(TypeScript)側でdata属性をJSON.parseしてReact側へ注入。
module ReactHelper
  def react_root(name, props)
    content_tag(:div, { id: name, data: { react_props: props } }) { }
  end
end
= react_root( \
  'react_app', \
  hoge: Hoge.new \
  ...
)
const app = document.getElementById('react_app') as HTMLElement
const reactPropsJSON = app.dataset.reactProps as string
const reactProps = JSON.parse(reactPropsJSON)

ReactDOM.render(<Hoge {...reactProps} />, app)

※ モデルのto_jsonが呼ばれる場合には、秘匿情報などが含まれないように気をつけます。

React-RailsというGemでも似たような方法が取られているようです。
https://github.com/reactjs/react-rails/blob/4dbcd6756dcd09c312abf95a73cbb6026f3f9a69/lib/react/rails/component_mount.rb

def react_component(name, props = {}, options = {}, &block)

導入したその他のライブラリなど

TypeScript

今回詳細は割愛しますが、新しいコードは全てTypeScriptで書いています。

React Hook Form

Formの管理には新興のOSSのReact Hook Formを採用しました。
https://react-hook-form.com/jp/
Formの値の管理をredux管理から外せる点が大きかったです。
Hooksベースの直感的なAPIも使いやすく、メンテナに日本人の方がいることも関係しているのか、早くからドキュメントの日本語対応がされています。

開発開始(2019/12)時には6000スター台でしたが、数ヶ月後の現在(2020/03)は8000スター台と人気も急速に高まっている印象があります。
今回の開発中に利用したかった useFieldArray の機能がリリースされたり、Issueを投げた際もすぐに修正されたりと、開発も活発なようです。

prettier

JSXにJavaScriptの式を埋め込んでいくとネストが深くなるので、各種エディタにprettierプラグインを入れて、保存時の自動フォーマットをONにしておくと、とても楽です。
余計なインデントなどを一切気にしないでロジックを書くことだけに集中できるので、地味に効率が上がります。
ESLintと併用するのが主流かと思いますが、prettier自体は非常に簡単に導入できるので、とりあえず入れておくのもいいと思います。

styled-components

CSS in JSにはstyled-componentsを使っています。
今回は詳細について割愛します。

今後について

社内の啓蒙

trocco®︎開発には、データエンジニアリングやクラウドインフラに強いメンバーが集まっていますが、自分も含めフロント専任エンジニアは2020年3月現在おりません。
今回のリプレイスの課題であった開発スピード、開発体験向上のためにも、社内向けにナレッジなどを共有していきたいと考えています。

テスト

TypeScriptの型チェックにより従来のJavaScriptで起こりうるバグの多くをコンパイル時に防ぐことができると思っていますが、必要に応じてフロントのユニットテストも追加していきたいです。

スタイルガイド

今回AtomicDesignに従ってReactコンポネントを切り出したので、それに沿ってStorybookも導入しています。
今後デザイナーさんとどう連携していくのがいいか、などを現在模索中です。

最後に

primeNumberでは共にサービスを作り上げてくれる仲間を積極的に募集しています。ご興味のある方はぜひご応募をお待ちしています。
https://primenumber.co.jp/recruit/