2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Storybook解説(Next.js, TailwindCSS環境)

Last updated at Posted at 2023-08-03

はじめに

最近実務でStorybookを触る機会があり、色々調べたものを備忘録として残しておきます。
なかなか面白かったので、興味のある方は覗いていってください!
Storybookに触れたことがない方・Storybookの導入を検討されている方に見ていただければ嬉しいです!
本記事を読めば、実際に動かしてみて、Storybookがどんなものなのかを理解することができます!

Storybookとは

そもそもStorybookとは何かですが、「UIコンポーネントカタログ」の一種で、フロントエンド開発において、必要不可欠なサービスです。
React、Vue、Angularなど様々なフレームワークに対応していて、無料で利用することができます。

UIコンポーネントカタログとは

Storybookを初めて聞いた方は、UIコンポーネントカタログと聞いてもイメージしづらいと思うので、その説明も併せて残しておきます。
UIコンポーネントカタログとは何かですが、Web開発で使用しているコンポーネントを一覧にするツールのことです。
ボタンのコンポーネントであれば、画面からボタンの色、ラベルの色、活性・非活性、サイズなどを変更して確認できます。

Storybook(UIコンポーネントカタログ)を導入するメリット

いくつかあるので、箇条書きで下記にまとめました!

  • コンポーネント単体でブラウザに表示可能なため、レイアウトや挙動を確認しながらコーディングができる
  • 画面からUIの操作が可能なため、ソースコードが読めない方でも扱える
  • UIがどのような動きをするかすぐに確認できる
  • バックエンドとは切り離して作ることができるため、バックエンドの実装を待たずとも開発ができる

実践

まずは触ってみてStorybookとは何なのかの理解を深めていきましょう。
ソースコードは用意しているので、下記手順を行ってみてください。

※今回はホスト環境の違いで動かないということがないように、VSCodeからDockerコンテナに入り、動作確認をするので、問題なく動くかと思います。

プロジェクトのクローン

ターミナルを開き、下記手順を実行してください。

# ホームディレクトリに移動
cd

# プロジェクトをクローン
git clone https://github.com/75ks/next-storybook.git

# プロジェクトディレクトリに移動
cd next-storybook

# クローンしたプロジェクトをVSCodeで開く
code .

VSCodeからコンテナに接続

この手順は別記事で載せていますので、こちらから参照してください。

今回はNext.jsのサーバーだけでなく、Storybookのサーバーも起動しているので、
localhost:6006に接続できるかも確認してください!

Docker上でもホットリロードを有効にする

クローンしたプロジェクトはDocker上で動かすため、下記設定がないとホットリロードが無効となってしまいます。
下記設定はプッシュしているため、個別で設定する必要はありませんが、詰まりやすいポイントなので、是非覚えておいてください。

Docker上で動くNext.jsのホットリロードを有効にする
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
+  // Webpackの開発モードの設定
+  webpackDevMiddleware: (config) => {
+    // 監視オプション
+    config.watchOptions = {
+      poll: 1000, //  変更チェックをする間隔(ミリ秒)
+      aggregateTimeout: 500, // 変更があってから再ビルドするまでの間隔(ミリ秒)
+      ignored: ["node_modules"], // 監視対象外ディレクトリ
+    }
+    return config;
+  },
}

module.exports = nextConfig

Docker上で動くStorybookのホットリロードを有効にする
import type { StorybookConfig } from "@storybook/nextjs";
import type { Configuration } from 'webpack';

