【個人開発反省】useEffectで「親切すぎる」機能を作ろうとして失敗した話 - シンプルが一番

# はじめに
私はClaude4.0を利用して個人開発アプリの作成に挑戦しています。
個人開発でReactアプリを作っていた時、「ユーザーにとって親切な機能を作ろう！」と思い、useEffectを使った自動初期化機能に挑戦しました。
しかし結果的に、実装が複雑になりすぎて自分でも理解ができなくなり、最終的にシンプルなボタン実装に戻すことになりました。
この経験から学んだ「適切な複雑さ」と「useEffectの正しい使い方」について振り返ります。

# 何を作ろうとしたのか？
**目標：親切なユーザー体験**
「アプリを開いたら即座に使える状態にしたい」という思いから、以下の動作を目指しました：

```
// 理想の動作フロー
1. アプリ起動
2. 自動的に認証状態をチェック
3. 未ログイン → 自動でゲストユーザー作成
4. すぐにアプリが使える + ログインを促すUI表示
```

これらの機能は確かに使いやすいのですが、**裏側の実装は相当複雑**だということを軽視していました。

# 実装された複雑なコード
**useEffectを使った自動初期化**

```jsx:❌ 複雑すぎた実装
function App() {
  const [userData, setUserData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [showAuthChoice, setShowAuthChoice] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const initializeApp = async () => {
      try {
        setIsLoading(true);
        
        // 1. 認証状態チェック
        const { data: { session }, error: sessionError } = await supabase.auth.getSession();
        
        if (sessionError) {
          throw sessionError;
        }
        
        if (session?.user) {
          // 2. 認証済みユーザーの処理
          const authUserData = await fetchUserProfile(session.user.id);
          setUserData(authUserData);
        } else {
          // 3. 未認証 → 自動ゲストユーザー作成
          const deviceId = await DeviceIdManager.getDeviceId();
          const guestUser = createGuestUser(deviceId);
          setUserData(guestUser);
          setShowAuthChoice(true); // ログイン促進UI表示
        }
      } catch (error) {
        console.error('初期化エラー:', error);
        setError(error);
        // エラー時のフォールバック処理
        const fallbackUser = createFallbackUser();
        setUserData(fallbackUser);
      } finally {
        setIsLoading(false);
      }
    };

    initializeApp();
  }, []); // 空の依存配列

  // ローディング中
  if (isLoading) {
    return <LoadingSpinner />;
  }

  // エラー状態
  if (error) {
    return <ErrorComponent error={error} />;
  }

  return (
    <div>
      <QuizComponent userData={userData} />
      {showAuthChoice && (
        <AuthPrompt 
          onLogin={() => loginWithGoogle()} 
          onClose={() => setShowAuthChoice(false)}
        />
      )}
    </div>
  );
}
```
# なぜuseEffectが使われたのか？
まず、なぜ「普通に関数内に書くだけ」ではダメなのかを理解する必要があります。

**Reactの根本的な仕組み：関数全体が何度も実行される**
```jsx:❌ 初心者がやりがちな間違い
function App() {
  const [count, setCount] = useState(0);
  
  // 「初期化処理だから1回だけ実行されるはず」と思い込み
  console.log('初期化処理が実行されました');
  const userData = createUser(); // 毎回新しいユーザーが作られる！
  console.log('作成されたユーザー:', userData.id);
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>User: {userData.id}</p>
      <button onClick={() => setCount(count + 1)}>
        カウントアップ
      </button>
    </div>
  );
}

// 実際のコンソール出力：
// 初期化処理が実行されました
// 作成されたユーザー: user_001
// ↓ ボタンを1回クリック
// 初期化処理が実行されました  ← また実行された！
// 作成されたユーザー: user_002  ← 別のユーザーID！
// ↓ ボタンをもう1回クリック  
// 初期化処理が実行されました  ← またまた実行された！
// 作成されたユーザー: user_003  ← また別のユーザーID！
```
**問題点：**

* Reactコンポーネントは「関数」（Appコンポーネントが関数）
* 状態が変わるたびに関数全体が再実行される
* 「初期化のつもり」の処理も毎回実行される（1回だけの実行になってくれない！）

