3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

bravesoftAdvent Calendar 2024

Day 10

Next.js App RouterでアドベントカレンダーUIを作ってみた

Last updated at Posted at 2024-12-09

はじめに

Next.jsを使用して、インタラクティブなアドベントカレンダーUIを実装してみました。
3Dアニメーションや雪のエフェクトを取り入れつつ、TypeScriptの型安全性も確保しています。

完成イメージ

実装で工夫した点

1. ハイドレーションエラーの解決

App Routerでの最大の課題は、サーバーサイドレンダリング(SSR)とクライアントサイドレンダリング(CSR)の整合性でした。

// クライアントサイドのみの処理を制御
const [isClient, setIsClient] = useState(false);

useEffect(() => {
  setIsClient(true);
}, []);

// ランダムな要素はクライアントサイドでのみ実行
const [currentSurprises] = useState(() => {
  const initialSurprises: SurpriseMap = {};
  if (typeof window !== 'undefined') {
    const shuffled = [...surprises].sort(() => Math.random() - 0.5);
    // ...
  }
  return initialSurprises;
});

2. TypeScriptの型安全性確保

オブジェクトのインデックスアクセスに関する型エラーに悩まされました。
インターフェースを定義することで解決しました:

interface Surprise {
  emoji: string;
  message: string;
}

interface SurpriseMap {
  [key: number]: Surprise;
}

const [currentSurprises] = useState<SurpriseMap>(() => {
  // ...
});

3. 3Dアニメーションの実装

CSSの3D変換を使用して、本物のドアのような開閉アニメーションを実現:

.door {
  transform-style: preserve-3d;
  transition: transform 0.5s;
}

.door.open {
  transform: rotateY(-105deg);
}

4. カレンダーグリッドの実装

7日ごとの改行を実現するために、配列を週単位で分割:

const renderCalendarGrid = () => {
  const weeks = [];
  let currentWeek = [];
  
  for (let i = 1; i <= 24; i++) {
    currentWeek.push(<Door key={i} number={i} />);
    
    if (currentWeek.length === 7 || i === 24) {
      weeks.push(
        <div key={weeks.length} className="flex justify-center w-full">
          {currentWeek}
        </div>
      );
      currentWeek = [];
    }
  }
  
  return weeks;
};

5. 雪のエフェクト

複数の雪の結晶文字を使い、ランダムな動きを実現:

const snowflakes = ["", "", "", "", "", "*"];

const getSnowflakes = () => {
  return Array.from({ length: 8 }).map((_, i) => ({
    flake: snowflakes[Math.floor(Math.random() * snowflakes.length)],
    delay: Math.random() * 5,
    duration: 2 + Math.random() * 3,
    position: Math.random() * 100
  }));
};

困った点と解決策

1. SSRとCSRの不一致

問題

  • Math.random()を使用した要素の生成
  • クライアントサイドでのみ必要なアニメーション

解決策

  • isClientフラグでレンダリングを制御
  • useEffectでクライアントサイド処理を分離

2. TypeScriptのエラー

問題

  • インデックスシグネチャの欠落
  • 暗黙的なany型

解決策

  • 適切なインターフェースの定義
  • 型アノテーションの追加

3. モバイル対応

問題

  • 小さい画面でのグリッドのはみ出し
  • タッチデバイスでの3Dエフェクト

解決策

  • レスポンシブなグリッドレイアウト
  • hover状態の適切な処理
<div className="max-w-7xl mx-auto p-4 sm:p-6">
  <div className="flex flex-col gap-4">
    {/* グリッドコンテンツ */}
  </div>
</div>

デプロイ方法

Vercelを使用して簡単にデプロイできます:

  1. GitHubにプッシュ
  2. Vercelでプロジェクトをインポート
  3. 自動デプロイの設定
# デプロイコマンド
vercel

まとめ

Next.js を使用したインタラクティブUIの実装は、
いくつかの課題がありましたが、適切な設計と型定義により解決できました。

特に以下の点が重要でした:

  • SSR/CSRの適切な使い分け
  • TypeScriptの型安全性確保
  • パフォーマンスとアニメーションの両立
3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?