every Tech Blog

株式会社エブリーのTech Blogです。

Hygen で加速する Next.js App Router 開発

はじめに

こんにちは、retail HUBで Software Engineer をしているほんだです。
この記事は、every Tech Blog Advent Calendar 2023

tech.every.tv

20 日目の記事です。他にもたくさんの記事が掲載されているのでぜひ確認してみてください。
今回は私が入社後初めて技術選定から参加した retail HUB 小売りアプリのフロントエンド開発で効率化またプロジェクト参加のハードルを下げるために導入した Hygen について紹介しようと思います。
この記事では Next.js App Router, Atomic Design, 複数サービスが統合されたプロダクトで生まれた課題点を Hygen の対話型コード生成機能でどのように解決したかまたどのように導入したかについて紹介していこうと思います!

Hygenとは?

Hygen は、開発プロセスを加速するシンプルかつ高速なコードジェネレーターです。プロジェクト固有のニーズに合わせて、またはグローバルな設定で利用することができ、繰り返し行う作業を自動化します。
generator を使って、質問文とテンプレートファイルを活用して、新しいファイルを生成します。Hygen には一般的なコマンドライン引数を用いた方法と対話的な入力方法の二種類でコードジェネレートする方法が存在し用途によって使い分けることが可能です。

Hygen を用いて解決したいこと

私が開発を進める上で直面した課題と、それを解決するために Hygen を導入することを考えた理由について説明します。Hygen を導入することで解決したい課題は主に二つあります。
一つ目は、React Server Component (RSC)の運用に関する課題です。Next.js App Router では、デフォルトのコンポーネントが RSC となっています。RSC では、ブラウザ専用の API や hooks が使用できないなど、Pages Router を用いたコンポーネント開発よりもより多くのことを意識する必要があります。私たちの会社では、App Router を用いたプロダクトが他にはなく、初めて App Router に触れる人が開発を始める際のハードルを下げること、そして必要な考慮点を減らすことが重要だと考えました。
二つ目の課題は、ディレクトリ構成に関するものです。今回のプロジェクトでは、異なるユースケースを持つ複数のサービスが同じ場所に存在する形となっており、さらに Atomic Design を採用しているため、コンポーネントの適切なディレクトリ構成を決定することが難しくなっていました。
これらの課題を解決するために、Hygen の導入を検討しました。

導入方法

install

初めにプロジェクトに Hygen を導入し npm run で実行できるように scripts に追加します。

$ npm i -D hygen
{
  "scripts": {
    "hygen": "hygen"
  }
}

初期化

次に Hygen の初期化を行います。

$ npm run hygen init self

初期化を行うことで generator, init という 2 種の generator と generator に help, new, with-prompt という 3 種の action,init に repo という action が作成されます。
generator は action の集合となっています。generator の action を指定することで実際に自分で作成した処理または初期化で作成された generator を作成するための処理を実行することが可能です。
初期化で作成された generator new または with-prompt action が新たな generator を作成するための選択肢となっていて new はコマンドライン引数を用いた generator, with-prompt は対話を用いた generator を作成するために利用します。

_template/
|-- generator/
|   |-- help/
|   |   |-- index.ejs.t
|   |-- new/
|   |   |-- hello.ejs.t
|   |-- with-prompt/
|   |   |-- hello.ejs.t
|   |   |-- prompt.ejs.t
|-- init/
|   |-- repo/
|   |   |-- new-repo.ejs.t

generator を用いるには下記のように generator_name, action_name のペアを指定する必要があります。

$ npm run hygen [generator_name] [action_name]

対話型コードジェネレーターの作成

新規ジェネレーターの作成

次にコンポーネントを開発するための generator と action を作成します。今回は対話型コード生成の機能を用いるため generator の with-prompt を選択し generator を作成します。

$ npm run hygen generator with-prompt --name component

プロンプトの作成

次に作成された component generator の with-prompt action の対話的な処理を行うためのファイル(prompt.js)を修正します。

_template/
|-- component/
|   |-- with-prompt/
|   |   |-- hello.ejs.t
|   |   |-- prompt.js    <-- fix

prompt.js には複数の prompt を設定することができます。prompt の形式、利用可能な type については enquirer を確認してください。

{
  // required
  type: string | function,
  name: string | function,
  message: string | function | async function,

  // optional
  skip: boolean | function | async function,
  initial: string | function | async function,
  format: function | async function,
  result: function | async function,
  validate: function | async function,
}

今回は選択式の質問を 2 種、自由入力の質問を 2 種、確認式の質問を 1 種の合計 5 つの質問を準備します。
また以降のテンプレートファイルで利用する対話的な質問の回答と回答をもとに作成した変数を作成します。

