RORK LABEN
CLOUD — Rork Max はクラウド上の Mac 群でネイティブ Swift をコンパイルします。Xcode をダウンロードする必要も Mac を所有する必要もありませんPLATFORM — Rork Max は iPhone・iPad・Apple Watch・Vision Pro に対応し、ゲームやウィジェット、Live Activities まで扱えますSHIP — ブラウザでビルドし、ストリーミングのシミュレータでプレビューし、QR コードで実機に入れ、そのまま App Store へ提出できますSPLIT — 通常の Rork は React Native (Expo) でクロスプラットフォームに生成します。広く速く出すなら本体、Apple の機能を深く使うなら Max という分かれ方ですCREDIT — 無料枠はおおむね週5プロンプト程度です。試すコストと作り切るコストは分けて見積もる必要がありますPRICE — Rork Max は Max プランで月 $200、通常の Rork は無料から始められ、有料プランは月 $25 からですCLOUD — Rork Max はクラウド上の Mac 群でネイティブ Swift をコンパイルします。Xcode をダウンロードする必要も Mac を所有する必要もありませんPLATFORM — Rork Max は iPhone・iPad・Apple Watch・Vision Pro に対応し、ゲームやウィジェット、Live Activities まで扱えますSHIP — ブラウザでビルドし、ストリーミングのシミュレータでプレビューし、QR コードで実機に入れ、そのまま App Store へ提出できますSPLIT — 通常の Rork は React Native (Expo) でクロスプラットフォームに生成します。広く速く出すなら本体、Apple の機能を深く使うなら Max という分かれ方ですCREDIT — 無料枠はおおむね週5プロンプト程度です。試すコストと作り切るコストは分けて見積もる必要がありますPRICE — Rork Max は Max プランで月 $200、通常の Rork は無料から始められ、有料プランは月 $25 からです
記事一覧/開発ツール
開発ツール/2026-08-04上級

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

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

React Native218Expo156Intl.SegmenterUnicode2バリデーション設計9

プレミアム記事

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

再現できませんでした。日本語で試しても、英語で試しても、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 の TextInputmaxLength
[...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-06-02
壁紙アプリ6本を1つのコードベースから出し分ける — app.config.ts と EAS で組むホワイトラベル構成
似た構造の壁紙アプリを別々のリポジトリで保守していると、修正が6箇所に分散して破綻します。1つのコードベースから APP_VARIANT で6本のアプリを出し分けるホワイトラベル構成を、app.config.ts と eas.json の実コードと、設定ドリフトを防ぐ検証スクリプトまで含めてまとめました。
開発ツール2026-07-30
Renovate が触ってよい依存と、触ってはいけない依存 — Expo SDK 57 の123パッケージを機械的に切り分ける
Rork 生成アプリに依存の自動更新を入れると、Expo が版を管理している123パッケージまで巻き込まれます。2026年7月30日時点の実測では、うち6件が npm の最新版へメジャー更新される差でした。除外リストを手で書かず SDK から機械生成する手順をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →