「見た目は同じなのに一致しない…?」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()` を使いこなせれば、文字コードに起因する「なぜか動かない」という悩みから卒業できる。ぜひ次のプルリクから、比較ロジックにこのエッセンスを取り入れてみてほしい。
現場からは以上だ。何か詰まったら、いつでも聞いてくれ。

コメント