はじめに
GMOコネクトの永田です。
少し前、Java のメジャーアップ(11→21)を Claude Code でやったら、かなり楽でした。「これなら、ずっと後回しにしていた Vue 2→3 もいけるのでは」——そう思って手を付けたら、意外と苦労した、という話です😇。
Vue 2→3 が必要なのは何年も前から明確でした(Vue 3 は2020年9月、Vue 2 は2023年末にEOL、EOLからも2年以上)。それでも修正だけで数人月、加えてフロント全体のリグレッション試験が要り、工数が見合わずに寝かせていました。同じ状況の現場は多いはずです。
今回のスコープは修正部分(全リグレッション試験より前)。期待は「Claude Code の進化と移行ノウハウの蓄積」でした。結果、手動よりは遥かに楽。ただ Java と同じ楽さではありませんでした。この記事は、なぜ Vue が Java より苦労したのかを構造で説明し、押さえる所と覚悟する所を持ち帰れるように書いた記録です。
先にまとめ
Java のメジャーアップと Vue2→3 は、AIエージェントから見た御しやすさがかなり違いました。
| 観点 | Java 11→21 + Spring | Vue2→3 |
|---|---|---|
| 破壊の出方 | 主にコンパイル時(javax→jakarta・削除API) | 多くが実行時(テンプレの意味論・リアクティビティ・v-model仕様・ライフサイクル) |
| 「緑」の意味 | 緑=ほぼ動く | 緑が嘘をつく(E2E・実機まで見ないと分からない) |
| Claude Code の効き | コンパイルを回せば高確率で収束 | 緑にする以上に「緑が嘘をつく場所を炙り出す」のが主戦場 |
| ランタイムと依存の分離 | 旧メジャーが新ランタイム対応なら言語だけ上げられる(Spring 5.3.x は Java 21 対応→javax のまま先送り) | 作り直しで旧メジャーが Vue 3 上で動かず、更新が強制バンドル(据え置き不可) |
| 撤去フェーズ | (概念上ない) | @vue/compat の「撤去」が独立フェーズで一斉に壊れる |
要点はこうです。
- @vue/compat は「入れて動かす」より「外す」ほうが本番。コンパイルが通るのに実行時で白画面になる
- 工数の山は、公式 Migration Guide に載っていない領域=Vueに依存する多数のライブラリの同時上げ。公式に載る破壊のほうは通読で防げる
- 「緑にする」より「緑が嘘をつく場所を炙り出す」(E2E・実機)が仕事の中心
だから肝は、着手前の机上検証と計画です。公式ガイドとその上の依存関係を先に確かめ、compat撤去まで工程に織り込む。そこから漏れる「公式に無い」分を、実行時の確認(E2E・実機)で拾う——これが近道でした。
1. 期待値:なぜ「Claude Codeなら楽勝」と思ったか
直前に Java のメジャーアップ(11→21、Spring 込み)を Claude Code でやっていました。これが楽でした。理由は単純で、Java は破壊的変更の多くがコンパイル時に出るからです。javax→jakarta の名前空間移行や削除されたAPIは、ビルドを走らせれば赤くなる。Claude Code にコンパイルループを回してもらい、赤が消えるまで直せば、かなりの確度で動く状態に収束します。型システムとコンパイラが安全網になっていて、「緑になった=ほぼ動く」の相関が高いわけです。
この体験を、そのまま Vue に持ち込みました。「Vue も同じように、ビルドを緑にすれば動くだろう」と。ここが最初の思い込みでした。
2. 現実:@vue/compat は「入れて動かす」より「外す」が本番
Vue2→3 には @vue/compat(Migration Build)という公式の移行用ビルドがあります。Vue 3 上で Vue 2 互換の挙動を出してくれるもので、噛ませると「一旦動く」ところまでは早い。ここは期待どおりでした。ただ注意が要ります。「compat で動いた」と「compat を外せた(=Vue 3 完全移行)」の間には、かなりの隔たりがあります。 前者をゴールと錯覚しやすいのが、最初の落とし穴でした。
問題はその先です。compat はそのまま使い続けるものではありません。公式が一時的な移行用ビルドと位置づけ、いずれ配布も止める予定と明記しています。残したままだと、互換シムと実行時警告のぶんの性能・バンドルのオーバーヘッド、compat 層そのものに起因する不具合(後述の lodash はこれ)、EOL 済み Vue 2 挙動への依存、が残ります。公式ガイドも最後のステップをこう書いています。
When all warnings are fixed, you can remove the migration build and switch to Vue 3 proper. Note you may not be able to do so if you still have dependencies that rely on Vue 2 behavior.
(警告をすべて直せば Migration Build を外して Vue 3 proper に切り替えられる。ただし Vue 2 の挙動に依存する依存関係が残っていると、それができないことがある)
つまり本番は compat の撤去です。ここは正直、目新しい話ではありません。「移行ビルドからの脱却」でつまずいた記録は日本語でもいくつも出ています。この記事で書きたいのは撤去の罠カタログ自体ではなく、なぜ Java と比べてここまで違ったのか(次章)です。なので撤去で何が起きたかは、後の対比に必要な分だけ短く挙げます。
ポイントは1つ。compat を外すとコンパイルは無エラーで通るのに、実行時に白画面になる。 静的解析では何も出ず、画面を開くかボタンを押した瞬間に初めて割れます。代表例だけ(すべて実行時にだけ現れます)。
-
v-model の一斉破断: compat が裏で
value/inputをmodelValue/update:modelValueに読み替えていた(COMPONENT_V_MODEL)。撤去でこれが消え、独自コンポーネントの v-model が一斉に切れる -
$parent・ライフサイクル旧名が黙って死ぬ:$parent.データ参照が no-op になり、beforeDestroyなどがエラーも出さず不発になる -
lodash で全描画クラッシュ: compat のゲッターがグローバル関数値に
.bindを呼び、自前.bindを持つ lodash と衝突して「Expected a function」で落ちる(vuejs/core #4403) -
type: Array | Objectで描画クラッシュ:|(ビット演算)が0になる自コードの粗を Vue 3 がinstanceof 0で弾いた。Vue 2 が黙認していた負債の露呈
キーメッセージは1つ。ビルドが緑でも終わりではない。破壊は実行時に潜んでいる。
3. なぜJavaより苦労したか(2つの軸に分ける)
「Java は楽、Vue は苦労」と一括りにすると、性質の違う2つの軸が混ざります。分けると、片方は本質的な差、もう片方は状況次第で Java にも起こり得る差でした。
軸1: 破壊がコンパイル時に出るか、実行時に潜るか(本質的)
Java の破壊は型システムが捕まえます。Vue にもコンパイル時に弾かれる破壊はありますが(テンプレートコンパイラが filters や構文を弾く)、厄介なのはその外側——リアクティビティ・v-model仕様・ライフサイクル・コンポーネント間の配線など、コンパイラに引っかからず実行時に潜る破壊です。だから Vue では「緑」が嘘をつく。Claude Code がコンパイルループで収束させられる範囲が、そもそも狭い。仕事の中心は「緑にする」ではなく「緑が嘘をつく場所を、E2Eや実機で炙り出す」に移りました。これは言語・フレームワークの性質そのもので、プロジェクトに依りません。AI移行で Java が本当に有利なのは、この一点です。
軸2: ランタイム更新とライブラリ更新を分離できるか(状況次第)
「多数のライブラリを同時に上げる」痛みは、実は Java 対 Vue の本質ではありません。今回の Java 移行は、言語だけ Java 11→21 に上げて、Spring は旧ライン(5.3.x=javax)に据え置きました。これができたのは、Spring 5.3.x が Java 8〜21 に対応していて、旧メジャーのまま新しい JDK で動いたからです(Spring 5.3 は Java 8〜21 をサポート)。おかげで jakarta 移行(Spring 6)を「先送り」できました。ただし"回避"ではなく先送りで、5.3.x の OSS サポートは 2024年8月に終了済み、いずれ移る必要があります。
Vue 3 にはこの逃げ道がありません。コアが作り直され、Vuetify 1.5 や vee-validate 2 を Vue 3 上で動かす手が無い=「新ランタイムでも動く旧メジャーライン」が存在せず、フレームワーク更新にライブラリ更新が強制バンドルされます。テストツールも例外ではなく、Vue Test Utils も v1→v2 へ上げないと既存テストが走りません(安全網を張り直しながら進むことになる)。つまり差は言語の良し悪しではなく、痛みの大半は「依存がその破壊を生き延びる/先送りできるか」で決まり、型システムとは別の軸だということ。Java も Spring 6 / jakarta へ踏み込めば同型の痛みを食らいます。型システムが決めるのは「破壊をどう見つけるか(コンパイル時か実行時か)」であって、「破壊を食らうか否か」ではありません。
補足: これは「Java 対 Vue」であると同時に「BE 対 FE」でもある
正直に言うと、今回の比較には Java=バックエンド/Vue=フロントエンドという差も混じっています。フロントエンドは、UI の見た目やコンポーネント間の配線を、ユニットテストにきっちり起こすことが多くありません。壊れる箇所がそもそも UT の外にあるので、"緑"の後ろ盾が薄く、確認は E2E や実機に寄ります。これは言語やフレームワークの差ではなく、FE と BE の差です。「Vue だから確認しにくい」と混ぜないよう、切り分けておきます。
+ Vue 固有の「撤去フェーズ」
もう1点、Vue には「@vue/compat を外す」という工程があります。Java の互換ビルドに当たるものが無いのに対し、Vue はこれが独立フェーズとして存在し、しかもそこで軸1の実行時破壊が一斉に噴き出します。
一言でいうと、Java が楽だったのは、型システムが安全網になっていた(軸1)ことに加えて、今回はライブラリの破壊を先送りできた(軸2)から。Vue はそのどちらも味方にできませんでした。Claude Code が悪いのではなく、今回の Vue 移行は Claude Code が最も得意とするコンパイル駆動ループが効きにくい形をしていた、という話です。
なお、この見方(型システムと依存の先送りで難易度を測る)は Vue2→3 に限りません。静的型のない・依存の多い大型移行なら、次に別のメジャーアップをやるときの当たり付けにも使えます。
4. そのハマり、公式のMigration Guideにあった?
軸1・軸2は構造の話でしたが、実際には他にも細かいハマりが色々ありました。それらを、移行が一段落した後で公式 Migration Guide と突き合わせて整理してみたんです。すると、ガイドに載っていたもの(読めば防げた)と、載っていなかったもの(読んでも防げない)に、きれいに分かれました。
軸を「公式 Migration Guide に載っているか」に置いたのには理由があります。各ライブラリの GitHub issue まで事前に漁れ、は要求が高すぎる。でも公式ガイドの通読なら、誰でも取れる準備です。この軸で分けると、こうなりました。
| ハマり | 公式ガイドに | メモ |
|---|---|---|
| v-model仕様変更・COMPONENT_V_MODEL | ◎ 明記 | breaking-changes/v-model |
.native削除・filters廃止・$children廃止・$listeners統合・ライフサイクル改名 |
◎ 明記 | 各 breaking-changes ページ |
| compat撤去でコンパイルは通るのに実行時破断(規模の痛み) | △ 原理のみ | 「警告を消してから撤去」「Vue 2依存があると撤去できない」までは書くが、大規模で一斉に壊れる体感までは書かれていない |
| 個別ライブラリの非互換(vee-validate・Vuetify等) | △ 一般論のみ | 「Vue 2依存があると撤去できない」まで公式。個別のライブラリ名は当然載らない(vee-validate #4379) |
| グローバルプロパティの関数バインドcrash(lodash等) | ✗ なし | vuejs/core #4403 の世界 |
lodash .bind衝突・$parent の no-op |
✗ なし | 公式外の細かい個別事例 |
type: Array | Object のクラッシュ |
✗ なし | 自コードの負債が露呈しただけ |
整理して見えた学びは2つです。
-
◎は、最初にちゃんと読めば防げた。 教科書的な破壊(v-model・
.native・filters・$children・ライフサイクル)は全部公式に載っています。ここは通読でケチらず潰す。「動かしながら気づく」より遥かに安い(=次章の「押さえる」) - ガイドを読んでもハマる層は残る。だから次回もリスクと工数に見込んでおく(見込みにくいが)。 時間を溶かしたのは公式外の (1) 個別ライブラリの非互換、(2) compat撤去の「規模」の痛み、(3) 自コードの負債の露呈。事前に具体化しにくくても、「読んでも防げない層がある」と分かっていればバッファは積めます(=次章の「覚悟」)
余談として、「世の中に Vue 3 移行ノウハウが溜まってきたはず」という当初の期待は半分しか当たりませんでした。◎は確かに溜まっている。でも自分が実際に踏む、個々はまれで種類の多い細かい落とし穴は、そもそも世の中にまとまっていません。
5. 次にやる人へ:押さえる/覚悟して
押さえる(準備で減らせる)
- 公式 Migration Guide の incompatible / partially-compatible リストを最初に通読する
- 使うライブラリの Vue 3 対応状況を先に洗う。対応版が無い・別物・根本非互換のものが工数の山
- 「compat撤去」を独立フェーズとして最初から見積もる
- 事前にコンテキスト化するのは公式・デファクトの一次情報まで。「世の中の苦労事例」の事前網羅は無理(踏む罠は事前に分からない)。それは踏んだ瞬間にピンポイントで引くもの
覚悟(情報を集めても残る)
- ビルドが緑でも終わりではない。実行時に確かめる仕組み(E2E・実機・診断ビルド)が本体
- UIライブラリの Vue 3 非対応が最大の山。全利用箇所を書き換えるより、旧APIを保った自前シム+CSS同梱で実体だけ Vue 3 化が安いことが多い。工数は「タグ数」でなく「どれだけ深く絡んでいるか」で決まる(
<v-app>だけに見えて実は数百箇所+内部DOM依存、が起きる) - compat撤去で v-model が一斉に切れる。どの段階で撤去するかを設計する
AIの使い方(今回効いたもの)
-
診断ビルドで炙り出す: 本番ビルドは
console.errorを落とすので、最小化と console 除去を一時的に止め、app.config.errorHandlerでコンポーネント名込みのスタックを溜める。本番でしか出ない描画バグが見える - 複数レンズの並列レビュー: 「compat撤去の網羅」「静かに消える変更」「サードパーティの実API突合」など観点ごとに独立サブエージェントを並列で。compat と無関係の既存バグまで同時に出た
-
機械変換は全数照合で担保:
::v-deep→:deep()の大量変換は、変換後のコンパイル済みCSSを差分照合して挙動不変を確認 - プレイブック化して横展開: grep パターンと before/after をまとめ、2件目はそれを渡して着手。手戻りが減った
まとめ
- @vue/compat は「入れて動かす」より「外す」が本番。コンパイルが緑でも動作は別物で、破壊は実行時に潜む
- 苦労の正体は2軸+α。①型システム(Javaはコンパイル時/Vueは実行時に破壊が潜む=本質的)②依存の先送り可否(今回はSpring 5.3.xに据え置いて回避/Vue 3 は作り直しで不可)。加えて Java=BE/Vue=FE の差も(FEはUTに起こしにくく確認がE2E頼り=言語差ではない)
- 「Javaが常に楽」ではない。javax→jakarta を踏めば同型の痛み。痛みの大半は「依存が破壊を生き延びるか」で決まる
- 公式 Migration Guide の◎は通読でケチらず潰す。工数の山は公式外の個別ライブラリ非互換で、読んでも防げない——だから次回もリスク・工数に見込む
- 手動より遥かに楽にはなったが、いちばん効いたのは「緑にする」ことより、着手前の机上検証と移行計画だった。公式ガイドとその上の依存関係(各ライブラリのVue3対応)を先に検証し、compat撤去まで工程に織り込む。実行時の確認(E2E・実機)は、そこから漏れる分の受け皿
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。