every Tech Blog

株式会社エブリーのTech Blogです。

User-Agentだけに頼らないフロントエンドの端末判定の考え方

はじめに

こんにちは、2025年4月にソフトウェアエンジニアとして新卒入社した黒高です。普段はデリッシュキッチンに関する開発に携わっています。

ウェブ版のデリッシュキッチンにおいて、クライアント端末の判定ロジックを見直す機会がありました。本記事では、「どのようにクライアントを識別し、どう補完していくと良さそうか」を整理します。

端末で処理を分けたいユースケース

スマホとPCで表示を切り替える場面として真っ先に思い浮かぶのはレスポンシブレイアウトですが、これは多くの場合CSSで完結します。JavaScriptのwindow.innerWidthを用いることで画面幅を取得し、処理を分岐させるといった方法もよく使われます。しかし、ログの収集やユーザー訴求内容の分岐、アプリストアへの誘導(例: iOS ならApp Store、AndroidならGoogle Play Store)などで、端末・機種によって処理を分けたい場面には別のアプローチが必要です。

一般的な端末判定方法

基本はUser-Agent(以降UA)ヘッダの内容によって、クライアントを判定しています。デリッシュキッチンで利用しているNuxt.jsでも@nuxtjs/deviceモジュールが用意されていますが、これはクライアントから送られるUAの内容から判別しています。

UAはブラウザやデバイスがサーバーに送る「自分の情報を含んだ文字列」で、OS・ブラウザ・バージョンなどが含まれており端末を判別する手がかりになります。詳しくは以下のドキュメントを参照してください。

developer.mozilla.org

User-Agentの利用方法

フロントエンドにおけるSSRではリクエストヘッダから参照し、ブラウザ実行時はnavigatorオブジェクトを読むだけで取得できます。

developer.mozilla.org

  • サーバサイド(SSR時) 受信したHTTPリクエストのヘッダにuser-agentが含まれているため、そのまま文字列を取り出せます。
  const http = require('http');
  http.createServer((req, res) => {
    const ua = req.headers['user-agent'];
    res.end(`UA: ${ua}`);
  }).listen(3000);
  • クライアントサイド(ブラウザ実行時) ブラウザ環境ではnavigator.userAgentが同じ情報を返すので、条件分岐などに利用できます。
  const ua = navigator.userAgent;

User-Agentの例

