🎨

バックエンドエンジニアがコンポーネント駆動開発に挑戦してみた!

2025/01/20に公開

バックエンドエンジニアがコンポーネント駆動開発に挑戦してみた!

はじめに

みなさん、こんにちは!バックエンドエンジニアのやまとです。
普段はPythonのFastAPIでAPI開発をしているんですが、最近Next.jsの勉強を始めてみました。

「うわ、フロントエンド開発って、バックエンドと全然違うじゃん...」

そんな戸惑いを感じていた時に出会ったのが、コンポーネント駆動開発でした。
今回は、バックエンドエンジニアの私がコンポーネント駆動開発に挑戦してみた体験談を共有したいと思います!

もしも間違いなどありましたら、コメントでご指摘いただけると嬉しいです。随時修正を入れていきたいと思います。

結論

最初に結論から言っちゃうと:
バックエンドエンジニアの私でも、コンポーネント駆動開発を使ったら、意外とスムーズにフロントエンド開発を進められました!

特に良かったのが、APIの設計と同じように「インターフェース」を意識しながら開発を進められる点。
バックエンドエンジニアの私にとっては、すごく馴染みやすかったんです。
それに、コンポーネントの再利用性とテストのしやすさは、マイクロサービスの考え方にも通じるものがあって、なんだか得心がいきました。

コンポーネント駆動開発ってナニモノ?

コンポーネント駆動開発って、UIの部品(コンポーネント)を小さい単位から作っていって、それらを組み合わせて画面を作っていく開発手法なんです。

FastAPIでAPI開発する時、エンドポイントごとにレスポンスの型や動作を決めますよね?
コンポーネント駆動開発も同じような感じで、UIの部品ごとに「入力(props)」と「出力(表示)」をはっきり決めていきます。

例えば、ボタンコンポーネントを考えてみましょう:

// Button.tsx
import React from 'react';

interface ButtonProps {
  variant: 'primary' | 'secondary';
  size: 'small' | 'medium' | 'large';
  children: React.ReactNode;
  onClick?: () => void;
}

export const Button: React.FC<ButtonProps> = ({
  variant,
  size,
  children,
  onClick
}) => {
  const baseStyles = 'rounded font-medium transition-colors';
  
  const variantStyles = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
  };
  
  const sizeStyles = {
    small: 'px-3 py-1 text-sm',
    medium: 'px-4 py-2',
    large: 'px-6 py-3 text-lg'
  };

  return (
    <button
      className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

このように、APIのリクエスト/レスポンスを定義するのと同じような感覚で、コンポーネントのインターフェースを定義していきます。

Storybookで確認する

FastAPIなら/docsエンドポイントでAPIの動作確認ができますよね。
コンポーネント駆動開発では、Storybookというツールを使って同じようなことができます:

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    variant: 'primary',
    size: 'medium',
    children: 'Primary Button',
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    size: 'medium',
    children: 'Secondary Button',
  },
};

!

Storybookは私にとって目からウロコでした。
APIドキュメントのように、コンポーネントの使い方や見た目を一覧できるんです!

つまずいたポイントと解決策

もちろん、全部うまくいったわけじゃないんです...😅

1. コンポーネントの粒度設計 - アトミックデザインとの出会い

最初は「このコンポーネント、どこまで細かく分けるべき?」ってすごく悩みました。
そんな時に出会ったのが、アトミックデザインという考え方。
これが、FastAPIでのエンドポイント設計の経験と上手く組み合わせることができました!

アトミックデザインでは、UIコンポーネントを以下の5つの階層に分類します↓

  1. Atoms(原子)
    • ボタン、入力フィールド、ラベルなどの最小単位
    • FastAPIでいう基本的なレスポンスモデルのような存在
// atoms/Button.tsx
interface ButtonProps {
  variant: 'primary' | 'secondary';
  size: 'small' | 'medium' | 'large';
  children: React.ReactNode;
  onClick?: () => void;
}

export const Button: React.FC<ButtonProps> = ({ variant, size, children, onClick }) => {
  // ...実装内容は前述のコードと同じ
};
  1. Molecules(分子)
    • 複数のAtomsを組み合わせた機能単位
    • FastAPIの基本的なエンドポイントに相当
// molecules/SearchInput.tsx
interface SearchInputProps {
  onSearch: (query: string) => void;
  placeholder?: string;
}

export const SearchInput: React.FC<SearchInputProps> = ({ onSearch, placeholder }) => {
  const [query, setQuery] = useState('');

  return (
    <div className="flex gap-2">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className="px-4 py-2 border rounded"
        placeholder={placeholder}
      />
      <Button 
        variant="primary" 
        size="medium"
        onClick={() => onSearch(query)}
      >
        検索
      </Button>
    </div>
  );
};
  1. Organisms(有機体)
    • 特定のコンテキストを持つ機能群
    • FastAPIのルーティンググループのような役割
// organisms/SearchHeader.tsx
interface SearchHeaderProps {
  onSearch: (query: string) => void;
  title: string;
}

