SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

BOXIL SaaSのフロントエンドをモノリポ構成 + Reactで仕切り直した話

こんにちは!! スマートキャンプでエンジニアをしている吉永(@__GGEasy)です!

私は現在、スマートキャンプの主力サービスであるBOXIL SaaSの開発にフロントエンド、バックエンド問わず携わっています。

今回は、弊社で新規サービスとしてリリースしたBOXIL SaaS質問箱(以下SaaS質問箱)にて、React.js(以下React)を私の主導で採用しました。

この記事では、フロントエンド整備に至った経緯や、今後のBOXIL SaaSにおけるフロントエンドの技術的な展望についてお話します。

BOXIL SaaSのフロントエンドで抱えていた課題感

さまざまな技術が同居している

BOXIL SaaSは直近できたプロダクトではなく、何年も前から開発保守されてきています。そのためフロントエンドの採用技術は、以下の技術の組み合わせを使用していました。

  • Vue.js + TypeScript(Vueのバージョンは2.x、以下Vue, TS)
  • Vanilla JS
  • CoffeeScript(以下Coffee)
  • Coffee + Vue
  • Coffee + jQuery

というようにページごとにさまざまな技術が同居している状態です。

以前、decaffeinateによるCoffee撲滅も検討しましたが、吐き出されるJSがES6になる問題にぶつかりました。

Uglifierの影響でコンパイルが通らない問題は解消しましたが、BOXIL SaaSのターゲットユーザーの属性を考慮すると、Internet Explorer対応の優先度が高くES6の記法が使えません。

そのため、Internet Explorerのサポートが終了した直近まではCoffeeを消す作業を中断していました。

webpackであれば簡単にトランスパイルができますが、現状すべてをwebpackに任せているわけではないので、decaffeinate -> ES5の記述に手動で変換する必要が出てきてしまいます。そこにコストを掛けるのは、もったいないよねというチームの判断です。

今回Reactを採用する過程でVueへの一本化も考えましたが、直近あったVue2.xからVue3へのアップデート対応にかなり破壊的変更があるため、2系と3系の同居ができない以上あまり使い続けるメリットを感じていませんでした。

Vue2.xと3の同居に関する検証結果も記事にしているのでぜひご覧ください。

Vue 2.xとVue 3を共存させようと思ったけどダメだった話

Vue + Atomic Designでの課題

これまで使用していたVueではAtomic Designを採用した共通化を重要視したコンポーネント設計をしていました。

詳細は後述しますが、現在BOXIL SaaSにこれといったデザインシステムがありません。

そのため、スタイルは同じだが挙動が違うといった問題や、挙動は同じだがスタイルが違うと言った問題が起こりました。

そうしているうちに、スタイルが違う場合はdeepメソッドで親コンポーネントから上書きするなど、可読性や保守性にかなり問題がある作りになってしまった部分が散見されました。

また、Atomic Designの通りにコンポーネントを作成した後、その後の保守で特にMoleculesの粒度などが見直されず開発され続けた部分がありました。

その部分はかなり肥大化してしまい、MoleculesとOrganismsの境界線が不明確になるなどの問題をかかえていました。

Atomic Design以外で良さそうなコンポーネントの管理手段になるものを探していたところ、以下のような記事が見つかりました。

Atomic Designをやめてディレクトリ構造を見直した話

弊社で困っていた部分とはまた違った理由でしたが、他社でもこのような動きがあるということがわかり、これをベースにカスタマイズしていくことにしました。

また、Coffeeのなかでnew Vue...といった記述をしている部分もあり、ここから仕切り直して開発するのは難しいよねといった気持ちもありました。

さらに、弊社の採用時にReactを使った開発を望む候補者が多いことから、これまでBOXIL SaaSで採用されていなかったReactの採用をしようといった声が上がっていました。

構成変更の検討

今年の2月頃、新規のサービス開発が始まるということで、弊社のエンジニアチームでも特に若手の3人が集められました。

私はそのなかでもフロントエンドの技術に興味を寄せていたため、その分野における技術導入や開発を任されました。

本件は新規で開発する部分が多く、既存の作りに引っ張られにくいことは分かっていました。

かつ仕様策定の期間からジョインしたため、仕様を考える傍らで新技術の検証期間を設けられそうだという話になりました。

質問箱のPMの方の記事はこちら

私は以前、BOXIL SaaSのwebpackの改善とReactの新規導入を試しています。もしよろしければそちらの記事もご覧ください!

若手エンジニアの俺がフロントエンドのビルドを早くしてReactも導入しちゃった話

どのようにフロントエンド構成を整備したのか

