はじめに
技術評論社様より発刊されているSoftware Designの2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。
これまでの連載で、ZOZOTOWNリプレイスプロジェクトの始まりから各部門の取り組みなどを紹介してきました。最終回となる今回は、フロントエンドの取り組みを取り上げ、これまでのまとめを行います。
目次
- はじめに
- 目次
- はじめに
- フロントエンドエンジニアの責務
- フロントエンドリプレイスプロジェクトの進め方
- Next.jsとの向き合い方
- Next.jsでリプレイスしていくうえでの課題
- 振り返り
- まとめ・今後の展望
はじめに
ZOZOTOWNのWebフロントエンドは約3年前からリプレイスを実施してきました。連載最終回となる今回はZOZOTOWNのWebフロントエンドリプレイスプロジェクトの進め方や、その過程で得られた技術・組織に関する知見について紹介します。全8回にわたる連載のまとめとして、リプレイスプロジェクトの今後の展望についてもお伝えします。
フロントエンドエンジニアの責務
当社におけるフロントエンドエンジニアの役割について、リプレイス前後のアーキテクチャを比較しながら紹介します(図1)。
フロントエンドリプレイス前
Windowsサーバ上で動作するIIS(Internet Information Services)でClassic ASP(VBScript)を利用して動的にHTMLを生成するサーバレンダリングを行っています。
HTMLの生成にはClassic ASP(ロジック)とHTML(テンプレート)の分離を可能にするテンプレートエンジンを利用しています。また、ブラウザ上ではJavaScriptライブラリのjQueryと一部React(TypeScript)を利用してインタラクティブなコンテンツの実装を行っています。
フロントエンドエンジニアの役割はリスト1に示されるように、Classic ASP(VBScript)で書かれたコード以外のHTML、CSS、JavaScriptを担当することです。
▼リスト1 テンプレートファイル
<!DOCTYPE html> <html> <head> <meta charset="Shift_JIS"> <link rel="stylesheet" href="/assets/style/index.css"> </head> <body> <header> (#%NoticeExists| <div class="badge"> <div class="badge-circle--count">(#*UnreadNoticeCount#)</div> </div> #|# #) </header> <div id='react-app'></div> <script src="/assets/script/index.js" charset="utf-8"></script> </body> </html>
リプレイス後
IISとClassic ASPで実装していた部分は、Next.js(Pages Router)とUIに必要なデータをマイクロサービスなどから集めて整形するBFF(Backend for Frontend)に分解されました。その結果、フロントエンドエンジニアはNext.js、バックエンドエンジニアはBFFと、管理する役割がサーバ単位で分割されました。
Next.jsを導入したことで、フロントエンドエンジニアの役割にいくつかの変化が生じました。Next.jsはサーバサイドレンダリング(SSR)や静的サイト生成(SSG)をはじめWebアプリケーションに必要な機能を提供します。そのためフロントエンドエンジニアとしてページルーティング、HTMLのキャッシュ管理、機能要件・SEOを考慮したレンダリングパターンの選定などの役割が増えました。また、機能要件・SEOを考慮してSSRを利用するためサーバのパフォーマンスやエラーなどのメトリクスを監視し、サーバの運用を行うことも求められるようになりました。リプレイス前と比較して、フロントエンドエンジニアはサーバを含めた技術をより一層駆使してユーザーに快適なWeb体験を提供できるようになりました。
フロントエンドリプレイスプロジェクトの進め方
ZOZOTOWNは2004年のサービス開始から複数の技術で構成され、多くの開発者が機能改修を行ってきたことで、機能同士の依存関係が複雑になっていました。その中で開発当時の設計意図を直接的には知らないリプレイス専任のチームがどのようにZOZOTOWNのWebフロントエンドをリプレイスするプロジェクトを進めていったかを紹介します。
リプレイスプロジェクトはページや機能ごとにいくつかのフェーズに分けて進行します。各フェーズは通常の開発工程(調査・設計・開発・テスト・リリース)に従って進行しますが、リプレイスプロジェクト特有の課題が多くあります。とくに注意が必要な工程について説明します。
調査
長く運用され変遷を遂げてきたZOZOTOWNには機能要件仕様書が存在しないため、稼働しているコードに記載されているものが仕様であり、要件でもあります。リプレイスプロジェクトの基本的な要件は既存システムの要件を漏れなくリプレイスすることです。そのため、要件をコードから読み解く調査がとても大事な工程となります。
調査工程では、既存システムの機能開発・保守をしているチームではないため機能の理解に時間がかかるという課題があります。また、IISとClassic ASPで実装されている部分をどのようにフロントエンド/バックエンドで分けてリプレイスを行うかの判断も必要です。そしてフロントエンドエンジニアがバックエンド技術で実装されている機能についても理解する必要があることが課題となります。
これらの課題に対して、IISとClassic ASPで実装されている機能の一覧、通信シーケンス図、画面遷移図を作成し、開発者が既存機能の要件・機能を理解できるようにしています。また、機能を一覧にすることでフロントエンド/バックエンドエンジニアどちらが実装するかを漏れなく判断し、設計後のフェーズでの実装漏れによる後戻りを防ぐようにしています。
設計
調査で作成した機能一覧を元に設計していきます。リプレイス後はモノリスではなくNext.jsとBFFのため、OpenAPIを使ったスキーマ設計、通信シーケンス図を作成することを行いフロントエンド/バックエンドそれぞれが独立して開発を進めていけるようにします。
レンダリングパターンについては「SEO観点で劣化しないことを確約できる変更以外はしない」というプロジェクトポリシーに沿って基本的に既存と同様にします。また、リプレイス対象ページによっては現在のアーキテクチャ設計時点でのコアの実装を行い、レイテンシーやファーストビューなどのパフォーマンス劣化を起こさずにリプレイスできるかどうか先行して検証するためにProof of Concept(PoC)を行うことがあります。
テスト
リプレイスは不具合が発生すると多くのユーザーに影響が出てしまいます。そのリスクを最小限に抑えるために、一部のユーザーだけにリプレイス後のシステムを提供するAkamai Application Load Balancerを利用したカナリアリリースを実施しています。提供する割合を徐々に増やしていき、全ユーザーに提供するまでに不具合が見つかった場合は提供割合を0%に戻して不具合を修正します。そのためリリース時のユーザー体験だけでなく、リリースを戻す際のユーザー体験に影響がないかもテストする必要があります。
Next.jsとの向き合い方
Custom Server
ZOZOTOWNではCustom ServerにWebフレームワークのFastifyを利用してNext.jsを起動しています。Fastifyは一般的に使われるNode.jsフレームワークのExpressよりも高い処理速度を持ち、Hooks APIにより複数用意されているライフサイクルイベントをフックして処理を簡単に実行できます。Custom Serverで行っている処理は次の4つです。
1. ロギング
FastifyのonResponseイベントをフックにしてサーバのアクセスログを出力しています。出力にはライブラリpinoを利用してJSON Lines形式で標準出力しています。
2. リダイレクト
ZOZOTOWNはデスクトップ向けとモバイルデバイス向けで別々のURLが存在します。そのため、モバイルデバイスでデスクトップ向けURLにアクセスがあった場合はモバイルデバイス向けのURLにリダイレクトする仕様があります。また、既存システムではURLにソース情報である.htmlが含まれており、リプレイスで.htmlなしのURLに変更するためリダイレクトを行います。
Next.jsの機能としてのRedirect、Middlewareを利用することも検討しました。しかしRedirectは柔軟な条件設定が難しく、Middlewareはリプレイス当初のNext.jsバージョンではExperimentalな機能であったため、Custom Serverでリダイレクトを行っています。
3. 既存システムからのForm POSTリクエストを受けるエンドポイント
リリーススコープを限定してリスクを最小限に抑えるため、既存システムでForm POSTリクエストを送っている箇所とリプレイス後のシステムの連携が必要なことがあります。ただし、Next.js(Pages Router)の getServerSideProps
ではForm POSTのbodyをパースするしくみがないため、Fastifyにリクエストを受けるエンドポイントを作成することでパースされたPOST bodyの取り扱い処理を行っています。
4. マルチプロセスでの起動
とあるページのリプレイスでPoCを行った際に現在のサーバ性能・台数ではリクエストをさばききれないことがわかりました。単純に台数を増やすだけではかなりのコストがかかるため、サーバのCPUリソースをできる限り使ってさばけるリソースを増やすためにマルチプロセスで起動する処理を実装しています。
next/link
next/linkはNext.jsでクライアントサイドのナビゲーションを実現するためのコンポーネントです。next/linkはページ遷移を行う際、サーバからHTMLではなくページを構成するデータ(json)を取得し、クライアントサイドでページを構築します。そのためページ全体を再読み込みするのではなく必要な部分だけを更新できるので、ユーザーにとってストレスのないページ遷移を実現できます。
リプレイスプロジェクト開始当初は、リプレイスされるページ間の遷移をクライアントトランジションでシームレスにすることを目指していました。ZOZOTOWNではバックエンドでURLを決定するロジックが多く、動的にURLが変わることがよくあります。そのため、Next.jsでリプレイス済みのURLの場合はnext/linkを使い、リプレイス前のURLの場合はaタグを使うAnchorコンポーネントを作成しました。コンポーネント化することで開発者がnext/linkを意識せずにできる限りクライアントサイドでの遷移になるようにしています。
Next.jsでリプレイス済みのURLか否かは、/pagesに存在するページのパスを生成してくれるpathpidaを利用してpropsのhrefと比較することで判定しています(リスト2)。
▼リスト2 Anchor コンポーネント
import { ReactNode } from 'react' import Link from 'next/link' const Anchor = ({ href }: { href: string, children: ReactNode }) => { const isDefaultAnchor = isNextApplicationPath(href) if (isDefaultAnchor) { return <a href={href}>{ children }</a> } return ( <Link href={href} passHref> <a href={href}>{children}</a> </Link> ) }
カナリアリリースと_next/data/*/jsonの関係
ZOZOTOWNリプレイス後の環境では、新バージョンのリリースに伴うリスクを低減するために、新・旧バージョンを段階的に切り替えるカナリアリリース(エラー件数が多い場合は自動で0%にロールバック)が採用されています*1。このため、リリース中はバージョンスキューのため新・旧の通信が入り混じることで _next/data/*/json
が404エラーになることがあります(図2)。また、リプレイス後の環境でブラウザを開いたままにしていて新バージョンのリリース後にクライアントサイドトランジションを行った場合も _next/data/*/json
が404エラーになります。
404エラーになることによるユーザー影響を懸念しましたが、Next.js側で別のバージョンの不一致が発生した場合はアプリケーションを再読み込みするハードナビゲーションを行うしくみとなっているため、ユーザーには影響がないことが確認されました。これによって無事、リプレイス後の環境からリリースのリスク低減のためのカナリアリリースを導入することができました。
Next.jsでリプレイスしていくうえでの課題
ソフト/ハードナビゲーションのHTTPリファラの違い
ソフトナビゲーションはhistoryを使用してURLを変更後にページに必要なデータ(json)を取得するため、ブラウザバックを行うと戻ったURLがリファラとなります。一方、ハードナビゲーションはページ全体が再読み込みされるため、戻る前の現在のページのURLがリファラとなります。
ZOZOTOWNでは、流入経路によって特殊なUIを表示する仕様や、前の選択状態を維持するためにリファラを利用していました。この状態でhistoryを使用したソフトナビゲーションにすることでブラウザバック時に問題が発生しました。
リファラは状況によってHTTPに乗らないこともあるため、依存しない実装に変更することも検討しました。しかし、この問題が発覚したタイミングがリリース直前で利用箇所も多く要件をまとめて設計することが難しかったため、問題が発生する特定のページからの遷移と特定ページへの遷移をハードナビゲーションに変更する対応を選択しました。
Shift_JISの取り扱い
ZOZOTOWNのサービス開始以降、現在もWindows Server上でIISとClassic ASP(VBScript)が稼働しています。その結果、システムには文字コードShift_JISが残っており、キーワード検索のURLクエリにもShift_JISでエンコードされた値が使用されています。リプレイス後も裏側のシステムは引き続きShift_JISでの処理を行うので、互換性維持のためShift_JISを扱う必要があります。しかし、Shift_JISでエンコードされたマルチバイト文字を含むURLに対してNext.js(JavaScript)でURLSearchParams APIを使用すると、application/x-www-form-urlencoded
形式でパースされてしまい文字化けしてしまうため、Shift_JISのまま扱うことができません。これは2つのユースケースで問題が発生しました。
1つ目はページネーションやソート順の変更など現在のURLに対して特定のクエリパラメータを変更したい場合です。Shift_JISでエンコードされたマルチバイト文字を含むパターンに対しては、URLSearchParams APIが登場する前のやり方と同じようにURL文字列を操作することで対応しました。
2つ目は getServerSideProps
でリクエストURLを参照したい場合です。リプレイス後は基本的にソフトナビゲーションで実装しているため、リクエストオブジェクトのURLではなくクライアントサイドナビゲーションの _next/data
を正規化した GetServerSidePropsContext
のresolvedUrlを参照する必要があります。しかし、resolvedUrlはNext.js内部でURLSearchParamsを利用しているため文字化けしてしまいました。この問題に対してはNext.js内部での処理によって文字化けが発生してしまうことがわかりハードナビゲーションに変更することを検討しました。検討した結果、問題が発生するケースの中に既存システムからソフトナビゲーションでCSRを行っている箇所があったため、このケースのみresolvedUrlを利用せずほかのケースはリクエストオブジェクトのURLを利用することで対応しました。
また、HTMLの文字コードをShift_JISからUTF-8に変更したことでも問題が発生しました。FormデータのエンコーディングはHTMLの文字エンコーディングに依存するため、リプレイス後も送信先が既存システムの箇所でUTF-8がShift_JISとして扱われることで文字化けが発生しました。Formはaccept-charset属性を指定することでエンコーディングを指定できるため、Shift_JISを指定することで文字化けの問題を解決して新・旧システムを連携できました。
振り返り
リプレイス後のフレームワークとしてのNext.js
1ページをピックアップし、Core Web Vitalsを使ってリプレイス前・後のシステムのパフォーマンス特性を比較しました。リプレイス後はTime to First Byte(TTFB)、First Contentful Paint(FCP)が改善されたことで、Largest Contentful Paint(LCP)までの時間短縮やTime to Interactive(TTI)が全体的に向上しました。一方でFCPとLCPの差が広がりページのレンダリングプロセスが遅くなっているため、今後の改善課題であることがわかりました。
開発者体験としては環境構築が簡単になったことや、JavaScriptのエコシステムを利用できることで開発効率が向上したことが挙げられます。また、表示ロジックがすべてJavaScript(TypeScript)で記載されることでテストがしやすくなったことも成果です。
一方でリプレイスならではの課題として既存システムとの共存があります。基本的に既存システムの機能仕様を変更する判断は行わずにリプレイスするため、現在のベストプラクティスと異なりフレームワークでサポートされていないことが数多くあります。そういった場合にフレームワークの制限の中で再現する必要性が挙げられます。
リプレイス専任チームにした話
プロジェクトが始まったころはチーム内で既存システムでの開発とフロントエンドリプレイスを並行して行っていました。プロジェクトとシステムを行き来しコンテキストスイッチを繰り返す必要があり、よりスムーズな進行を目指して専任チームで進めることとしました。そうすることで、スイッチする機会を減らしリプレイスプロジェクトに完全に集中できる環境を整えられました。
リプレイスを進め環境がモダンになっていく中で開発効率も上がり新しい人材も増え、現在ではフロントエンドリプレイスプロジェクトに3チームで並行して取り組めるようになりました。
不要機能の削除・調整
長く運用されてきたこともあり、既存システムにはデッドコードになっているものや古い機能のまま更新されずにいるものが数多くありました。リプレイス後のシステムになるべく負債を残さないように、また本来達成したいシステム入れ替えに大きく影響を与えないように、UIの刷新や機能の削除も積極的に関係者と調整して実施しました。
大きく複雑なシステムのため削除の判断がつかないものや、既存システムと並行して運用した際に問題がある場合など、その時点での判断を見送ったものも多くありますが、既存システムよりだいぶシェイプアップできました。
リリース
現在ZOZOTOWNのフロントエンドでは、ページや機能単位でリプレイスを行っています。既存システムと並行して開発していく関係で二重開発になってしまうケースや、リプレイスが完了すれば不要になる既存システムと整合性を保つための処理の開発などコストがかかっている場面もありますが、ビッグバンリリースによるリスクと天秤にかけて選択しています。
アプリケーションまるごとのリプレイスはしていないものの、ページによっては非常に複雑で大規模なリプレイスになってしまい、結果として非常に苦労することもありました。機能ではなくURL単位でリプレイスするなど小さくリリースしていく手段を複数持ち、適切に提案・判断できる状態にある必要性を感じました。
まとめ・今後の展望
これまで全8回にわたって、ZOZOTOWNリプレイスプロジェクトにおける取り組みや学びをさまざまな切り口で、紹介しました。
- 第1回:ZOZOTOWNリプレイスプロジェクトの全体アーキテクチャと組織設計
- 第2回:ZOZOTOWNリプレイスにおけるIaCやCI/CD関連の取り組み
- 第3回:API Gatewayとサービスメッシュによるリクエスト制御
- 第4回:ZOZOTOWNリプレイスにおけるマスタDBの移行
- 第5回:キャパシティコントロール可能なカートシステム
- 第6回:ZOZOTOWNにおけるBFFアーキテクチャ実装
- 第7回:検索機能リプレイスの裏側
- 第8回:フロントエンドエンジニアから見るZOZOTOWNリプレイスとまとめ・今後の展望
これらは、壮大なZOZOTOWNリプレイスプロジェクトの一部です。筆者たちは日々、試行錯誤を繰り返し、ZOZOTOWNという巨大なサービスのリプレイスに取り組んでいます。
ZOZOTOWNは、2004年12月のサービス開始から、基本的なアーキテクチャを変えずに成長してきました。そのアーキテクチャはきっと正解だったのだと思いますし、リプレイスに至るまで、開発や運用を続けてきたZOZOのエンジニアをリスペクトしつつ、これから先の未来におけるZOZOTOWNの成長のために、今考えられる最適なアーキテクチャを検討し、引き続きリプレイスを進めていきます。現在、アプリのAPIサーバのリプレイスや、基幹システムのリプレイスも進めていますので、今後またどこかで紹介できたらと思います。
最後になりますが、全8回にわたり、お読みいただきありがとうございました。読者のみなさんにとって、少しでも有益な情報になっていたらうれしいです。
本記事は、執行役 兼 CTOの瀬尾 直利、EC基盤開発本部 本部長の高橋 智也、ZOZOTOWN開発本部 ZOZOTOWN開発3部 フロントエンドリプレイスブロック ブロック長の新家 弘久、そして同 フロントエンドリプレイスブロックの森 泰樹によって執筆されました。
本記事の初出は、Software Design 2024年12月号 連載「レガシーシステム攻略のプロセス」の最終回「フロントエンドエンジニアから見るZOZOTOWNリプレイスとまとめ・今後の展望」です。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。
*1:前述の「テスト」項目で記載したカナリアリリースとは目的が異なるものです。