どうも鈴木です。好きなプレインズウォーカーは初代ガラクです。
最近スタンダードでは緑単が強かったみたいですが、新弾出てどうなるんでしょう。
それはさておき、今回はジモティーのフロントエンドをNext.jsに移行していくという話を書きます。
背景
ジモティーは今年でリリース後10年を迎え、その間、Webブラウザ版のジモティーはモノリシックなRailsアプリケーションとして開発を継続してきました。 当然ながら技術負債の蓄積はさけられず、特にフロントエンドの生産性の低下が顕著でした。
まず、以下の理由でフロントエンド単体で見た時に生産性が悪くなっていました。
- デザインガイドライン整備前の無秩序なCSS
- メンテナンス性の低いJavaScriptコード
- 一定以上の複雑さを持つ機能が無理矢理jQueryで書かれている
- 一部Vue.jsを採用しているがRailsのview上に実装するためにベターjQuery的な活用に留まっている
また、Railsエンジニアがフロントエンドからバックエンドの実装を担当するため、以下の問題がありました。
- フロントエンドとバックエンドが密結合しているため分業がしづらく、並行開発できない=リードタイムが長い
- チームとしてフロントエンドの専門的なスキルが伸びにくい
ということで、これらの負を解決し、以下を実現することを目的にフロントエンドの刷新に着手しました。
- フロントエンドエンジニアとバックエンドエンジニアが分担してそれぞれの領域に集中しやすくする
- 技術負債になっていたコードを置き換え、モダンなフロントエンド技術を採用することで、フロントエンド単体での開発生産性を上げる
制約
さて、やると決めたのは良いものの、ジモティーのサービスの特性を考えた時にいくつかの制約がありました。
- SEO上重要なページはSSR(Server Side Rendering)で実装する
- 既存ページを移行する際、UX・パス・機能は基本的に維持する
- (開発生産性のための再構築なので、UXの大幅変更により事業KPIに影響がでるのを避けるため)
これらを踏まえた上で技術選定しました。
技術選定
選択の理由も添えて紹介します。
- TypeScript: 採用
- 型安全性の恩恵を受けたいので採用
- React or Vue.js: Reactを採用
- 現時点でのTypeScriptの親和性を考えるとReactが優勢と判断
- 検討時、Vue 3が出てから間がなく知見が溜まっていないということも考慮した
- フロントエンドフレームワーク: Next.jsを採用
- SEO上重要なページでSSRを採用するためSSR可能なフレームワークは必須
- UIフレームワーク: 不採用
- 新規サービスのこちら ではMaterial UIを採用していたがジモティーでは独自のデザインシステムがあるため既製のCSSフレームワークを導入するとカスタマイズの工数がかえってかさむと判断。
- BFF(Backend For Frontend): 不採用
- 今回の目的に照らすと必須ではない
- 通信速度の悪化や運用・保守コストの増大などのデメリットが見込まれるため不採用
- BFF で裏側のRESTを束ねて、GraphQL + Apollo Clientでハッピーになるんや、みたいな話もあったけど一旦冷静になった
- StoryBook: 採用
- デザイナーとエンジニアのコミュニケーションがスムーズになる
- 長期的にメンテナンスするような自社サービスなら必須レベルと判断
移行の課題
ジモティーの場合、大量の既存ページがありそれらを一気にNext.jsに移行することは現実的ではないため、ページ単位あるいはまとまった機能単位で移行していきます。 したがって、移行済のページはNext.js、移行前のページとAPIコールはRailsにリクエストを振り開ける必要があります。
そこで問題になるのは以下の点です。
- リクエストの分配方法
- ログイン認証
- CSRF対策
リクエストの分配方法
以下の方法を検討しました。
- Next.jsとRailsでドメインを分ける
- Nginxをリバースプロキシとして使って振り分ける
- ALB(Application Load Balancer)のパスベースのルーティングで振り分ける
ドメインを分ける方法の場合、移行前後でページのURLが変わるデメリットが大きいと考え不採用にしました。
Nginxをリバースプロキシとして運用する場合、スケーリングやメンテナンスの対象となるサーバーが一種類増えるという問題があります。
一方、ALBについては既に使っているため管理対象が増えないという状況でした。
したがって、ALBのパスベースのルーティングを使って、Next.jsとRailsへの振り分けを行うことにしました。
ログイン認証
ブラウザからのリクエスト先がRailsの場合とNext.jsの場合ある、という前提でログイン状態を維持する必要があります。
この問題はSPAのログイン認証をどう実装するか、という問題とよく似ており、 こちらの 記事 が非常に参考になりました。
今回の場合は、Next(フロント)とRails(API)が同一Originのため、参考記事内の「Sessionを用い、SessionのCookieはSameSiteCookie」を採用することができます。
あとはNext.jsからRailsにAPIコールする箇所でクライアントから受け取ったCookieを引き渡すように実装すれば、Next.jsページとRailsページ(API)の間でログイン状態を維持することができます。
※図を挿入
CSRF対策
RailsではフォームにCSRFトークンを埋め込み、セッションに保持しているトークンと同一か検証することでCSRF対策を行っています。
一方、Next.js移行後の画面ではフォームをNext.jsが提供することになります。Railsが発行するCSRFトークンをNext.jsのフォームに埋め込めば従来の方法で検証が可能ですが、フロント〜Rails間でのCSRFトークンの通信が発生するので通信のオーバーヘッドを考慮すると好ましくありません。
そこで、以下の2つの対策を組み合わせて実施することにしました。
- SameSite Cookie
- Originヘッダの検証
SameSite Cookie
SameSite Cookieとはクッキーに指定できる属性の一つです。SameSite=Laxを指定するとブラウザは別サイトからのPOSTの際にCookieを送信しません。これによってCSRFを防ぐことができます。
2021年時点ではモダンなブラウザでSameSite=Laxがデフォルトになっていますが、まだ未対応のブラウザもあり得るため明示的に指定します。
Rails.application.config.session_store :cookie_store, same_site: :lax
Originヘッダの検証
古いブラウザでSameSite属性をサポートしていないものが存在するため、Rails側でのOriginヘッダの検証も実施します。
ALLOWED_ORIGIN = "https://example.com" def allowed_origin? origin = request.headers["origin"] # 同一Originの場合にOriginヘッダを付与しない挙動のブラウザもあるため、空の場合は許可する return true if origin.blank? ALLOWED_ORIGIN == origin end
We are hiring !
ジモティーではこの記事で紹介しているフロントエンドの刷新をはじめ、様々な挑戦のために新しい仲間を求めています! ご興味のある方はこちらを御覧ください。