LayerX エンジニアブログ

LayerX の エンジニアブログです。

バクラクの Vue3 移行戦略と詰まったポイント #LayerXテックアドカレ

こんにちは。LayerX バクラク事業部 バクラク申請・経費精算チーム エンジニアの @tatane です。 最近10万円超えのヘッドホンを買ってしまい、しばらく離れていたオーディオ沼にまた片足を突っ込んでしまいました。

この記事は LayerXテックアドカレ2023 21日目の記事です。前回は @serima が「LayerXの発信の"のびしろ"とCasual Nightのウラガワの話」を書いてくれました。次回は同じチームの @upamune が「エラーログ祭り改善の話」を書いてくれる予定なのでご期待ください!

今回はバクラク事業部で進めている Vue3 移行の話をご紹介します。

バクラクの Web フロントエンドの技術スタックについて

LayerX が提供している SaaS であるバクラクシリーズでは複数プロダクトを提供しており、それらの Web フロントエンドの技術スタックにはバリエーションがあります。

バクラクシリーズで最初のプロダクトである「バクラク請求書受取・仕訳」では Vue / Nuxt が利用され、その後しばらくは Vue / Nuxt が新規プロダクト開発時のスタンダードになっていました。

2022年にリリースした「バクラクビジネスカード」では、バクラクシリーズとしては初めて React(ここでは Next は未使用)が利用され、その後の新規プロダクト開発については社内向けプロダクトも含めすべて React / Next が利用されています。

事業部として新規プロダクト時に React / Next を推奨するようになった背景などに関しては、また別の機会にご紹介できればと思います。

Vue2 の EOL と Vue3 移行の必要性

バクラクシリーズで Vue が使われているプロダクトには以下のものがあります。

  • バクラク請求書受取・仕訳
  • バクラク申請・経費精算
  • バクラク電子帳簿保存
  • バクラク共通管理
  • プロダクト共通のファイルアップロードサービス
  • 社内向け管理画面

2023年の1月時点では、これらはすべて Vue2 / Nuxt2 を利用していました。

Vue 2 will reach End of Life (EOL) on December 31st, 2023.

ただ、公式ドキュメントにもあるように、Vue2 は2023年の年末に EOL を迎えてしまいます。

EOL を迎えた瞬間にプロダクトが動かなくなるというわけではないですが、EOL 以降はセキュリティ的な問題やブラウザ互換のためのアップデートがされなくなるため、なにかしら対応しなければいけません。

また、巷でよく言われる「Vue2 と TypeScript の相性の悪さ」も体感しており、例えばコンポーネント内で this を参照したときにうまく解決できない型があったり、Props の型にジェネリクスを使えなかったりと、特に React を TypeScript で書いていたようなメンバー*1からは不満の声もありました。

そのため、Vue3 の TypeScript との相性の良さも強い魅力の1つになり、Vue3 に移行しよう!という流れになりました。

ちなみに他の選択肢としては「React など他のライブラリへの移行」も検討しましたが、ライブラリレベルのリプレイスは工数的に現実的でなく、やるとしても Vue2 → Vue3 → React のような段階を踏んだほうが安全ということで、Vue3 への移行を決めました。

ロードマップ

バクラク事業部には「フロントエンド・カイゼン WG」という、プロダクトチーム横断で事業部のフロントエンド周りを相談するグループ*2があり、そこで各プロダクトチームのエンジニアと相談しながら移行ロードマップを検討しました。

ロードマップのポイントとしては「段階的に進めて不確実性を潰していく」です。

バージョンごとに段階的な移行を進める

Vue / Nuxt は「2系」「3系」とは言うものの、その境目にはグラデーション的にいくつかのバージョンが存在します。

  • Vue2.7:Vue2 に Vue3 のいくつかの機能がバックポートされた Vue のバージョン
  • Nuxt2.16:Vue2.7 を利用する Nuxt のバージョン
  • Nuxt Bridge:Nuxt2 に Nuxt3 のいくつかの機能がバックポートされた Vue のバージョン
  • Vue3 compat:Vue3 で Vue2 互換の挙動を担保できる移行ビルド

