【Storybook】Chromaticで生成するスナップショット数を最適化するTurboSnap

## Chromatic
[Chromatic](https://www.chromatic.com)はStorybookに対するクラウドベースのツールチェーンです。
StorybookをCDN上に公開したり、UIのテスト、レビュー、ドキュメント化を行うようなワークスペースを提供します。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/ecf467a9-d452-1130-89c3-fd0527665504.png)
引用: https://www.chromatic.com/docs/

ChromaticはStorybook上で行う[インタラクションテスト](https://storybook.js.org/docs/react/writing-tests/interaction-testing)の実施とビルド前の状態からビルド後の状態で見た目の変化を検知するビジュアルテストを実施します(比較対象となるビルド前の状態は[こちらの方法](https://www.chromatic.com/docs/test/#establish-baselines)で選ばれます)。
さらに、Chromaticは開発者だけではなくデザイナーやプロジェクトマネージャーを巻き込んでUIコンポーネントを組み立てるする場を提供してくれます。具体的には、StorybookをCDN上に公開してStorybookの共有を可能に、UIテストによって検知した差分をクラウド上でレビュー可能に、過去の変更やコンポーネントの一覧を可視化してくれます。

ChromaticのビルドはGitHub ActionsのようなCIを用いて行われることが多いです。UIテストで比較対象になるコミットは、Gitの履歴上でビルドされた一番近い位置ものが採用されます。

## スナップショット
Chromaticにはスナップショットという概念があります。これはChromaticによってStoryをメタ画像として生成したものを指します。

![スクリーンショット 2023-09-22 16.04.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/422d320b-5b5b-afc6-86c8-6650e9de6bac.png)

スナップショットはあくまでメタ画像なので実際にブラウザがペインティングする見た目とは異なりますが、スナップショット間における見た目の一貫性は保たれます。そして、スナップショットは主にビジュアルテストを行う際に見た目の比較に使われます。

このようなスナップショットの生成数に応じてChromaticではプランや請求額が決まります(プランによってはCustom DomainやIPに対する特典もあります)。
![スクリーンショット 2023-09-21 16.58.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/aee937cc-4b82-8202-d358-28572dbf2666.png)
スナップショットはビルドのたびに生成されます。スナップショットはStory数と生成するブラウザ数、ビューポイント数の掛け合わせた数だけ生成されます。ブラウザ数とビューポイント数はほとんど据え置きですが、Story数やビルド数は開発規模によって値が大きく変わります。

## TurboSnap
:::note
[バージョン8](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#turbosnap-vite-plugin-is-no-longer-needed
)のリリースで状況が少し変わっているので注意してください。
:::

TurboSnapはChromaticが生成するスナップショットを最適化する機能です。開発規模の肥大化によって増加するスナップショット数を抑えるのに役立つ便利な機能です。
TurboSnapを有効にするには以下の条件を満たす必要があります。
- Chromatic CLI v5.8以上
- Storybook v6.2以上
- Webpack
- Storyの設定が正しくされている
- CI経由で10回以上のビルドが成功し、最低でも一度受け入れられている
- Github Actionsの場合`push`による発火

TurboSnapを有効にすると、ビルドしたコミットと比較するコミットのファイル差分を元にWebpackの[dependency-graph](https://webpack.js.org/concepts/dependency-graph/)を用いて変更に影響のあるStoryのスナップショットだけを生成します。
ただし、比較対象のコミットで生成したビルドのレビューで受け入れられなかったStoryのスナップショットは引き続き生成されます。さらに、`package.json`の変更のように全体に影響を及ぼす変更があればTurboSnapの最適化はスキップされ全てのStoryに対してスナップショットが生成されます。

### 有効化
TurboSnapはCLIでは`--only-changed`を追加して有効にします。
```sh
chromatic --only-changed
```
特定のブランチでのみTurboSnapを有効にしたい場合は[picomatch](https://www.npmjs.com/package/picomatch#globbing-features)を元に指定します。`main`ブランチ以外で有効にする場合は以下のようにします。
```sh
chromatic --only-changed "!(main)"
```
`main`ブランチでは最適化を行わずに、全てのStoryに対してスナップショットを生成することでTurboSnapによる漏れを拾えます。

GitHub Actionsでは以下のように`with`で`onlyChanged`を加えることで有効になります。
```yml:.github/workflows/chromatic.yml
name: "Chromatic"

on: push

jobs:
  chromatic-deployment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Install dependencies
        run: yarn
      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          onlyChanged: true
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
```


TurboSnapが有効な状態でビルドすると、ワークスペースの`TESTS`の右側にTurboSnapの文字とその左に青円が現れます。
![スクリーンショット 2023-09-22 16.36.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/60e9fba8-2a06-7b2b-37db-07b0e14cdd63.png)
青円が表示されているときはTurboSnapによる最適化が行われています。周辺をホバーすることでどれほどのスナップショットが生成され、削減されたのかを確認できます(画像では全てのStoryがスキップされています)。
`package.json`を変更した場合はTurboSnapによる最適化が行われないので円の色が灰色になります。こちらもホバーすることで生成したスナップショット数となぜ行われなかったかが表示されます。
![スクリーンショット 2023-09-22 16.44.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/f3c556f1-9676-d7f6-1d06-4a299cda827b.png)

TurboSnapが有効であることはワークスペース以外では、ビルド時に出力されるメッセージからも確認できます。

### Vite
TurboSnapを利用する場合はwebpackを用いる必要があると書きましたが、[vite-plugin-turbosnap](https://github.com/IanVS/vite-plugin-turbosnap)を導入することでViteを用いたプロジェクトでもTurboSnapを利用できます。

まず、プラグインをdevDependenciesに導入します。
```shell
npm i -D vite-plugin-turbosnap
```
そして、StorybookにおけるViteの設定として`viteFinal`の`plugins`にvite-plugin-turbosnapからインポートしたものを追加することでTurboSnapが有効になります。
```ts
import { StorybookConfig } from '@storybook/react-vite';
import { ConfigEnv, loadConfigFromFile, mergeConfig } from 'vite';
import turbosnap from 'vite-plugin-turbosnap';
import path from 'path';

const configEnvServe: ConfigEnv = {
  mode: 'development',
  command: 'serve',
  ssrBuild: false,
};

const configEnvBuild: ConfigEnv = {
  mode: 'production',
  command: 'build',
  ssrBuild: false,
};

const config: StorybookConfig = {
  // お好みのStorybookの設定
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  async viteFinal(config, { configType }) {
    const isProduction = configType === 'PRODUCTION';
    const { config: userConfig } =
      (await loadConfigFromFile(
        isProduction ? configEnvBuild : configEnvServe,
        path.resolve(__dirname, '../vite.config.ts'),
      )) ?? {};

    return mergeConfig(config, {
      ...userConfig,
      plugins: isProduction
        ? [turbosnap({ rootDir: config.root ?? process.cwd() })]
        : [],
    });
  },
};

export default config;
```
Vite環境でTurboSnapを有効にするにはwebpackよりも厳しい条件が課されることに注意してください。
- Chromatic CLI v6.5.0以上
- Storybook v7以上
- Storybook v6.5以上かつ`@storybook/builder-vite` v0.1.22以上


### TurboSnapの無効化
TurboSnapによる最適化がスキップされるケースとして`package.json`の変更を紹介しました。
その他にもいくつかの条件で最適化がスキップされます。
- `package.json`と`package-lock.json`、`pnpm-lock.yaml`などの依存ファイルの変更
- `.storybook/main.ts`のようなStorybookの設定ファイルの変更
- `.storybook/preview.ts`が利用するファイルの変更
- Storybookで参照する`static`フォルダの変更
- ビルドの再実行
- Chromatic内のレンダリングエンジンのアップグレード

さらに、変更があったときにスキップするファイルを`externals`オプションを使って指定できます。
`src/theme.ts`と`css`配下のファイルを変えた時にTurboSnapをスキップさせる場合は以下のように指定します。
```sh
chromatic --only-changed --externals "src/theme.ts"　--externals　"*.css"
```

### スナップショットの生成に関与しないファイルを指定する
TurboSnapによるスナップショットの生成はStoryに関連するファイルが1つでもあれば行われます。しかし、視覚的な変化を及ぼさないことが明確なファイルも存在します。
そのようなファイルがあれば`untraced`フラグを用いて、スナップショットの生成に関与しないファイルとして指定できます。
```sh
chromatic --only-changed --untraced "backend/**"
```
視覚的な変化を及ぼすファイルをこれで指定してしまうと、生成すべきスナップショットを生成できない可能性があります。確実に影響がないファイルだけを指定するなど、利用には注意してください。

## おわりに
Chromaticはコンポーネントベースの開発で大活躍をしてくれる便利なツールです。しかし、開発規模が大きくなると一度のビルドで大量のスナップショットが生成されるため、予想外の金額的なコストがかかります。
この記事で紹介したTurboSnapを利用することで、1度のビルドで生成するスナップショット数を最適化して安定します。採用してみてはいかがでしょうか。