export const SearchHeader: React.FC<SearchHeaderProps> = ({ onSearch, title }) => {
  return (
    <header className="p-4 bg-white shadow">
      <h1 className="text-2xl mb-4">{title}</h1>
      <SearchInput onSearch={onSearch} placeholder="商品を検索..." />
    </header>
  );
};
  1. Templates(テンプレート)
    • ページのレイアウト構造を定義
    • FastAPIのミドルウェア層のような役割
// templates/SearchPageTemplate.tsx
interface SearchPageTemplateProps {
  children: React.ReactNode;
  headerTitle: string;
  onSearch: (query: string) => void;
}

export const SearchPageTemplate: React.FC<SearchPageTemplateProps> = ({
  children,
  headerTitle,
  onSearch
}) => {
  return (
    <div className="min-h-screen bg-gray-50">
      <SearchHeader title={headerTitle} onSearch={onSearch} />
      <main className="max-w-4xl mx-auto p-4">
        {children}
      </main>
    </div>
  );
};
  1. Pages(ページ)
    • 実際のページコンポーネント
    • FastAPIのメインアプリケーション層に相当
// pages/search.tsx
export default function SearchPage() {
  const handleSearch = async (query: string) => {
    // API呼び出しなどの処理
  };

  return (
    <SearchPageTemplate
      headerTitle="商品検索"
      onSearch={handleSearch}
    >
      {/* 検索結果コンポーネントなど */}
    </SearchPageTemplate>
  );
}

!

この階層構造を採用することで、以下のような利点がありました↓

  1. コンポーネントの責務が明確に

    • 各階層の役割がFastAPIの各層と似ているため、理解しやすい
    • コードの見通しが格段に良くなった
  2. 再利用性の向上

    • Atoms, Moleculesレベルのコンポーネントは、高い再利用性を実現
    • FastAPIのユーティリティ関数のように、プロジェクト全体で活用可能
  3. 開発効率の向上

    • 新機能追加時の設計方針が明確に
    • チーム間でのコミュニケーションが円滑に

このように、アトミックデザインとFastAPIの設計思想を組み合わせることで、より体系的なコンポーネント設計が可能になりました!

例えば、検索機能を持つヘッダーコンポーネントの場合↓

// molecules/SearchInput.tsx
interface SearchInputProps {
  onSearch: (query: string) => void;
  placeholder?: string;
  initialValue?: string;
}

export const SearchInput: React.FC<SearchInputProps> = ({
  onSearch,
  placeholder = '検索...',
  initialValue = ''
}) => {
  const [query, setQuery] = useState(initialValue);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSearch(query);
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className="px-4 py-2 border rounded"
        placeholder={placeholder}
      />
      <Button variant="primary" size="medium" type="submit">
        検索
      </Button>
    </form>
  );
};

2. 状態管理の課題

FastAPIではデータの流れが比較的シンプルですが、フロントエンドの状態管理は複雑でした。
以下のような方針で対処しています↓

  1. Props Down, Events Up

    • データは親から子へ渡す(Props Down)
    • 変更は子から親へイベントとして通知(Events Up)
    • APIのリクエスト/レスポンスの流れと似ていますね
  2. ローカル状態の最小化

// Bad: グローバルな状態に依存
const SearchHeader = () => {
  const globalState = useGlobalState();
  // ...
};

// Good: 必要な状態のみをPropsで受け取る
interface SearchHeaderProps {
  searchQuery: string;
  onSearch: (query: string) => void;
}

const SearchHeader: React.FC<SearchHeaderProps> = ({
  searchQuery,
  onSearch
}) => {
  // ...
};
  1. コンポーネントの責務分離
    • 表示用コンポーネントと状態管理用コンポーネントを分ける
    • FastAPIのルーティング層とビジネスロジック層の分離と同じ考え方

学んだこと・気づいたこと

実際にコンポーネント駆動開発を試してみて、バックエンドエンジニアの目線から、こんな発見がありました↓

  1. APIとUIコンポーネントって、実は似てる!

    • FastAPIのエンドポイント設計みたいに、UIコンポーネントもちゃんとしたインターフェースがある
    • APIのレスポンス設計と同じで、再利用できるように考えるのが大事
    • OpenAPIのドキュメントみたいに、Storybookで見た目と使い方が確認できる
  2. テスト駆動開発との親和性

    • コンポーネント単位でのテストが容易
    • モックデータを使った開発が効果的
    • APIのユニットテストと同じような考え方で実装できる
  3. チーム開発での利点

    • コンポーネントカタログが共通言語として機能
    • フロントエンドとバックエンドの連携がスムーズに
    • 変更の影響範囲が把握しやすい

おわりに

コンポーネント駆動開発のおかげで、フロントエンド開発への苦手意識がだいぶ減りました。
APIの設計で身につけた考え方が、こんなところで役立つとは思ってもみなかったです!

これからもNext.jsの学習を進めながら、コンポーネント駆動開発についてもっと理解を深めていきたいと思います。
もし私みたいにバックエンドからフロントエンドに挑戦してみようかなーって考えている方がいたら、ぜひコンポーネント駆動開発を試してみてください!

記事を読んでくださってありがとうございます!
質問や感想があれば、ぜひコメントしてください。
みなさんと一緒にコンポーネント駆動開発について学んでいけたら嬉しいです!

参考リンク

Discussion