LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

ヤフーのユーザー5,400万人から"同意"を得るための技術(HTML/CSS/JavaScript)

こんにちは。SWATチームの今谷と、LY会員サービス統括本部の木所です。

LINEとヤフーの会社合併に伴って、LINEヤフー株式会社が提供するサービスを利用するユーザーに「新プライバシーポリシーへの同意」をいただくためのモジュールを開発しました。本記事では、ヤフーのユーザー5,400万人から”同意”を得るための工夫について、実装上の不具合も交えてご紹介します。

なお、ご紹介する内容は、UIT × Bonfire Front-end Meetup #1 での発表が起点となっています。詳細についてより深く知りたい方は、発表資料Podcast をあわせてご参照ください。

会社合併と、新プライバシーポリシーのご案内

LINEとヤフー、それぞれで同意モジュールがあります。Web向けのほか、iOS・Androidアプリ向けにも提供しました。 Webの同意モジュールでは「Modal版」と「FullScreen版」の2タイプがあります。それぞれ、スマートフォン(縦向き・横向き)、タブレット、PC、それぞれの画面幅と高さに最適化されたUIがあります。

SWATチームによる開発支援

ヤフーの各対象サービスに提供するモジュールの開発はSWATチームが担当しました。2023年6月頃より開発を行い、10月の会社合併以降から順次、ユーザーへの提供を開始しました。

なお、SWATとは、全社横断で技術支援を行っているエンジニアチームです。SWATチームが普段取り組んでいる業務については、過去記事をご参照ください。

同意モジュールに求められること

同意モジュールは、ヤフーのサービスをご利用いただいている5,400万人 *1 すべてのお客様に提供されることを想定しています。

プライバシーポリシーへの同意は、法的・ビジネス的な観点からも重要度が高く、すべてのお客様が迷わずストレスなく確実に同意をできることが求められるコンポーネントです。そのため、サポート対象となる範囲が非常に広く、開発を行うにあたってさまざまな考慮が必要となりました。例えば、シェア率の低いデバイスへの対応、電波強度の弱い環境からのアクセスへの想定などです。

0.1%のシェア率であってもその影響の大きさから、許容することが難しい状況でした。締め切りが明確に定められた開発スケジュールであり、より専門的な知識と実装上の工夫が求められました。

*1 月間ログインユーザーID数。2023年9月末時点の数値(2023年度決算説明会資料より)

検証中に対応した不具合の事例と対策

開発を進める上で、当初は想定しなかった数多くの問題に対処する必要に迫られました。

その中でも特に、特定のOS・ブラウザーに依存した不具合について、具体的な画面イメージを添えながらご紹介します。いずれもお客様への影響が大きく許容できない事例であり、修正に向けた調査と対応が必要でした。

CASE1:画面のスクロールがカクつく

実機での検証を進める中で、iOS Safariでスクロールがカクつく問題がありました。 こちらの画面キャプチャのように勢いよく下にスクロールした際、途中で引っかかってしまいユーザー体験に課題がありました。

調査の結果、これはモーダルの上に重ねていた要素(「>>」のアイコン)による影響でした。

CSSで要素の重なりを指定する際は z-index を多用することが多いと思いますが、要素の重なりが明らかに思える場合は指定しないこともあるのではないでしょうか。しかし一部のブラウザーにおいては、要素の重なりを明示的に指定しない場合の挙動に違いがあります。

対応としては z-index: 0; などで要素の重なりを明示的に指定することとしました。指定が不要に思える箇所でも、実機での確認を行い想定通りのスタイリングができているか確認しましょう。

コード例:

#pp-agreement .inner .animation {
  z-index: 0;
}

※ 説明のために一部記述を省略しています

CASE2:折りたたみ内の文章が消える

プライバシーポリシーの本文は、アコーディオンUIを採用した文章の「折りたたみ」を提供しています。開発後の検証フェーズで見つかった実際の不具合として、折りたたみを開いたとき、本来あるべき文章が表示されないという現象がありました。

この問題に対する解消および予防策として、以下のような対応を行っています。

  • HTML(summaryタグ, detailsタグ)による実装とし、OSやブラウザー間の差異を軽減させる
  • ブラウザーが持つデフォルトの見た目を踏襲し、CSSでの過度な装飾を控える

コード例:

const expandItem = (item) => {
  return `
    <span class="item" data-type="${item.type}">
      <details>
        <summary>${item.value}</summary>
        ${item.items?.map((subItem) => {
          return <span class="item" data-type="${subItem.type}">${subItem.value}</span>
        })}
      </details>
    </span>
  `
}
#pp-agreement .inner .main .item[data-type="expand"] {
  white-space: inherit;
  padding-left: 1em;
}

#pp-agreement .inner .main .item[data-type="expand"] summary {
  text-indent: -1em;
  line-height: 21px;
  font-weight: bold;
  user-select: none;
  cursor: pointer;
}

#pp-agreement .inner .main .item[data-type="expand"] summary::before {
  content: "";
  display: inline-block;
  width: 4px;
}

※ 説明のために一部記述を省略しています

CASE3:最後まで読んだのに、ボタンが押せない

プライバシーポリシーの文章を最後まで読むと、「同意する」ボタンを押すことができます。しかし、Android(Google Chrome)のデバイスにおいて、最後まで読まれたにもかかわらず、ボタンが活性化せず押せない事象がありました。

