Rorkに「カート画面に在庫切れバッジを足して」とお願いした夜に、別件で見ていた商品詳細画面の余白がほんの2pxだけずれていることに気付いた、という経験はないでしょうか。私自身、Rorkで作ったECアプリで似たような状況に何度もぶつかりました。AIが書き換えた1ファイルが、共通コンポーネントを通じて遠く離れたスクリーンの見た目を静かに変えていたのです。
問題は「変わったこと」自体ではなく、「変わったことに気付けない」ことにあります。手元のシミュレーターで毎画面を見て回るのは現実的ではありませんし、ステージング環境のスクリーンショットを目視で比較するのも、画面が10を超えたあたりで限界がきます。ここで頼りになるのが、Storybook for React NativeとChromaticを組み合わせた視覚回帰テスト(Visual Regression Testing)です。
ここではRorkで生成したコードベースに対してStorybookとChromaticを後付けで導入する手順、AI生成UI特有のハマりどころ、そして「差分レビュー → 承認 → mainへ反映」までのチームワークフローを、私が実プロダクトで運用しているパターンに沿ってお伝えします。読み終えるころには、Rorkの提案を受け入れる前に「どこが変わるのか」を機械的に確認できる仕組みが、自分の手元でも立ち上げられるはずです。
なぜRorkにこそ視覚回帰テストが効くのか
視覚回帰テストは、もともとデザインシステムを抱える大規模チームのためのプラクティスでした。ただ、Rorkで個人開発を進める場合こそ、その価値が顕著に現れます。理由は3つあります。
第一に、Rorkはプロンプト一発でファイル横断の変更を行います。たとえば「ボタンの角丸を統一して」と指示しただけで、Button.tsx、Card.tsx、Modal.tsxの3ファイルが同時に書き換わることが珍しくありません。差分が広範囲に及ぶ以上、目視レビューでは抜け漏れが必然的に発生します。
第二に、AIは「指示外の周辺コードも整える」癖があります。意図して整えてくれる場合は助かりますが、意図せずに整えられた場合、あとから「なぜここが変わったのか思い出せない」という事態を招きます。視覚回帰テストはこうした「意図しない整え」を黙認しないための見張り番として機能します。
第三に、Rorkは生成コードの安定性を保証してくれません。これは批判ではなく単純な事実で、AIモデルが進化すればコードの書き味も変わります。であれば、人間の側で「見た目の最終形」を契約として固定する仕組みを持っておく方が、長期運用では安全です。Storybookに刻んだストーリーは、いわば「このコンポーネントはこう見えるべき」という静かな宣誓書です。
私はこれをやらずにRorkで3つアプリを作って公開し、3つともリリース後に「あれ、この画面の見た目、こんなだったっけ」と感じる場面に出くわしました。視覚回帰テストはAI時代の保険のようなものだと考えてください。
Storybook for React Native の現状(2026年4月時点)
Rorkプロジェクトに導入する際に最初に把握しておきたいのが、Storybook for React Nativeの選択肢です。2026年4月現在、選択肢は実質3系統あります。
@storybook/react-native は端末上で実際にコンポーネントを動かしながら確認するための公式パッケージです。実機の挙動が見られる反面、CIで動かすには工夫が要り、Chromaticに直接アップロードする用途には少し距離があります。
@storybook/react-native-web-vite は、React Native Webを通じてストーリーをブラウザ上で表示するアプローチです。ChromaticはWeb版Storybookを前提に動くため、視覚回帰テストの運用にはこちらが明確に向いています。私が今実プロジェクトで採用しているのもこの構成です。
そしてStorybook 9 系では、両者を1つのStorybook設定の中で切り替えられる仕組みが整いつつあります。とはいえ、安定運用を狙うならまずWeb側に振り切ってしまう方がトラブルが少ないというのが私の現時点の判断です。
「実機でしか出ないバグ(タッチ範囲、ジェスチャー、ネイティブモジュール)はどうするのか」という疑問は当然あると思いますが、それらは視覚回帰テストの守備範囲ではなく、Rork × Detox / Maestro でつくるE2Eテスト基盤の領域です。視覚回帰は「色・余白・配置・タイポ」を見張るための仕組みだと割り切ると、設計判断がぶれません。
ステップ1: 既存Rorkプロジェクトに後付けでStorybookを入れる
新規プロジェクトでないところがミソです。Rorkで作成済みのExpoプロジェクトを前提に、最小限のファイル変更で立ち上げます。前提環境は Expo SDK 53以降、React Native 0.79以降、Node 20以降です。
# 必要なパッケージを一括導入
npx expo install react-native-web react-dom @expo/metro-runtime
npm install --save-dev \
storybook@^9 \
@storybook/react-native-web-vite@^9 \
@storybook/addon-essentials@^9 \
@storybook/addon-interactions@^9 \
@vitejs/plugin-react@^4 \
vite@^5
# Storybook 設定ファイルを生成(Vite + React Native Web)
npx storybook@latest init --type react-native-web
# 期待する出力:
# ✔ Storybook configuration files were created in ./.storybook
# ✔ npm run storybook で起動できます--type react-native-web を指定するのがポイントです。指定し忘れて素のreactテンプレートを生成すると、react-nativeからのimportが解決されずビルドが落ちます(私はこれで一晩溶かしました)。
生成された .storybook/main.ts は、次のように react-native を react-native-web に解決させる設定を追加してから運用しています。
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-native-web-vite";
import { mergeConfig } from "vite";
const config: StorybookConfig = {
stories: [
"../components/**/*.stories.@(ts|tsx)",
"../app/**/*.stories.@(ts|tsx)",
],
addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
framework: {
name: "@storybook/react-native-web-vite",
options: {},
},
// Rorkが生成しがちな絶対パスエイリアス(@/components/...)を吸収
viteFinal: async (config) =>
mergeConfig(config, {
resolve: {
alias: {
"@": new URL("../", import.meta.url).pathname,
},
// 拡張子の解決順を明示。これがないと .web.tsx が拾われずに UI が崩れます
extensions: [".web.tsx", ".web.ts", ".tsx", ".ts", ".jsx", ".js"],
},
}),
};
export default config;extensions の並びを web.tsx から始めるのが地味に重要です。並びを間違えると、ネイティブ向けの Platform.select 分岐が不適切に解決され、Web上でだけビューが消えるという厄介な現象が起きます。実際にこれで30分悩みました。
ステップ2: 最初のストーリーを書く(Rork生成コンポーネントを“契約化”する)
導入が済んだら、Rorkに生成してもらった既存コンポーネントを1つ選び、その「あるべき見た目」をストーリーとして固定します。私はいつも、もっとも壊れやすそうなコンポーネントから始めます。具体的には、Button、Card、PriceTagのような他の画面にも埋め込まれる原子レベルのものです。
// components/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
title: "Atoms/Button",
component: Button,
// Rorkが将来「ボタンに loading 状態を追加」とコードを書き換えた時に
// ここで定義したストーリー数だけ視覚回帰スナップショットが撮られます
args: {
label: "購入する",
onPress: () => {},
},
parameters: {
// Chromatic 側で複数ビューポートを撮るための指定
chromatic: { viewports: [375, 414, 768] },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: "primary" },
};
export const Disabled: Story = {
args: { variant: "primary", disabled: true },
};
export const Destructive: Story = {
args: { label: "削除する", variant: "destructive" },
};
// 期待する見た目:
// - Primary: ブランドカラー背景 + 白文字
// - Disabled: 50% 不透明 + ポインターイベント無効
// - Destructive: 赤系背景 + 白文字Rorkでアプリを開発していると、コンポーネントのvariant(バリエーション)が雪だるま式に増えがちです。私は経験上、1つのコンポーネントに対しては「最低3つ、多くて7つ」のストーリーで打ち止めにしています。8つ目を書きたくなったら、それは別コンポーネントに分けるサインだと考えるくらいでちょうどよいバランスです。
ステップ3: Chromaticに繋いで視覚回帰テストを動かす
ローカルで npm run storybook が動くようになったら、Chromaticに接続します。Chromaticは無料枠でも月5,000スナップショットまで撮れるため、個人開発であれば長らく無償で運用できる点も助かります。
# Chromatic CLI の導入
npm install --save-dev chromatic
# Chromatic 公式サイト(chromatic.com)でプロジェクトを作成し、
# 表示されたプロジェクトトークンを CHROMATIC_PROJECT_TOKEN に設定
export CHROMATIC_PROJECT_TOKEN="chpt_xxxxxxxxxxxxxxxx"
# 初回アップロード(mainブランチのベースライン作成)
npx chromatic --project-token="$CHROMATIC_PROJECT_TOKEN" \
--build-script-name=build-storybook \
--exit-zero-on-changes
# 期待する出力(抜粋):
# Build 1 published.
# View it online at https://www.chromatic.com/build?appId=...
# 12 stories captured ← 撮影されたストーリー数--exit-zero-on-changes を初回は付けておくのがおすすめです。何も差分がない初回ビルドでもCIが赤くならず、ベースライン確立フェーズと運用フェーズを意図的に切り分けられます。2回目以降のCIではこのフラグを外し、差分があれば赤になるように戻します。
ステップ4: GitHub Actions で「Rorkが書き換えるたびに」差分を可視化する
Rork CompanionでPRを切る運用、もしくはGitHub上で手元の差分をPRにする運用を取っているなら、ChromaticをGitHub Actionsに乗せておくと一気に体験が変わります。RorkのプロンプトでPRが立つたびに、差分のサムネイルがChromatic側に並ぶ仕組みです。
# .github/workflows/chromatic.yml
name: Chromatic
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
visual-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Chromatic はベースラインとの差分計算のため履歴を必要とします
# depth: 0 を忘れるとベースライン未検出のエラーになります
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Run Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
# PR時はビルドが落ちることでブロックする(mainには差分を持ち込ませない)
exitZeroOnChanges: false
# スナップショット未承認のままmainへマージするのを防ぐ
onlyChanged: true
# 失敗時にChromaticのレビューURLをPRコメントに残すと、レビュアーが迷いません
- name: Comment Chromatic URL on PR
if: failure() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '視覚回帰差分を確認してください: ${{ steps.chromatic.outputs.url }}',
});
# 期待する挙動:
# 1. PR作成 → Chromatic が新しいビルドを作る
# 2. ベースラインと比較し、差分があれば PR を Failure にする
# 3. レビュアーが Chromatic 上で差分を承認 → CI が Success に変わる
# 4. main へマージonlyChanged: true を入れているのは、Rorkで小さな変更を頻繁に出すワークフローと相性が良いからです。コードベース全体ではなく「実質的に影響を受けたストーリーだけ」を再撮影してくれるため、Chromaticのスナップショット消費量を月1,000枚以下に抑えながら、ほぼリアルタイムなフィードバックが得られます。
ステップ5: AI生成UI特有のレビューワークフローを整える
ここまでが技術的な土台です。ですが、視覚回帰テストの真の価値は運用設計にあります。Rorkに任せきりにせず、人間が「承認するか却下するか」を毎回決める仕組みに昇華させましょう。
私のチーム(といっても私一人ですが)では、PRテンプレートに以下のチェックリストを埋め込んでいます。
<!-- .github/pull_request_template.md -->
## 視覚回帰チェック
- [ ] Chromatic ビルドがグリーンであることを確認した
- [ ] 意図的な差分は Chromatic 上で承認した(コメント欄に理由を1行書く)
- [ ] 意図せぬ差分は Rork に再プロンプトして取り除いた
- [ ] ベースライン更新に伴うリグレッションを確認した
(`Atoms/`配下が変わったら、依存する`Organisms/`の差分も必ず開く)
## Rorkプロンプト履歴
<!-- 今回 Rork に投げたプロンプトを貼ってください。-->
<!-- AIが将来の保守者に「なぜこの差分なのか」を説明できる唯一の手がかりです。 -->特に大事なのが3項目目の「意図せぬ差分はRorkに再プロンプトして取り除く」です。AIが追加で整えた要素を黙って受け入れていくと、デザインの一貫性が静かに崩れていきます。私は「意図せぬ差分が3件以上見つかったらPRを破棄して、もう一度プロンプトから書き直す」というルールを自分に課しています。一見遠回りに見えますが、結果として後工程の手戻りが激減しました。
このあたりの考え方は、AI生成UI全般の運用ノウハウとしてAIアプリ開発のためのデザインシステム構築術とも地続きの話です。デザインシステムを「先に固定」してから、視覚回帰テストで「後から守る」という二段構えが、もっとも崩れにくい運用になります。
よくある間違い・落とし穴
導入したものの運用がうまく回らない、というケースで私がよく目にする3つのアンチパターンをお伝えします。
第一に、フォントの読み込み忘れ。Rorkで生成したコンポーネントは expo-font で読み込むカスタムフォント前提のことが多いのですが、Storybook側で同じフォントを読み込まないと、Chromaticのスナップショットに本番とは違うフォールバックフォントが表示され、毎回「差分」と判定されます。.storybook/preview.tsx で useFonts を呼んでフォントが揃ってから <Story /> を返す形にしましょう。
// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { useFonts } from "expo-font";
import React from "react";
const preview: Preview = {
decorators: [
(Story) => {
const [loaded] = useFonts({
"Inter-Regular": require("../assets/fonts/Inter-Regular.ttf"),
"Inter-Bold": require("../assets/fonts/Inter-Bold.ttf"),
});
// フォント未ロード時は描画しない(チラつき・誤差分の温床になります)
if (!loaded) return null;
return <Story />;
},
],
};
export default preview;第二に、ダークモードを撮らないまま運用する。Rorkはダークモード対応コードを書くのは得意ですが、テスト側でモードを切り替えてあげないと、実態として「ライトモードしか守れていない」状態になります。Storybookの globalTypes でテーマを切り替え、Chromaticの parameters.chromatic.modes で両モードを同時に撮る設定にしておきましょう。
第三に、動的データを抱えたままストーリーを書く。new Date() や Math.random() をコンポーネント内で直接呼んでいると、撮影のたびに表示が変わって「常に差分あり」の状態になります。これは parameters.chromatic.disableSnapshot: true で逃げるのではなく、コンポーネント側で dateProvider のようにDIを切れるようにリファクタするのが正攻法です。リファクタ自体もRorkに任せられるので、面倒でも一度直しておく価値があります。
本番投入に向けた応用パターン
最後に、私が今運用している応用パターンを2つ紹介します。
ひとつは、プレミアム機能のストーリーを別ファイルに切り出すやり方です。PaywallModal.stories.tsx のような有料導線UIは、デザインの安定が課金率に直結します。Atoms/、Organisms/、Paywalls/ のように title を分け、Chromaticのレビュー時に「課金UIだけ厳しく見る」運用を作っています。実際、これを導入してから「気付かないうちに価格表記がずれていた」という事故がゼロになりました。
もうひとつは、Rorkプロンプトとストーリーをペアで保存するパターンです。ストーリーファイルの先頭コメントに、そのコンポーネントを生成・修正したときのプロンプトを残しておくと、3ヶ月後に同じUIを再生成する必要が出たときに迷いません。AI生成UIの保守は「過去のプロンプト」が一次史料になります。
// components/PaywallModal.stories.tsx
/**
* 生成プロンプト履歴:
* 2026-04-12: 「Pro / Premium / Premium感謝価格 の3プランを縦並びで」
* 2026-04-20: 「Premium感謝価格に『期間限定』バッジを追加」
*
* このコンポーネントを書き換える前に、このプロンプト履歴を読むこと。
* 履歴を更新せずに変更すると、3ヶ月後の自分が泣きます。
*/
import type { Meta, StoryObj } from "@storybook/react";
import { PaywallModal } from "./PaywallModal";
// ...(以下省略)このひと手間が、AIで作るアプリを「運用に耐える資産」に変えてくれます。視覚回帰テストはツールに過ぎず、本当に守りたいのは「自分が作りたかったUI」だという原点を思い出させてくれる仕組みでもあります。
Rork × Storybook × Chromatic の話は、突き詰めると「AIに任せきりにしないための儀式」を整える話です。プロンプトを投げて気持ちよくUIが出来上がる体験は素晴らしいですが、その気持ちよさの裏で何が壊れていたかを後から知るのは辛い経験です。今日この記事を閉じたら、まずは自分のRorkプロジェクトで一番壊したくないコンポーネント1つだけを選び、Button.stories.tsx を書いてChromaticの初回ビルドを通してみてください。それだけで、明日からのRorkとの付き合い方が確実に変わります。
視覚回帰テストの設計思想や運用パターン