1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ClaudeCode開発】シンプルな文字数チェッカーをJavaScriptで作ってみた

Last updated at Posted at 2025-08-12

はじめに

文字数カウントツールは数多く存在しますが、「毎日使うからこそシンプルに」という思想で、必要最小限の機能に特化した文字数チェッカーを開発しました。リリース後にユーザーから文字数カウント方式についてのコメントをいただき、Unicode文字数方式への改善も行いました。YouTube台本制作で毎日文字数チェックを行う中で感じた「余計な機能はいらない」という課題を解決しながら、正確な文字カウントを実現するツールです。

本記事について

今回の開発およびこの記事執筆は、Claude Code(Anthropic社のAI)との協業で進めました。
AI時代の新しい開発スタイルの実践例として参考になれば幸いです。

この記事で分かること

  • Unicode文字数方式とUTF-16コードユニット方式の技術的違い
  • 異体字セレクタや結合絵文字の正確なカウント方法
  • リアルタイム文字カウントのJavaScript実装方法
  • 3パターンの文字数カウント(改行含む・改行除く・空白除く)の実装
  • プライバシー配慮のクライアントサイド完結型設計
  • レスポンシブデザインでの文字カウント表示UI

この記事の対象者

  • フロントエンドの文字列処理に興味があるエンジニア
  • シンプルなWebアプリケーション開発を学びたい方
  • 日常的に文字数カウントツールを使用している方
  • JavaScriptでのリアルタイム処理実装に関心がある方

動作環境・使用技術

  • 開発環境: Windows 11, Claude Code
  • 言語: HTML5, CSS3, JavaScript(ES5準拠)
  • 特徴: 外部ライブラリ依存なし、単一HTMLファイル構成
  • デバイス対応: レスポンシブデザイン(PC・スマートフォン・タブレット)

自己紹介

ホネグミ代表のなおきちです。エンジニア/マーケターとして、クライアントの「こうしたい」をITと経営の両視点から形にしています。
最近はClaude Codeとの協業開発に注力しており、思想駆動型サービス開発を実践中です。

完成品・デモ

スクリーンショット 2025-08-12 163126.png

主な機能

  • リアルタイム文字カウント: 入力と同時に3パターンの文字数を表示
  • 3種類の文字数パターン: 改行含む、改行除く、改行・空白除く
  • プライバシー保護: 全てクライアントサイドで処理、データ送信なし
  • レスポンシブ対応: PC・モバイル両対応の快適なUI
  • 高速動作: 外部ライブラリ依存なしの軽量実装

技術構成

【アーキテクチャ概要】

単一HTMLファイル構成
├── HTML5 Structure
│   ├── semantic markup
│   ├── テキストエリア(textarea)
│   └── 3種類の結果表示エリア
├── CSS3 Styling
│   ├── レスポンシブグリッドレイアウト
│   ├── モバイルファーストデザイン
│   └── グラデーション・ホバー効果
└── JavaScript (ES5)
    ├── リアルタイム文字カウント処理
    ├── 正規表現による文字種別処理
    └── DOM操作によるリアルタイム表示更新

採用技術とその理由

1. 単一HTMLファイル構成

  • 配布・組み込みの簡便性
  • 外部依存関係の完全排除
  • 高速ローディングの実現

2. ES5準拠JavaScript

  • 幅広いブラウザ対応
  • 軽量で高速な処理
  • 依存ライブラリなしの純粋実装

3. クライアントサイド完結型

  • プライバシー保護の徹底
  • サーバーコスト削減
  • オフライン環境での動作

実装手順

1. HTMLの基本構造

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="文字数チェッカー【無料】でテキストの文字数をリアルタイムカウント!">
    <title>文字数チェッカー</title>
