新規プロダクトの開発に Nuxt 3 を採用して良かったこと

ANDPADフロントエンドエンジニアの小泉です。

昨年の夏頃、担当したプロダクトで新規リポジトリでの開発を立ち上げる機会があり、Nuxt 3 を用いて構築を行いました。

アップデートではなく新規で Nuxt 3 サイトを構築するのは業務としては初めての経験だったのですが、Vue 3・Nuxt 3 の様々な機能によって、型安全な状態を保ったまま快適な開発を進められ、かつ3ページの全体実装を約7営業日で形にすることができました。

この記事では、「いま新規サービスのゼロからの立ち上げにNuxt 3を選択するとどんな嬉しいポイントがあるのか」という実例をご紹介できればと思います。

担当したプロダクトについて

ANDPAD資料承認 | 製品のご紹介

2023年10月にリリースされた「ANDPAD資料承認」という、資料の申請・承認を一元管理する機能のフロントエンド開発を担当しました。

ただし、紹介サイトに掲載されている承認ページそのものは Next.js で作られており、申請の内容や所属部署などに応じた承認フローをカスタマイズするための「承認フロー設定」画面が、Nuxt 3 によって開発されている部分です。

一覧画面、詳細画面、編集画面の3ページがあり、一覧は検索や並び替えに対応しています。

編集画面では承認者・申請者・閲覧者の条件をそれぞれ設定し、承認者は1次・2次・3次……とステップごとに承認できるユーザー条件を細かくカスタマイズすることができます。

Nuxt 3 を選んだ理由

まず、この機能の開発が比較的タイトなスケジュールであり、かつ初期のフロントエンドの実装は私1人で行うことになっていたため、最もスピードを出せる構成として、自分が使い慣れていて、他のチームで開発しているプロダクトと同じ Nuxt 3 を選ぶのは自然な流れでした。

自分以外のメンバーが今後メンテナンスする場合についても、ANDPADのプロダクトの多くは Nuxt で開発されており、社内に Vue エンジニアも多いことから、保守性の観点でも特に懸念はありませんでした。

 

React・Next.js も選択肢にはありましたが、要件的にSSRを行う予定がなく、そのためにインフラを構築する利点も特になかったため、スタンダードな構成でNext.jsを活かすことは難しそうでした。

また、React・Next.js はちょうど Server Components や App Router といった新技術の登場によってベストプラクティスが大きく更新されようとしている時期であり、中途半端な知識でとりあえず動くものが作れたとしても、将来的な負債になってしまう可能性が高いと考えました。

これが Vue 3・Nuxt 3 の正式リリース前、2020~2022年頃の過渡期の状況が今も続いていたら少し悩んだかもしれません。しかし、現在の Vue 3・Nuxt 3 は、パフォーマンス・開発しやすさ・TypeScriptとの相性・関連ライブラリの選択肢などといった、ここ数年指摘されていた問題があらゆる面で解消されており、他のフレームワークと比べても十分に安定しているため、敬遠する理由は特にないと判断しました。

Nuxt 3 の新機能や魅力についてより深く知りたい方は、手前味噌ではありますが、私が以前執筆した記事も読んでみて頂ければと思います。

zenn.dev

 

ちなみに、承認機能そのものを Next.js で開発しているのは、別のメンバーで以前から開発されていたものを引き継ぐ形となったからです。

いくら Vue や Nuxt が慣れていて使いたいと思っていても、既に動いているシステムの Next・Nuxt 間のリプレースをわざわざコストをかけて行う合理的な理由にはならないので、こちらは Next.js のまま開発を続けています。

また、社内で開発されているデザインシステム Tsukuri が Vue と React の両方をサポートしているため、フレームワークによらず統一されたUXを担保できるというのも、このような意思決定の後押しとなりました。

tech.andpad.co.jp

実際、製品紹介ページの画面写真を見ても、機能の 1~3 が React で、4 のみ Vue で作られている別サイトであるというのは、なかなか気付きにくいのではないかと思います。

