Next.jsで学ぶ文字列変換の設計思想:camelCase変換ツールの実装から学んだこと
はじめに
プログラミングにおける命名規則(Naming Convention)は、コードの可読性を左右する重要な要素です。JavaScriptではcamelCase、Pythonではsnake_case、CSSではkebab-caseというように、言語やフレームワークによって推奨される命名規則が異なります。
本記事では、Next.jsで8種類の文字列変換に対応したツールを実装する過程で得られた設計上の学びと、実装のベストプラクティスを共有します。
実装する文字変換ツールの全体像
対応する変換形式
| 変換形式 | 例 | 主な用途 |
|---|---|---|
| camelCase | userName |
JavaScript/TypeScript変数名 |
| PascalCase | UserName |
React コンポーネント名、クラス名 |
| snake_case | user_name |
Python変数名、DB カラム名 |
| kebab-case | user-name |
CSS クラス名、URL スラッグ |
| UPPER CASE | USER NAME |
定数、見出し |
| lower case | user name |
通常の文章 |
| Capitalize | User Name |
タイトル表記 |
| UPPER_SNAKE | USER_NAME |
環境変数、定数 |
アーキテクチャ設計のポイント
1. 状態管理の設計思想
const [inputText, setInputText] = useState('')
const [selectedCase, setSelectedCase] = useState<string>('camelCase')
const [copiedIndex, setCopiedIndex] = useState<number | null>(null)
設計意図:
-
inputText: 入力データの単一ソース(Single Source of Truth) -
selectedCase: ユーザーの選択状態を保持(UIとロジックの分離) -
copiedIndex: UI フィードバック用の一時状態(UX向上)
この設計により、データフロー(入力 → 変換 → 出力)が明確になり、デバッグやテストが容易になります。
2. 変換ロジックの抽象化
変換処理をconvertCase関数として抽象化することで、以下のメリットが得られます:
- テスタビリティ: 純粋関数なのでユニットテストが書きやすい
- 再利用性: 他のコンポーネントでも使える
- 保守性: ロジックの変更が1箇所で済む
const convertCase = (type: string): string => {
if (!inputText) return ''
const lines = inputText.split('\n').filter(line => line.trim().length > 0)
const convertedLines = lines.map(line => {
// 各変換処理
})
return convertedLines.join('\n')
}
3. 正規表現パターンの統一設計
各変換処理で共通する「単語分割」の処理を統一:
// 共通パターン:既存のケース形式を単語に分割
const words = trimmedLine
.replace(/([A-Z])/g, ' $1') // PascalCase/camelCase対応
.split(/[\s_-]+/) // 区切り文字で分割
.filter(w => w.length > 0) // 空文字を除外
なぜこの設計が重要か:
- どの形式(
fooBar,foo_bar,foo-bar)からでも変換可能 - 変換処理のロジックが統一され、理解しやすい
- バグの混入リスクが減る
実装時につまづきやすいポイント
ポイント1: 大文字の検出と単語分割
課題: camelCase や PascalCase を単語に分割する方法
解決策:
.replace(/([A-Z])/g, ' $1')
この正規表現により、fooBar → foo Bar のように大文字の前にスペースを挿入します。
なぜこれが必要か:
split() だけでは camelCase を正しく分割できないため、事前に区切りを明示的に挿入する必要があります。
ポイント2: 余分な区切り文字の除去
課題: __hoge__puyo__ のような連続する区切り文字
解決策:
.replace(/^_+|_+$/g, '') // 先頭と末尾の_を削除
.replace(/_+/g, '_') // 連続する_を1つに
図解:
入力: __hoge__puyo__
↓ 先頭末尾削除
hoge__puyo
↓ 連続削除
hoge_puyo
ポイント3: 改行対応による一括変換
設計思想: ユーザーの作業効率を最大化
const lines = inputText.split('\n').filter(line => line.trim().length > 0)
const convertedLines = lines.map(line => {
// 各行を個別に変換
})
return convertedLines.join('\n')
なぜ改行対応が重要か:
実務では複数の変数名を一度に変換したいケースが多く、1つずつ処理するのは非効率です。
UX設計の工夫
クリップボードAPIの活用
const copyToClipboard = async (text: string, index: number) => {
await navigator.clipboard.writeText(text)
setCopiedIndex(index)
setTimeout(() => setCopiedIndex(null), 2000)
}
UX設計のポイント:
- コピー成功を視覚的にフィードバック(2秒間表示)
- ユーザーがコピーできたか不安にならない
レスポンシブデザインの考慮
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
- モバイル: 縦並び(1カラム)
- デスクトップ: 横並び(2カラム)
完成コード
'use client'
export const dynamic = 'force-static'
import { useState } from 'react'
import { Check, Copy } from 'lucide-react'
export default function CaseConverter() {
const [inputText, setInputText] = useState('')
const [selectedCase, setSelectedCase] = useState<string>('camelCase')
const [copiedIndex, setCopiedIndex] = useState<number | null>(null)
const convertCase = (type: string): string => {
if (!inputText) return ''
const lines = inputText.split('\n').filter(line => line.trim().length > 0)
const convertedLines = lines.map(line => {
const trimmedLine = line.trim()
let converted = ''
switch (type) {
case 'camelCase':
const words = trimmedLine
.replace(/([A-Z])/g, ' $1')
.split(/[\s_-]+/)
.filter(w => w.length > 0)
converted = words
.map((word, index) =>
index === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
)
.join('')
break
case 'snake_case':
converted = trimmedLine
.replace(/([A-Z])/g, '_$1')
.replace(/[\s-]+/g, '_')
.toLowerCase()
.replace(/^_+|_+$/g, '')
.replace(/_+/g, '_')
break
// 他の変換処理も同様
}
return converted
})
return convertedLines.join('\n')
}
const copyToClipboard = async (text: string, index: number) => {
await navigator.clipboard.writeText(text)
setCopiedIndex(index)
setTimeout(() => setCopiedIndex(null), 2000)
}
const cases = [
{ type: 'camelCase', label: 'camelCase' },
{ type: 'PascalCase', label: 'PascalCase' },
{ type: 'snake_case', label: 'snake_case' },
{ type: 'kebab-case', label: 'kebab-case' },
{ type: 'upper', label: 'UPPER CASE' },
{ type: 'lower', label: 'lower case' },
{ type: 'capitalize', label: 'Capitalize' },
{ type: 'UPPER_SNAKE', label: 'UPPER_SNAKE' },
]
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
{/* 入力側 */}
<div className="flex flex-col">
<label htmlFor="input">入力</label>
<textarea
id="input"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
className="flex-1 min-h-[450px] font-mono"
placeholder="hoge_puyo fooBar kebab-case"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
改行で単語を区切ってください
</p>
</div>
{/* 出力側 */}
<div className="flex flex-col">
<div className="mb-4">
<label>変換形式</label>
<div className="flex flex-wrap gap-2">
{cases.map((caseType) => (
<button
key={caseType.type}
onClick={() => setSelectedCase(caseType.type)}
>
{caseType.label}
</button>
))}
</div>
</div>
<div className="flex flex-col flex-1">
<div className="flex items-center justify-between mb-3">
<label>出力</label>
<button
onClick={() => copyToClipboard(convertCase(selectedCase), 0)}
disabled={!inputText}
>
{copiedIndex === 0 ? <Check /> : <Copy />}
</button>
</div>
<textarea
value={convertCase(selectedCase)}
readOnly
className="flex-1 min-h-[380px] font-mono bg-gray-50 dark:bg-gray-800"
/>
</div>
</div>
</div>
)
}
今後の展望
この実装を基に、以下のような拡張が考えられます:
- カスタム変換ルールの追加 - ユーザー定義の変換パターン対応
- 履歴機能 - 過去の変換結果を保存
- エクスポート機能 - JSON/CSVでの一括出力
- プラグイン対応 - VSCode拡張としての提供
得られた学び
- 純粋関数による実装でテストと保守が容易になる
- 正規表現の統一パターンにより変換ロジックが理解しやすくなる
- UXへの配慮(コピー機能、改行対応)が実用性を大きく向上させる
-
Next.jsの
'use client'ディレクティブでクライアントサイド処理を明示
文字列変換という一見シンプルな機能でも、設計思想やUX配慮を深めることで、実用的なツールに進化させることができます。
ツールの紹介
こちらで実際に動かして確認することができます。
是非活用してみてください。
文字変換
Discussion