freeeの開発情報ポータルサイト

Ruby on Rails の View に React 製社内デザインシステムを導入した話(freeeサイン)

はじめに by @solt9029

freeeサインの開発に携わっているソフトウェアエンジニアの塩出(@solt9029)です。

freeeのプロダクトには、freee会計やfreee人事労務をはじめとし、非常に多くのものが存在します。このような状況下で、各プロダクトがそれぞれ独自のデザインを採用してしまうと、プロダクト間で似たような操作に微妙に異なるデザインや体験が採用されてしまい、ユーザーの認知・学習コストが不必要に増加してしまいます。

そこで、ユーザーがプロダクト間で類似する操作や体験が統一的に行えるように、社内では「vibes」や「標準UI」といったデザインシステムが開発されてきました(vibesや標準UIの導入背景や詳細などについては、デザインシステム “Vibes” の育てかたデザインシステムを拡張し、プロダクト開発の共通基盤を目指すをご参照ください)。

freeeのプロダクトの中には、最初から社内でスタートしたプロダクトもあれば、もともとは別会社のプロダクトだったものがM&Aによって社内のプロダクトになったものもあります。そして、freeeサインもそのひとつです。もともとは株式会社サイトビジットが運営する「NINJA SIGN」というプロダクトでしたが、M&Aによって最終的に「freeeサイン」になりました。

もともとは別会社のプロダクトだったという背景から、freeeサインにはfreeeの他プロダクトとは全く違うシステム構成やデザイン思想で作られている部分も多いです。しかしそのままの状況では、freeeサインと他プロダクトを併用するユーザーにとって、操作方法の認知・学習コストが不必要に高くなってしまいます。また、社内では複数のプロダクトを開発するエンジニアも多くいるため、プロダクト間で開発体験が異なると開発生産性も大きく下がってしまいます。

freeeサインのプロダクト開発チームでは3ヶ月に5日間、エンジニア同士で自由にWorking Groupを作り主体的に開発を行うことができる制度があります。そこで、我々はフロントエンドWGを立ち上げ、社内全体の取り組みの一環としてfreeeサインにもvibesや標準UIを導入することにしました。

本記事では、その導入に取り組んだメンバー @solt9029 @yskw @hiarc から、vibes・標準UI導入の進め方や乗り越えた障壁などを紹介します。

freeeサインのシステム構成および課題 by @solt9029

先に、freeeサインのシステム構成と社内で標準的に利用される技術の違い、またvibes・標準UIを利用するにあたって存在していた課題について軽く紹介します。

バックエンド フロントエンド
freeeサインのシステム構成 Ruby on Rails Slim・ViewComponent・Hotwire(Stimulus・Turbo)・i18n
社内で標準的に利用される技術 Ruby on Rails(APIモード) React(TypeScript)

freeeサインについて、社内で標準的に利用される技術との一番の差分は、Viewを描画する際にもRubyコードが出現する点です。この状況から、どうしてもView層でDBアクセスやドメインロジックが詰め込まれてしまうことが少なからずあります。

一方で、vibes・標準UIはReactで作られたコンポーネントライブラリです。Reactは当然RubyコードではなくJavaScript(TypeScript)のコードであり、ユーザーのブラウザ側で実行されます。そのため、DBアクセスやバックエンド固有のドメインロジックをそのまま扱うことができず、この移行が大きな課題になることは目に見えていました。 ※ 厳密にはReactでもSSRやISRなどでサーバーサイドで実行されるものもありますが、ここでは触れません。

他にもi18nの仕組みがある点・Hotwireがフル活用されている点など、移行にあたって様々な課題がありました。利用しているフロントエンドの技術が違うために、単にReactへ移行する・社内デザインシステムを導入するといっても、乗り越えなければならない壁がたくさん存在していました。

Reactへの移行について

サーバからのデータ取得方法 by @yskw

前述した通り、freeeサインはRailsフレームワークを用いたMVCの構成となっています。
画面が描画されるタイミングでサーバからデータを含めたHTMLファイルがレスポンスされます。

ここで問題となってくるのが「Reactコンポーネントへどのようにデータを渡すか」です。
Reactは画面が描画された後、クライアント側でJSが実行されコンポーネントが描画されます。
RailsのMVC上で動作させると以下のようになると思います。

RailsのMVC上で動作させた場合のReactへのデータの渡し方

この構成で、サーバからReactへデータを取得するやり方を考えた結果、2つの案が出てきました。

  1. サーバから返却されるHTML内に利用するデータを含めて、Reactコンポーネント内でデータを読み込む
  2. サーバから返却されるHTML内に利用するデータを含めず、Reactコンポーネント内でサーバへリクエストを行う

react-rails という gem を利用すると Rails のテンプレートエンジンからReactコンポーネントへ簡単にpropsを渡すことができます。

react-railsを利用した場合のReactへのデータの渡し方

Reactコンポーネント内でfetch関数等を利用してサーバ側のデータを取得するなら以下のようになると思います。

サーバからfetchしてReactでデータを取得する方法

freeeサインのエンジニア内で検討した結果、社内の他プロダクトと設計をあわせて、Reactコンポーネント内でPrivate APIへデータを取得する方針となりました。

