scaffolding toolで始めるフロントエンド効率化

こんにちはとしきふです．毎月投稿4月号です．
今月はファイル雛形作成ツールであるscaffdogの紹介をしようと思います．
導入すると様々なメリットがあるのでぜひ選択の一つに考えてもらえると嬉しいです！

今回の最終アウトプットはこちらです．あわせてご覧ください．

https://github.com/toshikifu/scaffdog-project

# 前提  スキャフォールディングツールとは
**スキャフォールディングツール**とは，ソフトウェア開発においてプロジェクトの骨組みやテンプレートを自動生成する手法を指します．この手法を採用することにより，開発者の反復的な作業を削減し，効率的にプロジェクトを立ち上げることが可能になります．

メリットは他にもあり，コードを一貫性あるものにしたり，ヒューマンエラーを削減できたりとメリットが多いです．ファイルを作成するディレクトリを指定することでディレクトリ構造を考えず開発できることも大きな魅力だと感じています．

特にファイルが多いフロントエンド開発では初期段階から導入しておきたい設定の一つです．

フロントエンド(Node.js)におけるスキャフォールディングツールはいくつかあります．
- [hygen](https://github.com/jondot/hygen?tab=readme-ov-file)
- [plop](https://github.com/plopjs/plop)
- [scafffdog](https://github.com/scaffdog/scaffdog)

それぞれ特徴があるので，自身のプロジェクトにあったものを選択してみてください．

# 概要
今回はマークダウン形式でテンプレート内容を記述できる**scaffdog**を使ってみようと思います．
コンポーネント作成に必要な
- 本体(index.tsx)
- storybook(**.stories.tsx)

を作ってくれる設定をstep-by-stepで進めてみようと思います．

## 使用技術
使用技術の詳細は本記事では詳しく触れません．公式ドキュメントをご覧ください．
- Next.js

https://nextjs.org/
- Storybook

https://storybook.js.org/

- shadcn (オプション)

https://ui.shadcn.com/

# セットアップ
scaffdogを除く使用技術のセットアップをします．
## Next.jsプロジェクト作成
下記を参考に進めます

https://nextjs.org/docs/app/getting-started/installation

`create-next-app`を実行したらプロジェクト名を入力し，他の設定はEnter連打します．
```zsh
❯ npx create-next-app@latest
✔ What is your project named? … scaffdog-project
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
```

少し長い処理が終了したら，ディレクトリに入り，フロントエンドサーバーを立ち上げます．
```zsh
cd scaffdog-project
```

```zsh
npm run dev
```

http://localhost:3000 にアクセスし，下記イメージのような画面になれば完了です．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2540179/7d9bfe0f-0d45-41a7-9069-45eaae30c0e3.png)

## storybook
下記を参考に進めます．

https://storybook.js.org/docs/get-started/install

下記コマンドを実行し，設定を進めます．
```zsh
npm create storybook@latest
```

:::note warn
**experimental機能はオフにすること**
執筆時点(2025/04)では，実験的なフレームワークの提供をしているようです．本筋とは外れるのでnoとしました．
```zsh
✔ The addon requires the use of @storybook/experimental-nextjs-vite to work with Next.js.
https://storybook.js.org/docs/writing-tests/test-addon#install-and-set-up

Do you want to migrate? … no
```
参考：https://storybook.js.org/docs/get-started/frameworks/nextjs?renderer=react#with-vite
:::

設定完了後，自動でブラウザが立ち上がります．下記のような画面が立ち上がれば完了です．
![ss 45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2540179/e1e8de2c-c48a-4f11-9af0-d6c6d11d4be1.png)

## (option) shadcn/ui
コンポーネントのサンプルとしてshadcn/uiを利用したいと思います．
下記を参考に進めます．

https://ui.shadcn.com/docs/installation/next

下記コマンドを実行し，Enterを押してセットアップを完了します．
```
npx shadcn@latest init
```

適当にコンポーネントをインストールしてみます．
```
npx shadcn@latest add button
```
`src/components/ui/button.tsx`ができていれば成功です．

tailwindcssをstorybookでも有効化するために，下記のような変更を加えます．
```.storybook/preview.ts
import "../src/app/globals.css"; //　これを追加

import type { Preview } from "@storybook/react";

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;
```


動作確認のために，buttonのstoryを作成してみます．
```src/components/stories/button.stories.tsx
import { Button } from "@/components/ui/button";
import type { Meta, StoryObj } from "@storybook/react";

const meta: Meta<typeof Button> = {
  component: Button,
  args: {
    children: "Button",
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {};

```
無事作成されました．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2540179/d7b6793e-7128-4d37-bb58-76361f9b346d.png)


# scaffdogの設定
scaffdogのセットアップは下記を参考に進めます．
- パッケージをインストールする
```
npm install --save-dev scaffdog
```
- 初期設定をする
「Please enter a document name」と聞かれるのでhelloとしましょう．
```
npx scaffdog init
```


# テンプレート追加
本体とstorybookファイルそれぞれから，可変となる部分を変数にしてテンプレートを作成します．
`.scaffdog/component.md` に下記を記入します．

:::note info
`// ``` `となっている箇所を` ``` `に変更してください．
:::

```md
---
name: "react-component"
description: "Create React component + Storybook story"
root: "."
output: "**/*"
questions:
  component:
    message: "コンポーネント名（PascalCase）を入力してください"
    initial: "Sample"
---

# {{ inputs.component }}/index.tsx

```tsx
import type { FC, HTMLAttributes } from "react";

export type {{ inputs.component | pascal }}Props = HTMLAttributes<HTMLDivElement>;

export const {{ inputs.component | pascal }}: FC<{{ inputs.component | pascal }}Props> = ({
  className,
  children,
  ...props
}) => (
  <div className={className} {...props}>
    {children ?? "{{ inputs.component | pascal }}"}
  </div>
);

export default {{ inputs.component | pascal }};
// ```

# {{ inputs.component }}/{{ inputs.component }}.stories.tsx

```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { {{ inputs.component | pascal }} } from "./index";

const meta: Meta<typeof {{ inputs.component | pascal }}> = {
  component: {{ inputs.component | pascal }},
  parameters: {
    layout: "centered",
  },
};

export default meta;
type Story = StoryObj<typeof {{ inputs.component | pascal }}>;

export const Primary: Story = {
  args: {
    children: "{{ inputs.component | pascal }}",
  },
};
// ```

```

:::note
tips
`inputs.component | pascal `のようにフィルタを付けるとパスカルケースに変換することができます．これによってコンポーネント名をパスカルケースに統一することができます．

テンプレートファイルの書き方は公式ドキュメントを参照してください
https://scaff.dog/docs/templates/attributes
:::

# 動作確認
```zsh
npx scaffdog generate
# => react-component を選択 → 配置したいディレクトリを選択 →　Component 名を入力
```
これだけで，
```zdh
src/components/hello/
  ├── index.tsx
  └── Hello.stories.tsx
```
ができあがります．

# 例
https://ui.shadcn.com/docs/components/card

こちらのコンポーネントを例に，使用例を挙げてみます．

1. Card・switchコンポーネントをインストールする
    ```zsh
    npx shadcn@latest add card
    npx shadcn@latest add switch
    ```
2. `views`ディレクトリに`NotificationCard`コンポーネントを作成する
    ```zsh
    npx scaffdoc generate
    # => react-component を選択 → `src/views`を選択 →　`NotificationCard` と入力
3. `src/views/NotificationCard/index.tsx`を編集する
    ```src/views/NotificationCard/index.tsx
    import { BellRing, Check } from "lucide-react";
    import { cn } from "@/lib/utils";
    import { Button } from "@/components/ui/button";
    import {
      Card,
      CardContent,
      CardDescription,
      CardFooter,
      CardHeader,
      CardTitle,
    } from "@/components/ui/card";
    import { Switch } from "@/components/ui/switch";
    import { FC } from "react";
    
    const notifications = [
      {
        title: "Your call has been confirmed.",
        description: "1 hour ago",
      },
      {
        title: "You have a new message!",
        description: "1 hour ago",
      },
      {
        title: "Your subscription is expiring soon!",
        description: "2 hours ago",
      },
    ];
    
    export type NotificationCardProps = React.ComponentProps<typeof Card>;
    
    export const NotificationCard: FC<NotificationCardProps> = ({
      className,
      ...props
    }) => {
      return (
        <Card className={cn("w-[380px]", className)} {...props}>
          <CardHeader>
            <CardTitle>Notifications</CardTitle>
            <CardDescription>You have 3 unread messages.</CardDescription>
          </CardHeader>
          <CardContent className="grid gap-4">
            <div className=" flex items-center space-x-4 rounded-md border p-4">
              <BellRing />
              <div className="flex-1 space-y-1">
                <p className="text-sm font-medium leading-none">
                  Push Notifications
                </p>
                <p className="text-sm text-muted-foreground">
                  Send notifications to device.
                </p>
              </div>
              <Switch />
            </div>
            <div>
              {notifications.map((notification, index) => (
                <div
                  key={index}
                  className="mb-4 grid grid-cols-[25px_1fr] items-start pb-4 last:mb-0 last:pb-0"
                >
                  <span className="flex h-2 w-2 translate-y-1 rounded-full bg-sky-500" />
                  <div className="space-y-1">
                    <p className="text-sm font-medium leading-none">
                      {notification.title}
                    </p>
                    <p className="text-sm text-muted-foreground">
                      {notification.description}
                    </p>
                  </div>
                </div>
              ))}
            </div>
          </CardContent>
          <CardFooter>
            <Button className="w-full">
              <Check /> Mark all as read
            </Button>
          </CardFooter>
        </Card>
      );
    };

    ```
4. storybookで確認する
    storybookテンプレートができているので，特に問題がなければ変更が反映されています．
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2540179/5eea5c2f-a211-4e23-b982-0749e2578ffc.png)

# 締め
以上でスキャフォールディングツールscaffdogの導入の紹介を終わります．
スキャフォールディングツールを導入すると，作成漏れが減るだけでなく，コードフォーマットも統一されていくのでクリーンなフロントエンドを保つことができるようになります．
特にstorybookは更新を一度怠ると以後更新しない癖がついてしまうので早期に対策は講じておきたいですね．