🚀

glucose flightのフロント品質を支えるVRT戦略 ― モノレポ×自動テストの実践

に公開

はじめに

前回の記事では、glucose flightの技術スタック全体について紹介しました。

今回は、そのフロントエンド部分をより詳細に公開します。複数のアプリケーションを3名のエンジニアで開発・運用している弊社が、モノレポ構成による高速開発を実現し、さらに自動化によって開発者が品質管理を極限まで気にしなくて良い環境をどのように構築しているかを解説します。

特に、この記事ではVRT(ビジュアルリグレッションテスト)を自動化の実践例として詳しく紹介します。GitHub Actionsによる自動検知からPlaywrightでのスクリーンショット比較まで、ブラウザ間の表示差異を自動的に発見する仕組みを深堀りします。

フロントエンドモノレポ

モノレポ構成を選んだ理由

弊社では、複数のステークホルダー向けに異なるアプリケーションを提供しています:

  • 患者向けアプリ(PWA + モバイルアプリ)
  • 医療従事者向けダッシュボード
  • 管理栄養士向けダッシュボード
  • 運用管理者向けツール
  • レポート生成アプリケーション
  • 試験管理ダッシュボード

これら複数の異なるユーザー向けアプリを効率的に管理するため、フロントエンドはモノレポ構成を採用しました。

以前の課題

モノレポ導入前は、フロントエンドのリポジトリを分けて管理していました。その結果、以下の課題に直面していました:

  • ライブラリのバージョン管理の複雑化: Renovateが作成してくれるPRが各リポジトリに発行され、追いつかない。
  • 共通ロジックの散在: 日付処理やバックエンドのAPIクライアントをラップする処理が各リポジトリに散らばっていた。
  • UI/UXの不統一: 共通のUIコンポーネントを管理する仕組みがなく、似たようなコンポーネントが各プロジェクトで独自に実装されていた。

これらの課題を解決するため、モノレポ構成への移行を決断しました。

アーキテクチャ構成

以下は、フロントエンドモノレポから共通のバックエンドAPIに通信する全体構成を示したアーキテクチャ図です(簡略化しています)。

すべてのフロントエンドアプリケーションは、共通のバックエンドAPIに対して通信を行います。これにより、バックエンドの仕様変更が全アプリケーションに一貫して反映され、開発・運用の効率が向上します。

この構成により、新しいWebアプリケーションを追加する際も、既存のコンポーネントを組み合わせるだけで高速に開発できます。

ディレクトリ構成

glucose-flight-frontend/
├── apps/                          # アプリケーション層
│   ├── glucose-flight-app/        # 患者向けアプリ(PWA + Tauri V2)
│   ├── glucose-flight-dashboard/  # 医療従事者向けダッシュボード
│   ├── glucose-flight-dietitian/  # 管理栄養士向けダッシュボード
│   ├── glucose-flight-ops/        # 運用管理者向けツール
│   ├── glucose-flight-report/     # レポート生成アプリケーション
│   └── glucose-flight-sponsored-trial/  # 試験管理ダッシュボード
└── packages/                      # 共通ライブラリ層
    ├── glucose-flight-ui/         # UIコンポーネントライブラリ
    ├── glucose-flight-swr/        # API通信Hooksライブラリ
    ├── glucose-flight-core/       # 共通ビジネスロジック
    └── glucose-flight-rrv7/       # React Router V7ユーティリティ
    ...

この構成にしたメリット

上記の構成により、フロントエンド開発の生産性が劇的に向上しました。

  • 共通UIコンポーネントの再利用: packages/glucose-flight-uiとして約60個のコンポーネントを全アプリで共有。Storybookから選んで組み合わせるだけで、デザインの統一感を保ちながら新規画面を高速に実装できます。
  • API通信ロジックの共通化: packages/glucose-flight-swrでSWRベースのHooksを提供し、全アプリでバックエンドAPIから統一されたデータフェッチング戦略を実現。キャッシュやエラーハンドリングを意識せずに開発できます。
  • 依存関係の更新が一括で完結: Renovateによる依存関係の更新が1つのPRで完結し、以前のように各リポジトリに個別にPRが作成される煩雑さを解消。メンテナンス工数を大幅に削減
  • バックエンドAPIクライアントの共通化: OpenAPI仕様から生成したTypeScriptクライアントを全アプリで共有し、型安全なAPI通信を実現。
  • ビジネスロジックの一元管理: 日付処理やバリデーションなどの共通ロジックが散在せず、packages/配下で一元管理。重複実装を防止

