🚀

Nuxt2 → Nuxt3 アップグレードを完遂してみて

2023/05/02に公開

更新履歴

はじめに

Nuxt3 が2022年11月16日に正式リリースされました。
また、Nuxt2 が2023年12月31日に EOL を迎えるということで、今働かせていただいている現場でアップグレードプロジェクトを爆誕させ、その PL をやらせていただくことになりました。
そのアップグレードにおける激動の完遂までの道のりを本記事でまとめようと思いましたので、今後アップグレードを行おうとしている方や、現在実施している方、これから行おうとしている方に読んでいただければと思います。

目次

PJ 始動までの事前準備

まず、PJ 始動させるために、どのくらいの期間で終わるのかを概算で算出するためにどんな作業があるのかなどをざっくり洗い出して、見積もりを行いました。
大まかな流れとしては、下記のようなフローになるかな〜って感じで最初は見積もっていました。

  1. 本 PJ の作業を行うための Git ブランチワーク検討
  2. アップグレードに伴う使用ライブラリのアップグレード・代替の調査
  3. エディタ設定の変更(VSCode)
  4. Linter の更新
  5. Nuxt Bridge へのアップグレード
  6. ソースの修正(ひたすらエラー改修)
  7. Nuxt3 へのアップグレード
  8. ソースの修正(ひたすらエラー修正)
  9. Vuex から Pinia へ変更
  10. 競合修正(他の追加機能作業などに伴い、発生)
  11. アップグレードに伴うドキュメントまとめ
  12. 結合テスト
  13. リリース準備

上記作業に対し、ざっくり見積もりを出してみたところ、4人くらいの規模で半年以上1年未満で終わるような見積もりとなりました(笑)
ただ、上長からの要望としては、「もうちょい早く終わらせてほしい」といった要望もあり、改めて再見積もりなどを繰り返し、最終的な作業一覧は下記と通りとなりました。

  1. 本 PJ の作業を行うための Git ブランチワーク検討
  2. アップグレードに伴う使用ライブラリのアップグレード・代替の調査
  3. エディタ設定の変更(VSCode)
  4. Linter  の更新
  5. Nuxt Bridge へのアップグレード
  6. ソースの修正(ひたすらエラー改修)
  7. Nuxt3 へのアップグレード
  8. ソースの修正(ひたすらエラー修正)
  9. Vuex から Pinia へ変更
  10. 競合修正(他の追加機能作業などに伴い、発生)
  11. アップグレードに伴うドキュメントまとめ
  12. 結合テスト
  13. リリース準備

不要な作業として取り除いたのは、下記3つとなります。

  • Nuxt Bridge へのアップグレード
  • ソースの修正(ひたすらエラー改修)
  • Vuex から Pinia へ変更

1点目に関しては、そもそも Nuxt3 系が正式リリースされていることもあるので、わざわざ Bridge を挟まなくてもいけるのではないかとなり、不要と判断しました。
Vuex から Pinia への変更も、やるとなったらものすごい作業量となるので、使用できなくなったわけではない Vuex を変更する必要はないのではないかと判断し作業から取り除きました。(こちらの判断は後々、ものすごいチームを苦しめる羽目になります。。。詳細はこちらに記載してます。。。)

以上の作業を踏まえ、必要な作業に対し概算で見積もりポイント(※)を出してみて、人数を当ててみたところ、3ヶ月もあればちょっと余裕を持たせて終わることができるのではないかとなりました。
ちなみに概算で出した見積もりポイントの合計数は約 440pt となりました(こちらも終わった時の比較材料として覚えておいていただければと思います)

※ 現場ではスクラム手法を取り柄れており、作業に対するポイントをざっくり当てる作業を実施しました

いざ、アップグレード!

Nuxt2 から Nuxt3 へのアップグレードに関しては、公式に従って実施を行うはずでした。
しかし、Nuxt2→Nuxt3 へのアップグレードガイドは準備中となっていた(ずっと準備中な気がしていますがw)ため、公式に従って行う方法がない状況でした。
そのため、アップグレードに関しては、Nuxt Bridge を挟んで Nuxt3 にアップグレードする方法を取りました。

X: Nuxt2 → Nuxt3
O: Nuxt2 → Nuxt Bridge → Nuxt3