スクロールによる操作が有効である「scrollable」な要素に対して、垂直方向のスクロール量を取得するために Element.scrollTop を採用しています。AndroidのChromeではこちらのメソッドで正確な値を取得できない課題があり、1px未満の単位で誤差が発生してしまう事象が報告されています。

この問題への対処として Element.scrollTop の値を四捨五入する方法がありますが、これは不十分な対応です。端末によっては、四捨五入で切り上げることができない誤差があるためです。スクロール完了の検知を行うにあたって「scrollableな要素の高さ」と「Element.scrollTop で得られた値」を等価演算子で評価する実装は望ましくありません。

今回は対策として、scrollableな要素に対して「インセット」を設けておき、インセット(内側の余白)を基準としたスクロール完了の検知を行っています。

また、ウィンドウのリサイズ・端末の回転・アコーディオンの開閉によって、(レンダリングフェーズ後に)要素の高さが変動するため気をつけましょう。イベントリスナーによるリサイズイベントの検知では不十分な場合もあるため、複数のデバイスでの網羅的な確認が必要です。

コード例:

const content = app.querySelector(".content") as HTMLDivElement

// 文末までスクロールしたら最後までスクロールした判定とするように設定する
// NOTE: Android Chromeの一部でスクロール量の取得が正確でない場合があるための対応
const insets = 30
const scrolled = content.scrollTop + content.clientHeight

// 最後までスクロールしたか判定する
const isBottom = content.scrollHeight - insets <= scrolled

※ 説明のために一部記述を省略しています

幅広い環境へのサポートを想定した実装

ここからは、ヤフーのような大規模なサービスを想定した幅広い環境へのサポートを行うために、意識していた点についてご紹介します。サポートする範囲の下限を明確に定義し、サポート範囲内の環境で動作保証できる仕様を採用しつつ、OS・ブラウザー固有の仕様は採用しないことが大切です。

今回実装した同意モジュールは、ヤフーの各サービス上での動作を前提としています。モジュールが読み込まれた際、各サービスに与える影響をシビアに最小化する必要があります。

また、基本的な考え方として 古くから存在している仕様を、OS・ブラウザーのデフォルトに沿って採用する ことが大切です。以下のものを使用してしまうと、環境ごとの差異が大きくなるため使用を避けました。

実装で意識すると良いポイントについても具体的にご紹介します。

POINT1:サポート範囲内の標準仕様を確認する

例えば、HTML 5.2や HTML Living Standard はそれぞれ、2017年、2021年に勧告された標準仕様です。今回は、サポート範囲の要件を満たすために HTML 5.1を考慮した実装を行っています。

より詳細な情報は、WHATWGが提供しているドキュメントが参考になります。実装の途中で一度参照しておくとよいでしょう。

POINT2:実装のサポート範囲を明確にする

APIのリクエスト処理において、当初は fetch を採用した実装を行っていました。しかしながら fetch では対応できない環境が多く、より広い環境へのサポートを行うため XMLHttpRequest を採用しています。

POINT3:外部ライブラリに依存しない

グローバルに展開されたCSSやJavaScriptが各サービスに与える影響を考慮する必要があるため、外部ライブラリへの依存が許容できません。

わかりやすい一例としては、UIライブラリです。テンプレートエンジンやJavaScriptの構文を拡張するようなライブラリが利用できないため、テンプレートリテラルを使用したシンプルな実装を採用しています。

なお、テンプレートリテラルを使用した実装では、コード可読性に課題があることが多いと思います。以下のコード例のように、コンポーネントごとに実装を分割することで、コードの見通しが良くなり、保守コストの軽減につながります。

コード例:

const layer = () => {
  return isFullScreenUI ? `` : `<div class="layer"></div>`
}

const template = () => {
  return `
    ${layer()}
    <div class="inner">
      <div class="content">
        ${header()}
        ${main()}
        ${footer()}
      </div>
    </div>
  `
}

※ 説明のために一部記述を省略しています

POINT4:非サポートの擬似クラスを使用しない

例えば :has() は執筆時点ではほとんどのブラウザーでサポートされていますが、古い環境ではサポートされていません。代替となるコードへの書き換えを検討しましょう。そのほか :is():where() についても同様です。

POINT5:ブラウザー差異が大きい要素を避ける

具体的な例を示すと、アコーディオンやスクロールバーがそれにあたります。OSやブラウザー間での見た目において差分が大きく、すべての環境で確実に同じ見た目のコンポーネントを提供する難易度が高く、動作確認が困難です。

そのため、デザイナーを含めた関係者間で実装上の懸念を説明しベターな妥協点を模索しました。結果として、ブラウザー間で差異が大きい要素について、デフォルトの見た目を優先する仕様として進めることとなりました。

おわりに

本記事では、LINEとヤフーの会社合併に伴うプライバシーポリシー同意モジュールについて、フロントエンド開発における考慮や工夫についてご紹介しました。

今回ご紹介したようなシビアな要件は、よくある事例でないかもしれません。しかしながら、LINEヤフー株式会社が提供するサービスでは、例え0.1%のエッジケースの不具合であっても影響範囲が大きい課題であり、緻密な設計と実装が求められる場面が少なくありません。

ここまで本記事でご紹介したことが、皆さんにとって少しでもお役に立てれば幸いです。

参考(外部サイト)