Nuxt 3 を採用して良かったこと

ライブラリの選定で悩むことがほとんどなかった

Vue 3 のプロジェクトにおいては標準的に選ぶべきライブラリが明確であることが多いため、どのライブラリを使うかで悩むことがほとんどないのも嬉しいポイントです。

Nuxt 自体は言うまでもなく、データストアのPinia、テストツールのVitest、VSCode拡張のVolarなど、TypeScriptファーストで扱いやすいライブラリが新たなスタンダードとして提供されているため、迷わず使うことができます。

また、Web標準であるCSSに対応しているため CSS in JS フレームワークを別に検討する必要がなかったり、 useAsyncData() によって SWR に近いデータやローディング状態の管理を行えたりと、そもそも Vue・Nuxt がデフォルトでカバーしている領域が非常に広いため、実装以外に使う時間を短縮できました。

そして何といっても Composition API ベースのユーティリティツール詰め合わせ VueUse が本当に便利です。

vueuse.org

こちらを初手でとりあえず入れておくだけで、様々な機能を自分で実装することなく利用できるようになります。

結果的に今回使ったのは useElementVisibility() useElementSize() くらいでしたが、どちらも自前で再実装するのは地味に骨の折れる機能なので、VueUseを利用できて良かったです。VueUse は Tree Shaking に対応しているので、ビルドサイズが肥大化することもありません。

defineModel() で、コンポーネント分割が簡潔・合理的に行える

Nuxt 3.5(Vue 3.3)以降の script setup 構文で使えるようになった defineModel() は、同じ名前の defineProps()defineEmits() を簡潔に一括で指定できる新たなコンパイラマクロです。

コンポーネントで v-model を利用する際にこれまでは definePropsdefineEmits で文字列を揃えながら別々に指定する必要がありました。

それに対して、defineModel() を使うとリアクティブな1つの変数として宣言でき、自動的にそれぞれが同じ型になり、hoge.value = 'fuga' と書くことで親コンポーネントに emit されます。(ref や 書き込み可能な computed と同じ挙動)

<script setup> | Vue.js

この defineModel を使うことで、単純に記述量が減るのももちろん嬉しいのですが、defineModel() で宣言している変数は子コンポーネントでは責任を持っていないことが明確になるので、保守性・安全性の点でも優れていると思いました。

 

また、単純な入出力に特化したコンポーネントが書きやすくなることは、コンポーネントを気軽に分割するハードルを下げることにも繋がります。

よく、React などと比較した Vue の問題点として「1ファイルに複数コンポーネントを宣言できない」点が指摘されることがあります。

しかし、コンポーネントごとに必ず別ファイルに分かれていることは、1ファイルの肥大化を防ぎ、処理を追いやすくなるというメリットでもあるはずです。

ところが Options APIdefineComponent の時代は、「せっかくファイルを分割しても分割先のファイルのコード量が多くて読みづらい・書くのが面倒」という問題があり、コンポーネントの肥大化・冗長化に繋がりがちでした。

script setupdefineModel() を使うことで、子コンポーネントのコード量・実装負荷が大きく軽減され、読みやすさ・追いやすさを損なわずにコンポーネントを分割できるようになります。これは、 大規模開発において Vue を積極的に選ぶ理由の1つになる、大きな改善だと感じました。

なお、Nuxt 3.5 時点では実験的機能であり、Nuxt Config で有効化する必要がありました。

export default defineNuxtConfig({
  vite: {
    vue: {
      defineModel: true,
    }
  }
})

2023年末にリリースされた Vue 3.4 で正式機能になったので、Vue 3.4 / Nuxt 3.9 以降は設定不要で使えます。

正式機能になると同時に、コンポーネントの v-model 指定において最も推奨される書き方として大々的に推され始めたので、今後見る機会が増えてくると思います。気になる方はぜひ下記のドキュメントを読んでみてください。