共通UIコンポーネントライブラリ(glucose-flight-ui)

Radix UI + Emotionでの地道な実装

glucose-flight-uiは、全アプリケーションで使用される統一されたUIコンポーネントライブラリです。約60個のコンポーネントを提供しており、基本的なButton、Input、Selectから、Chart、Calendar、Chat、AIAssistantなどの複雑なコンポーネントまで網羅しています。

これらのコンポーネントは、Radix Primitiveをベースに、Emotionを駆使してラップして、エンジニア全員で地道に作り上げました。(個人的には結構楽しかった)

なぜRadix UI + Emotionなのか

  • Radix UI: スタイルを持たないヘッドレスUIコンポーネント。アクセシビリティが標準で担保されており、自由にスタイリング可能。
  • Emotion: CSS-in-JSによる動的なスタイリングとテーマ管理。型安全なスタイリングが実現できる。

コンポーネントにShadcn/uiやスタイリングにTailwindを採用しなかった理由は、破壊的変更への追従コストと、純粋なCSS記法で書けるライブラリを望んでいたためです。

Storybookによるコンポーネントカタログ

glucose-flight-uiのすべてのコンポーネントには、Storybookが用意されています。

以下は、Storybookで管理している約60個のコンポーネントの一覧です。コンポーネント数だけ見てもらえると幸いです。

Storybookコンポーネント一覧
Storybookのコンポーネントカタログ画面

開発効率の劇的な向上

フロントエンド開発時は、Storybookのカタログから使えるコンポーネントを探して使うというワークフローが確立しています。

このワークフローにより、新規画面の実装速度が飛躍的に向上しました。 以前は1つの画面を実装するのに丸1日かかっていたものが、既存のコンポーネントを組み合わせるだけで数時間で完成するケースも増えています。

  • 新規画面開発時に、既存のコンポーネントを探す手間が削減。
  • コンポーネントの使い方や実装例、Propsが一目で分かる。
  • デザインの統一感が自然に保たれる。
  • Storybookを見ていると楽しい、開発モチベ上がる!(これ結構重要)

glucose-flight-uiglucose-flight-swrの組み合わせにより、フロントエンド開発の生産性が爆上がりしました。 UIはStorybookから選び、API通信は共通Hooksを使うだけで、型安全かつ高速に機能を実装できます。

現在はエンジニア向けのツールとして運用しており、実装時に必要なコンポーネントをカタログから探して使うという流れが定着しています。

VRT(ビジュアルリグレッションテスト)の運用

モノレポ構成により高速開発を実現している弊社ですが、速度だけでなく品質を自動的に担保する仕組みも重要です。

その代表例が、Playwrightを用いたVRTです。UIコンポーネントの変更を自動検知し、ブラウザ間の表示差異をPR上で視覚的に確認できるため、開発者はブラウザごとの表示確認を手動で行う必要がありません。 ここでは、この自動化の仕組みを詳しく解説します。

実行タイミングと仕組み

VRTは以下のタイミングで実行されます:

  1. packages/glucose-flight-ui/配下のファイルに変更があったとき、GitHub Botが自動で検知
  2. PR上に「UIコンポーネントの変更が検出されました」という通知コメントが表示される。
  3. コメント内のチェックボックスにチェックを入れると、VRTが実行される。
  4. SafariとChromeの両方でスクリーンショットを取得。
  5. PR上に画像ベースで差分が表示される。

UI変更の検知
packages/glucose-flight-ui配下の変更を自動検知し、VRT実行を促す通知

この仕組みにより、UIコンポーネントの変更がある場合のみVRTを実行できるため、不要な実行を避けつつ、確実に視覚的な検証を行うことができます。

