Node/Reactのバージョンを汚さずにStorybookを別環境で動かす方法
はじめに
長年運用されているプロジェクトで、「既存のNode.jsやReactのバージョンは、互換性の問題で迂闊に上げられない...でも、Storybookのようなモダンな開発ツールを導入してコンポーネント開発を効率化したい!」と考えたことはありませんか?
まさにその課題に直面した私は、**「プロジェクトの中に、Storybook専用の別のNode環境を作ってしまおう」**という少し特殊な構成に挑戦することにしました。
このアプローチは、既存環境を汚さないという大きなメリットがありましたが、その中でぼちぼちエラーも出てたりしたので メモというか記録として書き置いておきます
今回の特殊な構成:プロジェクト内別環境
今回の構成のポイントは、プロジェクトルートにstorybookというディレクトリを切り、その中にStorybook専用のpackage.jsonを配置したことです。
my-legacy-project/
├── node_modules/       (本体の古い依存関係)
├── package.json        (本体:React 16, Node 14 など)
├── src/
│   ├── components/
│   │   ├── BfIcon/
│   │   │   ├── index.jsx
│   │   │   └── index.module.scss
│   │   └── ...
│   └── ...
└── storybook/          (👈 Storybook専用環境)
├── node_modules/   (SB用の新しい依存関係)
└── package.json    (SB用:React 18, Storybook 8 など)
これにより、本体の古い依存関係と、Storybookが要求する新しい依存関係を完全に分離できると考えました。しかし、この「分離」こそが、後のエラーの引き金となります。
Storybookの初期設定
- Storybook用ディレクトリの作成と初期化
プロジェクトのルートディレクトリで以下のコマンドを実行し、Storybook専用の環境を作成します。 
# storybookディレクトリを作成
mkdir storybook
# 作成したディレクトリへ移動
cd storybook
# storybook用のpackage.jsonを初期化
npm init -y
- Storybookのインストール
 
作成したstorybookディレクトリ内で、Storybookの初期化コマンドを実行します。
npx storybook@latest init
このコマンドにより、.storybookディレクトリと設定ファイル、およびサンプルストーリーが自動で生成されます。
- main.jsの編集
 
インストール完了後、生成されたstorybook/.storybook/main.jsファイルを開き、Reactの参照先をプロジェクト本体のnode_modulesに強制するための設定を追記します。
// storybook/.storybook/main.js
const path = require('path');
const config = {
  stories: ['../../src/**/*.mdx', '../../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-onboarding',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-webpack5',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
  // 以下を追記
  webpackFinal: async (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      // Reactの参照先を2階層上のnode_modulesに設定
      react: path.resolve(__dirname, '..', '..', 'node_modules', 'react'),
      'react-dom': path.resolve(__dirname, '..', '..', 'node_modules', 'react-dom'),
    };
    return config;
  },
};
export default config;
主な変更点:
- storiesのパスを、srcディレクトリを正しく参照できるように'../../src/...に変更。
 - webpackFinal設定を追記し、Reactの重複を回避。
 