Nuxt Bridge に関しては、公式に手順があり、そちらに従って実施しました。
Nuxt Bridge にアップグレードできたら、あとは下記のコメントで Nuxt3 にするだけといった感じです。

$ npx nuxi upgrade

ここまでできれば、Nuxt3 系のアップグレードは完了です。
細かなアップグレード方法に関しては、別途調べていただく感じでお願いします:bow:

エラー量がえぐい

Nuxt3 へのアップグレードまでは完了したのですが、もちろんサーバー起動は成功しませんでした。
エラーファイル数を調べてみるために、Nuxt3 / Vue3 に対応させた ESLint / StyleLint / Prettier の静的コード解析ツールに解析をさせてみました。
その結果、10865 ファイル中の 1380 ファイルがエラーとなりました。。。
ただ、静的コード解析ツールのエラーに関しては、全て必須で治すわけではなく、ルールの disabled 化という素晴らしい手段もあったため、修正方針に検討がついてないかつ、修正しなくとも影響はないものなどに関しては、ルールを disabled しまくりました。(苦笑)

Vuex に関しては Nuxt2 ではフレームワークの機能の1つとして存在していたんですが、Nuxt3 系からフレーワークの機能からは除外されているため、1から基盤構築などを実施する必要がありました。
基盤構築のみならまだ良かったんですが、各 Vuex modules の export の仕方も変更する必要があり、書き換えが必要なファイル数は、なんと約 1360ファイルでした。。。

さらに、スタイルでは node-sass を使用しており、node-sass は非推奨のため、dart-sass での書き方に変更する必要もありました。

// before
@import "style";

// after
@use "style";

こちらもほぼほぼのスタイルファイル(.scss)での書き換えが必要になりました。

この時点で、相当憂鬱になるレベルでの作業量が確定になったわけです。

Vuex との戦い

Nuxt の公式では、Vuex について下記のように述べています。

Nuxt no longer provides a Vuex integration. Instead, the official Vue recommendation is to use pinia, which has built-in Nuxt support via a Nuxt module.

NuxtはVuexとの統合を提供しなくなりました。代わりに、Vueの公式な推奨は、Nuxtモジュールを介してNuxtサポートを内蔵しているpiniaを使用することです。

そうなんです。Nuxt3 からは Vuex は推奨ではなくなったのです。(Pinia の方が圧倒的に使用しやすいのはそうなんですが、推奨でもなくなるとは、、、)
また、先ほども記載しましたが、Vuex モジュールは Nuxt フレームワークの1つの機能として内蔵されなくなってしまいました。
ここで、選択肢は2つあり、どちらかを選ぶ必要がありました。

  • Vuex のままで実装を行う(基盤構築なども実装する)
  • Vuex のソースを Pinia にリプレースする

先に結論を言うと前者を選ぶしかありませんでした。
理由は、作業工数にあります。
前者はすでに Vuex の実装がある程度整っており、Vuex の基盤構築の実装や軽微な Vuex modules などの修正対応のみで終わります。しかし後者だと、Pinia の導入と既存の Vuex のソースを全て Pinia に書き換える作業と、store.getters / store.dispatch を全て書き換える作業が必要になり、リリース日までに後者の作業を全て終わらせることは不可能という判断でした。。。
開発者として、推奨していない Vuex のまま実装を進めるのは苦し難かったのですが、期日というどうしようもないものに負けることになります。

基盤構築の苦しさ

Nuxt2 のフレームワークの機能と同じようなものを Nuxt3 では独自で実施する必要がありました。
Vuex の公式通りに実装するだけでは、もちろんうまくいきませんでした。。。(SSR だからっていう理由が大きいかもです)
最終的な設定は下記の通りです。

store/index.ts
import type { InjectionKey } from 'vue'
import { type ModuleTree, createStore, Store } from 'vuex'
import actions from './actions'
import getters from './getters'
import mutations from './mutations'
import { RootState } from './types'

const loadModules = (): ModuleTree<RootState> => {
  const moduleGlob = import.meta.glob(
    ['storeパス/*.ts'],
    { eager: true }
  )

  return Object.keys(moduleGlob)
    .filter((modulePath) => {
      return modulePath.includes('/moduleのエントリポイントファイル')
    })
    .reduce<ModuleTree<RootState>>((modules, modulePath) => {
      const key = modulePath.replace('./', '').replace('/moduleのエントリポイントファイル', '')
      return {
        ...modules,
        [key]: (moduleGlob[modulePath] as any).default
      }
    }, {})
}