freeeサインはMVC構成であるため、現状 Private API に相当するエンドポイントが存在しません。
社内のAPI標準ドキュメントを参照し、freeeサインに Private API を導入できるか調査したところ、いくつか課題がありすぐに導入することは難しいと判断しました。
新設するなら社内の標準に合わせていきたいため、 Private API の導入検討は後の課題としました。

したがって、短期的にはサーバから返却されるHTMLにReactで読み込みたいデータを含め、Reactコンポーネントに渡す構成を取る必要があります。

では、 react-rails gem などを用いて、HTML生成時に React コンポーネントの props にデータを渡す方針でいくか?
チーム内で検討を行いました。

将来的に Private API を導入し、クライアント側からデータフェッチする構成になることは予測できています。
短期的には Private API を用いずに React を導入していきますが、長期的に見た際、Private API 導入後に移行しやすいような構成にすることが大事であると判断しました。

Private API を用いた場合、ざっくり以下のような構成になると思います。

Private APIを用いた構成

react-rails gem などを用いた場合、ざっくり以下のような構成になると思います。

react-railsを用いた構成

図示したところ、構成の違いが分かりやすくなりました。
Private APIへ移行しやすくするために、なるべく似た設計にしていきたいです。

そこで、HTML生成時にReactコンポーネントのpropsにデータを渡さずにReactコンポーネントからHTML内のデータを読み込む構成にしました。
図にすると以下のような構成です。

ReactコンポーネントからHTMLデータを読み込む構成

Private APIを用いた構成と比較すると似た構成になりました。
画面描画後のデータの再取得など、上記構成では出来ないことも多いため、あくまでPrivate APIが導入されるまでの短期的な実装方針となります。

freeeサインではPrivate APIの導入が完了するまで、データ取得周りは上記の設計方針ですすめることとしました。

フロントエンドでの多言語対応 by @hiarc

課題

freeeサインでは、すべての画面に表示される文言をi18n(国際化)を使って日本語以外の言語にも対応しています。
i18nは、バックエンドでHTMLのレンダリングをする前に、指定したキーに対応する訳文を言語ファイルから見つけて解決する仕組みを提供しています。

i18nの仕組み

対して、Reactではブラウザ上で動くjavascriptでレンダリングを行うため、バックエンドでのi18nの訳文解決の仕組みをそのまま使うことができません。

対応

Reactで多言語対応をするためのi18nextというライブラリがあり、JSから多言語対応したテキストを参照するために使用しています。
i18nと同じようにt関数でキーを指定することで、json形式で用意した翻訳ファイルから言語設定に合わせたテキストを取得できるようにしています。

i18nextの仕組み

言語ファイルそのものの生成は、これまで開発者が日本語のテキストとキーのセットをyml形式で定義したものを、i18n-tasksというgemを使って多言語に自動変換する流れを取っていました。

フロントエンドのi18nextで使う言語ファイルも、できるだけ開発者の負担が少ない形で多言語に変換できるようにしました。
I18n-tasksで生成した多言語ファイルを、i18nextで扱えるようにjson形式に置き換えるrakeタスクを用意しておき、開発者が楽にフロント用の言語ファイルを出力できるようにしました。

i18n-tasksで生成した多言語ファイルをi18nextで扱えるようにjson形式に置き換えるrakeタスクの仕組み

i18nextの設定周りの話ですが、言語ファイルのjsonのkeyを型として登録しておくことで、t関数を使うときに値の補完をさせることができました。
これによりkeyの入力が楽になりました。

t関数で型補完が効く様子

vibes・標準UI化の進め方について by @hiarc

freeeサインの画面数はかなりの数があるため、フロントエンドWGの活動のみで、全ての画面をvibes/標準UIに置き変えていくことはしません。

WGの活動では以下のように、技術的にハードルが低いところから段階を踏んでvibes/標準UIの導入例を作っています。導入のバリエーションを増やしていくことで、freeeサインのエンジニアが開発中の画面でvibes/標準UIを使うソースイメージがつきやすい状態になることを目指しています。

  1. サーバーサイドの情報を使わない静的なページ
  2. サーバーサイドの情報をフェッチして使うだけのページ
  3. POST送信するページ
  4. モバイルに対応しているページ
  5. Stimulusを使っているページ(今後対応予定)
  6. TurboStreamを使っているページ(今後対応予定)

直近の活動としては、WG活動にfreee標準の実装に詳しいメンバーが参加し始めたこともあり、freee標準のAPIルールに則ったエンドポイント〜フロント実装の例を設けようとしています。
OpenAPIとコードジェネレーターの導入を予定しており、新規のエンドポイントとフロントエンドの繋ぎ込みを楽にしようとしています。理想例としてひとつ作り切ることで、他のエンジニアが開発する際の見本になればと考えています。

さいごに by @hiarc

フロントエンドにRubyのロジックが組み込まれている旧来のシステムに、新しいフロント資源を追加するのはチャレンジングな試みでしたが、同時に多くの学びもありました。
課題はまだ多く残っていますが、技術的な課題をひとつずつ解決しながら進めることで、freeeサインでもvibesや標準UIを使ってより洗練されたページを素早く作れるようにしていきます。
今回の取り組みが、同じような状況にある方々にとって少しでも参考になれば嬉しいです。