整備にあたって、まずはいくつか導入以前に存在しているフロントエンド課題の洗い出しや、社内での新規技術導入に対する温度感を一致させる必要があると感じました。

フロントエンド課題の洗い出しについては以前から積極的にやっていたこともあり、メンバー間である一定の意思疎通はできていました。

詳細に関しては割愛するため、上記に貼っている記事「若手エンジニアの俺がフロントエンドのビルドを早くしてReactも導入しちゃった話」を見ていただければと思います。

ここからは、SaaS質問箱の技術選定やディレクトリ構成などのルールを中心に紹介します。

SaaS質問箱のフロントエンド技術選定

ここからは、選定した技術で主要なものを紹介します。

React + TypeScript

先述した理由から、今回はReactを使用することにしました。

Next.js(以下Next)も採用候補にはあがりましたが、ログインセッション周りの処理の移行も含めるともう少し時間をかけて改修する必要があり、今完全に切り離すのは難しいといった判断の元見送りました。

SEO的にSPAでいいのかという話については、これまでVueで使っていたページでSEO的に特に問題なく検索流入が入っていたことや、そもそも検索流入を狙うサービスではないことから、特に直近問題になることはないだろうと判断しました。

また、当然といえば当然ですが、型は欲しいのでTypeScriptを導入しています。

OpenAPIをBOXIL SaaSのバックエンドで採用している点から、今回はAPIのリクエストやレスポンスの型の自動生成などもしてもらい、それをフロントエンドで使用しています。

Recoil

状態管理には今回Recoilを採用しました。

新しい状態管理ライブラリとして2年ほど前から話題になっており、かつすでに動作の確認などはできていたので少し思い切って採用しました。

styled-components

CSS in JSにするかどうかという話も一瞬あがりましたが、結局採用しました。

今のところ特に問題なく動作していますが、パフォーマンスに今後影響してきたら見直すかもしれません。

react-hook-form

フォームの動作や簡単なバリデーション周りの挙動を作るために使用しています。

react-router-dom

できるだけフロントエンドはRailsから引き離して開発をしたかったこともあり、Railsでルーティングさせるのではなく、react-router-domを使ってルーティングさせることを決定し、導入しました。

ディレクトリ構成の決め

ディレクトリ構成の前提となったお話

BOXIL SaaSチームでは過去、デザインシステムなどの導入を検討していた時期があります。

しかし、BOXIL SaaS本体のなかでも画面ごとにパーツの形が違ったり、細かい挙動が違うためデザインシステムと言った形で共通化するのは厳しいのではないかという意見がデザイナーから出ていたこともあり、断念していました。

また、ここ最近slim + scssで構成されていたページで、application.cssというすべてのCSSを読み込むことによるCSSによる名前被りや、無理なCSSの共通化による小さいスタイルの崩れが起きがちでした。

余分なCSSを読み込んでいるためパフォーマンス的にも良くないということも最近のCWV対応でわかりました。

その結果、最近ではpartialやcomponentが違うのであれば、都度CSSファイルを発行し、1ファイルに対して1CSSといったような構成になるようにしていました。

これはファイル数が増えてしまうデメリットはありますが、CWV対応で必要なCSSのみを読み込むようにした今、それはあまりデメリットとして感じられなくなりました。

共通化しない大規模アプリケーションのCSSの設計としては、以下のようなものがあり、実際に参考にさせていただきました。

https://ecss.benfrain.com/

最終的なディレクトリ構成

上記を踏まえて、以下のディレクトリにしました。(一部抜粋)

BOXIL
├ ...(Railsで使っているディレクトリ軍)
└ frontend_react(今回作成したReact用のディレクトリ)
  └ [Railsのコントローラー名]
    ├ assets
    │ └ ...(jpgなど)
    ├ components
    │ └ [ページ名]
    │   └ [コンポーネント名]
    │     ├ index.tsx(ロジック層)
    │     ├ presenter.tsx(見た目)
    │     └ styled.ts(CSSを書くとこ)
    ├ uiParts
    │ └ [コンポーネント名]
    │    ├ index.tsx(ロジック層)
    │    ├ presenter.tsx(見た目)
    │    └ styled.ts(CSSを書くとこ)
    ├ globalStates
    │ └ atoms
    │   └ [各種state].ts
    ├ hooks
    │ └ [hooks名]
    │   └ index.ts
    ├ layouts
    │ └ └ ...(各種レイアウト)
    ├ pages
    │ └ [コントローラーのアクション名]
    │   ├ index.tsx
    │   └ styled.ts
    └ routes
      └ index.tsx

frontend_react直下のディレクトリはRailsのコントローラー単位で切るモノリポ構成です。ディレクトリを跨ぎ利用する共通コンポーネントは基本作成していません。

