Next.js × Storybook8におけるSVG画像の対応

# はじめに
最近ポートフォリオサイトを作りはじめたのですが、SNSのアイコンをクリックすると、そのSNSのマイページに飛ぶようにしたかったので、そのコンポーネントを作成していました。

ただ飛ぶだけでもよいのですが、せっかくなのでマウスホバー時に色が変わるようにしたかったので、SVGに対応しようとしたのですが、結構時間がかかりました。特にStorybookの方で。

Storybook7の方は充実していたのですが、見たところ8では動かないものもあったので、一度ここでまとめたおこうと思った次第です。

今回は、Next.js × Storybook8ということで、Next.js側での対応と、Storybook側での対応と2つ書いていこうと思います。また、tailwindcssについては、svgを使用したコンポーネントについても正しく動作していることを確認できています。

# 環境
OS: macOS Sonoma 14.5
Next.js: 14.2.3
Storybook: 8.1.6
tailwindcss: 3.4.4

一応、`npm list`実行時の出力も。

```
% npm list       
<パス省略>
├── @biomejs/biome@1.8.0
├── @chromatic-com/storybook@1.5.0
├── @newhighsco/storybook-addon-svgr@2.0.16
├── @storybook/addon-essentials@8.1.6
├── @storybook/addon-interactions@8.1.6
├── @storybook/addon-links@8.1.6
├── @storybook/addon-onboarding@8.1.6
├── @storybook/addon-styling-webpack@1.0.0
├── @storybook/addon-themes@8.1.6
├── @storybook/blocks@8.1.6
├── @storybook/nextjs@8.1.6
├── @storybook/react@8.1.6
├── @storybook/test@8.1.6
├── @svgr/webpack@8.1.0
├── @types/node@20.14.2
├── @types/react-dom@18.3.0
├── @types/react@18.3.3
├── autoprefixer@10.4.19
├── eslint-config-next@14.0.4
├── eslint-plugin-storybook@0.8.0
├── eslint@8.57.0
├── next@14.2.3
├── postcss@8.4.38
├── prettier-plugin-tailwindcss@0.5.14
├── prettier@3.3.1
├── react-dom@18.3.1
├── react-icons@5.2.1
├── react@18.3.1
├── storybook@8.1.6
├── tailwindcss-animated@1.1.0
├── tailwindcss@3.4.4
├── tsconfig-paths-webpack-plugin@4.1.0
└── typescript@5.4.5
```

# Next.js でのSVG対応

### 解決法 (It works for me)

公式ドキュメントに記されていた通りで動作しました。手順はこの記事でもまとめていますが、うまくいかない場合は、以下のリンクから公式ドキュメントを参照ください。

https://react-svgr.com/docs/next/

1. svgr のインストール
以下のコマンドで、svgrをインストールしましょう。
`npm install --save-dev @svgr/webpack`



2. next.config に以下の記述を追加 
Nextのコンフィグファイルに以下の記述を追加します。結構追加箇所が多いので、今の設定と見比べながら、必要な箇所を抽出してください。

```next.config
module.exports = {
  webpack(config) {
    // Grab the existing rule that handles SVG imports
    const fileLoaderRule = config.module.rules.find((rule) =>
      rule.test?.test?.('.svg'),
    )

    config.module.rules.push(
      // Reapply the existing rule, but only for svg imports ending in ?url
      {
        ...fileLoaderRule,
        test: /\.svg$/i,
        resourceQuery: /url/, // *.svg?url
      },
      // Convert all other *.svg imports to React components
      {
        test: /\.svg$/i,
        issuer: fileLoaderRule.issuer,
        resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
        use: ['@svgr/webpack'],
      },
    )

    // Modify the file loader rule to ignore *.svg, since we have it handled now.
    fileLoaderRule.exclude = /\.svg$/i

    return config
  },

  // ...other config
}
```

3\. コンポーネントの実装
公式ドキュメントにまとめてあるものと同じ内容で。ファイル名は好きなようにつけてください。