コンポーネントの v-model | Vue.js

Typed Pages で型安全なルーティングを導入できた

Nuxt 3.5 のアップデートで実験的機能として登場した Typed Pages という機能も凄いです。

export default defineNuxtConfig({
  experimental: {
    typedPages: true,
  },
})

このオプションをオンにするだけで自動的に

  • useRouter()$router.push()<NuxtLink>to プロパティなどで存在するページのみをサジェストしてくれる
  • $route.name が string ではなく存在するパスの enum になる
  • useRoute() でページパスを指定することで、$route.params でURLの動的ルートに存在しないキーへのアクセスを型エラーにできる

といった恩恵を受けられます。

詳しい使い方は以下の記事にまとめていますので、興味のある方はぜひこちらも読んでみてください。

zenn.dev

型システムが発達していく中で、 Router 周りだけはどうしてもインデックス型などを使ったアクセスに頼らざるを得なかったのですが、この Typed Pages によって非常に型安全にルーターを利用できるようになりました。

Nested Routes による入れ子構造でのレイアウト管理を簡単に導入できた

Nested Routes は、URLパスの構造とページレイアウトの構造を対応させ、入れ子のようにページレイアウトを組むことができる機能です。(実は Nuxt 2 から使えたようなのですが、今まで使ったことがありませんでした)

機能そのものの考え方については、Nuxt ではなく Vue Router のドキュメントを読んだ方が理解しやすいです。

Nested Routes | Vue Router

/user/johnny/profile                     /user/johnny/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

Remix や Next.js などのフレームワークでサポートされている機能と基本的な考え方は同じなので、それらを使ったことがあればイメージしやすいと思います。

Nuxt における Nested Routes は、SSR以外の環境でも問題なく使える上に、これまでの pages/ ディレクトリの使い方の延長線上で導入できるため、気軽に使い始めることができました。

pages/ · Nuxt Directory Structure

Nuxt 3 で Nested Routes を使いたい場合、以下のように、フォルダ名と同じ名前のコンポーネントを配置します。動的パスの場合も同じです。

> pages/
  > abc/
    > def/
      > [id]/
        > index.vue
        > edit.vue
      > [id].vue
    > def.vue
  > abc.vue

abc.vuedef.vue のテンプレートタグでは、通常の pages 配下のページコンポーネントと同じように、 <NuxtPage /> を置いた部分に子ページのコンポーネントが描画されます。

<template>
  <div>
    <header></header>
    <NuxtPage />
  </div>
</template>

この状態で /abc/def/123456/edit/ のようなページにアクセスすると、自動的に abc.vuedef.vue といった親ページコンポーネントを全て通って描画されるので、abc.vue でヘッダーを定義して、def 配下で共通のサイドバーを描画して……といったことが簡単にできます。

もちろん、 Nested Routes を使わない場合は、 abc.vuedef.vue を削除して今まで通りにするだけです。

今回のプロダクトの場合は、承認フローの中身を表示する /approval_flows/[id]/ と編集する /approval_flows/[id]/edit/ で、データを取得してストアに保存する処理や、共通のコンポーネントを読み込んでいる箇所を [id].vue に書いています。

その上で、データをサーバーに保存する処理は /approval_flows/[id]/edit.vue に書くなど、/approval_flows/[id]/index.vue とページコンポーネントを分けることで、ページの共通部分と個別部分に整理しています。

今回は3ページだけなのでそこまで活用しきれていませんが、これからページが増えていくにつれて恩恵を受ける機会が増えそうだと感じました。

ビルド~デプロイが1分半で完了した

ANDPADでは多くのフロントエンドのデプロイ環境にAmplifyを採用しているのですが、Amplify 上でのビルドからデプロイまでわずか1分半で完了しました。自分が他に担当しているプロダクトでは5~7分ほどかかっていたので、かなり高速です。