メリット

  • ブラウザ間の表示差異を自動検出: ChromeとSafariでは、ユーザーエージェントスタイルやデフォルトCSSの違いにより表示が異なることがあります。VRTにより、Chromeで開発した見た目がSafariでは意図しない表示になっていないかを事前に発見できます。
  • 意図しないスタイル変更の検出: 1つのコンポーネント変更が他のコンポーネントに影響していないか確認できる。
  • レビューの質向上: 視覚的な変更をPR上で確認できるため、レビュアーが変更内容を理解しやすい。

実際の変更例とPR上での確認

VRTの効果を実感していただくため、実際のコード変更とPR上での差分表示を紹介します。

シナリオ:ブランドカラーとコンポーネントサイズの改善

今回は、以下のような実務でよくある改善を実施しました:

1. ブランドカラーのリニューアル(theme.ts)

プライマリカラーを深い青から明るい青に変更し、より親しみやすい印象に:

  primary: {
    scale: {
-     9: '#074877',  // 深い青
+     9: '#0066CC',  // 明るい青
    },
    // ...
  }

プライマリカラーの変更
プライマリカラーの変更による視覚的な影響(AIアシスタントコンポーネント)

2. ボタンサイズの最適化(button.tsx)

モバイルでのタップしやすさを考慮し、各サイズを拡大:

  case 'small':
    return css`
-     height: 24px;
-     padding-left: 10px;
-     padding-right: 10px;
+     height: 28px;
+     padding-left: 12px;
+     padding-right: 12px;
    `;

ボタンUIの変更
ボタンサイズ変更による視覚的な差分(上:Chromium、下:WebKit)

3. カードコンポーネントのパディング調整(card.tsx)

情報密度を下げて読みやすさを向上:

  const StyledCardContent = styled('div')`
-   padding: 16px;
+   padding: 20px;
  `;

カードUIの変更
カードパディング変更による視覚的な差分

PR上での視覚的な差分確認

これらの変更をPRに反映すると、以下のようにStorybookで作成された全コンポーネントのスクリーンショットの差分が自動で表示されます:

VRT差分の例
PR上でのVRT差分表示の例(2-up、Swipe、Onion Skinの切り替えが可能)

この例では、わずか数行のコード変更が、複数のコンポーネントの見た目にどう影響するかが一目で分かります。

レビュアーは、コードレビューだけでは気づきにくい視覚的な影響を画像で直接確認できるため、以下のような判断が容易になります:

  • ✅ 意図した変更が正しく反映されているか
  • ✅ 予期しない副作用が発生していないか
  • ✅ ブラウザ間(Safari / Chrome)でユーザーエージェントスタイルの違いによる表示差異がないか
  • ✅ デザインの一貫性が保たれているか

VRTの技術的な仕組み

ここからは、このVRTがどのように実装されているかを簡単に解説します。

1. GitHub Actionsによる自動化

VRTは2つのGitHub Actionsワークフローで構成されています。

① UI変更の検知とコメント通知(vrt-prompt.yaml)

jobs:
  check_paths:
    runs-on: ubuntu-latest
    outputs:
      ui_changed: ${{ steps.filter.outputs.ui }}
    steps:
      - uses: dorny/paths-filter@v3
        with:
          filters: |
            ui:
              - 'packages/glucose-flight-ui/**'

dorny/paths-filterを使用して、packages/glucose-flight-ui/配下のファイル変更を検知します。変更が検出されると、GitHub Botが自動でPRにレビューコメントを投稿します:

- name: Create Review Comment
  uses: actions/github-script@v7
  with:
    script: |
      await github.rest.pulls.createReview({
        owner, repo, pull_number,
        body: `## 🖼️ UIコンポーネントの変更が検出されました
        
        - [ ] VRTを実行する(チェックするとワークフローが開始されます)`,
        event: 'COMMENT'
      });

このチェックボックスをONにすることで、次のワークフローがトリガーされます。

② VRTの実行とベースライン更新(visual-regression-test.yaml)