**useEffectで「本当に1回だけ」を保証**
```jsx:✅ useEffectを使った正しい実装
function App() {
  const [count, setCount] = useState(0);
  const [userData, setUserData] = useState(null);
  
  // この部分は何度も実行される
  console.log('関数が実行された');
  
  // useEffectの部分は「1回だけ」を保証
  useEffect(() => {
    console.log('初期化処理：本当に1回だけ実行');
    const newUser = createUser();
    setUserData(newUser);
  }, []); // 空配列 = マウント時のみ実行
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>User: {userData?.id}</p>
      <button onClick={() => setCount(count + 1)}>
        カウントアップ
      </button>
    </div>
  );
}

// 実行結果：
// 関数が実行された
// 初期化処理：本当に1回だけ実行  (User: user_abc123)
// ↓ ボタンクリック
// 関数が実行された  (User: user_abc123) ← 同じユーザー！
// (初期化処理は実行されない)
```
**なぜ「1回だけ」が重要なのか**
```javascript:
// ユーザー作成が毎回実行されると...
❌ API呼び出しが何度も発生 → サーバー負荷
❌ 新しいユーザーIDが毎回生成 → データの整合性が破綻
❌ 初期化処理が重い場合 → パフォーマンス悪化
❌ ユーザーが混乱 → 「なぜIDが変わる？」

// useEffectで1回だけに制限すると...
✅ 初期化は最初の1回のみ
✅ 状態変更時は初期化処理は実行されない
✅ 予期した通りの動作
```
**useEffectを選んだ意図：**

* アプリ起動（コンポーネントマウント）と同時に自動初期化
* **でも絶対に1回だけ実行させたい**
* ユーザーが何もしなくても使える状態にする
* 「体験までの時間を最短にする」親切設計

# 問題点：複雑さが招いた混乱
**1. 状態管理が複雑すぎた**
```jsx:管理しなければならない状態が多すぎ
const [userData, setUserData] = useState(null);        // ユーザーデータ
const [isLoading, setIsLoading] = useState(true);      // ローディング状態
const [showAuthChoice, setShowAuthChoice] = useState(false); // UI表示制御
const [error, setError] = useState(null);              // エラー状態

// 状態の組み合わせパターンが爆発的に増加
// loading + error + userData + showAuthChoice = 16通りの状態
```
**2.エラーハンドリングが困難**
```jsx:
// どこでエラーが起きるかわからない
try {
  const session = await supabase.auth.getSession();  // エラー1
  const profile = await fetchUserProfile();          // エラー2  
  const deviceId = await DeviceIdManager.getDeviceId(); // エラー3
  const guestUser = createGuestUser();                // エラー4
} catch (error) {
  // どのエラーなのか判別困難
  console.error('どこかでエラー:', error);
}
```
**3.デバッグが大変**
```jsx:
// useEffect内の非同期処理はデバッグしにくい
useEffect(() => {
  const initializeApp = async () => {
    // この中でエラーが起きても、どのタイミングかわからない
    // コンソールログを大量に仕込む羽目に...
    console.log('1. 開始');
    console.log('2. セッション取得中...');
    console.log('3. ユーザー作成中...');
    // ...
  };
  
  initializeApp();
}, []);
```
# シンプルな解決策：ボタンクリック実装
結局、元のシンプルな実装に戻しました：
```jsx:
// ✅ シンプルで理解しやすい実装
function App() {
  const [userData, setUserData] = useState(null);

  // Google認証
  const handleGoogleLogin = async () => {
    try {
      const { data, error } = await supabase.auth.signInWithOAuth({
        provider: 'google'
      });
      if (error) throw error;
    } catch (error) {
      console.error('ログインエラー:', error);
      alert('ログインに失敗しました');
    }
  };

  // ゲストログイン（ボタンクリック時のみ）
  const handleGuestLogin = () => {
    const guestUser = createGuestUser();
    setUserData(guestUser);
  };

  // 未ログイン状態：選択画面を表示
  if (!userData) {
    return (
      <div className="login-screen">
        <h1>学習アプリへようこそ</h1>
        <button onClick={handleGoogleLogin}>
          Googleでログイン
        </button>
        <button onClick={handleGuestLogin}>
          ゲストとして始める
        </button>
      </div>
    );
  }

  // ログイン済み：アプリ本体
  return <QuizComponent userData={userData} />;
}
```
**シンプル実装のメリット**
**1. 理解しやすい**
```javascript
// 動作が直感的
ボタンクリック → 関数実行 → 状態更新 → 画面更新
```
**2.デバッグしやすい**
```jsx
// エラーの原因が特定しやすい
const handleGuestLogin = () => {
  console.log('ゲストログイン開始');
  const guestUser = createGuestUser(); // ← ここでエラーなら一目瞭然
  console.log('ゲストユーザー作成完了:', guestUser);
  setUserData(guestUser);
};
```
**3.エラーハンドリングが明確**
```jsx
// どの処理でエラーが起きたか一目瞭然
const handleGoogleLogin = async () => {
  try {
    await googleLogin();
  } catch (error) {
    console.error('Googleログインエラー:', error);
    // 具体的なエラー対応
  }
};

const handleGuestLogin = () => {
  try {
    const user = createGuestUser();
    setUserData(user);
  } catch (error) {
    console.error('ゲストユーザー作成エラー:', error);
    // 具体的なエラー対応
  }
};
```
# useEffectを使うべき適切な場面
useEffectは素晴らしい機能ですが、「1回だけ実行したい処理」がある時に使うものです。使いどころを間違えると複雑になります：
✅ **useEffectが適切な場面**
**1. 初期データの取得（1回だけ）**
```jsx
useEffect(() => {
  // アプリ起動時に1回だけユーザー設定を取得
  fetch('/api/user-settings')
    .then(res => res.json())
    .then(data => setSettings(data));
}, []); // 空配列 = 1回だけ
```
**2.設定の読み込み（1回だけ）**
```jsx
function App() {
  const [theme, setTheme] = useState('light');
  
  useEffect(() => {
    // アプリ起動時に1回だけ保存されたテーマを読み込み
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      setTheme(savedTheme);
    }
  }, []); // 1回だけ実行
  
  return (
    <div className={theme === 'dark' ? 'dark-mode' : 'light-mode'}>
      アプリ画面
    </div>
  );
}
```
**3.初期化処理（1回だけ）**
```jsx
function QuizApp() {
  const [questions, setQuestions] = useState([]);
  
  useEffect(() => {
    // アプリ起動時に1回だけクイズ問題を準備
    const initialQuestions = prepareQuizQuestions();
    setQuestions(initialQuestions);
  }, []); // 1回だけ実行
  
  return <div>クイズが{questions.length}問あります</div>;
}
```
今回の実装は、**アプリ起動時に1回だけ実行したい場合**という条件には合っていましたが、初心者には難し過ぎて扱い切れませんでした。

