🦔

小さなデプロイで大きな成果! Next.jsで実現するマイクロフロントエンド

2024/05/23に公開

はじめに

こんにちは、令和トラベルでフロントエンドエンジニアをしているyamatsumです。

ここでは複雑なWebアプリケーションを開発・保守する場合に有効なアーキテクチャパターンとして知られているマイクロフロントエンドをNext.jsを用いて実現した時の設計とその課題について紹介したいと思います。

※ この記事は令和トラベルのTech LT会で共有した内容を記事にしたものです。社外の方にもご参加いただけるTech LT会は connpass にて告知しています。

https://reiwatravel.connpass.com/event/313932/

マイクロフロントエンドとは

マイクロフロントエンドは、複雑なウェブアプリケーションを小さな独立したWebアプリやモジュールに分割するアーキテクチャパターンです。各Webアプリは異なるチームによって開発・保守することができ、一般に技術スタックやフレームワークの選択も自由です。

このWebアプリ群を組み合わせることで、以下のような利点が得られます。

  • 開発速度の向上: 独立したWebアプリの開発により、並行作業が可能になり、開発速度が向上します。
  • 保守性の向上: それぞれのWebアプリに焦点を絞ることで、保守性が向上します。
  • チームの自律性: それぞれのWebアプリを別のチームが担当することで、チームの自律性が高まります。
  • 技術スタックの自由度: 各Webアプリで異なる技術スタックを選択することができるため、最適な技術で開発することができます。

マイクロフロントエンドの設計

マイクロフロントエンドを実装する際には、主に2つの分割方法があります。

  1. 垂直分割

垂直分割は、画面全体を一つのモジュールとして開発し、そのモジュール内で機能ごとにサブモジュールを分割する方式です。例えば、SNSであれば、タイムライン、プロフィール、通知などの機能をサブモジュールとして開発することができます。

  1. 水平分割

水平分割は、画面を機能ごとに分割し、それぞれの機能を独立したモジュールとして開発する方式です。例えば、ECサイトであれば、商品リスト、カート、決済などの機能をそれぞれモジュールとして開発することができます。

どちらの分割方法が適しているかは、アプリケーションの要件やチームの構造によって異なります。

水平分割は、機能間の依存関係が少ない場合や、異なるチームがそれぞれの機能を開発する場合に適しています。一方、垂直分割は、画面全体のデザインやユーザー体験を統一したい場合や、同じチームで開発する場合に適しています。

NEWTにおけるマイクロフロントエンド

令和トラベルでは、海外旅行におけるあたらしい体験を目指す海外ツアー・ホテル予約アプリ「NEWT(ニュート)」を提供しています。

https://newt.net/

NEWTのWebアプリでは、Turborepoを活用したモノレポ構成で、ツアー予約、ホテル予約、旅行ガイドと3つのアプリを提供しています。NEWTにおけるモノレポ構成の詳細については、こちらをご覧ください。

https://zenn.dev/reiwatravel/articles/4f54c7670374a8

そんなNEWTではリスクの少ない小さなデプロイや、ドメインのコンテキストの境界ごとにアプリを分離することによる認知的負荷の軽減、現在の組織体制とそのスケールなどを考慮して3つのアプリをマイクロフロントエンドとして分割して開発しています。
ここでは実際にマイクロフロントエンドを実現している設計や、その課題などを含めて紹介したいと思います。

使用技術

  • Next.js(Pages Router)
  • Turborepo
  • GraphQL

垂直分割

NEWTのWebアプリはそれぞれが独立したNext.js製のアプリになっており、それらをまとめてTurborepoで管理しています。

ツアー予約のアプリがエントリーポイントとなり、ホテル予約(/hotel)、旅行ガイド(/mag)のそれぞれのbasePathへのリクエストを受け付けると、rewritesルールによるルーティングを行うようにしています。異なるアプリ間で情報を受け渡しする場合、揮発性データはクエリ文字列を利用するものの、全て同一ドメインになるため認証情報を含むcookieなどはアプリ間で共有できる形になります。

https://vercel.com/docs/edge-network/rewrites

https://nextjs.org/docs/app/api-reference/next-config-js/basePath

next.config.js
const addApp = (domain, basePath) => {
  return [
    {
      source: basePath,
      destination: `${domain}${basePath}`,
    },
    {
      source: `${basePath}/:path*`,
      destination: `${domain}${basePath}/:path*`,
    },
  ];
};
const nextConfig = {
  async rewrites() {
    return [
      ...addApp(HOTEL_DOMAIN, "/hotel"),
      ...addApp(MAGAZINE_DOMAIN, "/mag"),
    ]
  }
}