export const STORE_KEY = 'store のユニークキー'

export const key: InjectionKey<Store<RootState>> = Symbol(STORE_KEY)

export const initStore = () =>
  createStore<RootState>({
    state: {},
    getters,
    actions,
    mutations,
    modules: loadModules()
  })
plugins/vuex.ts
import { useHydration } from '#app'
import { initStore, key } from '@/store'

export default defineNuxtPlugin((nuxtApp) => {
  const store = initStore()
  nuxtApp.vueApp.use(store, key)

  // サーバサイド・クライアントサイドでのステート連携
  useHydration(
    'vuex',
    () => store.state,
    (value) => store.replaceState(value)
  )

  return {
    provide: {
      store
    }
  }
})

すごくシンプルな実装になっていますが、ここまで来るのにたくさんの苦労がありました。。。

ホットリロード機能の作成

開発モードで Vuex のソース修正をした場合、なんとホットリロードしてくれません。(監視対象外っぽかったです)
そのため、独自でホットリロード機能を作成する必要がありました。
下記はあくまでも例のソースとなります。

store/utils.ts
// store/index.ts から移動
export const loadModules = (): ModuleTree<RootState> => {
  const moduleGlob = import.meta.glob(
    ['storeパス/*.ts'],
    { eager: true }
  )

  return Object.keys(moduleGlob)
    .filter((modulePath) => {
      return modulePath.includes('/moduleのエントリポイントファイル')
    })
    .reduce<ModuleTree<RootState>>((modules, modulePath) => {
      const key = modulePath.replace('./', '').replace('/moduleのエントリポイントファイル', '')
      return {
        ...modules,
        [key]: (moduleGlob[modulePath] as any).default
      }
    }, {})
}

const isStoreKey = (value: any): value is Symbol => {
  return true /* store のキーかどうか */
}

const isStore = (value: any): value is Store<RootState> => {
  return value instanceof Store
}

/** modules のホットリロード対応 */
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    if (newModule === undefined) {
      return
    }

    const { key, store } = newModule
    // 変更があったファイルが store のキーと store 自体だった場合のみ
    if (isStoreKey(key) && isStore(store)) {
      const { $store } = useNuxtApp()

      const modules = loadModules()
      $store.hotUpdate({
        modules
      })
    }
  })
}

Vuex の機能で動かないオプション

Vuex を使用しているので、strict を on にしたいと思い、下記の設定を最初にしていました。

store/index.ts
export const initStore = () =>
  createStore<RootState>({
    state: {},
    getters,
    actions,
    mutations,
    modules: loadModules(),
    // ここ
    strict: true
  })

ただ、この設定だと、エラーになりました。。。(詳細なエラー内容はちょっと忘れちゃいました。。。)
原因は、Nuxt のライフサイクルと Vuex の strict オプションがうまく噛み合ってないことが原因でした。(確か、Vuex 側の watch イベントが Nuxt のライフサイクルと噛み合ってなかった記憶です)
そのため、strict オプションを使用したい場合は、Vuex が Nuxt3 に合わせてくれるようになるまで待つしかありません。(もう Nuxt3 での Vuex は非推奨って感じですね笑)
今回は仕方なく strict は off にすることに決定しました。
早く Pinia にリプレースしたいですね。。。

エラー解消後もサーバー起動できない

ここまでたくさんのエラーが出てきたわけですが、全てのエラーを解消してもサーバーを起動することができません。(予想はしてましたが)
エラーにはなってなくても Nuxt3 でのライフサイクルでは動かないソースがあったりと、ビルドが webpack から vite に変わったことによるビルド時のエラーなどなど、次から次へと新しいエラーが出迎えてくれました。
エディタ上ではエラーにならないけど、動かないソースとして2つほど例にあげます。
1つ目は、provide / inject です。
下記の書き方は Nuxt2 / Vue2 では正しく動いていました。

provide('key', { id: 1 })
const injectValue = inject('key')

ただ、こちらの書き方は Nuxt3 / Vue3 では injectValuenull となります。(undefined だったかな?)
同一階層での inject はできない仕様に変更されたことによるものです。

