◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り28日。API 連携を足したアプリは移行先の確認を●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り28日。API 連携を足したアプリは移行先の確認を●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-08-04上級

名前が20文字ちょうどでも保存できない — 文字数の数え方を1か所に決めるまでの実測

絵文字や合成文字が入ると、クライアントとサーバーで文字数が食い違います。4つの数え方をNode v22で実測し、切り詰め2,000件の破損率75.6%・UTF-8往復での置換文字化36.1%という結果から、カウントの正本を1か所に置く設計へ至るまでをまとめました。

React Native242Expo234Intl.SegmenterUnicodeバリデーション設計13

✦ プレミアム記事

アプリのレビュー欄に、短い報告が一件だけ届いたことがあります。「名前が保存できません」。文面はそれだけでした。

再現できませんでした。日本語で試しても、英語で試しても、20文字の上限どおりに動きます。何度か往復して、ようやく相手の入力が見えてきました。名前の末尾に家族の絵文字が並んでいたのです。

手元で同じ文字列を打ち込むと、入力欄のカウンタは「20 / 20」と表示されているのに、保存ボタンを押すとサーバーが弾き返してきました。画面は正しいと言い、サーバーは間違っていると言うのです。どちらも自分が書いたコードでした。

原因は単純で、しかし気付きにくいものでした。クライアントとサーバーが、別々の方法で文字を数えていたのです。

個人開発でアプリを何本か抱えていると、こういう「片方だけ正しい」バグは静かに増えていきます。今回はこの一件を起点に、数え方そのものを設計対象として扱い直した記録です。数値はすべて Node v22.22.3 のサンドボックスで実際に走らせて取りました。

同じ名前を、実装ごとに違う長さで数えている

まず、代表的な文字列を4つの方法で数えてみます。.length(UTF-16 コードユニット数)、スプレッド構文による [...s].length(コードポイント数)、Intl.Segmenter による書記素クラスタ数、そして UTF-8 でのバイト数です。

// seg_probe.mjs — 4つの数え方を並べて比較する
const seg = new Intl.Segmenter('ja', { granularity: 'grapheme' });
const graphemeCount = (s) => {
  let n = 0;
  for (const _ of seg.segment(s)) n++;
  return n;
};
 
const samples = [
  ['ASCII', 'Hello'],
  ['日本語', 'こんにちは'],
  ['家族(ZWJ結合)', '\u{1F468}\u200D\u{1F469}\u200D\u{1F467}\u200D\u{1F466}'],
  ['肌色修飾子つき', '\u{1F44D}\u{1F3FD}'],
  ['国旗(地域指示子)', '\u{1F1EF}\u{1F1F5}'],
];
 
for (const [label, s] of samples) {
  console.log([
    label,
    s.length,                       // UTF-16 コードユニット
    [...s].length,                  // コードポイント
    graphemeCount(s),               // 書記素クラスタ
    Buffer.byteLength(s, 'utf8'),   // UTF-8 バイト
  ].join('\t'));
}

実行結果です。左から順に、UTF-16 コードユニット数・コードポイント数・書記素数・UTF-8 バイト数を並べています。

入力.length[...s].length書記素UTF-8 バイト
Hello5555
こんにちは55515
が(濁点を分解した形)2216
👨‍👩‍👧‍👦(家族)117125
👍🏽(肌色修飾子つき)4218
🇯🇵(国旗)4218
葛󠄀(異体字セレクタつき)3217
각(ハングルを字母に分解した形)3319
กำ(タイ語)2216
❤️(異体字セレクタ16つき)2216
👩‍💻(職業を表す結合)53111

家族の絵文字が、実装によって 11・7・1・25 と読まれます。人間が見て「1文字」と感じるものに対して、4つとも違う答えを返しているわけです。

ここで注意しておきたいのは、[...s].length の立ち位置です。.length の素朴さを知った人が最初に手を出す修正先で、私も長らくそう書いていました。ところがこの表を見ると、[...s] は「正しい方向へ一歩近づいた」のではありません。家族の絵文字を 11 と数えるか 7 と数えるかの違いでしかなく、人間の感覚である 1 には届いていないのです。中途半端に直したぶん、問題が見えにくくなった状態だと言えます。

20文字の入力欄に、絵文字はいくつ入るのか

上限を 20 と決めたとして、その 20 が何を指しているのかで、入る絵文字の数は劇的に変わります。同じ家族の絵文字で試した結果です。

上限20の解釈入る個数この判定を書きがちな場所
.length <= 201個React Native の TextInput と maxLength
[...s].length <= 202個「絵文字対応した」つもりのバリデータ
書記素 20 以下20個ユーザーの期待
UTF-8 で 20 バイト以下0個DB のカラム定義・API のバイト制限

同じ「20文字」という言葉で、1個から20個まで開きが出ます。最後の行は特に厄介です。1個も入らない。バイト長で切っている API やカラムを持っていると、絵文字を1つ入れた時点で拒否されます。

私が踏んだのは、この表の1行目と3行目の食い違いでした。クライアントは書記素で数えて「20 / 20」と表示し、サーバーは .length で数えて 220 という値を見ていました。どちらのコードも自分で書いたのに、数え方を揃えるという発想がそもそも無かったのです。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦同じ文字列を4つの実装が別の数で数える実測表(家族絵文字は 11 / 7 / 1 / 25 と、実装ごとに全部違う数になります)
✦UTF-16単位で切り詰めた2,000件のうち書記素の途中で切れたのが75.6%、UTF-8往復で置換文字になったのが36.1%という破損率の実測
✦書記素単位のカウントとtruncateを1モジュールに閉じ込め、クライアントとサーバーで同じ判定を共有する実装コード一式
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-06-22
壊れたキャッシュで毎回起動時に落ちるアプリ——ユーザーが自力で抜け出せる「セーフモード起動」を設計する
永続化したキャッシュが壊れて起動のたびに同じ場所で落ちると、ユーザーには再インストールしか残りません。アプリ自身が早期クラッシュの連続を数え、対話可能になって初めて起動を確定し、危険な状態だけを段階的にリセットするセーフモードを、Expo(React Native)の実装で設計します。
⬡ 開発ツール2026-10-25
効果音が40回目で鳴らなくなる——expo-audio の createAudioPlayer は remove() だけでは片づきません
expo-audio で再生のたびに createAudioPlayer を作ると、Android では約40回で音が止まり、didJustFinish も届かなくなります。remove() と release() の役割の違い、作り方を変える三つの選択肢、止まったときの見張り方を整理します。
⬡ 開発ツール2026-10-19
チュートリアルの画像が欠けただけで、アプリごと立ち上がらなくなる設計でした。なくても動くはずの機能を、起動の必須の道から外します
一部の端末でだけ、アプリが一度も立ち上がらない。原因は、チュートリアルの画像が欠けると画面の生成ごと失敗する設計でした。任意の機能を起動の必須の道から外す関数と、出荷前の検査を組みました。