🤔

Next.js学習で気づいた2025年のフロントエンド開発:「なんでもクライアント」から「適切な役割分担」へ

に公開

きっかけ:違和感から始まった学習

最近Next.jsを本格的に学び始めて、「あれ?思ってたのと違う...」という違和感を覚えました。僕がイメージしていたのは「ReactをベースにしたSPAフレームワーク」だったんですが、実際に触ってみると全然違った。

サーバーコンポーネントとか、Server Actionsとか、「え、これってサーバーサイドの話じゃないの?」って混乱しました。でも学習を進めるうちに、これがまさに今起きている大きなパラダイムシフトなんだと気づいたんです。

少し前までの「なんでもクライアント」時代

少し前まではこんな開発スタイルだったと思います:

// よくやってたパターン(今思えば...)
function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // とりあえずクライアントで全部やっちゃえ
    fetch('/api/products')
      .then(res => res.json())
      .then(setProducts)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);
  
  if (loading) return <div>読み込み中...</div>; // これ、SEO死んでるよね
  if (error) return <div>エラー: {error.message}</div>;
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}

「とりあえずuseEffectでAPI叩いて、useStateで管理しとけばOK!」って感じでした。SPAが流行ってから、なんでもかんでもクライアントサイドでやるのが「モダン」だと思ってたんです。

でも最近、これって本当にすべてのケースで正しかったのかな?って疑問に思うようになりました。

実際に体験した「統合」への変化

Next.jsのサーバーコンポーネントを初めて書いたとき、正直「え、これだけ?」って拍子抜けしました:

// サーバーコンポーネントでの書き方
export default async function ProductListPage() {
  // あ、普通にawaitできるんだ...
  const products = await fetchProducts();
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}

「useEffect? useState? Loading state? そんなの要らないの?」って感じでした。でもこれ、よく考えてみると当たり前なんですよね。

サーバーでデータ取ってきて、HTMLにして返す。昔からずっとやってきたことじゃないですか。でも今回は、単純な「昔に戻る」わけじゃありません。これはサーバーサイドとクライアントサイドの統合なんです。必要な部分だけクライアントサイドで処理して、それ以外はサーバーサイドで効率的に処理する。

現実に直面:クライアントサイドの限界

実際にパフォーマンスを測ってみて愕然としました。僕が作ったSPAは:

  • 初回ロードが遅い(JSバンドルが重い)
  • SEOがガタガタ(クローラーがJSの実行を待てない)
  • モバイルで重い(古い端末だと特に)

「モダンなSPAを作ってるぜ!」って思ってたけど、実はユーザー体験を悪化させてたかもしれません。特にモバイルユーザーには申し訳ないことをしていた...

useStateとuseEffectからの卒業

Next.jsを学ぶ中で、従来の状態管理から脱却する方法をいくつか発見しました。

1. URLによる状態管理(これが目からウロコだった)

// app/products/page.tsx
export default async function ProductsPage({ searchParams }) {
  // URLパラメータが状態そのもの!
  const category = searchParams.category || 'all';
  const sort = searchParams.sort || 'name';
  
  // サーバーサイドでフィルタリング済みのデータを取得
  const products = await getProducts({ category, sort });
  
  return (
    <div>
      <ProductFilters currentCategory={category} />
      <ProductGrid products={products} />
    </div>
  );
}

これ、最初は「え、URLに状態を持たせるの?」って思ったんですが、よく考えるとめちゃくちゃ合理的なんですよね:

  • ブックマークできる
  • シェアできる
  • ブラウザの戻る/進むボタンが期待通りに動く
  • サーバーサイドでデータ取得時に状態がわかる

2. Server Actionsによる状態更新(これも新鮮だった)

// app/actions.ts
'use server'

export async function toggleFavorite(productId, userId) {
  // データベースを直接更新
  const result = await db.product.update({
    where: { id: productId },
    data: {
      favorites: { toggle: { userId } }
    }
  });
  
  // キャッシュを無効化
  revalidatePath('/products');
  
  return result.favoriteCount;
}
// components/FavoriteButton.tsx
"use client"
export default function FavoriteButton({ productId, userId, initialCount }) {
  const [count, setCount] = useState(initialCount);
  
  const handleClick = async () => {
    // Server Actionを直接呼び出し
    const newCount = await toggleFavorite(productId, userId);
    setCount(newCount);
  };
  
  return <button onClick={handleClick}>❤️ {count}</button>;
}