2点目は、require による読み込みです。
本プロジェクトでは、SVG の読み込みや、型ファイルのないパッケージの読み込みに require を使用していました。

const svg = require('assets/icon.svg?inline')
const pkg = require('型ファイルがないパッケージ')

Nuxt3 では Native ES Modules を使用しているため、require を全て import に書き換えなければなりません。
SVG に関しては、vite のプラグインである vite-svg-loader を使用して回避しました。
型ファイルのないパッケージに関しては、一旦独自で型定義を追加し、エラーにならないよう回避するよう対応しました。

types/型ファイルがないパッケージ.d.ts
declare module '型ファイルがないパッケージ'

SSR やユニバーサルコードのキツさ

なんといっても、こちらが一番厳しかったです。。。
一番よく出てきたログとしては、Hydration node mismatch です。
解決させるためには、Nuxt のライフサイクルを理解する必要があり、どこでなんの処理を行っているのかを把握する必要がありました。
一番よく起きていた Hydration node mismatch は、layouts 側で表示するデータを pages の vue ファイルの async data で fetch しているケースです。

layouts/default.vue
<template>
  <div v-for="(value, index) in data" :key="`${index}-${value}`">
    {{ value }}
  </div>
</template>

<script setup lang="ts">
// store composables
const store = useStore()

const data = store.getters['store/data']
</script>
pages/index.vue
<script setup lang="ts">
// store composables
const store = useStore()

await useAsyncData(async () => {
  await store.dispatch('store/data-fetch')
  return {}
})
</script>

なぜ発生するかに関しては、処理の流れに答えがあります。(下記はあくまでもざっくりした流れになるので、もっと細かく処理はあります。)

  1. サーバサイド:layouts setup
  2. サーバサイド:pages async data
  3. クライアントサイド:Hydration

もう答えがわかるかもですが、layouts setup が pages async data より先に走っています。
そちらにより、pages で fetch しているデータは layouts setup 時には存在していないため、そのデータを使用してレンダリングしている箇所は空表示になります。
ですが、Hydration で DOM を生成している時では、pages async data のデータは存在しているため、空表示にはなりません。
そちらが原因で Hydration node mismatch が発生となります。

上記の問題の修正方法としては、pages async data の処理を、pages middleware で実施するような変更で対応しました。

pages/index.vue
<script setup lang="ts">
definePageMeta({
  middleware: [
    async () => {
      // store provided by plugins
      const { $store } = useNuxtApp()

      await $store.dispatch('store/data-fetch')
    }
  ]
})
</script>

こちらの修正により、layouts setup より先に pages middleware の処理を動かすことができます。

上記以外にも SSR やユニバーサルコードによる修正は最後の最後までたくさんありました。。。

Nuxt3 に対応していないライブラリ対応

Nuxt3 へのアップグレードを実施するにあたり、使用しているパッケージが Nuxt3 に追いつけていないものの対応や Nuxt3 に対応は完了しているが、結構書き換えが必要なパッケージがたくさんあり、なんとその数合計20パッケージ分もありました。
その中でも一番苦労したものは認証機能です。
Nuxt2 の時は @nuxtjs/auth-next を使用していました。
こちらは Nuxt3 には対応しておらず、他を探す必要があったんですが、Nuxt3 で使用できる認証パッケージはなさそうとの報告がありました。
そのため、認証機能を独自で作成しなければいけなくなりました。
ですが、やはり認証機能ということもあり、なかなか実装が難しく、認証機能が完成しても不具合がなかなか消えませんでした。
なんとか無事にリリースできるレベルまでには持っていけましたが、完璧な完成物にはならなかったので、早く認証機能パッケージの公開を待っている感じです。。
その他にも、代替を探さないといけないパッケージなどが多くあり、若干の UI の変更があったり、SSR で正しく動かないなど、たくさんの問題と戦いがありました。

その他のパッケージ対応も全て書き出したいのですが、ものすごい大量の文章になるので割愛させていただきます。

Nuxt 自体に存在する不具合