今回で言えばfrontend_reactの下には質問箱を意味するconsultationsディレクトリを作成しました。

また、最近行われたカテゴリページの改善では、frontend_reactの下にcategoriesというディレクトリが作成されました。

さきほども言ったとおりデザインや挙動が違ったり、呼び出している色などもBOXIL SaaS内で一律で決まっているわけではなかったりするので、ButtonやTextFieldなども現状は都度作っています。

また、今回他と変わった構造になっているのはcomponentsとuiPartsなので、ここを重点的に解説します。

components

このディレクトリの中身はreact-router-domで定義したページごとに発行しています。SaaS質問箱では投稿画面が/consultations/newというパスで定義されていますが、その場合は以下のようになります。

  └ [consultations(Railsのコントローラー名)]
    ├ components
    │ └ [new(ページ名)]
    │   └ [TestComponent(コンポーネント名)]
    │     ├ index.tsx(ロジック層)
    │     ├ presenter.tsx(見た目)
    │     └ styled.ts(CSSを書くとこ)

となります。

これはAtomic DesignでいうOrganismsにあたります。

このコンポーネントはページごとに共通化はせず、単独の物として動作します。

デザインは共通の形だったとしてもコピペした状態でページごとに切り出し、中身の処理だけが違うといったような運用にしました。

また、presenter層を導入することで、APIの通信やロジックが関わる関数の実装はindex.tsxに任せ、簡単なifや基本的なDOMの構成はpresenterに任せる作りにしています。

uiParts

uiPartsはAtomic DesignでいうMoleculesやAtomsにあたります。

Railsのコントローラー名直下のページで使う、ButtonやTextFieldがここに入ります。

また、Atomic Designのように最小単位で切り出すというルールではないため、例えば同じデザインのタブがあるのであれば、ここに入れても大丈夫です。

最小単位として存在するというよりは、ここにあるものはそのページで共通パーツとして使えるよという意味合いで運用しています。

StoryBookなどは工数の都合上導入できていませんが、このあと導入する場合もこのディレクトりにあるコンポーネントが対象となります。

pages

そのページのトップになるディレクトリです。

ページごとに発行され、componentsやuiPartsをまとめたりするのはこのコンポーネントになります。

ディレクトリ構成を見直してよかったこと

ディレクトリ構造を見直した結果、以下のメリットが有りました。

まず、汎用性の高いコンポーネントかそうでないかという区切りだけになったため、これはAtomsだろうか...Moleculesだろうか...といったような悩みが少なくなりました。

Moleculesに関しては改修の結果汎用的じゃなくなったとしても、Moleculesフォルダの中に存在し続け、実際Organismsとの違いがわからなくなってしまっていた面もありました。

その解消による開発体験の向上は大きかったです。

APIなどが内部で叩かれていたり、そのページでしか使われないロジックが入ってしまうならcomponents/[ページ名]の中に入れればよいと考えました。

逆に汎用的ならuiPartsに入れればいいというルールは他の開発者にもわかりやすいものだと思います。

規約の多くなりがちなAtomic Designを下手に採用するよりも自然に理解しやすいこの構成は、人の入れ替わりが多い現状の組織にもマッチしている気がします。

また以前、特定コンポーネントへの強依存によって思わぬ部分がバグってしまった経験がありました。

そのため大きいコンポーネントは各ページごとに管理できる本構成は、依存度を一定以下に保ちやすくて良いなと感じています。

この構造のデメリット

まだ運用したての段階なので、これ!というデメリットは未だ見つけられてないですが、シンプルにcommitしたときのファイル数が増えます。

共通化という考え方からはかけ離れた結果なので、わかってはいましたが、プロジェクトの立ち上がり当初の、ButtonやTextFieldを作っているときの追加のファイル数が膨大になりがちでした。

最低でもindex,presenter,styledの3ファイルが増え、それに対してhooksを発行しようものならさらに増えるので、ビッグバンリリースになってしまう危険性やレビューで気が付ききれない可能性もあります。

しかし、この辺はPRを細かく出したり、リリースを細かくやったりと工夫のしようはかなりあるので、特性を理解しておけばあまり問題にはならないかなといった感じです。

今後の改善プラン

今回新構成を導入した後、フロントエンドに興味があるメンバーを集めて今後のフロントエンド改善をするための流れをまとめました。

今あとできる大規模な改善ではなく、すぐすぐできる細かい改善をメインでまとめましたが、以下のような問題が出てきました。

今後最優先でやらなくてはいけないことは以下のような感じです。

現状、BOXIL SaaS直下のディレクトリにはもともと存在していたfrontendという名前のVueディレクトリがあります。