jobs:
  check_should_run_vrt:
    steps:
      - uses: actions/github-script@v7
        script: |
          const hasCheckedBox = review.body && 
            review.body.includes('- [x] VRTを実行する');
          core.setOutput('should_run_vrt', hasCheckedBox ? 'true' : 'false');

  run_vrt:
    needs: check_should_run_vrt
    if: ${{ needs.check_should_run_vrt.outputs.should_run_vrt == 'true' }}
    runs-on: macos-latest-large
    steps:
      - name: Build Storybook
        run: npm run build-storybook
      
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      
      - name: Run visual tests update
        run: npm run test:visual:update

チェックボックスがONになると、Storybookをビルドし、Playwrightで全コンポーネントのスクリーンショットを取得します。

2. Playwrightによるスクリーンショット取得

Playwright設定(playwright.config.ts)

const config: PlaywrightTestConfig = {
  testDir: '.visual-tests',
  timeout: 10 * 60 * 1000,
  projects: [
    {
      name: 'chromium',
      use: { browserName: 'chromium' },
    },
    {
      name: 'webkit',
      use: { browserName: 'webkit' }, // Safari相当
    },
  ],
};

ChromiumとWebKit(Safari相当)の2つのブラウザでテストを実行します。

VRTテストコード(storybook.spec.ts)

test('ストーリーのスクリーンショットテスト', async ({ page }) => {
  for (const story of stories) {
    if (!story.isStable) {
      console.log(`Skipping experimental story: ${story.title}`);
      continue;
    }

    for (const viewport of story.viewports) {
      await page.setViewportSize({
        width: viewport.width,
        height: viewport.height,
      });

      await page.goto(
        `${STORYBOOK_URL}/iframe.html?id=${story.id}&viewMode=story`,
        { waitUntil: 'networkidle' },
      );

      await expect(page).toHaveScreenshot(
        `${story.id}-${viewport.name}.png`,
        {
          fullPage: true,
          maxDiffPixelRatio: 0.01,
        },
      );
    }
  }
});

Storybookの全ストーリーを巡回し、指定されたビューポートサイズでスクリーンショットを撮影します。

3. Storybookのタグシステムによる制御

各コンポーネントのStorybookストーリーには、VRT制御用のタグを指定します:

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: [
    'stable',           // VRT対象として含める
    'viewport:200x70'   // テスト対象のビューポート
  ],
};

タグの種類:

  • stable: VRTの対象として含める(本番運用中のコンポーネント)
  • experimental: VRTの対象外(開発中のコンポーネント)
  • viewport:200x70: カスタムビューポートサイズ
  • viewport:mobile: 定義済みビューポート(375x667)
  • delay:1000: スクリーンショット前の待機時間(ミリ秒)

これにより、開発中のコンポーネントはVRT対象外とし、本番運用中のコンポーネントのみを自動テストできます。

4. スクリーンショット差分の自動コミット

VRT実行後、新しいスクリーンショットは自動でPRブランチにコミットされます:

- name: Commit and push changes
  run: |
    git add -A packages/glucose-flight-ui/.visual-tests/
    if git diff --staged --quiet; then
      echo "更新すべき変更はありません。"
    else
      git commit -m "Update visual tests baseline."
      git push
    fi

これにより、PRの差分として変更前後のスクリーンショットがGitHub上で視覚的に確認できるようになります。

モノレポ運用の工夫と課題

モノレポのメリット

1. 共通UIカタログの活用

全アプリケーションで統一されたUIコンポーネントを使用できます。新規アプリケーションを追加する際も、既存のコンポーネントライブラリをそのまま使えるため、開発速度が飛躍的に向上します。

2. バックエンドAPIクライアントの共通化

弊社では、バックエンドのOpenAPI仕様から自動生成したTypeScriptクライアントを、npmの非公開パッケージとして公開しています。

このTypeScriptクライアントをpackages/glucose-flight-swrでSWRベースのHooksとしてラップすることで、全アプリケーションで統一されたAPI通信を実現しています。