これも最初は「え、フォームのaction属性みたい...古くない?」って思ったんですが、実はすごく理にかなってるんです。フォームが基本的にやりたいことって、データを送信してサーバーの状態を変更することですもんね。

実際の判断基準:いつサーバー、いつクライアント?

学習を進める中で、自分なりの判断基準ができました。

ただし、重要なのはSPAが完全に無価値になったわけではないということです。高度にインタラクティブなダッシュボードリアルタイムアプリケーションでは、今でもSPAが適切な選択肢です。

重要なのは「SPAかSSRか」の二択ではなく、プロジェクトの性質に応じた適切な選択なんです。

サーバーサイドでやること

「初回表示で見えてほしいもの」は全部サーバー

  • 商品一覧(ECサイトのメインコンテンツ)
  • ブログ記事(SEO重要)
  • ランディングページ(ファーストインプレッション大事)

実際にブログサイトを作った時、記事をサーバーコンポーネントで表示するようにしたら、Lighthouseスコアが80→95まで跳ね上がりました。体感でも明らかに速くなった。

クライアントサイドでやること

「ユーザーがいじくり回すもの」「リアルタイム性が重要なもの」はクライアント

  • フィルター機能(リアルタイムに変更したい)
  • いいねボタン(即座に反応してほしい)
  • チャット機能(リアルタイム性が命)
  • 複雑な分析ダッシュボード(WebSocket、ドラッグ&ドロップ、高度なデータ可視化)
  • オンラインエディター(Google Docsのような協調編集)

こういったケースでは、従来のSPAアプローチが今でも最適解です。

実際のハイブリッド例

// ダッシュボードページ
export default async function DashboardPage() {
  // ユーザー情報は最初に必要 → サーバーで取得
  const user = await getCurrentUser();
  const summary = await getDashboardSummary(user.id);
  
  return (
    <div>
      <h1>こんにちは、{user.name}さん</h1>
      
      {/* 静的なサマリー */}
      <SummaryCards data={summary} />
      
      {/* インタラクティブなチャート */}
      <InteractiveChart userId={user.id} />
      
      {/* リアルタイム通知 */}
      <RealtimeNotifications userId={user.id} />
    </div>
  );
}

体感した効果とこれからの開発

開発体験が意外と良くなった

最初は「複雑になったなあ」って思ったんですが、慣れてくると:

  • useEffectの依存配列で悩まなくなった
  • ローディング状態の管理が楽になった
  • バグが減った(サーバーエラーは一箇所で処理できる)

まとめ:単純な回帰ではなく、進歩した統合

結局、僕らは一周回って「サーバーサイドレンダリング」に戻ってきました。でも、ただ戻ったわけじゃありません。

昔のサーバーサイド: 全部サーバーでやってた(インタラクションが乏しい)
SPA時代: 全部クライアントでやってた(SEOとパフォーマンスで苦労)
: 必要なところだけクライアントでやる(いいとこ取り)

この「いいとこ取り」ができるようになったのが、Next.jsの凄いところだと思います。

業界の多様性も忘れずに

もちろん、開発者コミュニティでは様々な意見があります。ReactのServer Componentsに懐疑的な開発者もいれば、Vue、Svelte、htmxなど他の選択肢を選ぶ人もいます。重要なのは、プロジェクトに最適な技術を選ぶことです。

今後の開発で心がけたいこと

  1. 「これ、サーバーでやった方がよくない?」を常に考える
  2. URLを状態管理の一部として積極活用する
  3. Server Actionsでシンプルな状態更新を心がける
  4. 本当に必要な部分だけをクライアントコンポーネントにする

Web開発って、本当に振り子のように技術が移り変わりますね。でも今回の変化は、ただの回帰じゃなくて、進歩だと感じています。

昔の良いところ(サーバーサイドの安定性・速さ)と、SPAで得た知見(インタラクティブなUX)を組み合わせた、より良いアプローチに辿り着けた気がします。

Next.js、最初は「また新しいフレームワーク覚えなきゃ...」って思ってたけど、実際はWeb開発の本質を思い出させてくれるツールでした。

みなさんも、もし「なんでもクライアントサイド」に疲れてきたら、ぜひNext.jsのサーバーコンポーネントを試してみてください。きっと新しい発見があると思います!


参考資料

この記事が誰かの学習の参考になれば嬉しいです 🙂

Discussion