KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

Next.jsのApp Routerを利用してフロントエンド開発を効率化した話

カケハシでMusubi Insightの開発を行っている高田です。

以前、Angular のプロダクトを React(Next.js)にリプレイスしました!という記事を書きました。

本記事はその続きとなりますが、以前の記事はどちらかというとプロジェクト管理的な内容がメインだったので、今回は技術面を紹介できればと思います!

App Router の導入

今回の移行プロジェクトで技術選定を開始したのが 2023 年の 4 月頃です。 技術選定を行なったタイミングではまだ Pages Router が主流でしたが、ちょうど技術選定が終わる頃 Next.js のバージョンが 13.4 となり、App Router が安定版としてリリースされました。

正直なところ、安定版になった直後で実績となるプロジェクトや参考情報が少ない技術を採用するのは若干の不安はありました。 しかし、今後 App Router が主流になっていくだろうということで思い切って Musubi Insight にも App Router を導入することに決定しました。

採用した感想としては、とても機能が豊富で、初めはそのルールを覚えるのが大変でしたが、一度覚えてしまえば直感的にルーティングが可能で開発体験としては非常に良かったように感じます!

(App Router 自体の内容に関しては公式ドキュメントを参照ください。)

Musubi Insight のディレクトリ構成

開発を進めていく中で試行錯誤を繰り返し、紆余曲折ありましたが最終的に以下のようなディレクトリ構成になりました。

app ディレクトリの中はページに関するコンポーネントのみとし、その他のコンポーネントやコンフィグ、hooks などは全て外に出し、app ディレクトリ内の可読性を保つようにしています。

さらに app ディレクトリの中はこのようになっています。

トップページやエラーページなどは app ディレクトリ直下に配置していますが、その他はルートグループを使用してログインやログアウトなどの認証関連のページと、ユーザーがデータを確認するためのダッシュボード関連のページに分けています。

ルートグループは、ディレクトリをパスに反映させることなく構造を整理でき、さらにルートグループごとに共通レイアウトを定義できるというメリットがあるので採用しました。

その他にも error.tsx や not-found.tsx など App Router の機能を積極的に活用しています。

効率化のために行なった工夫

ここでは、App Router を使って開発効率を向上させた工夫について紹介します。

Musubi Insight のページは大きく 2 種類に分類でき、薬局や薬剤師のデータの推移をグラフで確認する推移ページと、法人・薬局・薬剤師の指定期間のデータをテーブル形式で確認する組織集計ページです。

この 2 つのページが、様々な指標ごとに存在しています。

推移ページのサンプル

組織集計ページのサンプル

もちろん、この 2 つに分類できないページも一部存在しますが、基本的にはこの 2 種類で、さらに使用するコンポーネントも共通化してありほとんど同じであるため、ページをテンプレート化できないかと考えました。

そこで、ページの作成には React コンポーネントを作成するのではなく、ページ設定のためのコンフィグを定義し、そのコンフィグの内容を元に、必要なデータのフェッチやコンポーネントの出し分けを行えるような実装としました。

イメージが湧きづらいと思うので、以下に例を示します。

まず page.tsx は推移ページと組織集計ページでそれぞれ 1 つずつです。

Dynamic Routeを使用して、任意のパスでページにルーティングされるようになっています。

そしてその動的なパスを slug として引数で受け取り、slug をキーにページのコンフィグを取得します。

const HistoryPage = ({ params: { slug } }: Params) => {
  const config = PageConfigs.getBySlug(slug, "history");
  if (!config) {
    console.error(`Page: PageConfig not found: history/${slug.join("/")}`);
    notFound();
  }

  // ...
};

(少し蛇足ですが)page.tsxの引数で受け取ったslugの扱いについては少し検討が必要でした。

Musubi Insightでは、パフォーマンスの観点から各コンポーネントはできるだけクライアントコンポーネントは使わずサーバーコンポーネントとするという方針をとっています。

そのため、サーバーコンポーネントではHooksが扱えないという制約があるので、Hooksが必要となる状態管理ライブラリでの管理は行わず、コンポーネントにバケツリレーで渡していく方式を取るようにしています。

また一部抜粋した推移ページのコンフィグはこのようになります。

export const SamplePageHistory = MIPageConfig.History.Multiple.create<Keys>({
  title: "サンプルページ",
  apiResources: [
    {
      // APIのエンドポイント
      ...ApiEndpoints.samplePage.history,
    },
  ],
  toolbar: {
    // データの集計期間
    frequency: {
      selection: ["daily", "weekly", "monthly"],
    },
    // データの表示期間
    dataPeriod: {
      selection: [6, 13, 25],
    },
  },
  // グラフの設定
  createCharts: ({ monthlyResult }) => [
    MIChartConfig.create({
      id: "sampleChart",
      x: monthlyResult.get("date"),
      width: "full",
      layoutOptions: {
        title: "月次サンプルグラフ",
        helpIconKey: "サンプル",
        showPercent: true,
        showTotal: false,
      },
      chartOptions: {
        legend: "top",
        yAxisLabel: ["件数\n(件)", "割合\n(%)"],
        yAxisOrder: [Chart.Type.BAR, Chart.Type.LINE],
      },
    }),
  ],
});

この例ではパラメータをかなり絞って見せていますが、ページのタイトルやデータ取得元の API エンドポイント、ツールバーに表示する期間選択の選択肢、グラフの配置や色の指定など、かなり細かく設定することができます。

またこれらのコンフィグは型定義されているので、型安全に設定を追加することができるようになっています。

そして page.tsx ではこれらのコンフィグを読み込み、共通コンポーネントを出し分けることで 1 つのページが完成するようになっています。

const HistoryPage = ({ params: { slug } }: Params) => {
  const config = PageConfigs.getBySlug(slug, 'history');
  const { toolbar } = config;
  // ...

  return (
    <GraphPage
      PageTitle={<PageTitle title={config.title} />}
      SelectedNameTitle={<SelectedNameTitle />}
      Frequency={<Frequency value={toolbar.frequency.default} tabs={frequencyTabs} />}
      CopyButton={<CopyButton label='値をコピー' />}
      AnnotationContents={toolbar?.annotations && toolbar?.annotations.contents}
      PrescriptionType={toolbar?.prescriptionType ? <PrescriptionButtons /> : null}
      PqUnit={toolbar?.pqUnit ? <PqUnitTabs /> : null}
      ExternalLink={toolbar?.externalLink && toolbar?.externalLink.component?.()}
      ...
    >
      <Guard>
        <Main slugs={slug} />
      </Guard>
    </GraphPage>
  );
};

コンフィグを使ってページを作成していく方式をとったことで、開発効率が大きく向上したり、マークアップを得意としないバックエンドエンジニアであっても同様の品質でページを作成できるようになりました。

まとめ

本記事では、Musubi Insight を Angular から React(Next.js)に移行する過程で、Next.js の App Router をどのように活用していったかにフォーカスして紹介しました。

まだ参考となるプロジェクトや記事などが少ない中だったので試行錯誤が必要でしたが、移行後は開発効率向上、開発体験の向上などポジティブな面が多く、移行して良かったと感じています。

類似したページの多い Musubi Insight ならではの方法な部分もありますが、何かしら参考になれば幸いです。

最後まで読んでいただきありがとうございました!

参考資料

Next.js公式ドキュメント

Feature-Sliced Design(ディレクトリ構成の検討時に参考にしました)