開発フロー:

  1. バックエンド(NestJS)がOpenAPI仕様を自動生成
  2. OpenAPI仕様からTypeScriptクライアントを自動生成
  3. TypeScriptクライアントをnpmパッケージとして公開
  4. glucose-flight-swrでHooksとしてラップ
  5. 全アプリで型安全なAPI通信を実現

メリット:

  • 型の一貫性: バックエンドの型定義がフロントエンドに自動で反映される
  • API変更への追従: バックエンドのAPI変更時、TypeScript型エラーで検出できる
  • キャッシュ戦略の統一: SWRによる自動キャッシュ・再検証が全アプリで統一
  • エラーハンドリングの共通化: 401エラーの認証リダイレクトなど、共通のエラー処理を実装

3. ビジネスロジックの一元管理

packages/glucose-flight-core/に共通のビジネスロジック(日付処理、バリデーション、データ変換など)を集約できます。

4. 依存関係の更新が一括で可能

Renovateによる依存関係の更新が、1つのPRで完結します。以前は各リポジトリに個別にPRが作成されていましたが、モノレポ化により大幅に効率化されました。

3名の少数精鋭チーム

前回の記事でも触れましたが、弊社では3名のエンジニアでこれらすべてを開発・運用しています。

重要なのは、フロントエンドエンジニア・モバイルエンジニア・バックエンドエンジニアという分業制ではなく、全員「エンジニア」としてプロダクト全体を見られる体制にしていることです。

モノレポ構成により、1人のエンジニアが複数のアプリケーションを把握できて開発できるため、少数精鋭でも高い生産性を維持できています。

最後に

THE PHAGE

今回は、glucose flightのフロントエンド開発におけるモノレポ構成による高速開発と、自動化によって開発者が品質を極限まで気にしなくて良い環境について解説しました。

特にVRTの実装を詳しく紹介しましたが、これは自動化の一例に過ぎません。共通UIコンポーネントライブラリや共通API Hooksも、開発速度と品質を両立させるための重要な基盤です。

もちろん課題もありますが、複数のアプリケーションを3名のエンジニアで開発・運用できているのは、モノレポ×自動化というこの高生産性なアーキテクチャのおかげだと確信しています。


株式会社ザ・ファージでは、Full TypeScriptでの開発により、フロントエンド・モバイル・バックエンド・インフラの垣根を超えてプロダクト全体を見られるエンジニアを募集しています。

私たちが目指すもの

日本には約1,000万人の糖尿病患者がいると言われており、適切な血糖管理は患者の生活の質を大きく左右します。しかし、従来の医療現場では、患者と医療従事者のコミュニケーションは限られた診察時間に制約され、日々の生活習慣の改善をサポートすることが困難でした。

私たちは、テクノロジーの力でこのような課題を解決し、患者が自分の健康をより良くコントロールできる世界を実現しようとしています。

このような方と一緒に働きたい

  • 技術で課題を解決することに情熱がある方
  • フロントエンドもモバイルもバックエンドも書ける、あるいは書けるようになりたい方
  • TypeScriptが好きで、Full TypeScript環境で開発したい方
  • アーキテクチャ設計やプロダクト全体の技術的意思決定に関わりたい方
  • スタートアップの環境で、裁量を持って開発に取り組みたい方

一緒に働く魅力

  • プロダクト全体を見渡せる:特定の領域に閉じず、フロントエンドからインフラまで一貫して開発できます。
  • 成長できる環境:医療ドメイン、LLM、大規模データ処理など、多様な技術領域に触れられます。
  • 社会的意義のあるプロダクト:患者や医療従事者から直接フィードバックを受け、プロダクトの価値を実感できます。

少しでも興味を持っていただけた方へ

弊社ではカジュアル面談を随時受け付けています。

本当はもっとUIコンポーネントの実装の工夫やSWR Hooksの設計についてもコードベースで深掘りしたかったのですが、記事が長くなりすぎるため今回は割愛しました。技術スタックの詳細やプロダクトについて、もっと詳しく知りたい方は、弊社のホームページからお気軽にご連絡ください。

一緒に働けることを楽しみにしています!

GitHubで編集を提案
THE PHAGE, Inc.

Discussion