2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-06-22

はじめに

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

何を作ろうとしたのか?

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

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

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

実装された複雑なコード

useEffectを使った自動初期化

❌ 複雑すぎた実装
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の根本的な仕組み:関数全体が何度も実行される

❌ 初心者がやりがちな間違い
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回だけ」を保証

✅ 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回だけ」が重要なのか

// ユーザー作成が毎回実行されると...
 API呼び出しが何度も発生  サーバー負荷
 新しいユーザーIDが毎回生成  データの整合性が破綻
 初期化処理が重い場合  パフォーマンス悪化
 ユーザーが混乱  なぜIDが変わる?」

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

useEffectを選んだ意図:

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

問題点:複雑さが招いた混乱

1. 状態管理が複雑すぎた

管理しなければならない状態が多すぎ
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.エラーハンドリングが困難

// どこでエラーが起きるかわからない
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.デバッグが大変

// useEffect内の非同期処理はデバッグしにくい
useEffect(() => {
  const initializeApp = async () => {
    // この中でエラーが起きても、どのタイミングかわからない
    // コンソールログを大量に仕込む羽目に...
    console.log('1. 開始');
    console.log('2. セッション取得中...');
    console.log('3. ユーザー作成中...');
    // ...
  };
  
  initializeApp();
}, []);

シンプルな解決策:ボタンクリック実装

結局、元のシンプルな実装に戻しました:

// ✅ シンプルで理解しやすい実装
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. 理解しやすい

// 動作が直感的
ボタンクリック  関数実行  状態更新  画面更新

2.デバッグしやすい

// エラーの原因が特定しやすい
const handleGuestLogin = () => {
  console.log('ゲストログイン開始');
  const guestUser = createGuestUser(); // ← ここでエラーなら一目瞭然
  console.log('ゲストユーザー作成完了:', guestUser);
  setUserData(guestUser);
};

3.エラーハンドリングが明確

// どの処理でエラーが起きたか一目瞭然
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回だけ)

useEffect(() => {
  // アプリ起動時に1回だけユーザー設定を取得
  fetch('/api/user-settings')
    .then(res => res.json())
    .then(data => setSettings(data));
}, []); // 空配列 = 1回だけ

2.設定の読み込み(1回だけ)

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回だけ)

function QuizApp() {
  const [questions, setQuestions] = useState([]);
  
  useEffect(() => {
    // アプリ起動時に1回だけクイズ問題を準備
    const initialQuestions = prepareQuizQuestions();
    setQuestions(initialQuestions);
  }, []); // 1回だけ実行
  
  return <div>クイズが{questions.length}問あります</div>;
}

今回の実装は、アプリ起動時に1回だけ実行したい場合という条件には合っていましたが、初心者には難し過ぎて扱い切れませんでした。

学んだこと:「適切な複雑さ」を見極める

1. ユーザー体験 vs 実装コスト

// 理想的なUX:自動初期化
実装コスト: 高い複雑な状態管理エラーハンドリング
保守コスト: 高いデバッグ困難テスト困難

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

// 個人開発ではまずは「シンプル」からステップアップすべき

2. 段階的な実装

// ✅ 推奨アプローチ
Phase 1: まずシンプルなボタン実装で動作確認
Phase 2: ユーザーフィードバックを収集
Phase 3: 本当に必要なら高度な機能を追加

// ❌ 避けるべきアプローチ  
Phase 1: いきなり完璧な自動化を目指す
 複雑すぎて挫折

3. 「親切」の定義を見直す

// 思い込んでいた「親切」
= 自動的に何でもやってくれる

// 実際のユーザーにとって「親切」
= 予測しやすい動作
= エラーが起きたときの分かりやすさ
= 自分で制御できる感覚

まとめ

今回の経験で学んだポイント:
useEffectについて

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

実装設計について

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

個人開発の心得

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

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

IT技術学習アプリ

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

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?