やはり、リリースしてすぐは不具合が多くあるものなんだなと思いました。
アップグレード対応していて、Nuxt2 ではこの動きなのに、Nuxt3 ではその動きになっていないことや、Nuxt3 での動きで、確実におかしいなど、そういったものがちょっと多くあった印象です。
その不具合がチームの不安につながったのも事実としてあります。
そういうものは運用カバーの方法、または違った表現で同じことができないかの方法を探します。
nuxt の不具合だったとしても、こちらのリリース日は変更できないので、どうにか違った方法で同じ動きを実現できないかを模索しまくり、実装するを行いました。
そういった作業を行うと同時に nuxt に issue を出して修正を待つような対応を実施しました。
issue は合計2つほど出させていただきました。(下記は最初に出した issue です。恥ずかしいので、あえて画像にしました笑)

スクリーンショット 2023-04-30 18.47.20.png

不具合の issue を出しましたが、nuxt チームはものすごいスピードで対応してくれました。(対応力も最強だなって思いました笑)
2022年11月16日にリリースされたのに、現時点で nuxt は v3.4.3 まで出ています。

レガシーコードに対するアップグレードのキツさ

こちらもなかなかきつかったですね笑
先人のメンバーが作成してくれたソースは納期を守るため、スピード重視でソースが組まれていました。(設計フェーズみたいなものはなく、とりあえず動くものを作成するようない感じ)
納期を守りしっかり動くものを作成していただけたものの、メンテナンス性に欠けるソースがたくさんある状態となっていました。
日々の開発でこのレガシーコードと向き合って開発を行いましたが、アップグレード対応がものすごくキツくなりました。(なんだ?この実装は?ってよくメンバーから聞いてました)

こちらも例を1つだけあげると、Vue が TypeScript との相性が良くなったことによる負債の浮き彫りです。
Nuxt2 時代は、Vue ファイルでの <template><script> 間で使用する変数は型が異なっていてもエディタ上ではエラーになっていませんでした。

<template>
  <!-- data props は number 型でもエラーにはならない -->
  <CustomComponent :data="name" />
</template>

<script lang="ts">
export default defineComponent({
  setup() {
    return {
      name: 'taro'
    }
  }
})
</script>

しかし、Nuxt3 / Vue3 からは、エディタ上でもエラーになってくれるよう TypeScript との相性が良くなりました。
嬉しい反面、上記のような型エラーが大量に出てきて、修正が大変でした笑

PL 業務 + プレイヤー(実装者)の両立の厳しさ

PL 業務として、進捗管理やメンバー管理・課題に対するアプローチなどなどを実施してかつ、実装者としてタスクを消化していく動きをしていました。
ですが、さすがにあれもこれもやるは厳しかったです。(毎日当たり前のように残業もしてました笑)
PL 業務に専念すればいいじゃんってなるかもですが、なんせメンバーが足りていない状態でもあったので、実装者としてもコミットする必要がありました。

そういった状況だったため、PL 業務の重要な部分と実装者を自分が担うようにし、その他の PL 業務はサブリーダーにお願いするような形で業務を進める形を取りました。(それでも作業は大量にありましたが笑)
その対応を実施しただけでも大分楽に仕事を進めることができました。

PL 業務として、特に難しかったこと

こちらは、進捗管理とメンバー管理でした。

進捗管理に関しては、Nuxt アップグレード対応は結構未知な部分が多く、やってみないとわからないことが多かったため、とにかく出てくるタスクをゴリゴリに進めていくスタンスを取ろうと思っていたんですが、それで満足するのは自分のみでその他メンバーは「このままでほんとに大丈夫?」といった不安などが多かった印象です。
未知な部分をどのようにして具体化するかといったところを考えるのがとても難しかったなと思っています。
実際に実施した対応を1つ紹介すると、それぞれのタスクの大項目を作成し、その項目に対し、ざっくりベースの見積もりを行います。
次に大項目に対してガントチャートを引き、それぞれ何人さいてどれくらいに終われば問題ないのかを可視化するといった対応になります。
そのガントチャートをメンバーに周知することで、1人1人がいつまでに何が終わってないといけないかを意識してタスクに着手してくれて、現状の進捗を把握することができるのと、そのガントチャート自体が安心材料になりました。

