UPSIDER Tech Blog

UPSIDERのフロントエンドをNuxtからNextへリプレイスしている話

はじめに

こんにちは、株式会社UPSIDERでフロントエンドチームに所属している久保です。

今回は、リリースから走り続けてきた法人カード「UPSIDER」(以下、UPSIDER)のフロントエンドを、NuxtからNextにリプレイスしている話を共有します。

リプレイスに至った背景

背景はいくつかあるのですが、主にNuxt3へのリプレイスコストが大きかったことが背景としてあげられます。UPSIDERはNuxtの2系で実装されており、TypeScriptを使用するためにvue-property-decoratorを使用しています。Nuxt3では元々破壊的変更が大きい中で、classベースのコードからNuxt3へのリプレイスコストは払えないという判断をしました。仮に、Nuxt3にリプレイスできても3系に対応していないライブラリの再選定も必要になり単純なリプレイスでは収まらない可能性がありました。

Next.jsを採用した理由

まず、リプレイスコストを最小に抑えつつ将来的に実現したいことが可能かどうかを判断軸としました。

リプレイスコストの観点では、vueの構文からjsxになるため1からのコーディングにはなりますが、vueをサポートしているライブラリはreactもサポートしてるものも多く、ライブラリをそのまま流用できるといったことからリプレイスコストを下げられるといった判断をしました。

また、将来的にLPはSSGを使用し静的なものとして配信したいといった構想があるためページ毎にSSGを実現できることは必須でした。

これらの判断を加味し、総合的に見てもエコシステムの充実度や実用例の数からデファクトスタンダードになりつつあるNext.jsを採用しました。

リプレイス後の技術

  • Next.js@13系
  • MUI@5系
  • Storybook@6系
  • react-query@3系
  • zod@3系
  • React Hook Form@7系

また、スキーマ駆動での開発を行なっているため、OpenAPIを用いたスキーマ定義から型定義とapi clientの自動生成も行なっています。

リプレイス戦略

リプレイス方法

UPSIDERは全体で約120のページが存在しています。そのため、全てをリプレイスしてからリリースするという、ビックバンリリースは実装やQA観点からもコストが大きいという判断をし採用を見送りました。他にも、マイクロフロントエンド的にリプレイスしていく案もありましたが、基盤が複雑になることを筆頭に現段階では諸々の恩恵を受けづらいのでは?となりこちらも採用を見送りました。

最終的には、ページ毎にリプレイスをしていくという一般的な手法を採用しました。routingに関しては、本番はIstioを用いてAというpathはNextへ、BというpathはNuxtにroutingさせています。開発環境に関しては、node.jsで簡単なプロキシを立てpathに応じてNextとNuxtにroutingさせています。

Screaming Architectureの導入

リプレイスするにあたりディレクトリ構成も見直しました。Nuxtのディレクトリ構成はファイルタイプで管理していく構成をとっており、ページ数やコンポーネントが多くなるにつれて見通しが悪くなっていました。また、Nuxtではほとんどが規約化されていないことも拍車をかけ、実装者に依存する形になっておりファイルタイプをどこで管理するのかという点においても一貫性がない状態でした。

これらを解決するためにScreaming Architectureを採用しました。(Features directoryとも呼ばれている認識です)

dev.to

簡単に説明すると、対象のFeatureに関連するファイルはFeature内に閉じ込めることを意識してディレクトリを構成しファイルを配置していきます。そのため、関連するファイル群が散らばりにくくなっており、対象ディレクトリの中を見れば、ほとんどはそのディレクトリの中にあるという状態を実現できます。

src/
└── features/
    ├── some-feature-A/
    │   ├── component-A/
    │   │   ├── index.ts
    │   │   ├── use-some-hook.ts
    │   │   ├── type.ts
    │   │   ├── styles.module.scss
    │   │   └── component-A.tsx
    │   ├── component-B
    │   └── component-C
    └── some-feature-B/
        ├── component-A
        ├── component-B
        └── component-C

もちろん、Feature間で共有したいロジックやコンポーネントは存在するため、src/componentsやsrc/utilsなどのグローバルのディレクトリは存在しているのですが、基本的にはFeatureの中で閉じるルールになっているため今の所ディレクトリの肥大化は抑えられています。

スキーマ駆動開発の導入

今までは、APIのドキュメントがなく実装者が直接responseやコードを見て型定義をしていました。新しいAPIに関しては、Notionにスキーマを書いて共有するという形をとれていたのですが、開発者が型定義しないといけないことは変わりはありませんし、時間が経つとスキーマを書いているNotionも見つけづらい状態になっていました。

上記の問題を解決するため、OpenAPIによるスキーマ定義を導入し、定義したスキーマから型とapi clientを自動生成もできるような仕組みを整え、実装者が開発に集中できるような環境を作りました。

また、スキーマのexamplesをjsonに変換する仕組みも整えることにより、フロントエンドがバックエンドの開発に依存しなくなった点も大きなメリットを感じています。

Storybookを使ったInteraction testの導入

今までは、実装したものは全て実装者およびQAで確認作業を行うというフローになっていました。しかし、これは資産にならずプロダクトが肥大化するにつれ確認作業も膨大な量になっていき、確認コストがかかり続けリソースを消費し続けてしまいます。

上記の問題を解決するためNext側ではStorybookを使ったInteraction testを導入しました。

storybook.js.org

導入した結果、当初課題としていたQAリソースの圧迫は軽減に向かっています。また、リファクタにおいても効果を発揮しており、テストが存在することによって安心してリファクタができる環境が整ってきています。

まとめ

「UPSIDERのフロントエンドをNuxtからNextへリプレイスしている話」を共有しました。

Nuxtが支えてきた基盤に敬意を払いつつ、より良いプロダクトを実現するためにNextへリプレイスしています。

今回は概要などの軽い紹介でしたが、今後はより深い部分も紹介できればと思っています。

宣伝

UPSIDERでは、成長企業のための法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できる請求書カード払いサービス「支払い.com」を提供しており、どの方面においてもエンジニアの採用を行なっております。

カジュアル面談もやっておりますので、少しでもご興味のある方は、ぜひご連絡ください!

herp.careers