</head>
<body>
    <div class="character-counter-wrapper">
        <div class="character-counter">
            <h2>📝 文字数チェッカー</h2>
            <p>テキストの文字数をリアルタイムでカウント。入力した文字はインターネットに一切送信されないため、安心してご利用いただけます。</p>
            
            <!-- テキスト入力エリア -->
            <div class="input-section">
                <label for="textInput">カウントしたいテキストを入力または貼り付けてください</label>
                <textarea 
                    id="textInput" 
                    class="text-input" 
                    placeholder="ここにテキストを入力してください..."
                    oninput="countCharacters()"
                ></textarea>
            </div>
            
            <!-- 結果表示エリア -->
            <div class="results">
                <div class="result-item">
                    <span class="result-number" id="totalCount">0</span>
                    <div class="result-label">文字数<br>(改行含む)</div>
                </div>
                <div class="result-item">
                    <span class="result-number" id="noBreakCount">0</span>
                    <div class="result-label">改行を除いた<br>文字数</div>
                </div>
                <div class="result-item">
                    <span class="result-number" id="noSpaceCount">0</span>
                    <div class="result-label">改行・空白を除いた<br>文字数</div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

2. 核心となる文字カウントロジック(Unicode文字数方式)

character-counter.js
function countCharacters() {
    // テキストエリアから入力値を取得
    const text = document.getElementById('textInput').value;
    
    // Unicode文字数方式でカウント(スプレッド演算子使用)
    
    // 1. 文字数(改行含む)- Unicode文字数でカウント
    const totalCount = [...text].length;
    
    // 2. 改行を除いた文字数
    // \r?\n で改行文字(CR+LFまたはLF)を除去
    const noBreakCount = [...text.replace(/\r?\n/g, '')].length;
    
    // 3. 改行・空白を除いた文字数
    // [\r\n\s\t ] で改行・半角空白・タブ・全角空白を除去
    const noSpaceCount = [...text.replace(/[\r\n\s\t ]/g, '')].length;
    
    // 結果をHTMLに反映
    // toLocaleString()で数値を3桁区切りで表示
    document.getElementById('totalCount').textContent = totalCount.toLocaleString();
    document.getElementById('noBreakCount').textContent = noBreakCount.toLocaleString();
    document.getElementById('noSpaceCount').textContent = noSpaceCount.toLocaleString();
}

Unicode文字数方式と従来の方式の比較

// 従来の方式(UTF-16コードユニット数)
const oldMethod = text.length;

// 新しい方式(Unicode文字数)
const newMethod = [...text].length;

// 異体字セレクタ付き文字の例
const testChar = '葛󠄀'; // 葛 + 異体字セレクタ

console.log(testChar.length);          // 3(UTF-16コードユニット)
console.log([...testChar].length);     // 2(Unicode文字数)

// サロゲートペア文字の例
const surrogatePair = '𩸽'; // 鱼偏の漢字

console.log(surrogatePair.length);     // 2(UTF-16コードユニット)
console.log([...surrogatePair].length); // 1(Unicode文字数)

// 結合絵文字の例
const combinedEmoji = '👩‍👩‍👦‍👦'; // 家族絵文字

console.log(combinedEmoji.length);     // 11(UTF-16コードユニット)
console.log([...combinedEmoji].length); // 7(Unicode文字数)

3. レスポンシブCSS設計

responsive-design.css
/* リセット・ベーススタイル */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* メインコンテナ */
.character-counter-wrapper {
    font-family: 'Helvetica Neue', Arial, sans-serif !important;
    line-height: 1.6 !important;
    color: #333 !important;
    background: #f5f7fa !important;
    padding: 20px !important;
}

/* 中央配置とカード設計 */
.character-counter {
    max-width: 800px !important;
    margin: 0 auto !important;
    background: white !important;
    border-radius: 10px !important;
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05) !important;
    padding: 20px !important;
}

/* テキストエリアのスタイル */
.text-input {
    width: 100%;
    min-height: 300px;
    padding: 15px;
    border: 2px solid #e0e6ed;
    border-radius: 10px;
    font-size: 16px;
    font-family: inherit;
    resize: vertical;
    transition: all 0.3s ease;
    background: #f8f9fa;
}

.text-input:focus {
    outline: none;
    border-color: #2c5aa0;
    background: white;
    box-shadow: 0 0 0 3px rgba(44, 90, 160, 0.1);
}

