代表的な面接トピック

フロントエンド面接:オフラインファーストのフォーム同期体験の設計

フロントエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

オフラインファーストのフォームを設計してください。ユーザーが電車内で編集し、通信が回復したときにアプリが同期します。別のデバイスが同じフィールドを変更していた場合、上書き、データ損失、ステータスの混乱をどのように防ぎますか?

プロンプトと対象範囲

このフロントエンドシステム設計の設問は、状態管理と分散更新を組み合わせたものです。単にキャッシュを追加するのではなく、ローカルデータ、ミューテーションキュー、サーバーバージョン、および可視化された復元状態を結びつけた回答が求められます。

面接官が見ているポイント

  • ローカル下書き、保留中のミューテーション、サーバー確定状態の分離。
  • コンフリクト戦略の選択と、自動マージできないフィールドの特定。
  • リトライ、重複送信、ブラウザの終了、複数タブの処理。
  • オフライン、同期中、コンフリクト、エラーの各状態を理解しやすく、復元可能にすること。

確認すべき明確化のための質問

各フィールドが独立しているか、金額などの監査対象や高リスクなフィールドがあるかを確認します。添付ファイルの有無、サーバーのバージョン管理や操作ログのサポート、Last-Write-Wins(最終書き込み優先)が許容されるか、ブラウザの対応状況、ローカル保持の要件などを確認します。

30秒で答える要約

耐久性のあるローカルストアとしてIndexedDBを使用し、すべての編集をclientMutationIdとベースのサーバーバージョンとともに記録します。UIは即座にローカルの結果を表示し、保留中(pending)としてマークします。オンライン復帰時にアプリまたはService Workerがキューを送信します。サーバーはバージョンチェックと冪等性(べきとう性)を用いて、成功、コンフリクト、またはバリデーションエラーを返します。安全なフィールドは自動マージ可能ですが、高リスクなフィールドは明示的な選択のために差分(diff)を表示します。すべての状態がリトライまたは取り消し(Undo)をサポートします。

ステップ別の詳細解説

1. 最初に状態モデルを定義する

各下書きはserverVersion、localVersion、syncState、lastErrorを持ちます。保留中の各ミューテーションは、clientMutationId、フィールドの変更内容、作成日時、ベースバージョンを保持します。読み取りはローカルストレージから行い、ネットワークレスポンスはバージョンチェック後にのみマージされるため、古いレスポンスが新しい編集を上書きすることはありません。

2. 復元可能なファクトをIndexedDBに永続化する

IndexedDBは、より大きな構造化オフラインデータに適していますが、設計にはスキーマのアップグレード、クォータ(容量制限)エラー、ブラウザによる退避(eviction)への対応を含める必要があります。下書き、ミューテーション記録、コンフリクトを別々に保存し、下書きとキュー記録を1つのトランザクションでコミットします。機密フィールドは最小限に抑え、ログアウト時や有効期限切れ時に消去します。

3. 同期トリガーと冪等性を設計する

オンライン時は即座に試行し、オフライン時はキューに入れます。Background Syncがサポートされている環境では、Service Worker用のユニークなタグを登録しますが、このAPIを万能な配信保証として扱ってはなりません。すべてのリクエストにclientMutationIdとベースバージョンを含め、同じIDを再送した場合はミューテーションを重複適用せずに同一の結果を返すようにします。

4. コンフリクトポリシーをフィールドのリスクに合わせる

ラベルなどの独立したフィールドは、フィールドごとのバージョンでマージできます。金額、権限、コンプライアンス関連の記述などは、暗黙的にLast-Write-Winsを適用すべきではありません。コンフリクト発生時は、ローカル値、サーバー値、編集時刻を並べて表示し、ユーザーの選択によって新しいミューテーションを作成します。自動マージの場合はその入力内容を記録する必要があります。

5. 失敗、リトライ、複数コンテキストの処理

ネットワーク障害に対しては、キューの順序を維持しながら指数バックオフ(exponential backoff)を使用します。バリデーションエラーは、無限リトライではなく編集可能なエラー状態とします。複数タブ間はBroadcastChannelやバージョンチェックを通じて調整し、古いミューテーションが重複送信されないようにします。耐久性のあるキューは次回起動時に再開し、UIには保留中の件数と最後のエラーを表示します。

優れた回答例

私ならまずフィールドのリスクとオフラインの境界を定義します。クライアントはIndexedDBに下書きとミューテーションキューを保存します。各ミューテーションはclientMutationIdとベースのserverVersionを持ち、UIは即座にローカルの結果を保留中として表示します。アプリまたはService Workerが再接続後にキューを送信し、サーバーはバージョンチェックと冪等性を用いて成功、コンフリクト、またはバリデーションエラーを返します。独立したフィールドは自動マージできますが、高リスクなフィールドはローカル対サーバーの差分を表示し、確認を求めます。ネットワークエラーはバックオフし、ビジネスロジックのエラーは再編集可能にします。複数タブはバージョン通知を使用して、古いデータが新しい作業を上書きしないようにします。インターフェースはオフライン、同期中、コンフリクト、失敗、保存済みの各状態を明確に区別します。

よくある間違い

  • ミューテーションキューやベースバージョンを持たず、最新のフォームのみをキャッシュすること。
  • タイムスタンプやLast-Write-Winsをすべてのフィールドに一律適用すること。
  • Background Syncをすべてのブラウザで保証されたものとして扱うこと。
  • 冪等性キーなしでリトライし、重複データを作成してしまうこと。
  • コンフリクトをコンソールに出力するだけで、ユーザー向けの復元手段を用意しないこと。
  • スキーマアップグレード、クォータ、複数タブ、ブラウザの終了を無視すること。

フォローアップ質問と回答

2つのタブが同時に編集した場合はどうなりますか?

各タブがバージョン変更をサブスクライブし、ベースが古くなった場合にマージを要求します。送信前にバージョンを再チェックします。サーバー側でも共有ミューテーションIDによって重複排除が行われます。

オフラインデータが漏洩する可能性はありますか?

ローカルには必要なフィールドのみを保存し、機密コンテンツを暗号化し、保持期間を短縮し、ログアウト時、共有デバイスの使用時、またはポリシー変更時に消去します。

ブラウザがBackground Syncをサポートしていない場合はどうしますか?

アプリの起動、フォーカス取得、接続状態の変更、ショートポーリングをフォールバックのトリガーとして使用し、レコードが保留中であることをユーザーに伝えます。システムの正しさがこのAPIに依存してはなりません。

自動マージが安全なのはどのような場合ですか?

フィールドのセマンティクス(意味構造)が独立しており、バージョンのベースが判明していて、一時的な不一致が許容される場合に限られます。金額、権限、在庫、監査フィールドは明示的な確認を必須とすべきです。

公開情報ソース

関連する質問