const config: StorybookConfig = {
  stories: [
    "../stories/**/*.mdx",
    "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
    '../app/components/**/*.stories.ts',
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-onboarding",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
+  // Webpackの設定
+  webpackFinal: async (config: Configuration) => {
+    // 監視オプション
+    config.watchOptions = {
+      poll: 1000, // 変更チェックをする間隔(ミリ秒)
+      aggregateTimeout: 500, // 変更があってから再ビルドするまでの間隔(ミリ秒)
+      ignored: ["node_modules"], // 監視対象外ディレクトリ
+    };
+    return config;
+  },
};
export default config;

解説

localhost:6006で接続した画面がStorybookの画面になります。
ここで作成したコンポーネントの動作確認をすることができます。
画像赤枠部分のストーリーが作成したコンポーネントのものになります。
※赤枠外の上部の「EXAMPLE」配下のストーリーは、Storybook導入時にデフォルトで作成されたものです。
image.png

コンポーネント

これからコンポーネントとストーリーの紐付き方について説明していきますが、その前にまずボタンコンポーネントファイルを確認してみましょう。

CustomButton.ts
interface Props {
  /** ボタンの文言 */
  label: string;
  /** 非活性にするか */
  disabled?: boolean;
  /** ボタンのサイズ */
  size?: "small" | "medium" | "large";
  /** ボタンの色 */
  color?: "blue" | "red" | "green" | "gray";
  /** クリックイベント */
  onClick: () => void;
}

export const CustomButton: React.FC<Props> = ({
    label,
    disabled = false,
    size = "medium",
    color = "blue",
    onClick
}: Props) => {
  const sizeClasses = {
    small: "px-2 py-1 text-sm",
    medium: "px-4 py-2",
    large: "px-6 py-3 text-lg",
  };
  const colorClasses = {
    blue: "enabled:bg-blue-400 enabled:hover:bg-blue-300",
    red: "enabled:bg-red-400 enabled:hover:bg-red-300",
    green: "enabled:bg-green-400 enabled:hover:bg-green-300",
    gray: "enabled:bg-gray-400 enabled:hover:bg-gray-300",
  };

  const sizeClass = sizeClasses[size];
  const colorClass = colorClasses[color];

  return (
    <button 
      className={`${sizeClass} ${colorClass} disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-bold rounded`}
      disabled={disabled}
      onClick={onClick}
    >
      { label }
    </button>
  )
}

Propsインターフェースを作成して、コンポーネントに渡す情報を定義しています。
ここではlabeldisabledsizecoloronClickの5つの情報を持っており、labelonClickだけ必須としています。
必須以外は初期値を設定しています。

このボタンコンポーネントはCustomButtonという名前でexportしています。

ストーリー

Features

次に本命のボタンストーリーファイルを確認します。
まずは、Features.stories.tsです。
Featuresでは、コンポーネントのカタログを作成しています。
カタログなので、画面からの変更はできないようにしています。
主にボタンデザインの確認用に使用します。

Features.stories.ts
import type { Meta, StoryObj } from "@storybook/react";
import { CustomButton } from "./CustomButton";

const meta: Meta<typeof CustomButton> = {
	title: "components/Button/Features",
	component: CustomButton,
	parameters: {
		layout: "centered",
		controls: { disable: true },
	},
	argTypes: {
		disabled: { control: "boolean" },
		label: { control: "text" },
		size: { control: "inline-radio" },
		color: { control: "inline-radio" },
	},
};
export default meta;

type Story = StoryObj<typeof CustomButton>;

export const Enabled: Story = {
	args: {
		disabled: false,
		label: "Enabled Button",
	}
};

export const Disabled: Story = {
	args: {
		disabled: true,
		label: "Disabled Button",
	}
};

export const Small: Story = {
	args: {
		label: "Small Button",
		size: "small"
	}
};

export const Medium: Story = {
	args: {
		label: "Medium Button",
		size: "medium"
	}
};

export const Large: Story = {
	args: {
		label: "Large Button",
		size: "large"
	}
};

export const Blue: Story = {
	args: {
		label: "Blue Button",
		color: "blue",
	}
};

export const Red: Story = {
	args: {
		label: "Red Button",
		color: "red",
	}
};

export const Green: Story = {
	args: {
		label: "Green Button",
		color: "green",
	}
};

export const Gray: Story = {
	args: {
		label: "Gray Button",
		color: "gray",
	}
};
meta

Storyのmetaデータを設定します。詳細は下記の通りです。

  • title: Storybookの画面に表示される名前(/で階層を入れることが可能) ※画像赤枠
  • component: 紐付けるコンポーネント
  • parameters: ストーリー表示に関するオプション
    • layout: ストーリーをどこに配置するか
      • centered: 中央に表示
      • fullscreen: 幅と高さいっぱいに表示
      • padded: コンポーネントの周囲に余白を追加
    • controls: Controlsタブの表示
  • argTypes: Propsを定義
    • Props.control: Storybook画面のControls > Controlの表示を設定

※argTypesの詳細は後で紹介します。
image.png

Story

StoryObjのジェネリクスにexportしたコンポーネントを指定することで、そのコンポーネントのStoryのひな形を作成します。
作成したStoryをオブジェクトの型に指定することで、そのオブジェクトはストーリーをして扱われ、Storybookの画面に表示されます。
オブジェクトのargsにはPropsを設定し、Storybookに表示される初期値を設定します。
今回はEnabledDisabledSmallMediumLargeBlueRedGreenGrayの9つのストーリーを作成しています。

Playground

次にPlayground.stories.tsです。
Playgroundでは、コンポーネントのサンドボックスを作成しています。
サンドボックスなので、画面から変更できるようにしています。
主にボタンの動作確認用に使用します。

Playground.stories.ts
import type { Meta, StoryObj } from "@storybook/react";
import { CustomButton } from "./CustomButton";

const meta: Meta<typeof CustomButton> = {
	title: "components/Button/Playground",
	component: CustomButton,
	parameters: {
		layout: "centered",
	},
	tags: ["autodocs"],
	argTypes: {
		disabled: { control: "boolean" },
		label: { control: "text" },
		size: { control: "inline-radio" },
		color: { control: "inline-radio" },
	},
};
export default meta;

type Story = StoryObj<typeof CustomButton>;

export const Playground: Story = {
	args: {
		label: "Playground Button",
	}
};

Featuresと異なる点は2つあります。

1つ目は、metaのtags: ["autodocs"]がある点です。
これがあることによって、ドキュメントが自動生成され、ドキュメントからも変更が可能となります。
ドキュメントでは、今の状態をコードに変換してくれる機能も備わっています。
Docsがドキュメントになります。
image.png

2つ目は、metaのparameters.controlsがない点です。
Featuresでは、disable: trueを設定していたため、Storybook画面のControls > Controlが表示されていませんでしたが、Playgroundでは表示されるため、ドキュメント以外の画面からも変更が可能となります。
image.png
metaのargTypes.Props.controlの種類はいくつかあり、Propsの型によって設定できるcontrolに制限があります。

Playground.stories.ts
...
//(省略)
	argTypes: {
		disabled: { control: "boolean" },
		label: { control: "text" },
		size: { control: "inline-radio" },
		color: { control: "inline-radio" },
	},
//(省略)
control 説明
array 'object' 配列の値を扱うJSONベースのエディタを提供する。また、rawモードでの編集も可能。
{ control: 'object' }
boolean 'boolean' 可能な状態を切り替えるためのトグルを提供する。
{ control: 'boolean' }
enum 'check' 複数のオプションを選択するための、積み重ねられたチェックボックスのセットを提供する。
{ control: 'check', options: ['email', 'phone', 'mail'] }
'inline-check' 複数のオプションを選択するための一連のインラインチェックボックスを提供する。
{ control: 'inline-check', options: ['email', 'phone', 'mail'] }
'radio' 利用可能なオプションに基づいて、積み重ねられたラジオボタンのセットを提供する。
{ control: 'radio', options: ['email', 'phone', 'mail'] }
'inline-radio' 利用可能なオプションに基づいて、インライン化されたラジオボタンのセットを提供
{ control: 'inline-radio', options: ['email', 'phone', 'mail'] }
'select' オプションから一つの値を選択するセレクトを提供する。
{ control: 'select', options: [20, 30, 40, 50] }
'multi-select' オプションから複数の値を選択するセレクトを提供する。
{ control: 'multi-select', options: ['USA', 'Canada', 'Mexico'] }
number 'number' すべての可能な値の範囲を含む数値入力を提供する。
{ control: { type: 'number', min:1, max:30, step: 2 } }
'range' すべての可能な値を含む範囲スライダーを提供する。
{ control: { type: 'range', min: 1, max: 30, step: 3 } }
object 'file' URLの配列を返すファイル入力を提供する。特定のファイルタイプを受け付けるように、さらにカスタマイズすることもできる。
{ control: { type: 'file', accept: '.png' } }
'object' オブジェクトの値を扱うJSONベースのエディタを提供する。また、rawモードでの編集も可能。
{ control: 'object' }
string 'color' 色の値を選択するためのカラーピッカーを提供する。さらに、カラープリセットのセットを含むように設定できる。
{ control: { type: 'color', presetColors: ['red', 'green']} }
'date' 日付を選択するためのデートピッカーを提供する。
{ control: 'date' }
'text' 自由形式のテキスト入力を提供する。
{ control: 'text' }

さいごに

これだけでStorybookの大枠を理解し、作成することができます。
難しい設定などはないので、慣れてしまえばすぐに扱えるようになると思います!
今回は実際に動かしてみて、Storybookがどんなものなのかを理解することが目的だったので、興味のある方は本記事を参考に、1から是非作ってみると良いと思います!

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?