デバイス ブラウザ User-Agent の一例
iPhone (iOS) Safari Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1
iPhone (iOS) Chrome Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/135.0.7049.83 Mobile/15E148 Safari/604.1
iPad (iPadOS13以前) Safari Mozilla/5.0 (iPad; CPU OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1
iPad (iPadOS13以降) Safari Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15
Android (Pixel 7) Chrome Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36
PC (Windows 11) Chrome Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0
PC (macOS) Chrome Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15

ChromeのiOS向けビルドはApp Storeの制約によってWebKitを利用するため、UAにはCriOSというトークンが含まれます。Windows 環境での一般的なChromeのUAは上記のようにWindows NTを含むことが多いです。

ここで注意したいのが、iPadOS13以降では「iPad(ただしiPad miniを除く)」が、macOS Safari と同一のUAを送るようになっていることです。これは以下の文献で明言されています。

“With the exception of iPad mini, Safari on iPad will now send a user-agent string that is identical to Safari on macOS.” New WebKit Features in Safari 13 | WebKit より

詳細は後述しますが、この場合「タッチパネルかどうか」でiPadとMacの区別をすることができます。

User-Agentによる端末判定の一例

以下は、next-modules/device/src/runtime/generateFlags.tsで定義された判定ロジックの一つです。

    function isIos(userAgent: string): boolean {
        return /iPad|iPhone|iPod/.test(userAgent)
    }

このように、端末を表す文字列の正規表現を用いた単純なロジックであることがわかります。しかし、これでは"iPad"という文字列が含まれない「iPadOS13以降」では falseになってしまう問題があります。

User-Agent判定を補完する技術・アプローチ

UA判定による主な問題点

UAによる判定はシンプルですが、次のような課題があります。

問題点 説明 影響例
偽装可能性 UA 文字列は開発者ツールなどで簡単に変更可能 実際は iOS なのに Android と判定される
仕様変更リスク iPadOS 13 以降のように UA 仕様が突然変わる "iPad" が含まれなくなり誤判定(前述)
情報過多とプライバシー問題 UA 文字列には不要な情報も多く含まれる プライバシー保護の観点から制限の方向へ

User-Agent Client Hintsについて

従来のUser-Agent文字列は、情報が過剰で偽装も容易、さらにブラウザの仕様変更によって判定が壊れやすいという課題がありました。これに対してGoogleはPrivacy Sandboxの一環としてUser-Agent Reductionを提案し、その代替としてUser-Agent Client Hints(UA-CH)が標準化されています。

なお、UA-CHの正式な仕様はUser-Agent Client Hints W3C Draftで公開されており、User-Agentの情報量削減とプライバシー配慮の観点から標準化が進められています。詳しい背景や概要は以下のサイトも参考になります。

privacysandbox.google.com

UA-CHでは、必要な情報だけをブラウザが段階的に返す仕組みを導入し、プライバシー保護を考慮しつつOSやブラウザ情報を安全に取得できます。基本情報(OS名やモバイル判定)はnavigator.userAgentDataから取得します。

if (navigator.userAgentData) {
  const { platform, mobile } = navigator.userAgentData;
  console.log(platform, mobile); // 例: "Android", true
}

詳細情報は必要に応じてgetHighEntropyValues()を通じて明示的にリクエストします。

if (navigator.userAgentData) {
  navigator.userAgentData
    .getHighEntropyValues(["platformVersion", "architecture", "model", "uaFullVersion"])
    .then(ua => console.log(ua)); 
    // 例: { platformVersion: "13.0.0", architecture: "arm", model: "Pixel 5", uaFullVersion: "120.0.6099.129" }
}

UA-CH利用によるメリットとして、以下が挙げられます。

  • デフォルトで基本情報のみ、詳細情報は明示的要求が必要→クライアントのプライバシー保護につながる
  • navigator.userAgentDataなどで正規表現パース不要、扱いやすさ向上
  • 従来のUAから少しずつUA-CHに移行可能で、既存コードへの影響が小さい

しかし、2025年9月現在ではChromium系ブラウザを中心に対応が進んでいるものの、Safariや一部ブラウザでは未対応のままになっています。したがって、判定ロジックをUA-CHだけに置き換えることは避けた方が良いでしょう。

Feature Detectionについて

developer.mozilla.org

こちらのドキュメントでは、UA文字列による判定の問題点と、その代替としてのFeature Detectionの有用性が説明されています。これは、「ブラウザ(や端末)の名前/バージョンを調べて分岐する」のではなく、「操作中の環境が特定の機能をサポートしているかどうかを調べて、その結果に応じて処理を変える」方式です。例えば、以下のような実装例が挙げられます。

  // タッチ操作が可能かどうかを直接判定
  const touch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
  if (touch) console.log('タッチUIを有効化');

Feature Detection で解決できることとして、以下が挙げられます。

  • 機能が使えるか直接確認し、誤判定リスクを抑制できる
  • UAでは判断しづらい最新APIの対応可否を正しく判定
  • UA仕様変更やリダクションの影響を受けにくい堅牢なコードにつながる

OSや端末詳細の判定自体はできないものの、UAに依存しないこのような書き方は積極的に取り入れるべきだと感じました。

端末判定ロジックの実装例

私が担当した実装では、「iPhoneの場合に分岐したコードを、iPadにも適用させたい」という状況がありました。注意すべきポイントは、「iPad以外のタブレットやAndroidは条件外」「前述したiPadOS13以降の識別問題を正しく対処する」ことの2点であり、これは前述した関数の利用だけでは不十分です。

今回はUA-CHが利用可能なら優先し、使えない場合は従来のUA判定 + タッチサポート + プラットフォーム情報を組み合わせるアプローチを取りました。なお、Macintosh系でタッチパネルをサポートするPCがないことを前提としています。

iPhone、Android、iPad、それ以外を判定したいときのコード例は以下の通りです。

export async function detectDevice() {
  // UA-CHが使える場合は優先
  if (navigator.userAgentData) {
    const { platform, mobile } = navigator.userAgentData;
    if (platform === 'iOS' && mobile) return 'iphone';
    if (platform === 'iOS' && !mobile) return 'ipad';
    if (platform === 'Android') return mobile ? 'android' : 'other';
    return 'other';
  }

  // UAとタッチ対応を組み合わせた判定
  const ua = navigator.userAgent;
  const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
  const isIpad = /iPad/.test(ua) || (/Macintosh/.test(ua) && isTouch);

  if (/iPhone/.test(ua)) return 'iphone';
  if (/Android/.test(ua)) return /Mobile/.test(ua) ? 'android' : 'other';
  if (isIpad) return 'ipad';
  return 'other';
}

最後に

今回の見直しで感じたのは、ライブラリやモジュールの判定をそのまま使うだけではなく、本当に正しいか・最新仕様に追随できているかをちゃんと確認することの大切さです。

また、UA文字列に依存しすぎない設計になるよう、機能の有無を直接見るFeature Detectionを基本とする方針が良さそうです。

UA-CHの利用も一つの手段ではありますが、iOS端末で非対応の現状を踏まえると、実際に利用するメリットはまだ乏しいとも言えます。導入するならフォールバックを含めた段階的な移行を前提に、今後の対応状況を見ながら慎重に検討していきたいです。

参考文献