すべて Vue2.6 以前かつ Nuxt2.15 以前だったバクラクシリーズでは、以下のような段階を踏んで移行を進めることにしました。

  1. Vue2.7 に上げる
  2. Nuxt2.16 に上げる
  3. Nuxt Bridge に上げる
  4. Vue3 compat / Nuxt3 に上げる
  5. compat の利用をやめて、通常のビルドにする

※ Nuxt Bridge は Vue2, compat は Vue3 であるため、「Nuxt Bridge, compat」の段階は踏めませんでした。

※ さらに前段階として「Vue3 で非推奨となる構文をチェックする ESLint ルールを入れておく」ということを2022年時点から始めていました。

大きなコードベースでいきなり Vue3 / Nuxt3 に上げなかったのは正解で、段階的な移行は問題発生時の原因切り分けに役立ちました。

事業部の全体最適になるような順番で、1プロダクトごとに段階的に不確実性を潰していく

バクラクシリーズで Vue / Nuxt を利用しているプロダクトを先程列挙しましたが、その中でも特に小さいものが「プロダクト共通のファイルアップロードサービス」です。

これはファイルのアップロード画面だけを提供するプロダクトであるため、Nuxt の page に相当するものが2つしかなく、結果として外部ライブラリ依存も最小になっていました。

最初にこのプロダクトを Vue3 まで上げたのですが、他のタスクの合間に進めても2週間ほどでリリースまで完了することができました。

これにより、「バクラクのプロダクトにおける一般的な Vue / Nuxt の書き方からの移行イメージ」や「apollo*3 や bootstrap-vue など、バクラクの Vue プロダクトで共通して使われる外部ライブラリで Vue3 移行できること」などがクリアになりました。

なにより「バクラクのプロダクトでも Vue3 移行できるんだ!」という成功体験が大きかったです。

※ 「Vue3 移行できる」と書いたものの、バクラクシリーズが依存している bootstrap-vue は compat の利用が必須であり、bootstrap-vue が脱 compat するか、バクラクが脱 bootstrap-vue するまでは完全な Vue3 移行はできません…!この後の「Vue3」は「Vue3 with compat」と同義です。

続いて、「バクラク申請・経費精算」の Vue3 移行をおこないました。

Vue / Nuxt を利用しているバクラクシリーズでは、「バクラク請求書受取・仕訳」と「バクラク申請・経費精算」が特にコード量も多く複雑なコードベースの2トップであり、その片方を先に検証することで、バクラクシリーズ全体の Vue3 移行で出てくるであろう不確実性の多くは潰せるだろうという算段です。

また、当時「バクラク申請・経費精算」のプロダクトチーム OKR に品質向上が含まれていたことや、Vue3 移行まわりでメインで動いていた筆者が所属するプロダクトチームであったことも大きかったです。

Vue2.7, Nuxt2.16, Nuxt Bridge までは、それぞれの段階で長くとも1週間ほどの期間で移行を完了することができました。

大変だったのが Vue3 / Nuxt3 移行で、実際に着手してからリリースできるまでに1ヶ月強の工数がかかりました。 当初の見積もりでは2週間ほどで置き換えが完了する予定だったので、不確実性の高い大型バージョンアップ作業はバッファを取っておいたほうがいいですね…!

バクラク申請・経費精算の Vue3(compat) 対応が本番にリリースされて盛り上がっている図

これまでの知見を活かし、直近では他プロダクトの移行も徐々に進んでいます。 今月はついに「バクラク請求書受取・仕訳」が移行完了しました!めでたい🎉

社内でのナレッジ蓄積と共有

プロダクトが1つだけ・エンジニアが1人だけであれば、勢いで移行して完了!ですが、バクラクシリーズのように複数プロダクトを複数チームで開発しているとそうもいきません。