```StarIcon.tsx
import Star from './star.svg'

const Example = () => (
  <div>
    <Star />
  </div>
)
```

4\. 新規設定ファイルの追加
新たに、.d.tsファイルとして設定ファイルを追加します。
名前はなんでもよいらしいのですが、私は src/types下にsvgr.d.tsという名前で作成しました。

```svgr.d.ts
declare module '*.svg' {
  import { FC, SVGProps } from 'react'
  const content: FC<SVGProps<SVGElement>>
  export default content
}

declare module '*.svg?url' {
  const content: any
  export default content
}
```

ここで終わってもいいのですが、このままだとサイズを指定する際に、左上頂点が基準となってしまい、左上の箇所だけが切り取られて表示されてしまいます。
せっかくなので、Imageコンポーネントのように、全体のアスペクト比を保って縦横幅を調整できるようにしようと思います。
こんな感じに。

```
<MyIcon height={22} />
```

同じく新たに、.svgrrc.jsをファイルを作成します。
私はルートディレクトリに置きましたが、他の場所でも動作するかもしれません。
なお、ここまで書いてから改めて調査したところ、とある方のの[Next.js12および13におけるSVG埋め込み](https://gist.github.com/lukebussey/8387bb038629dccc01a62487614f44df?permalink_comment_id=4329300)というドキュメントを発見しました。こちらでは、svgr.config.jsという名前で作成するよう書いてあったため、こちらでも良いかもしれません。
.svgrrv.jsでうまくいかない場合は、svgr.config.jsにしてみると良いかもしれません。

```.svgrrc.js
module.exports = {
  dimensions: false,
};
```

5\. 既存の設定ファイルに追記
tsconfig.json の include に、 `"svgr.d.ts"`を追加します。
この追加するファイル名は、4.で追加したファイルの名前と一致させて、かつ <font color="Red">include の先頭に配置する必要がある</font>ようです。
```tsconfig.json
{
  "include": [
    "svgr.d.ts",　   // 先頭に配置！
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ]
  // ...other config
}
```


Next側はこれで設定が終わったはずです。次に、Storybook側に移ります。

# Storybook8 でのSVG対応

### 解決法 (It works for me)
[issue18557](https://github.com/storybookjs/storybook/issues/18557)や[こちらのページ](https://medium.com/@jgatjens/load-svg-files-as-components-nextjs-storybook-8e3b117e27f0)を参考にしました。

1. パッケージのインストール
以下のコマンドで、svgr/webpackをインストール。
`npm install @svgr/webpack --save-dev`


2. main.js に記述を追加
SVG関連の記述を、.storybook/main.js に追加します。
```main.js
    webpackFinal: async (config) => {
        config.module = config.module || {};
        config.module.rules = config.module.rules || [];

        // This modifies the existing image rule to exclude .svg files
        // since you want to handle those files with @svgr/webpack
        const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg'));
        if (imageRule) {
            imageRule['exclude'] = /\.svg$/;
        }

        // Configure .svg files to be loaded with @svgr/webpack
        config.module.rules.push({
            test: /\.svg$/,
            use: ['@svgr/webpack'],
        });

        return config;
    },
```

これでStorybook側も動くと思われます。アスペクト比維持のサイズ調整も含め、Storybookに対応しているはずです。

# おわりに
参考文献そのままだと動かなかったり、以前は動いていたけれどもバージョンアップで動かなくなったりしていました。
また、Next.js、Storybookに両対応したsvgrパッケージ適用が、私が探した中では見つからなかったので、投稿いたしました。

# 参考文献
- [Next.js - SVGR](https://react-svgr.com/docs/next/)
- [Github issue 18557 (Storybook)](https://github.com/storybookjs/storybook)
- [Load SVG files as components NextJS + Storybook](https://medium.com/@jgatjens/load-svg-files-as-components-nextjs-storybook-8e3b117e27f0)
- [lukebussey/NextJS 12 & 13 SVG Embed.md](https://gist.github.com/lukebussey/8387bb038629dccc01a62487614f44df?permalink_comment_id=4329300#file-nextjs-12-13-svg-embed-md)