module.exports = {
  prompt: ({ prompter }) => {
    return prompter
      .prompt([
        // 対象のサービスについて
        {
          type: "select",
          name: "service_name",
          message: "Please select the service name.",
          choices: ["common", "mart", "users"],
        },
        // Atomic Designのステージを選択
        {
          type: "select",
          name: "stage",
          message: "Please select the stage of Atomic Design.",
          choices: ["atoms", "molecules", "organisms", "templates"],
        },
        // ステージ以下の詳細なディレクトリ名を入力
        {
          type: "input",
          name: "dir",
          message: "Please enter the detailed directory name.",
        },
        // コンポーネント名を入力
        {
          type: "input",
          name: "component_name",
          message: "What is the name of component?",
        },
        // クライアントコンポーネントとして作成するか確認する
        {
          type: "confirm",
          name: "component_type",
          message: "Is it client component?",
        },
      ])
      .then((answers) => {
        const { service_name, stage, component_name, dir, component_type } =
          answers;
        // 入力したサービス名、Atomic Designのステージ、詳細なディレクトリ名をもとにパスの作成を行います。
        const path = `${service_name}/${stage}${dir ? `/${dir}` : ``}`;
        const abs_path = `src/components/${path}`;
        return { ...answers, path, abs_path };
      });
  },
};

テンプレートファイルの作成

最後に対話によって得られた回答をもとに出力するために用いるテンプレートファイル(hello.ejs.t)を修正します。
出力ファイルを tsx にするために拡張し tsx を追加します。 Hygen では change-case ライブラリを使うことで文字列のケースを容易に変更することができます。今回の例ではh.changeCase.pascalCase(component_name)を用いることでcomponent_nameをパスカルケースに変換しています。コンポーネント名はパスカルケースにしたいがファイル名はスネークケースにしたいといったケースにも対応できるため一度目を通しておくことを推奨します。
今回は説明のために hello.tsx.ejs.t にコメントを追加していますが実際のテンプレートファイルにコメントを記述しておくとそのコメントも出力されてしまうのでコメントを削除して利用してください。

_template/
|-- component/
|   |-- hello.tsx.ejs.t <-- fix
|   |-- index.js
---
# ファイルの出力先を設定します。
to: <%= abs_path %>/<%= h.changeCase.pascalCase(component_name) %>.tsx
---
# クライアントコンポーネントが選択された場合'use client';を設定します。
<% if (component_type) { -%>
'use client';
<% } -%>
import React from "react";

# 入力されたコンポーネント名をパスカルケースに変換して設定します。
const <%= h.changeCase.pascalCase(component_name) %> = () => {
  return (
  );
}

export { <%= h.changeCase.pascalCase(component_name) %> }

generator と action の作成はここまでで終了です。Hygen の説明にある通りシンプルかつ高速に対話型コードジェネレーターが作成できたのではないでしょうか。次の章から実際に作成したものの使い方を説明していきます。

実際に使ってみる

作成した generator と action を実際に使ってみましょう!
下記のコマンドを入力することで component generator の with-prompt action が実行されます。

$ npm run hygen component with-prompt

一問目は対象サービスを選択する質問が表示されます。

> test@0.1.0 hygen
> hygen component with-prompt

? Please select the service name. …
❯ common
  mart
  users

二問目は Atomic Design のステージを選択する質問が出力されます。

✔ Please select the service name. · common
? Please select the stage of Atomic Design. …
❯ atoms
  molecules
  organisms
  templates

三問目は詳細なディレクトリ名を入力する質問が出力されます。

✔ Please select the stage of Atomic Design. · atoms
? Please enter the detailed directory name. › test

四問目はコンポーネント名を入力する質問が出力されます。

✔ Please enter the detailed directory name. · test
? What is the name of component? › test-test

最後にクライアントコンポーネントを用いるかの選択をする質問が出力されます。デフォルトは false になっています。

✔ What is the name of component? · test
? Is it client component? (y/N) › true

以上の対話に答えていくことで対象のコンポーネントを適切なサービスの適切なステージに一致するディレクトリ内に生成することができます。

Loaded templates: _templates
       added: src/components/common/atoms/test/testTest.tsx

作成されたファイルは以下のようになります。
クライアントコンポーネントに関する回答通りファイル先頭に'use client';が入力されコンポーネント名で入力した test-test がパスカルケースの testTest というコンポーネント名になっていることがわかります。

'use client';
import React from "react";

const TestTest = () => {
  return (
  );
}

export { testTest }

感想

Next.js App Router を使ったプロジェクトでは、Page Router に慣れた人ほど'use client';を忘れたり、サーバーコンポーネントで web-only API を使用して意図しない動作につながることがあります。実際に後から App Router を導入し始めたチームメンバーではまっている人がいたので Hygen を活用することで開発速度を向上させることができると思います。
また今後 storybook を用いた VRT や単体テストを行う時にそれらのファイルを自動生成することでテスト忘れを無くすことにも寄与できるのではないかと感じています。
今回のプロジェクトでは、Hygen の対話型コード生成機能を使用しました。自身のプロジェクトへの理解が浅い段階でも、提供される選択肢によるサポートは対話型の長所であると感じました。この対話形式を通じて、提供される選択肢を使いながらコード生成を行うことで、プロジェクトの理解を深める手助けになると考えています。また今回は実装例として提供していませんが慣れてきたらコマンドライン引数として対話の回答を渡す action を生成することがより効果的だと感じました。
簡単なハンズオン形式のブログでしたが読んでいただき、ありがとうございます。25 日まで毎日 tech blog が更新されるので他の記事もぜひチェックしてみてください!

参考