バクラク事業部では各プロダクトごとにチームが存在し、各チームでもフロントエンドがあまり得意ではない(=各自での Vue3 のキャッチアップが難しい)メンバーもいます。

そこで、今回の移行では以下の取り組みと共に移行を進めました。

  • 「フロントエンド・カイゼン WG」や、「エンジニア共有会*4」で相談・共有しながら進める
  • 「Vue3 / Nuxt3 移行 Tips」という Notion ページに、各プロダクトが各バージョンのマイグレーションでやったことやハマったポイントをまとめていく
  • チームメンバー向けに「Vue3 移行後の書き方ポイントまとめ」のドキュメントを用意する*5

具体的に移行作業で詰まったポイント

Vue / Nuxt 本体の移行に関しては、基本は公式のマイグレーションガイドに従いました。

Migration to Vue 2.7 — Vue.js

Vue 3 Migration Guide | Vue 3 Migration Guide

Migrate to Nuxt 3: Overview

ここでは、移行作業で詰まったことをいくつかピックアップして紹介できればと思います。

vee-validate v4 への移行

正直一番大変なのがここでした…!

バクラクの Vue プロダクトでは、フォームライブラリとして vee-validate を利用しています。

Vue2 のときは vee-validate v3 を利用していたのですが、Vue3 の VNode まわりの変更により vee-validate v3 の API が機能しなくなってしまい、結果として Vue3 では vee-validate v4 への移行が必須になっています。(ちなみに compat と vee-validate v3 の共存も不可

他の方が書かれている Vue3 移行記事にもよく出てくるように、vee-validate v3, v4 では完全に別ライブラリと言えるほどインターフェースが変わっており、一筋縄ではいかない移行になりました。 詳細については、私も参考にさせていただいた以下の記事を参照ください。

大規模アプリのVue3アップデート対応知見まとめ

Nuxt 3 アップデートで VeeValidate v3 から v4 に移行するには - ANDPAD Tech Blog

バクラクでは以下の2パターンで移行を進めました。

  • vee-validate v3 で動く vee-validate v4 互換のコンポーネントを用意し、それを経由して置き換える
  • 素直に vee-validate v4 に上げて、各所で v4 の API に対応する

「バクラク請求書受取・仕訳」では、前者の vee-validate v4 互換コンポーネント経由で置き換えを進めました。(そのうち別で記事を書いてくれるかも…?)

筆者の担当した「バクラク申請・経費精算」では、後者のようにいきなり vee-validate v4 に上げて、素直に置き換えを頑張ることにしました。

互換コンポーネントを経由しなかったことについては、以下のような理由があります。

  • 将来的には vee-validate v4 のスタイルの API を利用したいと思っており、今後「互換コンポーネント→ vee-validate v4」の時間を取れるか不明だったので、工数を確保できているうちに一気に移行してしまいたかった。
  • 互換コンポーネントの場合でも結局は vee-validate を利用している各所での置き換えが発生するため、工数削減としては大きなメリットはなかった。

この意思決定に関しては、先ほどの「段階的に進める」に違反してしまっているためオススメはできないですが、プロダクトチームの開発ロードマップの兼ね合いなどで vee-validate の移行作業を一気に終わらせてしまいたいチームの選択肢の1つとしてはアリだと思います。

v-on のインラインステートメントで構文エラーになる

移行初期にとりあえずビルドしようとすると、以下のエラーが発生しました。

[vite:vue] Error parsing JavaScript expression: Unexpected token, expected ","

よくよく調べていくと、v-on(省略記法は @ )のハンドラを複数行のインラインステートメントで記述した場合、 Vue2 ではそのまま複数行の処理を書いても動きましたが、 Vue3 ではアロー関数などで記述する必要がありました。

// Vue2 ではこれでも問題ない
@change="
  doSomethingA()
  doSomethingB()
"
// Vue3 ではこうしないとビルドできない
@change="
  $event => {
    doSomethingA()
    doSomethingB()
  }
"

一部タグ間のスペース(空白)が削除される

バクラクでは bootstrap-vue を利用しているのですが、Vue3 にすると button 間の margin が無くなっていることに気づきました。 どうやら、bootstrap-vue が付与しているタグ間のスペースによって隙間が生まれており、Vue3 ではそれが削除されているようです。

issue を見ると、Vue loader がデフォルトではタグ間のスペースを削除する設定になっています。 Vue3.1.0 からは compilerOption でスペースの挙動のハンドリングが可能になっているので、以下のように設定することでタグ間のスペースを残すことができました。

// nuxt.config.ts

vite: {
  vue: {
    template: {
      compilerOptions: {
        whitespace: 'preserve',
      },
    },
  },
},

static フォルダを読み込めない

Nuxt3 では、icon などの public なファイルを置くディレクトリのデフォルト値が static ディレクトリではなく、 public ディレクトリを参照されるようになっていました。

そのため、デフォルト値を上書きする必要があります。

// nuxt.config.ts

vite: {
    dir: {
        public: 'static',
    },
}.

Options API の head をそのまま使えない

バクラクのプロダクトでは、開発途中から Composition API に移行し始めたものの、依然として Options API で書かれているコンポーネントも多いです。

defineComponent のままだと Nuxt3 では head が使えないため、 defineNuxtComponent に置き換える必要があります。

defineNuxtComponent · Nuxt Utils

vuedraggable が compat で動かない

ドラッグアンドドロップで要素を動かせるコンポーネントに vuedraggable を利用していたのですが、compat で動かすと vuedraggable 利用箇所でエラーが発生していました。

該当 issue を見ると draggable.compatConfig = { MODE: 3 }; の指定で動くとのことですが、手元のコードでは以下のようにすることで正常に動くことを確認できました。

import draggable from 'vuedraggable'
const Draggable = {
  ...draggable,
  compatConfig: {
    MODE: 3,
  },
}
...
components: { Draggable }

$root.$on が使えなくなった

Events API | Vue 3 Migration Guide

Vue3 では $on $off $once といったイベント周りのインスタンスメソッドが削除され、各コンポーネントインスタンスは自身のコンポーネント以下で emit されたイベントを補足できなくなりました。

この変更により困ったのが bootstrap-vue の b-modal のイベントです。

Modal | Components | BootstrapVue

bootstrap-vue では、 $root.on でイベントを補足することで、モーダルの開閉などを検出していました。

export default {
  mounted() {
    this.$root.$on('bv::modal::show', (bvEvent, modalId) => {
      console.log('Modal is about to be shown', bvEvent, modalId)
    })
  }
}

「バクラク申請・経費精算」ではモーダル開閉のイベントをルートコンポーネントで補足する処理は必要だったため、自前のイベントバスを使ってグローバルに emit できるようにし、それを利用した b-modal のラッパーコンポーネントを用意しました。

// eventBus

import emitter from 'tiny-emitter/instance'

type EventName = 'bv::show::modal'

export default {
  $on: (eventName: EventName, ...args: any[]) => emitter.on(eventName, ...args),
  $once: (eventName: EventName, ...args: any[]) => emitter.once(eventName, ...args),
  $off: (eventName: EventName, ...args: any[]) => emitter.off(eventName, ...args),
  $emit: (eventName: EventName, ...args: any[]) => emitter.emit(eventName, ...args),
}
// ラッパーコンポーネント

<script setup lang="ts">
// NOTE: Vue3では$root.$onが使えなくなったので、自前のeventBusに対応したBModalを用意
import { BModal as modal } from 'bootstrap-vue'
import eventBus from '~/utils/eventBus'

// https://bootstrap-vue.org/docs/components/modal#comp-ref-b-modal-events
const emits = defineEmits(['cancel', 'change', 'close', 'hidden', 'hide', 'ok', 'show', 'shown'])
const attrs = useAttrs()
const show = (e: any) => {
  eventBus.$emit('bv::show::modal', attrs.id)
  emits('show', e)
}
</script>

<template>
  <modal
    v-bind="$attrs"
    @cancel="$emit('cancel')"
    @change="$emit('change')"
    @close="$emit('close')"
    @hidden="$emit('hidden')"
    @hide="$emit('hide')"
    @ok="$emit('ok')"
    @show="show"
    @shown="$emit('shown')"
  >
    <template v-for="(_, name) in $slots" #[name]="slotData">
      <slot :name="name" v-bind="slotData" />
    </template>
  </modal>
</template>
// 利用箇所

- $root.$on('bv::modal::show', _ => {
-   // なにかしらの処理
- })
+ eventBus.$on('bv::show::modal', () => {
+   // なにかしらの処理
+ })

...などなど、このような詰まりポイントが他にも沢山ありました。

Vue3 にしてみて

また、巷でよく言われる「Vue2 と TypeScript の相性の悪さ」も体感しており、例えばコンポーネント内で this を参照したときにうまく解決できない型があったり、Props の型にジェネリクスを使えなかったりと、特に React を TypeScript で書いていたようなメンバーからは不満の声もありました。

最初に Vue2 の課題感として上のような内容を挙げていましたが、実際に Vue3 にしてみるとこれらの課題の多くは解決されました! .vue のコンポーネントファイルで作業しているときによく分からない理由で型解決に失敗することは激減し、Vue3.3 から導入された Generic Components など Vue3 の TypeScript フレンドリーな新機能を活用したコードも書かれ始めています。

また、上記以外にも次のような Vue3 移行(というより vite 移行)の恩恵を受けています。

  • ホットリロード(HMR)が爆速になった
  • CIでのビルドが爆速になった

ビルドが爆速になって喜んでいる図

単純に移行するだけでも開発者体験の部分でのメリットが思ったよりも大きく、早速沢山の恩恵を受けています。 今後はこれらの開発者体験の向上を顧客体験の向上へ繋げていきます!

※ Vue2.7 でもバックポートされているので Vue3 でのメリットとしては強く言及しないですが、移行を期に script setup を積極的に利用したところ、書き心地が良くなったり template 内で参照されない未使用変数に気づきやすくなったりしてメリットが大きいなと感じます。

おわりに

今回はバクラクで Vue3 移行に取り組んでいる話を紹介させていただきました。

Vue3 移行はまだ道半ばであり、移行後の Vue3 のポテンシャルも活かしきれていない状況です…!

バクラク事業部ではフロントエンドのカイゼンをこれまで以上に進めていこうと思っており、プロダクトエンジニアに加えて「フロントエンドEnabling」といったロールの採用も始めています。

ハタラクをバクラクにするプロダクトを一緒に開発していきたい人、スペシャリストとして事業部全体のフロントエンドをカイゼンしていきたい人を大募集中です!もしご興味ありましたら、ぜひカジュアル面談からお話させてください!

OpenDoor でお待ちしております!

jobs.layerx.co.jp

最近では LayerX Casual Night というお酒を飲みながら技術の話をゆる~く行うイベントも開催しておりますので、ぜひご参加ください!

jobs.layerx.co.jp

採用情報はこちら↓

jobs.layerx.co.jp

*1:2022年以降バクラク事業部に入社したフロントエンド寄りのエンジニアは、全員が今まで React をメインに使っていた

*2:最近社内で WG(Working Group)の建付け整理の話があり「バクラク Web フロントエンドギルド」として生まれ変わった。WG は特定のゴールの達成で解散する短期的なグループだが、ギルドは解散せず中長期的に活動を続ける。

*3:バクラクのプロダクトでは、主に GraphQL を利用している

*4:毎週金曜日にバクラク事業部のエンジニア全員でLTや共有などをおこなうイベント

*5: チーム全員がフルスタックになるためのフロントエンド開発Tips - LayerX エンジニアブログ にある Notion ページ