0
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?

【個人開発】YouTubeと連携するポモドーロタイマー「Pomodoro Flow」をZustandを使って開発した話

Posted at

1. はじめに

はじめまして!keniと申します。
Qiita初投稿です。どうぞよろしくお願いします。

この記事では、個人の集中環境を最適化するために開発した、YouTube連携型ポモドーロタイマー「Pomodoro Flow」の技術的な側面について解説します。

既存のタイマーでは満足できず、「好きなYouTube動画を作業用BGMにしたい」という一心で開発をスタートしました。本記事では、特に以下の点について共有します。

  • 採用した技術スタックとその理由
  • YouTube IFrame Player API を使った動画連携の実装
  • 状態管理ライブラリZustandの導入メリット
  • 開発中に直面した課題と、その解決策

2. 対象読者

  • Reactを使った個人開発に興味がある方
  • Zustandの具体的な使用例を探している方
  • YouTube IFrame Player API の実装でハマりたくない方

3. 使用技術

  • フレームワーク: React
  • ビルドツール: Vite
  • 言語: TypeScript
  • 状態管理: Zustand
  • YouTube連携: react-youtube (YouTube IFrame Player APIのラッパー)

4. 実装のポイント

4-1. YouTube動画との同期

本アプリのコア機能です。タイマーの開始・停止とYouTubeの再生・停止を同期させる必要がありました。

実装には、YouTube IFrame Player APIをReactで使いやすくしてくれるreact-youtubeライブラリを採用しました。これにより、ReactコンポーネントとしてYouTubeプレイヤーを直感的に扱えます。

タイマーの状態と動画の再生状態は、Zustandストア(後述)で一元管理。タイマーがrunning状態になったらplayer.playVideo()を、stopped状態になったらplayer.pauseVideo()を呼び出す、というシンプルな同期処理を実現しています。

import YouTube from 'react-youtube';
import { useTimerStore } from './stores/timerStore';
import { useYouTubeStore } from './stores/youtubeStore';

const YouTubePlayer = () => {
  const { isRunning } = useTimerStore();
  const { player, setPlayer } = useYouTubeStore();

  // isRunningの状態が変わったら動画を再生/停止
  useEffect(() => {
    if (!player) return;
    isRunning ? player.playVideo() : player.pauseVideo();
  }, [isRunning, player]);

  const onReady = (event) => {
    // プレイヤーインスタンスをストアに保存
    setPlayer(event.target);
  };

  return <YouTube videoId="YOUR_VIDEO_ID" onReady={onReady} />;
};
4-2. なぜ状態管理にZustandを選んだか

タイマー、タスク、YouTubeプレイヤーなど、複数の状態が複雑に絡み合うため、状態管理ライブラリの導入は必須でした。

当初はContext APIを検討しましたが、再レンダリングの最適化が難しく、パフォーマンスへの懸念から断念。一方でReduxは、個人開発にはボイラープレートが多すぎると感じました。

Zustandは、これらの問題を解決してくれました。

  • ボイラープレートがほぼない
  • Context Providerが不要
  • hooksライクな直感的なAPI

以下がタイマー用のストアの定義です。非常にシンプルに記述できることがわかります。

// src/stores/timerStore.ts
import { create } from 'zustand'

interface TimerState {
  seconds: number;
  isRunning: boolean;
  startTimer: () => void;
  stopTimer: () => void;
}

export const useTimerStore = create<TimerState>((set) => ({
  seconds: 1500,
  isRunning: false,
  startTimer: () => set({ isRunning: true }),
  stopTimer: () => set({ isRunning: false }),
}))

このシンプルさのおかげで、状態管理の設計に時間を取られることなく、本来の機能開発に集中できました。

5. ハマった点:ブラウザの自動再生ポリシー

開発中に直面した最大の課題は、ブラウザの自動再生ポリシーです。

【問題】 タイマーが0秒になり、次のサイクル(作業 or 休憩)が始まっても、YouTube動画が自動で再生されませんでした。コンソールには「play() failed because the user didn't interact with the document first.」といったエラーが表示されます。

これは、ユーザーのクリックなどの明確なインタラクションなしにメディアを再生することをブラウザがブロックするためです。

【解決策】 この問題を回避するため、タイマーをスタートさせる最初の1回だけ、ユーザーに明確な「開始ボタン」をクリックしてもらうUIを設けました。

  1. ユーザーがサイトを訪れ、URLをセットする。
  2. ユーザーが「タイマースタート」ボタンをクリックする。
  3. このクリックイベントをトリガーにして、player.playVideo()とタイマーの開始処理を同時に実行する。

一度ユーザーのインタラクションを介して再生許可を得ることで、それ以降はタイマーの完了イベントに連動したplayVideo()pauseVideo()が正常に動作するようになります。

6. おわりに

「Pomodoro Flow」の開発を通して、モダンなフロントエンド技術(Vite, React, Zustand)の恩恵を大いに感じることができました。特にZustandは、個人開発における状態管理の決定版と言っても過言ではないかもしれません。

間違ってる内容あれば、教えていただけますと勉強になりますので、是非ご指摘いただければと思います。

▶︎ ぜひ触ってみてください!: Pomodoro Flow

0
1
0

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
0
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?