# 学んだこと：「適切な複雑さ」を見極める
**1. ユーザー体験 vs 実装コスト**
```javascript
// 理想的なUX：自動初期化
実装コスト: 高い（複雑な状態管理、エラーハンドリング）
保守コスト: 高い（デバッグ困難、テスト困難）

// シンプルなUX：ボタンクリック
実装コスト: 低い（明確な処理フロー）
保守コスト: 低い（理解しやすい、テストしやすい）

// 個人開発ではまずは「シンプル」からステップアップすべき
```
**2. 段階的な実装**
```javascript
// ✅ 推奨アプローチ
Phase 1: まずシンプルなボタン実装で動作確認
Phase 2: ユーザーフィードバックを収集
Phase 3: 本当に必要なら高度な機能を追加

// ❌ 避けるべきアプローチ  
Phase 1: いきなり完璧な自動化を目指す
→ 複雑すぎて挫折
```
**3. 「親切」の定義を見直す**
```javascript
// 思い込んでいた「親切」
= 自動的に何でもやってくれる

// 実際のユーザーにとって「親切」
= 予測しやすい動作
= エラーが起きたときの分かりやすさ
= 自分で制御できる感覚
```
# まとめ
今回の経験で学んだポイント：
**useEffectについて**

* 複雑な初期化処理には向かない
* 単純なデータフェッチやイベント設定に最適
* 非同期処理が多いと状態管理が困難になる

**実装設計について**

* 「親切すぎる」機能は実装コストが高い
* シンプルな実装から始めて段階的に改善
* ユーザー体験と実装コストのバランスを考慮

**個人開発の心得**

* 完璧を目指さず、動くものを先に作る
* 複雑さは段階的に追加する
* 「なぜその機能が必要か？」を常に自問

「技術的にできること」と「やるべきこと」は違う、ということを改めて学んだ貴重な経験でした。

# IT技術学習アプリ
私が作成に取り組んでいる「IT技術学習アプリ」です。React等の技術について学習できるアプリになっています。
現在は開発中のベータ版ですが、周辺技術も学習しながら継続して改善中です。

https://probable-computing-machine-g3ut.vercel.app/