最初のターゲット:BfIconコンポーネント
まずは、name propsに応じてSVGを出し分けるシンプルなアイコンコンポーネント BfIcon をStorybookに表示させることを目標にしました。
// BfIcon/index.jsx (一部抜粋)
import React, { memo } from 'react'
import PropTypes from 'prop-types'
import style from './index.module.scss'
const BfIcon = memo(({ name, color }) => {
  switch (name) {
    case 'analytics':
      return <svg>...</svg>
    case 'arrowLeft':
      return <svg>...</svg>
    // ... 他のアイコンが続く
    default:
      return null
  }
})
export default BfIcon
この index.jsx と同階層にstoriesファイルを作成しました( BfIcon/index.stories.jsx )
import React from 'react'
import BfIcon from './index' // コンポーネントファイルをインポート
// コンポーネントの`switch`文から利用可能なアイコン名をすべてリストアップ
const iconNames = [
  'analytics', 'arrowLeft', 'arrowRight', 'arrowTop', 'apple', 'asc', 'asc_single', 
  'attention', 'calendar', 'camera', 'close', 'circle-allow-right', 'circle-allow-top', 
  'circle-allow-bottom', 'circle-triangle-bottom', 'circle-close', 'confirm', 'copy', 
  'desc', 'desc_single', 'dots', 'file', 'refresh', 'download', 'duplicate', 
  'externalLink', 'fanclub', 'fill-circle-allow-top', 'google', 'heart', 'lock', 
  'lottery', 'notice', 'pencil', 'picture', 'video', 'place', 'plus', 'preview', 
  'question', 'return', 'rewind', 'setting', 'scratch', 'scratch-empty', 'search', 
  'sort', 'ticket', 'trash', 'upload', 'user', 'viewer', 'warning', 'internet', 
  'danger', 'check', 'youtube', 'liveCircle', 'link', 'streaming', 'paid', 
  'follower', 'reverseView',
]
// Storybookのメタ情報をエクスポート
export default {
  title: 'Components/BfIcon',
  component: BfIcon,
  // propsをStorybookのUIで操作するための設定
  argTypes: {
    name: {
      control: { type: 'select' },
      options: iconNames,
      description: '表示するアイコンの名前を指定します。',
    },
    color: {
      control: 'color',
      description: 'アイコンの色を指定します。',
    },
  },
  tags: ['autodocs'], // ドキュメント自動生成用のタグ
}
// -----------------------------------------------------------------------------
// ストーリーの定義
// -----------------------------------------------------------------------------
// 基本的なアイコン表示
// `name` propをドロップダウンから選択して確認するための基本的なストーリー
export const Default = {
  args: {
    name: 'analytics',
    color: '#857F89', // デフォルトカラー
  },
}
// 色の変更
// `color` propを変更した際の表示を確認するストーリー
export const CustomColor = {
  args: {
    name: 'heart',
    color: '#D7003A', // カスタムカラーの例
  },
}
// 全アイコン一覧
// 利用可能なすべてのアイコンを一覧で表示するストーリー
export const AllIcons = {
  render: (args) => (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
      {iconNames.map((name) => (
        <div
          key={name}
          title={name}
          style={{
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            width: '120px',
            height: '90px',
            border: '1px solid #eee',
            borderRadius: '8px',
            padding: '10px',
            boxSizing: 'border-box',
          }}
        >
          <BfIcon name={name} color={args.color} />
          <span style={{ marginTop: '12px', fontSize: '12px', color: '#333', textAlign: 'center' }}>
            {name}
          </span>
        </div>
      ))}
    </div>
  ),
  // このストーリーでは個別の`name`選択は不要なため、コントロールを無効化
  argTypes: {
    name: {
      table: {
        disable: true,
      },
    },
  },
  // このストーリー用のデフォルト引数
  args: {
    color: '#333333',
  },
}
yarn storybook
でStorybookを起動します。
起動時に表示されたエラー達と対処法
TypeError: importers[path] is not a function
原因は、分離されたStorybook環境が、本体のsrcディレクトリにある.scssファイルをどう処理すればよいか知らなかったためでした。
分離しているとはいえ、コンポーネントのソースコードは本体のものを直接参照するため、Storybook側にも SCSSを解釈するための設定が必要だったのです。
解決策として、Storybook環境にSCSSを処理するアドオンを追加しました。
storybookディレクトリでパッケージをインストール
cd storybook
npm install -D sass style-loader css-loader @storybook/addon-styling
.storybook/main.js にアドオンを登録
JavaScript
// .storybook/main.js
const config = {
// ...
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-styling', // 👈 この行を追加
],
// ...
};
export default config;
getCurrentParameter & No existing state found
TypeError: Cannot read properties of undefined (reading 'getCurrentParameter') Uncaught (in promise) TypeError: No existing state found for follower with id: 'storybook/test-provider'.
原因は、Storybook環境(storybook/ディレクトリ)にインストールしたアドオンの非互換性でした。特に 'storybook/test-provider' というエラーメッセージから、テスト関連のアドオン @storybook/addon-interactions が悪さをしていると特定できました。
解決策は、問題のアドオンを特定し、無効化することです。
// storybook/.storybook/main.js
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
// '@storybook/addon-interactions', // 👈 原因だったアドオンをコメントアウト
'@storybook/addon-styling',
],
これでついにBfIconが表示されました。

まとめ
「既存環境を汚さずにStorybookを導入する」という目的で採用した環境分離構成は、最終的に成功しました。
- 環境を分離しても、ソースコードの依存関係(SCSSなど)は解決が必要。
 - 分離した環境でも、アドオンの互換性問題は発生する。
 
最大の課題は「Reactの重複」。webpackFinalでエイリアスを設定し、Reactインスタンスを統一することが必須。
この構成は確かに複雑で、一手間かかります。しかし、どうしても本体の環境に手を加えられないレガシーなプロジェクトにモダンな風を吹き込むための、強力な選択肢の一つであることもまた事実です。
この記事が、同じように古い環境と戦う開発者の皆さんの一助となれば幸いです。
株式会社SKIYAKIのテックブログです。ファンクラブプラットフォームBitfanの開発・運用にまつわる知見や調べたことなどを発信します。主な技術スタックは Ruby on Rails / React / AWS / Swift / Kotlin などです。 recruit.skiyaki.com/
Discussion