新規リポジトリとはいえ Vue コンポーネントの数は40個強と少なくはなく、それがこの速さでデプロイまで完了するというのは、体験として非常に快適でした。

amplify.yml では ~/.npm ディレクトリのキャッシュを設定したくらいで、特に最適化などは行っていません。

これは2023年9月時点での話なので、Vue 3.4 でのパフォーマンス改善や、より高速なパッケージマネージャーの採用などによって、もしかしたらさらに速くできる余地があるかもしれません。

Mastodonクライアント「Elk」が実装の参考になった

Elk は2023年1月に公開されたNuxt 3 製の Mastodon クライアントで、Vue・Nuxt 開発のコアメンバーでもある 三咲智子 (Kevin Deng)氏Anthony Fu 氏らによってオープンソースとして開発されています。

GitHub - elk-zone/elk: A nimble Mastodon web client

Vue・Nuxt のコントリビューターによってメンテナンスされているため、規模の大きな Nuxt 3 プロジェクトのリファレンスとして、ドキュメントを読むだけではわからない部分も含めた知見が詰まっていて、非常に参考になりました。

特になるほど! と思ったのが、 components 配下のファイル名の命名ルールです。

elk/components at main · elk-zone/elk · GitHub

「ディレクトリ名とコンポーネント名の先頭が完全一致していると、AutoImportで参照されるコンポーネント名では繰り返しが省略される」という仕様があることに、リポジトリを眺めていて初めて気づきました。

例えば、

components/account/AvatarIcon.vue
components/account/AccountAvatarIcon.vue
components/account_avatar/AccountAvatarIcon.vue

これらのコンポーネントは、全て <AccountAvatarIcon> という名前で自動インポート(.nuxt/components.d.ts)に登録されて呼び出せるようになります。

これを利用すると、ファイル名単体で意味を表現しながら、自動インポートの名前を短くすることができます

ファイル名を末尾だけにすると、VSCodeなどのエディターのタブに表示される名前がわかりにくくなり、同じファイル名のコンポーネントが乱立してしまい通常のインポートが難しくなるという問題がありますが、この方法を使えばそれを避けられ、また命名に悩むことも減って非常に開発しやすくなりました。

ちなみにこのファイル名のネーミングルールは、Nuxt公式ドキュメント の components/ ディレクトリのページでも推奨されています。

おわりに

Nuxt 3 がリリースされて約1年が経ち、特に Nuxt 2 からの移行については苦労話も多く聞こえてきますが、それは Vue 3 / Nuxt 3 の技術基盤がゼロから刷新されていることの副作用であり、新規プロダクトの立ち上げに用いる Nuxt 3 は非常に強力な選択肢であるということを、今回の開発で再確認しました。

この記事を読んでくださった方にも、Nuxt 3 での新規開発にポジティブな要素がたくさんあることを感じてもらえたら嬉しいなと思います。

もちろん、他のフレームワークにも利点がある……というより今はどのフレームワークも優秀なので、どれを選んでも大きく困ることはないと思います。しかし、だからこそ流行やイメージに左右されずにチームや製品にとって最適な技術を選び、かつその技術でしっかりと価値を出せることが重要だと考えています。

そういった意味で、今回は Nuxt 3 だからこそできるスピード感のある開発を行いながら、型安全性などの保守性・パフォーマンスなどの品質も決して犠牲にしていない、バランスの良い技術選定ができたと思っています。

ANDPAD 内でも、 Next.js を採用しているプロダクトも引き続き増えていますし、一方で Nuxt 3 を新規採用したプロダクトも開発されています。

それぞれのチームで進んでいる部分の知見を交換できるのも、様々な機能・プロダクトが同時に開発されているANDPADの開発チームならではの魅力です。

アンドパッドでは、「幸せを築く人を、幸せに。」というミッションの実現のため、一緒に働く仲間を大募集しています。 会社や事業、開発チームにご興味を持たれた方は、下記のサイトをぜひご覧ください!

engineer.andpad.co.jp