• #Vue.js
  • #Cloudflare
  • #Architecture
  • #Decision

Message Mockup Tool - Vue.js移行の必要性分析

結論

現時点では Vanilla JS のままで十分。 ただし、以下の条件に該当する場合は Vue.js への移行を検討する価値がある。


前提知識:「状態(state)」とは何か

プログラミングにおける「状態」とは、アプリが「今どうなっているか」を表すデータのことです。

身近な例で理解する

【ECサイトの状態の例】

┌─────────────────────────────────────────────────────┐
│ 状態(state)= アプリの「今の状況」を表すデータ      │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ユーザー情報:                                      │
│    ├── ログインしてる? → true / false             │
│    ├── 誰がログインしてる? → { name: "田中", ... }│
│    └── カートに何が入ってる? → [商品A, 商品B]     │
│                                                     │
│  UI の状態:                                         │
│    ├── メニューは開いてる? → true / false         │
│    ├── ローディング中? → true / false             │
│    └── エラーメッセージ → "在庫切れです"           │
│                                                     │
│  フォームの状態:                                    │
│    ├── 入力された名前 → "山田太郎"                 │
│    ├── 入力されたメール → "yamada@example.com"     │
│    └── バリデーションエラー → { email: "無効" }    │
│                                                     │
└─────────────────────────────────────────────────────┘

今回のMessage Mockupツールの「状態」

// message-mockup.js の state がまさに「状態」
const state = {
  // ========== データの状態 ==========
  sender: 'You',           // 送信者の名前(今どうなってる?)
  receiver: 'Friend',      // 受信者の名前(今どうなってる?)
  messages: [...],         // メッセージ一覧(今何がある?)
  nextId: 7,               // 次のメッセージID(今いくつ?)

  // ========== UI の状態 ==========
  isAnimating: false,      // アニメーション再生中?(今どうなってる?)
  isRecording: false       // 録画中?(今どうなってる?)
}

現状は6個の状態 → シンプルなので管理しやすい

状態が増えるとどうなるか

サービス化すると、こうなる:

const state = {
  // ========== 元からある状態(6個)==========
  sender: 'You',
  receiver: 'Friend',
  messages: [...],
  nextId: 7,
  isAnimating: false,
  isRecording: false,

  // ========== 認証で追加(4個)==========
  user: { id, email, name },    // ログイン中のユーザー
  isAuthenticated: false,       // ログインしてる?
  accessToken: '...',           // 認証トークン
  tokenExpiry: Date,            // トークン有効期限

  // ========== 課金で追加(5個)==========
  plan: 'free',                 // 現在のプラン
  usageCount: 3,                // 今月の使用回数
  usageLimit: 5,                // 上限
  subscriptionId: '...',        // Stripe契約ID
  billingCycleEnd: Date,        // 次回請求日

  // ========== 設定で追加(3個)==========
  theme: 'light',               // ダークモード?
  language: 'ja',               // 言語設定
  defaultApp: 'x',              // デフォルトのアプリ

  // ========== UIで追加(4個)==========
  isSettingsOpen: false,        // 設定画面開いてる?
  isUpgradeModalOpen: false,    // アップグレードモーダル開いてる?
  currentTab: 'editor',         // 今どのタブ?
  sidebarCollapsed: false,      // サイドバー閉じてる?

  // ========== 履歴で追加(2個)==========
  savedMockups: [...],          // 保存済みモックアップ
  exportHistory: [...]          // エクスポート履歴
}
// 合計: 6 + 4 + 5 + 3 + 4 + 2 = 24個の状態!

状態が多いと何が問題か

// 問題1: 状態を変えたら、UIを手動で更新しないといけない
function login(user) {
  state.user = user
  state.isAuthenticated = true

  // 忘れると表示がおかしくなる!
  updateHeader()        // ヘッダーのログイン状態を更新
  updateSidebar()       // サイドバーのユーザー名を更新
  updateUsageDisplay()  // 使用回数表示を更新
  checkPlanLimits()     // プラン制限をチェック
  // ... 他にも更新が必要な場所があるかも?
}

// 問題2: どこで状態が変わったか追跡困難
// 「なぜか isAnimating が true のままになるバグ」
// → コード全体を検索して、どこで変更してるか探す必要がある

// 問題3: 状態同士の依存関係
// 「プランが free なら usageLimit は 5、pro なら無制限」
// → この関係を手動で維持しないといけない

Vue.js なら自動で解決

// Vue.js: 状態を変えると、UIが自動で更新される
const user = ref(null)
const isAuthenticated = computed(() => !!user.value)  // 自動計算

// user.value = { name: '田中' } と代入するだけで
// isAuthenticated が true になり
// 関連するUI(ヘッダー、サイドバー等)が全部自動更新される

