TECH PLAY

jQuery

イベント

該当するコンテンツが見つかりませんでした

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

こんにちは!みなさん、テストしてますか? 第2回の前編 では、E2Eテストの基幹部分とも言える 要素探索 の技術の変遷について扱い、 中編 では 実装 の技術の変遷について扱いました。 後編では、どのようにブラウザを介してWebアプリケーションを自動操作するのか、つまり 自動操作技術 について触れたいと思います。また、UIを自動操作して実施するテストという点から、E2Eテストには良くも悪くも様々な目的が期待されてしまっていましたが、これらはWebアプリケーション開発技術の変遷と共に徐々に変わってきました。こうした E2Eテストの目的 についても触れたいと思います。 記事一覧:モダンなE2Eテストの考え方をマスターしよう 【第1回・前編】まずはやってみよう – Playwrightを使ったハンズオン(事前準備編) 【第1回・後編】まずはやってみよう – Playwrightを使ったハンズオン(テスト自動化編) 【第2回・前編】E2Eテストの歴史 -要素探索技術の変遷- 【第2回・中編】E2Eテストの歴史 -様々な実装技術- 【第2回・後編】E2Eテストの歴史 -自動操作技術と目的の変遷 自動操作技術の変遷 さて、この連載では一貫してPlaywrightを使っています。PlaywrightはいわゆるE2Eテストフレームワークですが、大きく分けると「Webブラウザを自動操作するコンポーネント」と「自動テストを記述するコンポーネント」で成り立っています。 このうち、「自動操作」のほうには様々な変遷がありました。あまりに古いものは自分も良く知らない部分が多いので、おおむね2016年以降の主要なマイルストーンについて記載します。 Selenium 3 と Webdriver CDP(Chrome DevTools Protocol)とヘッドレスChromeを用いた自動テストの流行 開発者体験を重視したツールの流行 Selenium 3 と Webdriver Seleniumは、2025年現在で利用できるものの中では、もっとも歴史の長いブラウザ自動操作ライブラリです。複数のブラウザを統一されたAPIで自動操作できる、というのが強みで、たくさんのテストケースをたくさんのブラウザインスタンス上で実行するためのインフラも用意しています。 クロスブラウザの複雑性をライブラリ側で吸収するというアイディアと、ブラウザとSelenium Serverの中継ぎをするWebDriver部分は仕様を公開して各ブラウザベンダーに実装を任せるという思想そのものは良かったのですが、自動テストインフラの複雑さを招くことにもつながり、インフラの構築やメンテナンス、テスト実行時のトラブルシューティングなどの別の辛さを招いてしまうことも多かったです。 加えて、Selenium WebDriverの(少なくとも当時の)設計思想は「UI上で実際にユーザーが可能なインタラクションを模倣する」というものだったため、テストのためのモック/スタブを作りにくかったり、ネットワークスロットリングなどで特殊な環境を再現した上でのテストが難しいという弱点もありました。 また、仕組み上全てがHTTPベースのコミュニケーションになってしまう点もパフォーマンス上問題になるケースが多く、特にページロードや要素の表示待ちなどが非常に長くなるケースがありました。当時E2Eテストに「不安定」「遅い」という印象を持っていた人たちは、おそらくこれらに苦しめられたいたことでしょう。 一方で、色々と問題はありつつも、自動テストのための大統一APIを作るというビッグピクチャーに向けて今もなお前進し続けているプロジェクトであることは疑いの余地はなく、自動テストエンジニアとして生きるならぜひ動向を追い続けたいプロジェクトの一つです。 ちなみに、HTTPベースの単方向通信しか出来なかったのを改善するために、新しくWebDriver BiDiという仕様が策定されています。こちらについては後述します。 CDP(Chrome DevTools Protocol)とヘッドレスChromeを用いた自動テストの流行 ChromeがHeadlessモードをサポートしたことと、CDP(Chrome DevTools Protocol)をテストに使うことでSeleniumの弱点をカバーできると考えて、CDPをベースにしたハイレベルAPIを実装したのがPuppeteerです。当初はCDPを使っていたのですが、現在は後述するWebDriver BiDiを用いています。 Seleniumがあくまでユーザーに出来る操作のみにフォーカスしていたのに対し(参考: Selenium使いのためのPuppeteer解説|Qiita )、PuppeteerはCDPを用いるためネットワーク速度のスロットリングやスタブなど様々な開発者向け機能に対応しており、テストしやすさを改善していました。 一方で、Seleniumユーザーたちの多くがJavaやPythonなどでテストコードを書いていたのに対して、PuppeteerはJavaScriptのみの対応でした。これは普段UIを扱うフロントエンドエンジニアたちには自然だった一方で、JavaScriptの非同期APIに慣れ親しむ前の自動テストエンジニアたちにとってはかなり悩みのタネで、筆者も「自動テストスクリプトが順番通りに動いてくれない……おれはただテストを自動化したいだけなのに……」と毎日悪戦苦闘していたのを覚えています。 ちなみに、パフォーマンスの点について公平のために補足しておくと、Chrome/Chromiumブラウザの自動操作を担うWebDriver実装であるChromeDriverもまたCDPベースで実装されています。ですが、やはりWebDriver自体の通信がHTTP通信であることによるオーバーヘッド自体が大きかったため、速度の面でPuppeteerの方が有利でした。 また、SeleniumとPuppeteerの大きな違いとして、Selenium Gridのような大規模テストインフラを構築する機能の有無がありました。これは大量の実機テスト実行環境を束ねる目的では重要なのですが、CI/CD環境の中でChromiumをインストールしてテストを回すようなケースではそもそも不要なものでもありました。 開発者体験を重視したツールの流行 Cypress さて、Puppeteerの登場で、あくまで筆者の肌感ではあるものの、自動テスト界隈の人気は二分された印象がありました。 テストコードが書けるたくさんのテストエンジニアを中心にたくさんの自動テストを実行したい→Selenium 開発者が日常の開発サイクルの中でガンガンE2Eテストを回していきたい→Puppeteer そうすると、開発者はどうしても 開発者体験 の良さに目が行ってしまいます。例えば、ドキュメントが豊富であるとか、コードが書きやすいとか、デバッグ用のツールキットが充実しているとか、普段の開発エコシステムの中に組み込みやすいとか、そういった具合です。 そんな中で登場したのがCypressです。Cypressははフロントエンドの開発体験をウリにしたツールで、当時の開発者たちが慣れ親しんでいたjQueryのメソッドチェーンを踏襲した書きやすいAPI、フロントエンドエコシステムとの親和性、デバッグ体験の良さなど、良いところがたくさんありました。 一方で、仕組み上複数タブ・ウィンドウの切り替えが出来ないことや、クロスドメインiframeがテストできないことなどは、テスト対象のウェブサイトによっては致命的でした。ちなみに、Cypressのドキュメントは本当に徹底していて、これらのトレードオフまでつまびらかに解説されています。 Cypress docs: https://docs.cypress.io/app/references/trade-offs こうした課題はありつつも、上述した開発者体験の良さ、ならびにこうしたトレードオフまで充分に解説されたドキュメントなどは非常に開発者フレンドリーで、多くの開発者たちに親しまれていました(余談ですが、筆者はあるオンラインカンファレンスでCypressの中の人が「ドキュメントが充実しているのもCypressのいいところで、困ったことがあったらCommand+Kで一発で検索できる」と誇らしげに語っているのを見て、とても良いことだなと感心した覚えがあります)。 Playwright さて、Cypressのメジャーリリースとほぼ同時期に、本連載でも使っている Playwright がα版として産声を挙げました。自動操作の方法としてはPuppeteerが使っているCDPというものになるのですが、この方法は名前の通りChromium系のブラウザ(Chrome、Chromium、Edge)でしか使えないので、FireFoxやSafariはテスト用にビルドしたものを使っています。 個人的には非常にバランスの取れた、良い意味でいいとこ取りのツールだと捉えています。開発者体験の観点からCypressと人気を二分していましたが、その後Cypressと似た機能を取り入れることでより強力なツールになりました。 余談: Selenium4・Webdriver-BiDi 冒頭で紹介したSeleniumですが、何となくオワコンのように見えてしまいがちですが、きちんとメンテナンスされ続けており、2022年には待望の新メジャーバージョンが登場しました。本記事のPuppeteerの項目で「PuppeteerはCDPを直接触れるのでテストが楽」というようなことを書きましたが、Selenium4は待望の cdp エンドポイントが実装され、ブラウザによりますがCDPによる豊富なデバッグ機能にアクセスできるようになりました。 また、Seleniumの根幹となるWebdriver規格も進化しており、新たにWebdriver-BiDiというものが提案されています。BiDiはBiDirectional、つまり双方向の略です。SeleniumがHTTPベースの単方向通信のみのツールだったのを、Webdriver-BiDiはWebsocketベースの双方向通信のものに変えています。これにより、ページの表示待ちなどのパフォーマンスが改善しました。 Puppeteerの話の中で触れたとおり、現在PuppeteerはCDPベースからWebdriver-BiDiベースに変わっています。これがより進んでいくと、クロスブラウザテストのやりやすさはより高くなっていくはずです。 目的/役割の変遷 さて、この「E2Eテストの歴史」は、主にE2E自動テストで使われる技術の変化にスポットを当てることで、「本/記事によって書いてあることが全然違う」という状態を解きほぐすことを目的にしていました。締めくくりとして、これらの技術が何に対して使われるのかの変遷についても理解しておきましょう。 手続き的UIの時代: UIテスト = E2Eテスト JavaScriptによるインタラクティブな表現が可能になった直後のWebアプリケーションは、UIの変化をDOMツリーの操作によって行っていました。例えば、以下のサンプルは簡単なToDoアプリの実装です。ページ全体を読み込み直すことなく、ToDoアイテムの追加/削除のタイミングでデータをバックエンドサーバーに送信しています。 <div id="todoApp"> <input type="text" id="todoInput" placeholder="新しいタスク"> <button onclick="addTodo()">追加</button> <ul id="todos"></ul> </div> <script> function addTodo() { const text = $('#todoInput').val(); if (text) { // バックエンドにPOST送信 $.post('/api/todos', {text: text}, function(todo) { // 成功時にDOMに要素を追加 $('#todos').append(`<li data-id="${todo.id}">${todo.text} <button onclick="deleteTodo(${todo.id})">削除</button></li>`); $('#todoInput').val(''); }); } } function deleteTodo(id) { // バックエンドにDELETE送信 $.ajax({ url: `/api/todos/${id}`, method: 'DELETE', success: function() { // 成功時にDOM要素を削除 $(`li[data-id="${id}"]`).remove(); } }); } </script> DOMツリーを直接編集するということは、状態を再現させるためにはそこまでの手続きを再現させなければいけないということでもありました。再現させるためにはバックエンドも(データベースなども含め)完全なものを準備する必要があるため、必然的にUIテスト=E2Eテストという構図が生まれていました。 宣言的UIの時代: UIテストとE2Eテストの分離 一方、Reactに代表される宣言的UIフレームワークは、「状態を引数として受け取り、UIを返却する」関数としてUIを定義しています。同じToDoアプリをReactで書くと以下のようになります。 function TodoApp() { const [todos, setTodos] = useState([]); const [inputText, setInputText] = useState(''); const addTodo = async () => { if (inputText) { const response = await fetch('/api/todos', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({text: inputText}) }); const newTodo = await response.json(); setTodos([...todos, newTodo]); setInputText(''); } }; const deleteTodo = async (id) => { await fetch(`/api/todos/${id}`, {method: 'DELETE'}); setTodos(todos.filter(todo => todo.id !== id)); }; return ( <div> <input value={inputText} onChange={e => setInputText(e.target.value)} placeholder="新しいタスク" /> <button onClick={addTodo}>追加</button> <ul> {todos.map(todo => ( <li key={todo.id}> {todo.text} <button onClick={() => deleteTodo(todo.id)}>削除</button> </li> ))} </ul> </div> ); } これにより、状態を再現させるための手続きを踏まなくても、特定の状態をテストできるようになります。 また、特徴的なのがUIをいくつかのコンポーネントのまとまりとして構成しており、各コンポーネントを分けてテストすることも可能である点です。子コンポーネントたちも親と同様に状態を受け取る関数として定義されているので、コンポーネントごとに状態を変えられるようになりました。 同時に、WebフロントエンドのビルドはバックエンドのWebアプリケーションフレームワークと別のフレームワークが担当することも増え、フロントエンドUIのみを分離してテストする傾向が増えてきました。その結果、純粋にUIの挙動だけをテストしたい場合はUIコンポーネントテストで済ませ、バックエンドとの統合における不具合の検知やCUJ(クリティカルユーザージャーニー: もっとも重要なユーザー導線)をE2Eテストで守る、という考え方が広まってきました。 まとめ この後編では、自動操作技術の変遷と、E2Eテストの目的の変遷について、流れを追う形でまとめてみました。 第2回はこれで終わりです。続く第3回では、E2Eテストが他のテストレベルとどう違うのか、どのような目的で行われるのか、どのように使い分けるべきなのか、などについて深堀りしていきたいと思います。 【連載】モダンなE2Eテストの考え方をマスターしよう 【第1回・前編】まずはやってみよう – Playwrightを使ったハンズオン(事前準備編) 【第1回・後編】まずはやってみよう – Playwrightを使ったハンズオン(テスト自動化編) 【第2回・前編】E2Eテストの歴史 -要素探索技術の変遷- 【第2回・中編】E2Eテストの歴史 -様々な実装技術- 【第2回・後編】E2Eテストの歴史 -自動操作技術と目的の変遷 The post 【第2回・後編】E2Eテストの歴史 – 自動操作技術と目的の変遷 first appeared on Sqripts .
GENIEEの技術進化の軌跡:オープンソースから最新技術スタックまで こんにちは、GENIEE歴14年目の経営情報システム開発部部長をしていますYinoueです。(今は人事部で採用も兼務しています)。GENIEEはこの春16期目に入り、提供しているプロダクトは20を超えました。今回は、そんなGENIEEの技術の遷移についてお話しします。GENIEEは、広告技術とマーケティング技術の両方で多くの進化を遂げてきました。その過程でどのような技術選定が行われ、どのようにシステムが進化してきたのかを詳しく見ていきましょう。 初期のLAMP環境とオープンソースSSP GENIEEの初期段階で
はじめに 技術評論社様より発刊されている Software Design の2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。 これまでの連載で、ZOZOTOWNリプレイスプロジェクトの始まりから各部門の取り組みなどを紹介してきました。最終回となる今回は、フロントエンドの取り組みを取り上げ、これまでのまとめを行います。 目次 はじめに 目次 はじめに フロントエンドエンジニアの責務 フロントエンドリプレイス前 リプレイス後 フロントエンドリプレイスプロジェクトの進め方 調査 設計 テスト Next.jsとの向き合い方 Custom Server 1. ロギング 2. リダイレクト 3. 既存システムからのForm POSTリクエストを受けるエンドポイント 4. マルチプロセスでの起動 next/link カナリアリリースと_next/data/*/jsonの関係 Next.jsでリプレイスしていくうえでの課題 ソフト/ハードナビゲーションのHTTPリファラの違い Shift_JISの取り扱い 振り返り リプレイス後のフレームワークとしてのNext.js リプレイス専任チームにした話 不要機能の削除・調整 リリース まとめ・今後の展望 はじめに ZOZOTOWNのWebフロントエンドは約3年前からリプレイスを実施してきました。連載最終回となる今回はZOZOTOWNのWebフロントエンドリプレイスプロジェクトの進め方や、その過程で得られた技術・組織に関する知見について紹介します。全8回にわたる連載のまとめとして、リプレイスプロジェクトの今後の展望についてもお伝えします。 フロントエンドエンジニアの責務 当社におけるフロントエンドエンジニアの役割について、リプレイス前後のアーキテクチャを比較しながら紹介します(図1)。 図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エラーになります。 図2 Version Skewでの通信 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では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com *1 : 前述の「テスト」項目で記載したカナリアリリースとは目的が異なるものです。

動画

書籍