そこで使われているeslintやpackage.jsonがfrontendディレクトリの中にはなく、ルート直下に置かれていました。

SaaS質問箱開発時だけではそれを直している時間がなかったため、いったん以下のようなディレクトリ構成で開発をしました。

BOXIL
├ frontend(Vue)
├ frontend_react(React)
│ ├ package.json(React)
│ ├ eslint(React)
│ └ tsconfig(React)
├ package.json(Vue)
├ eslint(Vue)
└ tsconfig

ですが、このままだとfrontendとfrontend_reactの違いに迷う羽目になります。また、各種ディレクトリの中に入ってnpm installをするのもめんどうです。

それに対する今後の対応策を以下のように定めました。

チームメンバー内で話したメモをそのまま貼り付けているため、わかりにくくなってしまっているので図解します。

今後のディレクトリ構造を以下のようにしました。

BOXIL
├ frontend
│ ├ react(React)
│ │ ├ package.json(React)
│ │ ├ eslint(React)
│ │ └ tsconfig(React)
│ └ deprecated_vue
│   ├ package.json(Vue)
│   ├ eslint(Vue)
│   └ tsconfig
└ package.json

Vueのディレクトリが物騒な名前になってますが、これにはちゃんとした理由があります。

フロントエンドの一本化

今回Reactの導入により、フロントエンドの組み合わせは以下のようになりました。

  • Vue + TS
  • Vanilla JS
  • Coffee
  • Coffee + Vue
  • Coffee + jQuery
  • React + TS

お気づきでしょうか。Reactが導入されただけでは何1つ「改善」はされていません..。

むしろReactが増えたことにより、新しく開発チームに入った人はVue, Vanilla JS, Coffee, jQuery...とキャッチアップする領域が広がってしまいます。

そのため今後、BOXIL SaaS開発チームとしてフロントエンド技術の一本化をしたいと考えています。

Internet Explorerの終了に伴いこれまで開発改善の足を止めていたdecaffeinate-> ES5の記述に直す必要がなくなったことからまずはCoffeeがBOXIL SaaSのコード上から消えることになります。ありがとう、そしてさようならCoffee。

本来はここからVueのコンポーネントも含めてReactの一本化を目指すのがベターかと思いますが、工数的にそこまで大きな技術負債の改善ができるのはまだ先だと考えています。

明らかにレガシーになってしまった言語は負債ですが、Vueになっているものを優先度上げて対応する必要がないと考えたのも大きな理由の1つです。

そのため、Vueのディレクトリをdeprecatedとし、保守以外で新規ページの発行はせず基本今後SPAのページを作る場合はReactでの開発にまとめます。

そうすることにより、いったん以下のような構造になります。

  • Vue
  • Vue + TS
  • Vanilla JS
  • jQuery
  • React + TS

あとはゆっくりjQueryを剥がせばすぐすぐの一本化とは行かずとも、ある程度採用技術がまとまってくるのではないかと考えています。

WorkSpaceの活用

現状各種ディレクトリに入りnpm installをしていますが、それだと各種ディレクトリにlockファイルができてしまったり単純に2回やるのがめんどくさいです。

WorkSpaceを使うことによりtopから各種パッケージを一元管理できるうえに、モノリポ構成としてもきれいなものになると考えています。

これをやるためには上記のディレクトリ改善をやったあと、CI / CDをいじるタイミングでまとめて新しい構造に変更するのが良いと考えています。

まとめ

...ということで、今回はBOXIL SaaSのフロントエンドに革命を起こすべくフロントエンド構成を立て直してみたという話をしました。

しかし、負債を作らないつもりで開発していても知らず知らずのうちに負債は出来上がっていきますし、「Vueを導入した方もCoffeeを導入した方も負債を作るつもりで導入したわけではない」ということを理解しておくのが大事だと私は考えています。

また、新しいものを導入しただけでは負債は改善されておらず、むしろ新しい技術が増えたことでより負債になる技術が増えたのが事実です。

これを避けるためには今後も定期的に技術負債の改善やアーキテクチャの見直しが必要で、個人的には特にSSRできていない今の状態が不服です。

しかし改善作業や技術の導入には自身の知識だけでは足りないものや、時間的に足りないといった事実も多々あると思います。

そのため今回は部署問わず知見のあるメンバーと積極的に話し合い、自分だけで決めずチームとして方針を決めて取り組むことを意識しました。

結果、別PJのメンバーがPRを上げてくれたり、アドバイスをくれたりと導入に協力してくれました。

とても感謝しています。

この記事が同じくレガシーフロントエンドの改善で悩んでいる組織の役に立てれば幸いです。