この記事は、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とする」**という狂気の構成だ。
Sheets
Repo
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;
});
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.createElement と innerHTML だけで戦いを挑んだ結果、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);
});
}
このコードには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)では、以下の構成へ完全移行する。
- Frontend: React + TypeScript (コンポーネント指向・型安全)
- Build: Vite (高速ビルド・バンドル最適化)
- Database: SQLite / Better-SQLite3 (ローカルDBによる高速検索)
- State: TanStack Query (サーバー状態の適切な管理)
「壊すことを恐れるな。それは進化の過程だ。」 "Do not fear breaking things. It is the process of evolution."
このリファクタリングの旅路は、まだ始まったばかりだ。