この記事は、VRCGF (VRChat Group Finder) のアルファ版における技術的な意思決定と、その悲惨な末路についての詳細な記録(Post-Mortem)である。私は意図的に「フレームワークなし(Vanilla JS)」を選択したが、その代償は想像以上に大きかった。 This is a detailed post-mortem of the technical decisions and disastrous outcomes in the VRCGF Alpha. I intentionally chose "Vanilla JS", but the price was higher than imagined.

技術的負債は、クレジットカードのようなものだ。今は時間を買えるが、後で利子をつけて返さなければならない。—— VRCGFの初期開発において、私は意図的に「フレームワークなし(Vanilla JS)」という選択をした。その代償として支払った「利子」について、実際のソースコード(index.html: 500行超)を晒しながら、詳細に振り返る。 Technical debt is like a credit card. You buy time now, but pay it back with interest later. In VRCGF's early development, I deliberately chose "Vanilla JS". I will detail the "interest" paid, exposing the actual source code.

1. アーキテクチャ:砂上の楼閣1. Architecture: House of Cards

VRCGFは「サーバーレス」を謳っている。しかし、その実態はクラウドネイティブとは程遠い、つぎはぎのRube Goldbergマシン(ピタゴラ装置)だった。 VRCGF claims to be serverless, but its backend reality is surprising. Not a cloud-native solution, but a patchwork Rube Goldberg machine.

The "AppScript" Backend

バックエンドエンジニアが不在(私一人)の状況で、非エンジニアでもデータを管理できるようにする必要があった。そこで採用したのが、**「GoogleスプレッドシートをDBとし、GitHubをCDNとする」**という狂気の構成だ。

Google
Sheets
➜ (GAS)
GitHub
Repo
➜ (Raw)
Electron
App

Google Apps Script (GAS) が定期的にシートを読み込み、巨大なJSONファイルとしてGitHubリポジトリにコミットする。アプリ側は、GitHubのRaw URL (raw.githubusercontent.com) をフェッチするだけだ。

// main.js: キャッシュバスターの実装
ipcMain.handle('load-local-data', async () => {
    // GitHubのキャッシュは強力すぎるため、クエリパラメータで無理やり回避
    const bustCacheUrl = `${DATA_URL}?t=${new Date().getTime()}`;
    console.log("Fetching data from:", bustCacheUrl);

    const response = await fetch(bustCacheUrl);
    const data = await response.json();
    
    // オフライン用にローカル保存(ここで致命的なミスを犯す。後述)
    fs.writeFileSync(CACHE_PATH, JSON.stringify(data));
    return data;
});
Reviewer's Voice
「天才的なハックに見えるが、スケーラビリティは皆無だ。データ量が1MBを超えた時点で、GitHubのAPIレート制限や、GASの実行時間制限(6分)に引っかかる未来が見える。これはプロダクトではなく、ただのスクリプトだ。」

Security Split: Cloudflare Workers

もちろん、ユーザーのアカウント情報を公開リポジトリ(GitHub)に置くわけにはいかない。そこだけは理性が働いたようだ。認証周りは **Cloudflare Workers (D1 Database)** に切り出されている。

// index.html
const API_BASE_URL = "https://vrcgf-backend.unilab.workers.dev"; 

async function performLogin() {
    // 認証情報はGitHubではなく、セキュアなWorkersへ
    const res = await fetch(`${API_BASE_URL}/login`, { 
        method: 'POST', 
        body: JSON.stringify({ email, password }) 
    });
}

つまり、「読み取り専用の公開データはGitHub(CDN代わり)」「書き込みが必要な機密データはCloudflare D1」というハイブリッド構成をとっているのだ。 これにより、サーバーコストを極限まで抑えつつ、セキュリティリスクを回避している。


2. フロントエンド:スパゲッティの海2. Frontend: Sea of Spaghetti

ReactやVueといった現代の武器を捨て、document.createElementinnerHTML だけで戦いを挑んだ結果、index.html は500行を超える混沌と化した。

DOM Manipulation Hell

以下は、イベントリストを描画する実際のコードである。データバインディングが存在しないため、HTMLを文字列として結合している。

