🌚

Next.jsにおけるSVGファイルの表示方法を考える

2022/10/15に公開約4,400字2件のコメント

はじめに

フロントエンド開発を進める際に、Next.jsでのSVGでの扱い方について考える機会があったので、Zennで共有しようと思った次第です。

本来、Webサイト内でSVGファイルを表示させる方法は、大きく分けて以下3点だと思われます。

  1. imgタグのsrcにSVGファイルへのパスを設定して読み込ませる。
  2. CSSファイル内のbackground-imageにパスを設定して読み込ませる。
  3. HTML内でSVGをインラインコードで埋め込む。

※ 細かく話すとSVG以外の拡張子の画像でもインライン化できたり、CSSでもSVGをインラインで読み込ますことが可能ですが、今回は割愛させてください🙏

1と2の方法だと、他の画像ファイル同様、簡単に読み込ませることができますが、SVG内のスタイルをHTMLもしくはCSSで変更できません・・・

3の方法だと、HTMLやCSSによるスタイルの変更が可能になります。また、外部ファイルへのHTTPリクエスト回数を減らすことができることから、SVGは基本的にインラインで埋め込むことを推奨したいです。imgタグのように遅延的or非同期に読み込むためのプロパティを容易に設定できない課題はありますが、Next.jsだとDynamic Import等を利用することで回避できます(下記参考)
https://zenn.dev/toono_f/articles/6c8ef6e4e771b9

ただし、Next.js(React)内でSVGのインライン埋め込みを実現するには、SVGファイル内のコードをJSX記法に対応させる修正が必要になります。容易に変換してくれるWebサイトやプラグインはありますが、SVGファイルが増えるごとにコードを貼り付けたり、npm scriptを毎度実行する必要があるので、多少手間がかかります🧐

そこで、SVGファイルをReactコンポーネントとして直接importできるように、以下のセットアップを進めます。

セットアップ

  1. @svgr/webpackをインストールします。
yarn add -D @svgr/webpack
  1. next.config.jsに以下記述を追加します。
next.config.js
const nextConfig = {

~中略~

+ webpack: (config) => {
+   config.module.rules.push({
+     test: /\.svg$/,
+     use: [
+       {
+         loader: "@svgr/webpack",
+       },
+     ],
+   });
+   return config;
+ },
+ images: {
+   disableStaticImages: true, // importした画像の型定義設定を無効にする
+ },
};

本設定を行うことで、SVGファイルをReactコンポーネントとしてimportできるようになります。また、SVGファイル内のインラインコードの修正も、Next.jsの開発環境におけるホットリロードの対象になるので、開発効率も向上するはずです。

babel-plugin-inline-react-svgというプラグインを導入することでもSVGを呼び出せるように設定することは可能なのですが、SVG内の変更を検知できないため、修正するたびに_nextのディレクトリの削除が必要になり面倒でした。また、ES2020から利用できるようになったDynamic Importの機能を利用する意味でも@svgr/webpackを導入することをオススメします👍

また、disableStaticImages: trueを設定することで、Next.jsでデフォルトで定義されている画像の型定義設定を無効にできます。無効にすることで、次に行うSVGの型定義を有効化できます。

  1. 任意の場所にindex.d.tsを作成し、SVGの型定義を行います。
src/types/index.d.ts
declare module '*.svg' {
  const content: React.FC<React.SVGProps<SVGElement>>;
  export default content;
}

※ Next.jsを導入すると、デフォルトでtsconfig.json内に"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],が設定されているため、上記ファイルをどこに配置しても問題ないですが、今回はsrcにtypesディレクトリを作成し、その中に配置しました。

  1. (上記設定がすぐ反映されないことが稀にあるので)VsCodeを再起動します。

実際にSVGをReactコンポーネントとして呼び出してみる

上記設定が終わったら、実際にSVGをコンポーネントとして呼び出してみましょう。

今回は例としてIconViewというコンポーネントの中でSVGファイルをimportします。SVG自体は他の画像ファイルと同じく、publicディレクトリに配置しています。

IconView.tsx
import Icon from "public/images/icon.svg";
export const IconView = () => {
  return (
    <Icon />
  );
};

おそらく、想定通りに表示できるはずです。
ちなみに下記のようにSVGタグに本来設定できるプロパティをpropsとして渡すことが可能です。