メンバー管理に関しては、メンバー1人1人がどういった感情(不安や悩み)を持っているのかなどを把握する動きがなかなかできなく、不安・不満につながっていただろうなと感じています。
また、メンバー全員が同じ方向に向かっていない時もあったりして、そこに気づくのも遅れた時もありました。
そこに対するアプローチとしては、やはり 1on1 の実施でした。(厳密には自分とサブリーダーとの 2on1 でしたが)
その対応のおかげでメンバーが何を考えているのかや、どんな悩みを抱えているのかを可視化することができました。また、メンバー全員を同じ方向に向けることもできたので、とてもいい時間になったなと思っています。
1on1 ではなくとも、普段の業務の中で定期的にメンバーと雑談始まりでの会話をしたりしました。その時間を作ることはとても大切だなと強く思っています。

概算で洗い出した作業以外にたくさん出てくるその他作業

概算で見積もった際に出てきた作業以外に、実際にタスクに着手していると、その他作業がたくさん出てきました。
大きく漏れていた作業としては、インフラ作業とモンキーテスト工程です。

インフラ作業は、今回のプロジェクトで言うと、実際に稼働しているインフラ環境で正しく動かすための調査や負荷テストなどのタスクとなります。
当たり前っちゃ当たり前ですが、しっかり漏れていました笑

モンキーテスト工程は、結合テストの作業とは別で、アップグレードによって UI / UX の不正が多くあり、その状態で結合テストに入っても意味がなさそうなため発生した作業内容となります。
こちらの作業によって見つかった不具合はなんと 150 個ほどです。とんでもない数でした。。。

最終的な作業工数

アップグレードによる大量のソース修正や、大量に出てきた不具合修正、その他対応が必要となった作業など、全て合わせたポイント数は合計 730pt となりました。
概算で出した際の見積もりポイントの合計が 440pt だったので、それの約2倍弱くらいといった感じです笑
概算の見積もり時でも多めに見積もったりしたのに、それを遥かに上回る数値となってしまいました。。。
また、当初は3ヶ月もあればちょっと余裕を持たせて終わることができるとあったんですが、ポイントの膨らみ的にもその期間に終わらせることはできませんでした。
かかった工数としては4ヶ月半に及びました。
期日を過ぎてしまうことに関しては、上長にめちゃくちゃ頑張って調整していただけました。ほんとに感謝しかないです。

プロジェクト全体で意識したこと

こちらは、「とにかく楽しむ」「リリースも絶対間に合う自信を持つ」 を意識して作業に着手していたと思っています。
リーダーである以上、ネガティブ要素はメンバーに伝わってはいけません。
また、憂鬱になるほどのエラー量でもあり、メンバーとしてもテンションが下がる感じでもありましたが、そこを楽しく開発できるような環境にするため、楽しい気持ちは伝えていたと思っています。

個人的に、リーダーが暗いとそのリーダーについていく・ついていきたいメンバーは1人もいないと思っています。
どんなにきつい状況でも明るく、謎に終わる自信を持っているリーダーも不安要素はありますが、なぜか「いけるんじゃね?」の気持ちになったりすると思います。(自分はそう思う人です笑)
なので、ワイワイ盛り上がれる環境作りは意識していたと思っています。

また、チーム運用の部分に関しては、「なるべく資料はわかりやすくまとめる・管理する」 を意識したと思っています。
プロジェクトを通して色々ドキュメントが増えていきますが、どこに何があるかわかるようにドキュメント一覧のシートを作成したり、1つのドキュメントには1つのメインテーマを持たせて、違う内容なら違うドキュメントにまとめたりなどなど。
当たり前の内容ではありますが、プロジェクトを進めているとこの当たり前が崩れていったりします。(急いでいたから適当になったや、バージョンアップとは関係ないから少し適当に作成しちゃったなどの理由かなと思ったりしてます)
なので、そこの軸はブレないよう管理していたかなと思っています。

まとめ

長い文章になりましたが、まだまだ書き漏れていることが多くある感じです。(今後、書き足したい内容があった場合は、書き足していこうと思います笑)
プロジェクト全体を通して、リーダースキルと Nuxt の知識が多くつきました。
また、メンバー1人1人にほんとに助けられたと思っています。(すごくいいチーム開発だったなと思いました)
Nuxt4 のアップグレード対応が今後出てきた時に、ここで得られたノウハウを活かしてもいきたいなと思っています。

今後アップグレードを行おうとしている方や、現在実施している方、これから行おうとしている方とって、いい記事になっていれば幸いです。

Discussion