// index.html (Line 420~)
function renderHomeList() {
    const container = document.getElementById('homeListGrid'); 
    container.innerHTML = ''; // 毎回全消去して再描画(重い!)
    
    picks.forEach((ev, index) => {
        const div = document.createElement('div');
        
        // 【脆弱性】XSSの温床
        // ev.title に "<script>..." が含まれていたら終わりだ
        div.innerHTML = `
            <div class="text-mac-subtext ...">${index + 1}</div>
            <div class="flex-1 min-w-0">
                <h4 class="text-white ...">${ev.title}</h4>
            </div>
        `;
        
        // 【メモリリーク】イベントハンドラを都度生成
        // ループの回数だけ関数オブジェクトが作られ、GCの負荷となる
        div.onclick = () => openModalFromCard(div);
        
        container.appendChild(div);
    });
}
Technical Debt: Imperative UI

このコードには3つの致命的な問題がある。

1. 再描画コスト: データが1つ変わるだけでリスト全体を破壊して作り直している(Reflow/Repaintの嵐)。
2. XSS脆弱性: ユーザー入力(イベント名)をエスケープせずに出力している。
3. 状態管理の欠如: renderHomeList が呼ばれるタイミングが予測不能で、検索フィルター適用後にデータが上書きされるバグが多発した。

CSP (Content Security Policy) の敗北

セキュリティ意識が高いふりをしてCSPを設定しているが、中身を見ればザルであることがわかる。

<meta http-equiv="Content-Security-Policy" 
      content="script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com ...">

'unsafe-inline' を許可している時点で、CSPの本来の目的(XSS防止)は果たされていない。Tailwind CSSをCDNで動かすためだけに、セキュリティの大原則を曲げてしまったのだ。


3. パフォーマンス:メインプロセス殺し3. Performance: Killing the Main Process

Electronアプリが「重い」と言われる原因の半分は、開発者の無知にある。main.js にあるたった一行のコードが、アプリ全体のUXを破壊していた。

Sync Write in Main Process

// main.js
ipcMain.handle('load-local-data', async () => {
    // ...データ取得...
    
    // 【大罪】メインスレッドでの同期書き込み
    fs.writeFileSync(CACHE_PATH, JSON.stringify(data));
    
    return data;
});

fs.writeFileSync は、書き込みが完了するまでNode.jsのイベントループを完全に停止させる。 データサイズが数MBになると、この瞬間にOSレベルでウィンドウのドラッグやリサイズがフリーズする。 GUIアプリにおいて、I/Oは絶対に非同期(fs.promises.writeFileで行わなければならない。これはElectronの憲法第一条だ。


4. セキュリティ:開かれた扉4. Security: The Open Door

利便性を優先するあまり、最も危険なAPIを無防備に公開してしまった。

// main.js
ipcMain.handle('open-external', async (event, url) => {
    await shell.openExternal(url); // バリデーションなし!
});

もし前述のXSS脆弱性を突かれ、window.electronAPI.openExternal('file:///C:/Windows/System32/cmd.exe') などを実行されたらどうなるか? あるいはフィッシングサイトへの誘導も容易だ。 shell.openExternal をラップする場合は、必ずURLのホワイトリスト検証(httpsのみ、許可ドメインのみ)を行わなければならない。

さらに、アップデート機能においても致命的な設定がある。

// コード署名証明書を持っていないため無効化
autoUpdater.verifyUpdateCodeSignature = false;

これは「中間者攻撃をしてくれ」と言っているようなものだ。 UniLabが個人のアトリエであるうちは許されるかもしれないが、パブリックなツールとして配布するなら、コード署名はコストではなく「義務」である。


5. 結論:リファクタリングへの誓い5. Conclusion: Vow to Refactor

今回の解剖を通じて、VRCGFが抱える「技術的負債」の全貌が明らかになった。 DOM操作の泥沼、スプレッドシート依存のバックエンド、そして同期I/Oによるパフォーマンス劣化。 これらはすべて、「速さ」を優先して「質」を後回しにした結果だ。

しかし、私はこのプロトタイプを捨てない。ここにあるのは「失敗」ではなく、「ユーザーが本当に求めていた機能のリスト」だからだ。 次期バージョン(Beta)では、以下の構成へ完全移行する。

The Modern Stack (Planned)
  • Frontend: React + TypeScript (コンポーネント指向・型安全)
  • Build: Vite (高速ビルド・バンドル最適化)
  • Database: SQLite / Better-SQLite3 (ローカルDBによる高速検索)
  • State: TanStack Query (サーバー状態の適切な管理)

「壊すことを恐れるな。それは進化の過程だ。」 "Do not fear breaking things. It is the process of evolution."

このリファクタリングの旅路は、まだ始まったばかりだ。

We are Hiring

この「技術的負債」を一緒に返済してくれる
クレイジーなエンジニアを募集しています。

View Open Positions