IconView.tsx
import Icon from "public/images/icon.svg";
export const IconView = () => {
  return (
-    <Icon />
+    <Icon
+      width={10}
+      height={10}
+      stroke={"#fff"}
+      strokeWidth={"2.5px"}
+      fill={"none"}
+      style={{ display: "block"}}
       />
  );
};

とても便利にSVGを扱うことが可能になりました✨
TypeScriptを利用しているなら予測補完も効くので、開発体験も良き良きです。

SVGファイルはsvgタグの他にgpathのようなHTMLタグで構成されているはずですが、それぞれで指定されている(SVGファイル内の)strokefill等の値を事前に削除しておけば、上記のようなpropsの設定が優先されるはずです。

importされた際にSVGファイル内のコードが変わってしまう問題

今回導入した@svgr/webpackに組み込まれているsvgoという最適化ツールがデフォルトだと有効になってしまっていることが原因で上記の問題が発生し、(線が一部表示されないなど)SVGが想定通りに表示されないことがありました。

そこで、next.config.jsに以下記述を追加します。

next.config.js
const nextConfig = {

~中略~

  webpack: (config) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: [
        {
          loader: "@svgr/webpack",
+	  options: {
+           svgo: false, // 圧縮無効
+         },
        },
      ],
    });
    return config;
  },

~中略〜

};

これでsvgoが無効になり、SVGファイル内のコードが勝手に変換されなくなったはずです🌞

svgoはインラインコードを自動で圧縮してくれるので便利ですが、上記の問題が発生することがあるので、利用時には注意が必要ですね。

Storybookへの対応

StorybookでもSVGをコンポーネントとして扱うためには、.storybook/main.jsに以下記述を追加してください。

.storybook/main.js
~前略~

module.exports = {

~中略~

  webpackFinal: async (config, { configType }) => {
+   config.resolve.modules = [...(config.resolve.modules || []), path.resolve("./")]; // 絶対パスでimportできるようにする
+   config.module.rules.push({
+     test: /\.svg$/,
+     use: [
+       {
+         loader: "@svgr/webpack",
+         options: {
+           svgo: false, // 圧縮無効
+         },
+       },
+     ],
+   });
+   const fileLoaderRule = config.module.rules.find((rule) => rule.test && rule.test.test(".svg"));
+   fileLoaderRule.exclude = /\.svg$/;
+   return config;
  },
};

※上記記述はWebpack5の利用を想定

ここまで完了すれば、SVGをインラインで便利に読み込めるようになったはずです!

続きはこちら・・・
https://zenn.dev/toono_f/articles/28cd00a3764d97

Discussion

@svgr/webpack っていうライブラリあるんですね!知らなかったので勉強になりました!

3の方法だと、HTMLやCSSによるスタイルの変更が可能で、外部ファイルへのHTTPリクエスト回数を減らすことができるので、Webサイトの表示速度向上に繋がり、Core Web Vitalsにも好影響が期待されます。本理由から、SVGは基本的にインラインで埋め込むことを推奨したいです。

このコメントなのですが、 Core Web Vitals の観点で SVG をインラインにした方が良いというのは場合によりけりかと思いました!
例えば巨大な SVG をインラインに埋め込むとメモリ喰ったり初期描画の負担にもなりえますし、遅延的に SVG を表示したいケースではブラウザネイティブに <img src='test.svg' loading='async' /> とかにした方が良いかなと。

ついでですが、コード例にある React.Fragment ( <></> )も不要かなと思いました!

// 記事にあるコード例
import Icon from "public/images/icon.svg";
export const IconView = () => {
  return (
    <>
      <Icon />
    <>
  );
};
// React.Fragment (<></>) はなくて OK
export const IconView = () => <Icon />

コメントありがとうございます🙌

例えば巨大な SVG をインラインに埋め込むとメモリ喰ったり初期描画の負担にもなりえますし、遅延的に SVG を表示したいケースではブラウザネイティブに <img src='test.svg' loading='async' /> とかにした方が良いかなと。

上記を考慮した表現ができておりませんでした・・・
該当箇所を修正しました。ご指摘感謝です🙆‍♀️
実は現在、Dynamic Importを使ってSVGコンポーネントを非同期に読み込ませるための記事を執筆中なので、身が引き締まる次第です🔥

ついでですが、コード例にある React.Fragment ( <></> )も不要かなと思いました!

こちらも指摘ありがとうございます!うっかり消し忘れておりました🙇‍♀️

ログインするとコメントできます