複数の開発チームの機能開発を止めずに Nuxt3 へアップデートしました

はじめに

SWE の sunecosuri です。
アンドパッドでは多くのプロダクトで Nuxt を使用しており、 使用していたバージョンの Nuxt2 が EOL を迎えたため、 Nuxt3 へアップデートしました。 この記事では、複数のプロダクトチームが開発する環境でどのように 機能開発を止めずに Nuxt のメジャーアップデート に取り組んだのかご紹介します。

背景

今回、アップデートに取り組んだアプリケーションでは、1週間に1度の定期リリースが行われており複数のプロダクトチームが同じリポジトリで開発し、リリース前に品質保証テストと受け入れテストを行っています。 ここでは後に紹介するアップデートの方針を採用した背景を説明します。

1. 大きなリリースによるアップデートに失敗した

昨年、アップデート用のブランチを切って変更を集約する形で一気にNuxtをアップデートしようと挑戦しましたが、想定した期限内に完遂できませんでした。原因としては、以下の点が挙げられます。

  • 複数のプロダクトチームが並行して機能開発を行っているため、頻繁にコンフリクトが起きていた
  • 1回の変更量が膨大なことで認知負荷が高く、不具合を見つけた際の原因の特定に時間がかかっていた
  • 各プロダクトの機能開発ロードマップを考慮したリリース計画を立てる難易度が高い

2. 機能開発を止めずにお客様へ価値提供したかった

今回のアップデート対象となるアプリケーションには、ANDPADのコア機能である施工案件管理と黒板といった複数のプロダクトを含んでいます。
これらの機能は、既存機能の改善と新機能の追加が強く求められています。
最近では、黒板AI自動作成といった革新的な機能がリリースされ、お客様からの高い評価を得ています。このような魅力的な機能を継続的に提供し、さらなる改善を図るためには技術的なアップデートが欠かせません。
アップデートによるセキュリティやパフォーマンスの向上、新技術の採用などが重要である一方、プロダクト改善の施策が次々と控えているため、可能な限り開発を止めたくないというリクエストがありました。

以上のことを踏まえ、機能開発の手を止めることなく Nuxt3 へのアップデートが求められていました。

課題

これらの背景を踏まえた上で、本アプリケーションのアップデートには以下のような課題があると考えました。

リリース管理の複雑さ

複数のプロダクトチームが並行して機能開発を行っているため、リリース管理が非常に複雑になっています。この複雑さには以下の要因があります。

  • 開発チームとの連携
    アップデート作業による変更を適用しても並行している機能開発によって追加されたコードが適用した変更内容を考慮されていないものであった場合、意図しない挙動を生む恐れがあります。
    もし不具合が確認された場合、原因の特定に多くの時間がかかります。結果としてリリースを見送ることもあります。
  • リリース計画の調整
    今回の Nuxt アップデートといったシステム全体の変更を伴う作業では、各プロダクトの機能開発ロードマップを考慮したリリース計画が必要です。各チームのスケジュールと全体のリリース計画を調整するためには、デグレや不具合の影響範囲も考慮しながら計画を立てる必要があり、これが大きな負担となります。

取り組んだこと

この課題に対して、私たちは以下のように取り組みました。

1. 変更予定の内容と実施時期を透明化する

私たちは基本的にアプリケーションのリリースプロセスと同じ1週間のタイムボックスを設定し、アップデート作業による変更予定の内容を毎週Slackチャンネルで共有しました。変更予定の内容を開発チームに対して事前に共有することで、期待しない挙動があったときもアップデート作業による影響だと見当をつけてもらえる効果が期待できます。

変更予定の内容やテスト方針、注意してほしいことなどのアナウンスしている様子

また、タイムボックスの最初のタイミングで告知した変更を適用しました。こうすることで開発チームはアップデートした内容を含んだ状態で開発を進めることができ、その際にコンフリクトが発生しても迅速に対処できます。

2. アップデート作業による変更量を小さく頻繁にリリースする

背景で書いたように、一度に大きなリリースでアップデートを実施した場合、不具合を見つけた際の原因の特定に時間がかかります。 それに対して、小さな変更を頻繁にリリースする方法では変更1回あたりのインシデントリスクを抑え、問題の迅速な対処が可能になります。
具体的には以下のような取り組みで大幅な変更を極力しないですむようにしました。

  • Nuxt Brdige を利用してアップデートする
  • 互換レイヤーを作成してアップデートする
  • codemod script を作ってアップデートする

