方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【インタビューAI】Bootstrapからshadcn UIへの変更:高速で軽量なUIの実現

bootstrap-shadcn-ui
この記事は約14分で読めます。

はじめに

1時間の音声を15秒で文字起こしし自然な会話形式に自動変換する「インタビューAI」を個人開発中。

昨日、ReachBestという「AIを活用して高校生の海外大学への出願をサポートするサービス」を創業している、Ryusei君と話をする機会があり、開発しているインタビューAIのデモを見せて、UIはBootstrapで作成していることを伝えたところ、Shadcn UIというUIフレームワークを勧められたので、早速乗り換えることにした。

https://reachbest.co

shadcn UIに変更するメリット

shadcn/uiは、Radix UIとTailwindCSSを用いて開発された、高い自由度を持つUIコンポーネント集。

shadcn UIは、必要なコンポーネントのみを個別にインポートして使用できるため、アプリケーションが軽量になる。これに対して、Bootstrapは通常、全体のスタイルシートをインポートし、その中に不要なスタイルも含まれることが多い。これがファイルサイズや読み込み時間に影響する可能性がある。

https://ui.shadcn.com

また、shadcn UIは、Reactのコンポーネントとして設計されているため、必要に応じてレンダリングされ、DOMの変更も最適化されている。BootstrapのJavaScriptコンポーネントは、依存関係が多く、時にはパフォーマンスに影響を与えることがある。

ReactでBootstrapからshadcn UIに変更する手順

Bootstrapのアンインストール

Bootstrapをプロジェクトのから削除する(今回はfrontend/で実行)。

ShellScript
npm uninstall bootstrap react-bootstrap

shadcn UIのインストール

shadcn UIをプロジェクトに追加する(今回はfrontend/で実行)。

ShellScript
npm install @shadcn/ui

必要な依存関係

shadcn/uiはTailwind CSSをベースにしているため、Tailwind CSSもインストールして設定する必要がある。以下のコマンドを実行して、Tailwind CSSをインストールする。

ShellScript
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Tailwind CSSの設定

tailwind.config.jspostcss.config.jsが生成されるので、tailwind.config.jsを開いて以下のように設定する。

今回は、ブランドカラーをテーマの拡張として、brand, brand-dark, brand-lightを作成した。

JavaScript
// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      colors: {
        brand: "#3d5a80",
        "brand-dark": "#2a4563",
        "brand-light": "#4a7ea8",
      },
    },
  },
  plugins: [],
};

CSSファイルの設定

src/index.css(またはプロジェクト内の適切なCSSファイル)を以下のように設定します。

JavaScript
@tailwind base;
@tailwind components;
@tailwind utilities;

App.tsxのコードを変更

アプリケーションのエントリーポイントであるApp.tsxのコードを、shadcn UIを使ったボタンやナビゲーションの実装に変更する。

TypeScript
// App.tsx
import { onAuthStateChanged } from "firebase/auth";
import React, { useCallback, useEffect, useState } from "react";
import { Link, Navigate, Route, Routes, useNavigate } from "react-router-dom";
import "./App.css";
import Login from "./components/Auth/Login";
import PasswordReset from "./components/Auth/PasswordReset";
import Register from "./components/Auth/Register";
import SubscriptionForm from "./components/SubscriptionForm";
import TitleAndHeaderGenerator from "./components/TitleAndHeaderGenerator/TitleAndHeaderGenerator";
import Transcription from "./components/Transcription/Transcription";
import { deleteAccount, logout } from "./services/authService";
import { auth } from "./services/firebaseConfig";

import Navbar from "./components/Navbar";
import { Button } from "./components/ui/button";

type UsageData = {
  totalMinutesUsed: number;
  maxMinutes: number;
  titlesAndHeadingsUsage: number;
  maxTitlesAndHeadingsUsage: number;
  resetDate: string | null;
  plan: string;
};