「状態10個」の目安

状態の数管理難易度推奨
1-5個簡単Vanilla JS で十分
6-10個まだ管理可能Vanilla JS でギリギリ
11-20個複雑化Vue.js 検討
20個以上カオスVue.js + 状態管理ライブラリ

Vue.js移行が必要になる条件

1. 状態管理の複雑化

現状(Vanilla JS):

const state = {
  sender: 'You',
  receiver: 'Friend',
  messages: [...],
  isAnimating: false,
  isRecording: false
}

問題が発生するケース:

  • ログイン状態の追加
  • 課金プラン・使用回数の管理
  • ユーザー設定の永続化
  • 複数画面間での状態共有
// 肥大化した状態の例
const state = {
  // 元の状態
  sender: 'You',
  messages: [...],

  // 認証関連
  user: { id, email, name, avatar },
  isAuthenticated: false,

  // 課金関連
  plan: 'free', // free | pro | enterprise
  usageCount: 0,
  usageLimit: 5,
  billingCycle: 'monthly',

  // 設定関連
  preferences: { theme, language, defaultApp },

  // UI状態
  isAnimating: false,
  isRecording: false,
  isSettingsOpen: false,
  isUpgradeModalOpen: false,

  // 履歴
  savedMockups: [...],
  exportHistory: [...]
}

Vanilla JSの問題点:

  • 状態変更時の UI 同期が手動(renderMessages(), renderPreview() を都度呼ぶ)
  • 状態の依存関係が見えにくい
  • デバッグが困難(どこで状態が変わったか追跡しづらい)

Vue.jsの解決策:

// composables/useAuth.ts
export const useAuth = () => {
  const user = ref(null)
  const isAuthenticated = computed(() => !!user.value)
  // 状態変更 → UI自動更新
}

// composables/useBilling.ts
export const useBilling = () => {
  const plan = ref('free')
  const canExport = computed(() => plan.value !== 'free' || usageCount.value < 5)
}

判断基準: 状態が10個以上になったら移行を検討


2. 複数画面・ルーティングの必要性

現状: 単一HTMLファイルで完結

問題が発生するケース:

/                    → ランディングページ
/app                 → メインツール
/app/history         → 作成履歴
/app/settings        → 設定
/pricing             → 料金プラン
/dashboard           → 使用状況ダッシュボード
/login               → ログイン
/success             → 決済完了

Vanilla JSでの実装:

// 自前でルーティング実装が必要
window.addEventListener('hashchange', () => {
  const route = window.location.hash.slice(1)
  if (route === '/history') showHistoryPage()
  if (route === '/settings') showSettingsPage()
  // ... どんどん増える
})

Vue.jsの解決策:

pages/
├── index.vue          # ランディング
├── app/
│   ├── index.vue      # メインツール
│   ├── history.vue    # 履歴
│   └── settings.vue   # 設定
├── pricing.vue
└── login.vue

判断基準: 3画面以上必要になったら移行を検討


3. コンポーネントの再利用

現状: 1つのアプリ(X/Twitter DM)のみ

問題が発生するケース: アプリ選択UIがあるが、現状はX DMのみ実装:

<div class="app-grid">
  <button data-app="discord">Discord</button>
  <button data-app="imessage">iMessage</button>
  <button data-app="messenger">Messenger</button>
  <button data-app="whatsapp">WhatsApp</button>
  <button data-app="x" class="active">X</button>
</div>

複数アプリ対応時のVanilla JS:

// コードの重複が大量発生
function renderXPreview() { ... }
function renderDiscordPreview() { ... }
function renderIMessagePreview() { ... }
// 共通部分の抽出が難しい

Vue.jsの解決策:

<!-- 共通のMessageBubbleコンポーネント -->
<MessageBubble
  :text="message.text"
  :sender="message.sender"
  :theme="currentApp"  <!-- x | discord | imessage -->
/>