2.1. Nuxt Bridge を利用してアップデートする

「アップデート作業による変更量を小さく頻繁にリリースする」に基づいて、私たちは「Nuxt Bridge」を使ってアップデートしました。これは、Nuxt2 を使いながらも Nuxt3 の一部機能を先行して利用できるようにするツールです。

最初は bridge: false の状態で導入し、Nuxt bridgeの各オプションを切り替えることで互換性のある API を利用し段階的にアップデートを進めました。
詳しくは公式のアップデートガイドにも記載がありますが、大きく以下のステップで分けてアップデートを進めました。

  1. Nuxt v2.17に上げる(Vue 2.7に上げる)
  2. Nuxt Bridge を導入する
  3. 公式のアップデートガイドに沿って変更を適用する

2.2. 互換レイヤーを作成してアップデートする

Vue や Nuxt のメジャーアップデートに伴って、廃止される API や破壊的な変更を含む API があります。
多くの場合、廃止される API の代替となるような API に置き換えたり、破壊的な変更に合わせた実装へ変更します。
このように新しい API に合わせるにあたって、既存のロジックを変更することがあります。
平行して機能開発が進むので、マージ時のコンフリクトや変更漏れなどが頻発することが予想されます。

そこで、既存の実装におけるユースケースを満たす API を実装し、破壊的な変更をその内部で吸収するような互換レイヤーを作成しました。

例えば、Nuxt2 で提供されている useMeta といった関数は Nuxt3 からは useHead を利用する必要があるのですが互換性が維持されていません。 useMetaの実際の使われ方を調べてみると title プロパティのみを使っていたため、そのケースにのみ互換性を持たせた以下のようなコードを作成しました。

// capi.ts
import { useHead } from '@unhead/vue'

type InitializeGetter = () => { title: string }
type UseLegacyMetaParams = undefined | { title: string } | InitializeGetter
type UseLegacyMetaReturnValue<T> = T extends InitializeGetter
  ? undefined
  : { title: Ref<string> }
export const useLegacyMeta = <T extends UseLegacyMetaParams = undefined>(
  init?: T
): UseLegacyMetaReturnValue<T> => {
  if (init === undefined) {
    const head = { title: ref('') }
    useHead(head)
    return head as UseLegacyMetaReturnValue<T>
  } else if (typeof init === 'function') {
    useHead(init)
    return undefined as UseLegacyMetaReturnValue<T>
  } else {
    const head = { title: ref(init.title) }
    useHead(head)
    return head as UseLegacyMetaReturnValue<T>
  }
}
// useMeta を使っている page component
+ import { useLegacyMeta } from '~/vue2-compat/capi'

- useMeta(() => {
+ useLegacyMeta(() => {
  // ~~~
})

このように独自で作成した互換レイヤーを利用することで、各コンポーネントなどの実装を大幅に書き換えることなく破壊的な変更に対応しました。

2.3 codemod script を作ってアップデートする

互換レイヤーによって、いくつかの破壊的変更に対応できますが、例えば Vue SFC における構文レベルの変更 (.native modifier の廃止など) は互換レイヤーを実装できません。
このような変更に対しては直接的にコードを変更する必要があります。
コードの変換作業を codemod *1 として実装することで、再現性がある状態で機械的にコードを変換できます。
また、 codemod にはテストを定義できます。 既存の実装に意図しないような表現があり変換に誤りがあった場合でも、テストケースを追加しつつ codemod 自体を改善することで適切な変換に改善できます。
このように、手作業では努力をすることでしか実現が難しい品質の維持や変更のやり直しがやりやすくなるというメリットがあります。

さらに、「 codemod の追加 → 適用」といった2つのステップに分離できるので、リリースタイミングをコントロールしやすく、既存の開発に影響を与えずに安心してアップデートが行えました。

実際にどのようにして codemod を作っているのか詳細に関しては、別の記事として公開を予定しています。

おわりに

以上、 Nuxt3 へのアップデートについての取り組みをいくつか紹介しました。
Nuxt3 に限らずアプリケーション全体における変更を継続的に行う際に、機能開発の手を止めずリリースできるようにするための1つのやり方としてなにか参考になる情報が提供できていれば嬉しいです。

もし、この記事を読んで Nuxt なら任せてくれ といった方やアンドパッドに興味を持った Web フロントエンド 開発者がいらっしゃれば、ぜひお気軽にご連絡もしくはご応募ください。


hrmos.co

*1:ソースコードに対して何かしらのルールに基づいて変更を加える技術を指します。