SVGRを使ってSVGのIconコンポーネントを自動生成してStorybookも出力したい
これはアソビュー Advent Calendar 2021の2日目の記事です🎄
アソビューでフロント周りのテックリードをやってる井上です。ここ一年で海外ドラマ(主にSF)にどっぷりハマってしまい、現在Netflix、Amazon Prime、Disney+、Apple TVに並行課金してます。年末も見たいものが目白押しでやめ時がわからないですね。😨
さて、最近デザインシステム(のためのコンポーネントライブラリ)を構築している中でSVGアイコンの扱い方について見直したのでそちらについて共有したいと思います。
👴 画像ファイルの思い出
5、6年かもっと前の時代のフロントエンドではアイコンなどのリクエストのレイテンシを短くするためにCSSスプライトなど使って1つのpngファイルから分割したりしていましたね。懐かしい。。
そのうちベクター形式の画像であるSVGがどのブラウザでも使えるようになり、React、Vueなどコンポーネント志向なライブラリが台頭してきてからは、シンプルなアイコンなどはSVG画像を使って容量を小さく抑えつつ、(material UIやフラットデザインの流行も影響してるとは思います)コンポーネントのchunk分割に合わせてコンポーネントの中で分割するような形が主流になったと思ってます。今回はそんなSVG画像で作ったアイコンのReact内での取り扱いについてです。
📕 主要ライブラリのSVGアイコンの扱い方について
create-react-appやNext.jsなどはデフォルトでそれぞれのやり方でSVGを直接importできます。
Nextの場合(ちょっとbabelの設定が必要)
create-react-appもだいたい同じ感じです。(内部的にはsvgr使ってるんですね)
ちなみにアソビューのアプリケーションではこれまではそれらのフレームワークは使わずにWebpackで設定することが多かったのでreact-svg-loaderを使って同じような事を実現してきました。
直接Reactコンポーネントとして扱えることで下記の様にstyled-componentsで色を制御することもできるので使い勝手はいいと思います。(※ 太古の昔のように画像をパターン分用意したりする必要もなく、styled-componentsやemotionなら、css-modulesなどのようにクラス名を介す必要もなく直接制御できるので)
😐 課題感
ただ、react-svg-loaderで運用する中でどうしても不便だなと思う点がいくつか有りました。具体的には下記です。
importを書く時のサジェストが効かない
まず、Typescriptでsvgをimportするためには型定義ファイルが別途必要です。Webpackの場合こういったcustom.d.tsのようなものです。importしてコンパイルする分には問題ないですが、これだとimport文書く時にファイル名までサジェストまでしてはくれません。そのため、SVGファイルをディレクトリに追加する→そのファイルのパスを目検でファイル名まで正確に指定してimport文を書く、ということが必要でした。もちろんファイルパスを間違えてるとコンパイル(tsc)時にエラーで落ちます。
storybookにしづらく一覧化できない
SVGファイルを直接importしていくようなやり方だとstorybook用のstoryは一つ一つ書いてくしか有りません。そのため、現状は特にSVGファイル用のstorybookなどは作っておらずどんなアイコンが現状あるかはSVGディレクトリをローカルなりでFinderで開いて確認するような形を取っていました。
💁♂ 実現したいこと
- SVGを追加したらReactコンポーネントにさっと変換したい!
- デザインシステム用のリポジトリのモジュールとしてnamed exportしたい
- 作ったコンポーネントをstorybookで一覧表示、まで手間かけずに楽にやりたい。
- 今回デザインシステム用に構築してるアプリケーションとは別のリポジトリで管理したく、Next.jsやwebpackなどには依存せずに実現したい。
🤔どうやるか
他社でやってる事例あるかな?と思って探してみたところuberが似たようなことをやっているようでした。
しかし、結構力技なんですね、、!うーむ。
何かこの辺助けてくれる先人の知恵の結晶ライブラリ無いかな(きっとあるはず)と思って探すとありました。SVGRというずばりなものが!
ありがたい!
⚛️ SVGRとその設定
SVGRとは?
SVGファイルを最適化してJSXの変換してReactコンポーネントとしてラップするところまでやってくれるツールです!
WebpackやNext.jsに対応しており、ビルドの設定として組み込むことで前述のSVGファイルを直接importすることも実現できるのですが、今やりたいことはCLI機能で実現できました。
まずは@svgr/cliをプロジェクトにimportしましょう。
※今回の記事は最新版のv6.0.0(なんとこれを書いてる途中の11/29にリリースされた、、!)を前提にしています。
yarn add @svgr/cli -D
ディレクトリ構造
下記のようなディレクトリ構造で諸々のファイルを配置しました。
script/svg-generate
├ svgs // svgファイルの置き場
│ ├ chevron-normal.svg
│ ├ exclamation-mark-circle.svg
│ └ location-arrow.svg
├ svgr.config.js // コンフィグファイル
├ template.tsx // コンポーネントのテンプレート
└ index-template.tsx // indexファイルのテンプレート
svgr.config.js
svgr用の設定ファイルです。今回はとりあえずこれだけ設定しています。typescriptを生成したいのでそちらのオプションと、後述のテンプレートファイルの指定です。
template.tsx(コンポーネントのテンプレート)
出力されるコンポーネント用のテンプレートです。詳しくはこちら。
今回は特に手を加えていないですが、出力されるコンポーネントに何か共通的に加えたい処理がある場合はこちらに書きます。(デフォルトのままでしたら特にこのファイルを作らずオプション指定も不要です。)
index-template.tsx(indexファイル用のテンプレート)
svgrの実行結果としてこのような構造でファイルが出力されます。SVGのコンポーネントとは別に生成されるこのindex.tsxがindexファイルです。
Icon
├ index.tsx //←これがindexファイル
├ AwesomeSVGComponent1.tsx
├ AwesomeSVGComponent2.tsx
└ AwesomeSVGComponent3.tsx
このindexファイルの中身はデフォルトではこういった形でnamed exportをしてくれます。
このおかげで他のコンポーネントから下記のようにnamed importが可能になっています。サジェストも補完もしてくれるので安心です!
import { IconXmarkCircleNormal } from '../Icon'
storybookのためにindex-template.tsxを修正
ただし、今回はこれだけではなく、Storybookも生成したいのでindex-template.tsxに手を加える必要があります。出力したい結果ファイルの内容をtemplate literalsで書いていく感じです(まあまあ力技ですね)。長いですが下記です。
Icon
というprefixを付けてexportしたかったのでところどころ付けてるところがミソです。こういった形である程度自由に書けるのでStorybookのStory fileとしての記述もこのindexファイルに含めてしまおうという作戦です。
package.jsonのscript
SVGファイルの入ったディレクトリを引数にし、-d
で出力ディレクトリを指定、--config
でconfigファイルを指定します。
これで準備完了です。では使ってみましょう。
☺️結果
元となるディレクトリ構造再掲
script/svg-generate
├ svgs // svgファイルの置き場
│ ├ chevron-normal.svg
│ ├ exclamation-mark-circle.svg
│ └ location-arrow.svg
├ svgr.config.js // コンフィグファイル
├ template.tsx // コンポーネントのテンプレート
└ index-template.tsx // indexファイルのテンプレート
コマンド実行します。
yarn icon:generate
src/components/Icon
ディレクトリに下記のような構造でファイル出力されました。
Icon
├ index.tsx
├ IconChevronNormal.tsx
├ IconExclamationMarkCircle.tsx
└ IconLocationArrow.tsx
個々のコンポーネント
indexファイル
いいですね!Storybook用のexportも想定通りちゃんと出力されています。
既に出来上がっているStorybookの設定で起動してみると下記の通り出力されます 🎉(Storybookの細かい設定は省略しますがmain.jsのstoriesに出力されたindex.tsxを指定しました)
これでどんな見た目の何という名前のアイコンコンポーネントがあるか一目瞭然です!
まとめ
svgrを利用することでSVGの画像管理の課題が解決できました。SVGファイルをディレクトリに入れてスクリプトを実行すれば勝手にReactコンポーネントとStory fileが生成されます。Storybookはchromaticを使って簡単に共有できるようになってるのでアイコンに関してのデザイナとの認識合わせ(今プロダクションで何が使われてるんだっけ?みたいな)もはかどりそうです。
こちらを活用して最近始めたばかりのデザインシステム(のためのコンポーネントライブラリ)の構築を効率良く進めていければと思います。
アソビューではデザインシステムを作ったりサイトのSPA化を進めたり、などなどフロントエンド周りでやること、やりたいことが山積みです!一緒にそれらを進めて行ってくれる仲間を大募集中です!
ちょっと話聞いてみたいかも?と思った方、ぜひカジュアル面談しましょう!