【実務・中級編】 String.prototype.normalize() によるUnicode正規化 – JavaScript実践ガイド

「見た目は同じなのに一致しない…?」JavaScriptにおけるUnicode正規化の深淵と解決策

現場でフロントエンドをやっていると、一度は必ずぶち当たる壁がある。

ユーザーから送られてきた文字列を比較しているのに、なぜか `===` が `false` を返す。デバッグコンソールでログを見ても、見た目は全く同じ「が」という文字。頭を抱えた経験はないだろうか?

そう、犯人はUnicodeの正規化(Normalization)だ。今回は、この泥沼にハマらないための、シニア層なら知っておくべき「文字コードの生存戦略」を叩き込んでいく。

—

なぜ「見た目が同じ」で「一致しない」ことが起こるのか

結論から言うと、Unicodeには「同じ文字を表現するのに複数のバイナリパターン」が存在するからだ。

例えば「が」という文字。
1. 合成済み文字: 「が」という一つのコードポイント(`\u304C`)
2. 分解された文字: 「か」+「濁点」(`\u304B` + `\u3099`)

コンピュータからすれば、この2つは「全くの別物」だ。しかし、WebアプリのUI上ではどちらも「が」と表示される。この状態でユーザー入力をバリデーションしたり、検索フィルタリングを行おうとすると、エンジニアが意図しない挙動でバグが量産されることになる。

String.prototype.normalize() という切り札

この問題を解決するのが、ES2015から標準搭載された `String.prototype.normalize()` だ。こいつは、バラバラなUnicodeの並びを一定のルール(形式)に統一してくれる。

4つの正規化形式(知っておくべきはNFCとNFKC)

`normalize()` には引数として4つの形式を渡せるが、実務で覚えるべきは実質2つだ。

  • `NFC` (Normalization Form Canonical Composition):

分解されている文字を、可能な限り合成して一つにする。Web開発のデフォルトはこれだ。 多くのデータベースやOSがこの形式を好む。

  • `NFKC` (Normalization Form Compatibility Composition):

NFCの処理に加え、「互換性」のある文字も統一する。例えば、「①」を「1」に変換したり、全角英数を半角に寄せたりする。検索機能などで「ユーザーがどう入力しても同じものとして扱いたい」場合に最強の武器になる。

—

実践:現場で使える「最強の比較ロジック」

現場でバリデーションやフィルタリングを組む際、以下のように正規化を挟むのがプロの作法だ。

/

  • ユーザー入力の揺れを吸収し、厳密に比較するためのユーティリティ

/
function normalizeString(str) {
if (typeof str !== ‘string’) return ”;

// NFKCで全角半角や互換文字を統一し、NFCで正規化する
// 多くのケースで NFKC が最も「人間の直感」に近い挙動を示す
return str.normalize(‘NFKC’);
}

const inputA = ‘が’; // 合成済み文字
const inputB = ‘か\u3099’; // 分解された文字

// そのまま比較すると false
console.log(inputA === inputB); // false

// 正規化してから比較すると true!
console.log(normalizeString(inputA) === normalizeString(inputB)); // true

// おまけ:全角数字の統一例
console.log(‘①’.normalize(‘NFKC’) === ‘1’); // true

—

ブラウザの裏側で起きていること

ブラウザが `normalize()` を実行する際、裏側では「Unicode正規化アルゴリズム(UAX #15)」に基づいたテーブル検索が行われている。

ブラウザのレンダリングエンジン(BlinkやWebKit)は、テキストを画面に描画する際、フォントのグリフデータと照らし合わせる。この時点で内部的に正規化が行われることもあるが、JavaScriptのメモリ上にある文字列データが勝手に書き換わることはない。

だからこそ、開発者が意識的に「比較の前には正規化」というプロセスを挟む必要があるんだ。特に検索機能や、ユーザーのプロフィール編集画面などでこの処理を忘れると、ユーザーから「保存したはずのデータが見つからない」「なぜかエラーが出る」といった不可解な報告を受けることになる。

—

現場のシニアからのアドバイス

「正規化さえすれば全て解決!」と過信するのは禁物だ。

1. パフォーマンスの罠: 非常に巨大な文字列に対して毎回 `normalize()` を呼ぶのはコストがかかる。必要なタイミング(バリデーション時や保存前)に絞るべきだ。
2. データベースとの整合性: MySQLなどのDB側でも文字セット(`utf8mb4`)と照合順序(Collation)の設定が重要だ。アプリ側だけで正規化を完結させず、DB側の設定も必ず確認しておこう。
3. 意図的な変換: `NFKC` は強力だが、時には「1」と「①」を区別したいケースもあるはずだ。要件に応じて、本当に正規化が必要なのか、それとも置換だけでいいのかを見極めてくれ。

この `normalize()` を使いこなせれば、文字コードに起因する「なぜか動かない」という悩みから卒業できる。ぜひ次のプルリクから、比較ロジックにこのエッセンスを取り入れてみてほしい。

現場からは以上だ。何か詰まったら、いつでも聞いてくれ。

コメント

タイトルとURLをコピーしました