それぞれのアプリをマイクロフロントエンドとして分割することで、独立した小さなデプロイが可能になっています。アプリはVercelにホスティングを行っており、プレビュー環境の作成時には独立したデプロイにより、不要なデプロイは無視されつつ、必要なデプロイは小さく並列で走ることでデプロイ時間の短縮につながり、開発者体験が向上しています。

水平分割

NEWTにおける各LPはデザイナーが主体となってWebflowにより作成されています。基本的には静的なコンテンツになるため、APIへのリクエストなどは行われないですが、旅行という商品の特性上、価格を含むツアーカードなど動的なコンテンツを表示したい要件があります。

この要件に対して、NEWTではiframeを利用した水平分割を行っています。
NEWT本体ではツアーIDを含むエンドポイントを提供し、Webflow側のiframeで呼び出すことで対応するツアーカードを表現できます。
これによってNEWT本体のツアーカードコンポーネントを再利用しつつ、デザイナーのチームとエンジニアのチームが完全に独立して成果物を管理できるようになっています。

またNEWT本体で管理しているツアーカードについては、Incremental Static Regeneration (ISR)により指定した時間だけキャッシュされるようにしているため、LPへのリクエストごとにAPIへのリクエストが発生しないようにもなっています。

マイクロフロントエンド間のインタラクションがほぼないことやツアーカードのサイズが固定であり、流動的なレイアウトにはならない点など、iframeをマイクロフロントエンドに採用した場合の欠点を回避できる要件のため、採用できているアーキテクチャともいえます。

課題とその対策

コードの重複

マイクロフロントエンドにおいてはコードの重複は悪いアプローチではなく、むしろ闇雲な共有化の方が良いアプローチとはされていません。なぜなら一元化されたライブラリまたはコンポーネントは、複数のチームの利用にまたがる開発体制では、コードに変更があった場合は全てのチームで変更を適用しないといけなくなるためです。

しかし、現在の令和トラベルのフロントエンドチームは、一つのチームで全てのアプリを管理する体制をしいています。また、ロジックを含めて重複したコンポーネントが多数存在していることから、デザインシステムとして利用されるコンポーネントライブラリとは別に、ビジネスロジックを含む共通ライブラリを切り出すことでマイクロフロントエンド間のコードの一元化を図っています。

Next.js固有のrewritesやbasePathの問題

rewritesやbasePathにはバグが見られることが多く、例えばこのIssueを見ると

https://github.com/vercel/next.js/issues/56368

今回の例の様にNext.js製のアプリからNext.js製のアプリにrewritesルールを適用した場合、開発環境においてエラーログが無限に生成されるというバグが存在します。
このバグについてはHMR(Hot Module Replacement)が問題に寄与しているらしく、ワークアラウンドとしてHMRのリクエストをmiddlewareで除外することで対応しています。

middleware.ts
export const config = {
  matcher: [
    "/((?!/_next/webpack-hmr).*)",
  ],
};

また、basePathに関しては例外が許容されず、basePathのルールに当てはまらないエンドポイントでrewriteなどを行った場合、強制的にbasePathが付与されます。例えば、ホテル予約アプリにはbasePathとして/hotelが設定されている状態で、以下のrewritesルールを適用すると期待値としてはrewritesルール適用後のエンドポイントは/sampleになりますが、結果は/hotel/sampleとなります。つまりbasePathを設定する場合は、そのbasePathのルールから外れることがない前提でURL設計を行わなければなりません。

next.config.js
...
  {
    source: `/sample`,
    destination: `${HOTEL_DOMAIN}/sample`,
  },

また、build成果物に対してもbasePathが付与されるため気をつけなければいけません。

build成果物 生成後のパス
next/image /[basePath]/_next/image
ISRのキャッシュ /[basePath]/_next/data

観測性の低下

Vercelを用いたマイクロフロントエンドアーキテクチャでは、モノリシックアーキテクチャと比べて、以下の観測性の課題が発生します。
その主たる課題がログの分散です。各マイクロフロントエンドアプリは独立したデプロイ単位となり、ログも分散的に生成されます。
これにより、全体的なシステムの状況を把握するためには、複数のログを収集し、分析する必要があります。

その課題を解決するためにLogDrainを活用してCloud Loggingにログを集約しています。
LogDrainは、Vercelで生成されたログを外部のロギングサービスに転送するための機能です。これにより、VercelのログをCloud Loggingで集中管理し、分析や可視化を行うことができます。

https://vercel.com/docs/observability/log-drains

まとめ

マイクロフロントエンドは、複雑なウェブアプリケーションを開発・保守する場合に有効なアーキテクチャパターンですが、導入にはいくつかの課題もあります。特にNext.js特有のrewritesやbasePathを利用する場合には、注意すべきポイントがいくつかあり、これらの課題を理解した上で、慎重に検討する必要がありそうです。

令和トラベル Tech Blog

Discussion