<!-- アプリごとのスタイルはpropsで切り替え -->
<style>
.bubble[data-theme="x"] { background: #1d9bf0; }
.bubble[data-theme="discord"] { background: #5865f2; }
.bubble[data-theme="imessage"] { background: #34c759; }
</style>

判断基準: 2つ以上のバリエーションを作る場合は移行を検討


4. フォーム・バリデーションの複雑化

現状: シンプルな入力のみ

senderInput.addEventListener('input', (e) => {
  state.sender = e.target.value || 'You'
  renderPreview()
})

問題が発生するケース:

  • 課金フォーム(カード情報、住所)
  • プロフィール編集(画像アップロード、複数フィールド)
  • バリデーション(必須チェック、形式チェック)

Vanilla JSでの実装:

// バリデーションロジックが散乱
function validateEmail(email) { ... }
function validateCard(card) { ... }
function showError(field, message) { ... }
function clearError(field) { ... }
// 各フィールドにイベントリスナーを手動設定
// エラー表示の更新も手動

Vue.jsの解決策:

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.email" :class="{ error: errors.email }" />
    <span v-if="errors.email">{{ errors.email }}</span>
  </form>
</template>

<script setup>
const { form, errors, validate } = useForm({
  email: { required: true, email: true },
  name: { required: true, minLength: 2 }
})
</script>

判断基準: 入力フィールドが10個以上、またはバリデーションが必要な場合


5. テスト容易性

現状: テストなし(小規模なら問題ない)

問題が発生するケース:

  • 課金機能の導入(お金が絡むのでバグは致命的)
  • チーム開発(他人のコード変更で壊れないか確認)
  • 継続的な機能追加

Vanilla JSのテスト:

// DOM操作のテストは困難
test('メッセージ追加', () => {
  // DOMをセットアップ
  document.body.innerHTML = `<div id="messages-list"></div>`
  // グローバル変数を初期化
  state.messages = []
  // 関数を呼び出し
  addMessage()
  // DOMを検証
  expect(document.querySelectorAll('.message-item').length).toBe(1)
})

Vue.jsのテスト:

// コンポーネント単位でテスト可能
import { mount } from '@vue/test-utils'
import MessageEditor from './MessageEditor.vue'

test('メッセージ追加', async () => {
  const wrapper = mount(MessageEditor, {
    props: { messages: [] }
  })
  await wrapper.find('.add-btn').trigger('click')
  expect(wrapper.emitted('add')).toBeTruthy()
})

判断基準: 課金機能導入時、またはチーム開発時


6. 既存インフラの活用

現状のプロジェクト構成:

mdx-playground/
├── apps/web/           # Nuxt 3 プロジェクト(既存)
│   ├── nuxt.config.ts
│   ├── wrangler.toml   # Cloudflare設定済み
│   └── content/        # コンテンツ
└── ...

Vanilla JS版のデプロイ:

  • 別プロジェクトとして管理
  • 別のCloudflare Pages設定
  • 別のドメイン/サブドメイン

Vue.js版(Nuxtに統合):

apps/web/
├── app/pages/
│   ├── tools/
│   │   └── message-mockup.vue  # ここに追加
│   └── ...
└── wrangler.toml  # 既存の設定を流用

メリット:

  • 認証システムの共有
  • API Routesの共有
  • デプロイパイプラインの統一
  • ドメインの統一(yoursite.com/tools/message-mockup

判断基準: 既存のNuxtプロジェクトに他のツール/機能がある場合


判断フローチャート

サービス化したい
    │
    ▼
ログイン・課金機能が必要?
    │
    ├─ No → Vanilla JSのままCloudflare Pagesへ
    │
    ▼ Yes
    │
画面は3つ以上必要?(ダッシュボード、履歴、設定など)
    │
    ├─ No → Vanilla JS + Supabase/Firebase Auth
    │        (認証SDKだけ追加)
    │
    ▼ Yes
    │
複数のモックアップ種類を作る?(Discord, iMessage等)
    │
    ├─ No → まだVanilla JSで頑張れる
    │        (ただし保守コストは上がる)
    │
    ▼ Yes
    │
Vue.js移行を推奨

現実的なアドバイス

今すぐやること(Vanilla JS版)

# 1. Cloudflare Pagesにそのままデプロイ
wrangler pages deploy ./public

# 2. 認証はSupabaseを使う(SDK軽い)
npm install @supabase/supabase-js

# 3. StripeはCloudflare Functionsで
# functions/api/checkout.js を作成

移行のトリガー

以下のいずれかに該当したら、Vue.js移行を計画:

  1. 状態が10個以上 になった
  2. 画面が3つ以上 必要になった
  3. Discord/iMessage版 も作ることになった
  4. チームメンバー が増えた
  5. バグ修正が困難 になった(どこで何が起きてるかわからない)

移行コスト目安

項目工数
基本構造の移行1-2日
日時ピッカー0.5日
アニメーション0.5日
動画エクスポート1-2日
テスト1日
合計4-6日

まとめ

観点Vanilla JSVue.js
初期開発速度✅ 速い△ 設計必要
保守性(小規模)✅ 問題なし△ オーバーキル
保守性(大規模)❌ 困難✅ 良好
状態管理△ 手動同期✅ 自動同期
テスト❌ 困難✅ 容易
再利用性❌ コピペ✅ コンポーネント
学習コスト✅ なし△ あり

推奨: まずVanilla JSでリリースし、上記トリガーに該当したら移行を検討。