キャンドルナイトアプリ Vue3移行について

# はじめに

こんにちは、GxPのモニコ（入社4年目）です。
この記事は[グロースエクスパートナーズAdvent Calendar 2024](https://qiita.com/advent-calendar/2024/gxp)の19日目の記事です。

昨年度から小田急さんが年に一度開催しているイベント「キャンドルナイト」のアプリ開発に参画しているのですが、フロントエンドのVue2が2023/12/31を以てEOL（[公式参照](https://v2.vuejs.org/eol/)）となったため、それに伴いVue3への移行対応を実施しました。今回はその内容についてご紹介させていただきます。

※今回の記事とは別で今年のキャンドルナイトの記事も例年通りアドベントカレンダー最終日（2024/12/25）に挙がる予定なので楽しみにしていただけると幸いです。

:::note info
※これまでのキャンドルイベンプロダクト開発は下記をご参照ください
2020年12月 Candle Night @ Shinjuku Central Parkイベントへのプロダクト提供
[『新宿中央公園キャンドルナイトの塗り絵用投稿・鑑賞サイトについて』](https://qiita.com/rando-gxp/items/979a79371d79f08e7472)
2021年12月 Candle Night @ Shinjukuイベントへのプロダクト提供
[『2021年版キャンドルナイトプロダクト開発のあゆみ(Candle Night @ Shinjuku) ～canvasとの闘いを乗り越えて～』](https://qiita.com/guest-gxp/items/18ed57270aff82cfea80)
2022年12月 Candle Night @ Shinjukuイベントへのプロダクト提供
[『2022年版キャンドルナイトプロダクト開発のあゆみ(Candle Night @ Shinjuku)』](https://qiita.com/guest-gxp/items/ac5719b514db746d0e9f)
2023年12月 Candle Night @ Shinjukuイベントへのプロダクト提供
[『2023年版キャンドルナイトプロダクト開発のあゆみ(Candle Night @ Shinjuku)』](https://qiita.com/guest-gxp/items/6f980e5f7aa471e36bb1)
:::

# 対応方針

参考情報としてVueプロジェクトの規模・移行対応の実施期間等は以下の通りです。

- 開発メンバー：5人
- プロジェクト規模：小規模
    - src配下のファイルが30～35ファイル程
    - 行数としては長いものでも500行～600行程
- 実施期間：7月下旬～9月中旬
    - スケジュール自体は3月時点で引いていたものの1月～6月は時間取れず...
    - 時期的に比較的手の空いていたメンバー（自分）が隙間時間を見つけて実施
- 期限：9月下旬まで
    - 10月から今年のイベントに向けて新機能開発が開始するため


上記の内容をもとに今回は工数や時間の兼ね合いから公式で紹介されている[Vue3移行ガイド](https://v3-migration.vuejs.org/ja/)をベースに「最低限去年のアプリが動作すること」を目標に極力最小限の範囲で移行作業を進めました。

今回実施した大まかな内容は下記の通りです。
- 新規プロジェクト作成（Vue3 + Vite）
- ページ単位での移行
- 各種ライブラリのバージョンアップ
- 環境変数設定の修正

逆に対象外とした対応及びその理由は下記の通りです。
- ストア（Vuex → Pinia）の移行
    - 推奨ではあるが、Vuex自体は使用可能なため
    - Vuexバージョンアップが比較的軽微
- 開発支援ツール（linter, bundler 等）の導入
    - Viteに対応したライブラリ選定・導入時間の削減のため
- Composition APIへの移行
    - Vue2の書き方Option APIが廃止になったわけではないため

いずれも非推奨事項ではなく、今回の目標を達成する上で切り捨ててもアプリの動作に問題がない対応であると判断しため、対象外としています。

こちらの対応方針のもと実施した移行作業について紹介します。

# Vue3移行対応

続いて実施したVue3移行対応について下記の順番で紹介します。

1. 新規プロジェクト作成（Vue3 + Vite）
1. ページ単位での移行
1. 各種ライブラリのバージョンアップ
1. 環境変数設定の修正
1. 社内環境へデプロイ


## 1. 新規プロジェクト作成（Vue3 + Vite）

下記コマンドを実行し、新規プロジェクトを作成します。
```
npm init vue@latest
```
コマンド実行後の各種設定は以下の通り（工数の兼ね合いから一旦linterは導入しない）
```
Vue.js - The Progressive JavaScript Framework

√ Project name: ... vue3-client
√ Add TypeScript? ... No
√ Add JSX Support? ... Yes
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... No
√ Add Vitest for Unit Testing? ... No
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? » No
```
基本的な土台の作成手順は以上です。こちらのプロジェクトに旧プロジェクトのファイル群を徐々に移行・修正していきます。

## 2. ページ単位での移行

全体的なタスクを洗い出した結果は下記の通りです。
TOPページ及び共通コンポーネント（ヘッダー、フッター等）から移行作業を進め、徐々に各種ページへの移行を手分けして実施する方針で作業を進めていきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946852/c77701ff-6751-4561-a10e-e08d62530969.png)

基本的には[Vue3破壊的変更](https://v3-migration.vuejs.org/ja/breaking-changes/)の内容やライブラリバージョンアップ等で画面が表示されなくなったり、コンソールでエラーが発生したり、挙動がおかしくなったりするので、そのたびに「コメントアウトやコンソール出力を駆使して原因を炙り出し調査・修正していく」を地道に行っていました。今回はVue3の書き方（CompositionAPI）への移行は行わず、従来の書き方（OptionAPI）のまま移行していく方針で作業を進めたこともあり、大幅な修正はなく思いのほか修正量自体は少なかった印象です。

#### 特に印象に残っている修正事項

印象に残っているのは`mounted`のタイミングでこれまでとれていたHTML要素がVue3移行後 `undefined` になり後続処理でエラーが発生するようになったことです。`mounted`時点でDOM要素のレンダリングが完了していないことが原因のようだったので、`nextTick`を使用して取得を試みたものの問題は解決せず。
```
async mounted () {
    ....
    console.log(document.getElementById('animation-wrapper')); // Vue3移行後、undefinedとなる
    this.radius = Math.floor(document.getElementById('animation-wrapper').clientWidth * 0.15) 
    ....
}

```
調査進めたところ、`nextTick`はDOM更新前に実行されるらしく、DOM更新後のタイミングで実行する場合`setTimeout`を使うと良いとのことです。修正後、無事当該要素取得できるようになりました。

参考記事：https://zenn.dev/fuku710/articles/2b0b66a3283f7e

```
async mounted () {
    ....
    setTimeout(() => {
        console.log(document.getElementById('animation-wrapper'));
        this.radius = Math.floor(document.getElementById("animation-wrapper").clientWidth * 0.15)
    });
    ....
}
```

## 3. 各種ライブラリバージョンアップ

こちらの作業は「ページ単位での移行」作業の流れで必要なライブラリのみをバージョンアップしつつ、アプリケーションに導入していきました。主にバージョンアップだけでなく、アプリケーションコードの修正が発生したライブラリ群は下記の通りです。

- VueRouter：Vueアプリのルーティング（ページ遷移）を管理するライブラリ
- Vuex：Vueアプリの状態管理ライブラリ
- Vuetify：VueアプリのMaterial Designスタイルとコンポーネントを提供するUIライブラリ

### VueRouter

VueRouter3系から4系へのアップデート。変更内容は下記参照。

https://router.vuejs.org/guide/migration/

新規プロジェクト作成時点でアップデート自体は済んでおり基本的にはrouter設定を随時追加していく作業ではあったのですが、プロジェクト作成時点ではRouterの履歴管理設定がhistoryモードになっていたため、既存プロジェクトに合わせてhashモードで追加修正が必要でした。

```diff_javascript
// vue3-client/src/router/index.js

const router = createRouter({
-  history: createWebHistory(import.meta.env.BASE_URL),
+  history: createWebHashHistory(),
  routes: [...], // 徐々にルーティング設定追加
})
```

### Vuex

下記観点から既存のVuex3系からVue4系へのアップデートを対応することを選択しました。

- Vuex自体は非推奨ではない（ただし、Vue3公式ではPiniaを推奨）
- Vuex3系からVuex4系の変更はVue3対応のマイナーアップデートとして位置づけられており、変更内容も比較的少ない（変更内容は下記参照）

https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html

修正としてはVue3破壊的変更に伴うVuexのセットアップ修正のみでした。
```diff_javascript
// vue3-client/src/store/index.js

- import Vuex from 'vuex'
+ import { createStore } from 'vuex'

- const store = new Vuex.Store({
+ const store = createStore({
  state: { /* state */ },
  mutations: { /* mutations */ },
  actions: { /* actions */ },
  ...
})

- Vue.use(Vuex)
+ app.use(store)
```

### Vuetify

Vuetify2系から3系へのアップデートしました。他のライブラリ同様セットアップを修正。

```diff_javascript
// vue3-client/src/plugins/vuetify.js

- const vuetify = new new Vuetify({ ... })
+ const vuetify = createVuetify({ ... })

- app.use(vuetify)
+ Vue.use(vuetify)
```

あとは「ページ単位の移行」の中でVuetifyコンポーネントの不具合が生じる度に都度、下記のガイドや公式で各コンポーネントの設定可能なプロパティ等の確認を行い、修正を実施しました（詳細はここでは割愛します）

https://vuetifyjs.com/en/getting-started/upgrade-guide/#general-changes

## 4. 環境変数設定の修正

WebpackからVite移行に伴い、環境変数の指定もVite準拠になるように対応しました。

https://ja.vite.dev/guide/env-and-mode

まず、環境ごとに.envファイルを作成し、プロジェクト直下に配置します。
```
vue3-client
├─ .env             // デフォルト設定
├─ .env.localhost   // ローカル環境
├─ .env.development // 社内環境
└─ .env.production  // 本番環境
```
次に各環境ごとのbuildスクリプトを用意します。modeオプションで指定している値をもとに.envファイルを.env.${環境名}ファイルの設定で上書きしてくれます。
```
// vue3-client/package.json

"scripts": {
    "build": "vite build --mode localhost",
    "build:dev": "vite build --mode development",
    "build:prd": "vite build --mode production",
    ...
  },
```
あとは各.envファイルにて環境変数を指定すれば基本的な設定は完了なのですが、指定する環境変数名は接頭語として`VITE_`を付けないとViteが環境変数を読み取らない仕様になっているので注意が必要です（環境変数が誤ってクライアントに漏れてしまうことを防ぐためとのこと）
```
VUE_APP_BASE_URL = http://xxxxx  // NG
VITE_APP_BASE_URL = http://xxxxx // OK
```

アプリケーションコード内で参照する際には`import.meta.env.${環境変数名}`で参照可能です。既存プロジェクトでは`process.env.${環境変数名}`で参照していたため、こちらに関しては「ページ単位の移行」の中で一緒に修正しました。

Vue3移行作業としては以上です。あとは実際に社内環境に挙げて確認するのみとなります。

## 5. 社内環境デプロイ & 動作確認

### 社内環境デプロイ

デプロイに関してはVue3移行前後で特に変わりなく、下記の流れをシェル叩いて実施。

- フロントアプリを環境に合わせてbuild
- バックエンドのSpringアプリケーションのresource配下にフロントbuild成果物を配置
- AzureのWebAppにフロント・バックエンドのbuild成果物をデプロイ（＆再起動）

特に何もなくデプロイ完了しました。

![1835933978-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946852/48919a20-c0cd-c385-9c04-9edd5e44d0d2.png)

### 動作確認

アプリの特性として年に一度全体デザインの調整が入ることもあり、動作確認では機能面に重きを置いて確認していきました。

特に念入りに確認した観点は以下の通りです。

- javascriptによる複雑な制御を入れている機能
    - `getElementById`でHTML要素取得できない等もあったので念入りに確認
    - 特に「塗り絵画面」や「みんなの塗り絵鑑賞画面」といったメイン機能に関してはjavascriptで複雑な制御を入れていたため
- バージョンアップしたライブラリ群の挙動
    - `VueRouter`や`Store`等の全体挙動や各画面で頻繁に利用しているライブラリはもちろん、バージョンアップのみでアプリケーションコードの修正は特に入れてないライブラリ（バックエンド通信で利用している`axios`等）がこれまで通りの挙動か
- スマホでの挙動
    - 基本的には利用想定はスマホのため、PC上でできていた操作がスマホでも問題なく行えるか

画面的には「塗り絵画面」「鑑賞画面」といった毎年使い回す機能を持つ画面は特に念入りに確認を入れました。結果的にはバグは「Routerのhistoryモード設定誤り」のみで修正した上で動作確認を終え、今年の新機能開発に入りました。

## Vue３移行後挙がった課題

Vue３移行が終わり、今年の新機能開発中に判明した課題について紹介します。

本題に入る前の前提事項としてアプリのOGP画像の設定は[VueMeta](https://vue-meta.nuxtjs.org/)という「動的にメタタグを切り替える」プラグインを利用しており、動作確認ツールでは下記を利用しています。

https://rakko.tools/tools/9/

課題として挙がった内容はOGPが適切に表示されないというもので、動作確認ツールではOGP表示が適切に行われるのですが、実アプリ（facebook, Slack等）では表示されない問題が発生しました（以下、動作確認ツールの画面）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946852/00cb256f-650e-5fcc-9314-5d34808143a6.png)

こちらに関してはアプリで使用するメタタグは基本固定であることからプラグインの使用自体をやめ、`index.html` に直接メタタグを設定することで表示可能となりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946852/428dcd52-00f0-3455-016d-f6306eed2dc0.png)

ただ、別課題として今度は「facebookのみ文字化け発生して表示される」が発生。こちらに関しては解決できず、一旦来年に見送りとなりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946852/c50cdaa0-dde0-888c-749f-4cdd159212eb.png)

今後はOGPの表示確認は動作確認ツールではなく、実アプリで必ず確認するようにしようと思います（当たり前のことですが...）

# 所感

アプリの全体像の整理と理解ができるいい機会になったので良かったです。個人的に実案件の方では最近バックエンド・インフラをやることが多かったのでいい刺激になりました。新たに上がった課題や開発支援ツール（linter, bundler等）導入、Pinia移行、Composition APIへの移行など改善できる点はまだまだあるので今後見直していきたいと思います。

# 最後に

今年のキャンドルナイトはこの記事で紹介したアプリをベースに機能追加やデザイン調整等を実施しました。アプリは[こちら](https://dcn-s.gxp.jp/#/) からアクセス可能なので、ぜひ一度アクセスして使用して頂けると幸いです。また、例年通りアドベントカレンダー最終日12/25に今年のキャンドルナイトの記事も挙がるのでお楽しみに！