const App: React.FC = () => {
  const navigate = useNavigate();
  const [activeService, setActiveService] = useState<string>("transcription");
  const [transcriptionData, setTranscriptionData] = useState<any>(null);
  const [rewrittenText, setRewrittenText] = useState<string | null>(null);
  const [audioFileUrl, setAudioFileUrl] = useState<string | null>(null);
  const [titleHeaderData, setTitleHeaderData] = useState<any>(null);
  const [currentUser, setCurrentUser] = useState<any>(null);

  const [isUploading, setIsUploading] = useState<boolean>(false);

  const [usageData, setUsageData] = useState<UsageData>({
    totalMinutesUsed: 0,
    maxMinutes: 30,
    titlesAndHeadingsUsage: 0,
    maxTitlesAndHeadingsUsage: 3,
    resetDate: null,
    plan: "free",
  });

  const fetchUsageData = useCallback(async (uid: string) => {
    const backendUrl = process.env.REACT_APP_BACKEND_URL + "/api/user-usage";
    try {
      const response = await fetch(`${backendUrl}?uid=${uid}`);
      if (!response.ok) throw new Error(`サーバーエラー: ${response.status}`);
      const data = await response.json();
      setUsageData({
        totalMinutesUsed: data.totalMinutesUsed || 0,
        maxMinutes: data.maxMinutes || 30,
        titlesAndHeadingsUsage: data.titlesAndHeadingsUsage || 0,
        maxTitlesAndHeadingsUsage: data.maxTitlesAndHeadingsUsage || 3,
        resetDate: data.resetDate || null,
        plan: data.plan || "free",
      });
    } catch (error) {
      console.error("使用回数データの取得エラー:", error);
    }
  }, []);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (user) {
        setCurrentUser(user);
        fetchUsageData(user.uid);
      } else {
        setCurrentUser(null);
      }
    });
    return () => unsubscribe();
  }, [fetchUsageData]);

  const handleLogout = async () => {
    try {
      await logout();
      setCurrentUser(null);
      navigate("/login");
    } catch (error) {
      console.error("Logout failed:", error);
    }
  };

  const handleDeleteAccount = async () => {
    if (window.confirm("本当に退会しますか?")) {
      try {
        await deleteAccount();
        setCurrentUser(null);
        navigate("/login");
      } catch (error) {
        console.error("Account deletion failed:", error);
      }
    }
  };

  return (
    <div className="app-container">
      <Navbar
        currentUser={currentUser}
        onLogout={handleLogout}
        onDeleteAccount={handleDeleteAccount}
      />
      <div className="container mx-auto mt-20 pt-5 px-4">
        <div className="flex justify-center mb-12 mt-4">
          <div className="flex space-x-4">
            <Link to="/transcription">
              <Button
                variant={
                  activeService === "transcription" ? "primary" : "outline"
                }
                className="w-auto"
                onClick={() => setActiveService("transcription")}
              >
                文字起こしAI編集
              </Button>
            </Link>

            <Link to="/title-header-generator">
              <Button
                variant={
                  activeService === "title-header-generator"
                    ? "primary"
                    : "outline"
                }
                className="w-auto"
                onClick={() => setActiveService("title-header-generator")}
              >
                タイトル小見出し自動生成
              </Button>
            </Link>
          </div>
        </div>

        <Routes>
          <Route path="/" element={<Navigate to="/transcription" />} />
          <Route path="/transcription" element={<Transcription />} />
          <Route path="/title-header-generator" element={<TitleAndHeaderGenerator />} />
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />
          <Route path="/password-reset" element={<PasswordReset />} />
          <Route path="/upgrade-plan" element={<SubscriptionForm />} />
        </Routes>
      </div>
    </div>
  );
};

export default App;

変更後のトップ画面がこちら

BootStrapの時と同じレイアウト・色で作成しているので、見た目は変化ないが、少しはレスポンス早くなったかもしれない。

まとめ

shadcn UIは、Reactベースのアプリケーションに最適なUIフレームワーク。Radix UIとTailwind CSSを使用しており、軽量で柔軟性の高いコンポーネントの利用が可能。Bootstrapに比べて、不要なスタイルを取り除き、必要なコンポーネントだけをインポートすることで、アプリケーションのパフォーマンスを向上させることができる。

shadcn UIを導入することで、DOMの最適化やリソースの効率化が図れ、全体のレスポンス速度も向上する。特に、カスタマイズ可能なデザインや軽量な構成が必要なプロジェクトにおいて、そのメリットを最大限に活かせるかもしれない。

タイトルとURLをコピーしました