/* 結果表示グリッドレイアウト */
.results {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 15px;
    margin: 30px 0;
}

/* 結果カードデザイン */
.result-item {
    background: linear-gradient(135deg, #2c5aa0 0%, #4a7bc8 100%);
    color: white;
    padding: 20px;
    border-radius: 10px;
    text-align: center;
    box-shadow: 0 4px 12px rgba(44, 90, 160, 0.2);
    transition: transform 0.3s ease;
}

.result-item:hover {
    transform: translateY(-3px);
}

.result-number {
    font-size: 2.2rem;
    font-weight: bold;
    display: block;
    margin-bottom: 8px;
}

/* モバイル対応 */
@media (max-width: 600px) {
    .character-counter-wrapper {
        padding: 10px !important;
    }
    
    .character-counter {
        padding: 15px !important;
    }
    
    .text-input {
        min-height: 200px;
        padding: 12px;
    }
    
    .results {
        grid-template-columns: 1fr;
        gap: 10px;
    }
    
    .result-number {
        font-size: 1.8rem;
    }
}

Unicode文字数方式への改善経緯

ユーザーコメントからの気づき

リリース後、Qiitaで以下のようなコメントをいただきました:

葛 1文字、葛󠄀 3文字、𩸽 2文字、👩‍👩‍👦‍👦 11文字という結果になりましたが仕様ですか?

このコメントで、従来のtext.length方式(UTF-16コードユニット数)では特殊文字が正確にカウントできないことが判明。

他のWebサービスと比較した結果、「1,2,1,7文字」というUnicode文字数方式の結果が一般的であることが確認できました。

改善実装の技術的詳細

// 改善前(UTF-16コードユニット数)
function oldCountCharacters() {
    const text = document.getElementById('textInput').value;
    const totalCount = text.length; // UTF-16コードユニット数
    // 異体字や結合絵文字で不正確な結果
}

// 改善後(Unicode文字数)
function newCountCharacters() {
    const text = document.getElementById('textInput').value;
    const totalCount = [...text].length; // Unicode文字数
    // 異体字やサロゲートペアで正確な結果
}

スプレッド演算子の動作原理:

  1. [...text]は文字列をUnicodeコードポイント単位で配列に分解
  2. サロゲートペア(2つのUTF-16コードユニット)を1つの文字として扱う
  3. 異体字セレクタは基本文字と別の文字としてカウント(他のWebサービスと同様)

工夫したポイント・苦労した点

1. Unicode文字数方式への改善実装

課題: 従来のtext.lengthでは特殊文字の正確なカウントが不可

解決策: スプレッド演算子を使用したUnicode文字数カウント

// 改善前(UTF-16コードユニット数)
const oldTotalCount = text.length;
const oldNoBreakCount = text.replace(/\r?\n/g, '').length;
const oldNoSpaceCount = text.replace(/[\r\n\s\t ]/g, '').length;

// 改善後(Unicode文字数)
const newTotalCount = [...text].length;
const newNoBreakCount = [...text.replace(/\r?\n/g, '')].length;
const newNoSpaceCount = [...text.replace(/[\r\n\s\t ]/g, '')].length;

効果: 異体字やサロゲートペアでも他のWebサービスと一致する正確な結果

2. 文字カウントパターンの設計

課題: 用途に応じた適切な文字数計算方法の提供

解決策: 3種類の文字数パターンを同時表示

// パターン1: 改行含む(最も基本的)
const totalCount = [...text].length;

// パターン2: 改行除く(文章の実質文字数)
const noBreakCount = [...text.replace(/\r?\n/g, '')].length;

// パターン3: 改行・空白除く(純粋な文字数)
const noSpaceCount = [...text.replace(/[\r\n\s\t ]/g, '')].length;

効果: YouTube台本、記事執筆、SNS投稿など様々な用途に対応

3. リアルタイム処理のパフォーマンス最適化

課題: 入力のたびに処理が発生するため、重い処理だとUIが重くなる

解決策: 軽量な文字列処理のみで構成

// 重い処理を避け、シンプルな文字列置換のみ使用
function countCharacters() {
    const text = document.getElementById('textInput').value;
    
    // 正規表現も最小限に抑制
    const totalCount = text.length;  // 最高速
    const noBreakCount = text.replace(/\r?\n/g, '').length;  // 高速
    const noSpaceCount = text.replace(/[\r\n\s\t ]/g, '').length;  // 高速
    
    // DOM更新も最小限
    document.getElementById('totalCount').textContent = totalCount.toLocaleString();
    document.getElementById('noBreakCount').textContent = noBreakCount.toLocaleString();
    document.getElementById('noSpaceCount').textContent = noSpaceCount.toLocaleString();
}

4. 全角・半角空白の適切な処理

課題: 日本語特有の全角空白( )の処理

解決策: 正規表現で全角・半角空白を包括的に処理

// [\r\n\s\t ] で以下を除去:
// \r\n : 改行文字(Windows/Mac/Linux対応)
// \s   : 半角空白
// \t   : タブ文字
//     : 全角空白(日本語特有)
const noSpaceCount = text.replace(/[\r\n\s\t ]/g, '').length;

5. プライバシー保護の実装

課題: ユーザーの入力データを安全に処理

解決策: 完全クライアントサイド処理

// サーバー送信なし、外部API呼び出しなし
// 全ての処理をブラウザ内で完結
function countCharacters() {
    // ローカル処理のみ
    const text = document.getElementById('textInput').value;
    // ... 文字数計算処理
    // ネットワーク通信は一切発生しない
}

6. レスポンシブデザインでの視認性確保

課題: 3つの結果を小画面でも見やすく表示

解決策: CSS Gridによる柔軟レイアウト

.results {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 15px;
}

@media (max-width: 600px) {
    .results {
        grid-template-columns: 1fr; /* モバイル: 縦並び */
    }
}

パフォーマンスの最適化

1. 単一ファイル構成によるローディング最適化

従来の構成(複数ファイル):
├── index.html
├── style.css  ← HTTPリクエスト
├── script.js  ← HTTPリクエスト
└── (外部ライブラリ) ← HTTPリクエスト

今回の構成(単一ファイル):
└── index.html(CSS・JS内包) ← HTTPリクエスト1回のみ

効果: 初期ローディング時間を大幅短縮

2. ES5準拠による互換性確保

// ES6以降の機能を避けてES5で実装
// const, let → var
// テンプレートリテラル → 文字列結合
// アロー関数 → function記法

function countCharacters() {
    var text = document.getElementById('textInput').value;
    var totalCount = text.length;
    // ... 古いブラウザでも確実に動作
}

3. 数値表示の最適化

// toLocaleString()で3桁区切り表示
// 例: 12345 → 12,345
document.getElementById('totalCount').textContent = totalCount.toLocaleString();

まとめ

シンプルな文字数チェッカーの開発を通じて、以下の知見を得ました:

  1. Unicode文字数方式の重要性: 特殊文字も正確にカウントするための技術的必須要素
  2. ユーザーフィードバックの価値: 実用ツールはユーザーの声で進化する
  3. 機能の厳選の重要性: 毎日使うツールこそシンプル設計が効果的
  4. リアルタイム処理の最適化: 軽量な文字列処理でスムーズなUX実現
  5. プライバシーファーストの設計: クライアントサイド完結で安全性確保

技術的にはシンプルながら、Unicode文字数方式で正確な計測を実現し、日常的に使える実用性の高いツールを開発できました。ユーザーからのフィードバックを受けた改善が、より正確で信頼できるツールへと進化させるきっかけとなりました。

Unicode文字数カウント機能の実装や、特殊文字を扱うWebアプリケーション開発に興味がある方の参考になれば幸いです。


実際のサービス

免責事項: 本ツールはシンプルな文字数カウント目的で作成されました。文字数の計算結果は参考値であり、特定のプラットフォームでの実際の文字数制限とは異なる場合があります。

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?