はじめに
こんにちは。Restaurant Service Devグループの高岡です。現在ぐるなびウエディングのフロントエンド開発・運用を行っています。
ぐるなびウエディングは結婚式場検索・予約サービスで、検索・会場詳細・特集・ランキングなど多様な機能を持つ大規模なウェブアプリケーションです。
そのぐるなびウエディングはリニューアルプロジェクトが進行しており、今年の7月に二次会検索と会場ページをリリースしました。
今回は大規模なウェブサービスのリニューアルにおいてチーム開発の効率性と保守性を両立するフロントエンドアーキテクチャをどう設計するか、ぐるなびウエディングリニューアルプロジェクトで実践した取り組みについて紹介します。
目次
- はじめに
- 目次
- 開発体制
- 採用した技術スタック
- アーキテクチャの核となる3つの設計方針
- Package by Feature アーキテクチャの採用
- React Server Components 時代のアーキテクチャ
- RSC時代の Container/Presentational パターン
- 運用して見えた成果と課題
- まとめ
開発体制
二次会検索と会場ページをリリースするまで、以下の体制で開発を進めていました。
- バックエンドエンジニア:4名
- フロントエンドエンジニア:4名
- 開発ペース:毎週平均80件のPR(多いときには100件超)
この高速開発環境で品質を保ちながら効率的に開発するため、以下の設計方針でアーキテクチャを構築しました。
採用した技術スタック
実績と移植性を重視し、以下の技術スタックを採用しました。
フレームワーク:Next.js App Router
- 選定理由:SSR/CSR/SSG を簡単に実現でき、SEOやパフォーマンス向上に寄与
- App Router採用の背景:v13.4.0以降で利用可能になり、Page Router はメンテナンスモードとなったため
スタイリング:CSS Modules
- 選定理由:既存資産の流用が容易で、移植性が高い。かつ App Router に対応している
- 他選択肢の検討:
- CSS in JS:App Router 対応が過渡期で、将来の安定性に不安
- Tailwind CSS:デザインシステム構築には良いが、学習コストと移植性が課題
グローバルな状態管理:Jotai
- 選定理由:Recoil に似た直感的な API で可読性が高く、パフォーマンスも良好
コンポーネント管理:Storybook
- 選定理由:開発・テスト・ドキュメント作成の効率化としてデファクトスタンダード
テストフレームワーク:Vitest + React Testing Library
- 選定理由:Jest と互換性がありながらより高速なテスト実行が可能であり、Vite を使っていないプロジェクトでも導入可能なため。React Testing Library は React でのテストとしては代表的なツール
ビジュアルリグレッションテスト:storycap + reg-suit
- 選定理由:コスト面に優れプラグインにより PR 単位で意図せぬ差分が発生していないか確認しやすい。AWS を利用していることから環境構築の障害も特にないため
E2Eテスト:Playwright
- 選定理由:社内で実績が豊富にあり、Next.js でも標準のツールとして紹介されているため
アーキテクチャの核となる3つの設計方針
高速開発と保守性を両立するため、以下の3つを軸にアーキテクチャを設計しました。
- Package by Feature ベースのディレクトリ設計
- React Server Components(RSC)を活用した Server/Client Component の適切な使い分け
- RSC時代に適応した Container/Presentational パターンの採用
Package by Feature アーキテクチャの採用
従来手法の課題
同様のリニューアル事例で社内の共通ナレッジとして共有されているレストラン検索では Atomic Design を採用して開発していました。 しかし、レストラン検索では開発していく中で以下の課題が挙げられていました。
- コンポーネントの分類基準が曖昧
- 機能横断的な修正時に、複数のディレクトリを跨ぐ必要がある
- チーム開発での認識齟齬が発生しやすい
この問題を解消するため、Atomic Design ではなく Package by Feature の仕組みを採用することにしました。
Package by Feature の利点
bulletproof-reactでも採用されているベストプラクティスで、以下の利点があります。
- 機能単位での開発:関連するファイルが1箇所にまとまる
- 直感的な構造:機能やデザインを改修する際の場所が探しやすい
- チーム開発の効率化:担当機能が明確になり、並行開発しやすい
実際のディレクトリ構成
src/ ├── app/ # Next.js App Router ├── features/ # 機能別ディレクトリ │ ├── search/ # 検索機能 │ ├── venue/ # 会場詳細機能 │ ├── special/ # 特集機能 │ └── ranking/ # ランキング機能 └── components/ # 共通コンポーネント
ただ、一般的な Package by Feature では features にすべての機能が features 直下に並びますが、それだと管理が難しいため大分類(search/venue/special/ranking)を設けることで数多くの機能を整理しています。
React Server Components 時代のアーキテクチャ
App Router + RSC の導入背景
Next.js v13.4.0以降、App RouterとReact Server Components(RSC)が本格利用可能になりました。Page Router がメンテナンスモードとなり、今後のアップデート追従を考えると App Router 移行は必須の選択でした。
ただし、App RouterとRSCを使った本格的な開発実績は少なく、Server Component と Client Component の使い分けに関する調査と認識合わせを徹底的に行いました。
Server/Client Component 使い分けの重要なポイント
実装・調査を通じて発見した重要な注意点をご紹介します。
1. Client Component は「クライアント専用」ではない
よくある誤解:Client Component はブラウザ上でのみレンダリング実行される
実際:Client Component もサーバーサイドでレンダリング(SSR)される
そのため、SEO対策を理由にServer Componentにする必要はありません。
2. Client Component の「感染」に注意
Client Component に import されたコンポーネントは、自動的に Client Component となります。
// ❌ 問題のあるパターン 'use client' import ServerOnlyComponent from './ServerOnlyComponent' // これもClient Componentになってしまう // ✅ 推奨パターン 'use client' import { ReactNode } from 'react' function ClientComponent({ children }: { children: ReactNode }) { return <div>{children}</div> } // Server ComponentをpropsとしてUIツリーに組み込む
3. Server Component を Client Component から直接 import は不可
ただし、propsとして渡すことは可能です。これを活用してモーダルの実装などに応用できます。
// モーダルの例 function Modal({ children, isOpen, onClose }: ModalProps) { // モーダルの表示制御はClient Component return isOpen ? <div>{children}</div> : null } // Server Componentの内容をchildrenとして渡す <Modal isOpen={isOpen} onClose={handleClose}> <ServerSideContent /> {/* Server Componentのまま */} </Modal>
RSC時代の Container/Presentational パターン
従来パターンの課題
Container/Presentational パターンは、React初期にFlux全盛期にDan Abramov氏によって提唱された設計手法でした。
従来の責務分割:
- Container Components:データの読み取り・振る舞い(Fluxのaction呼び出しなど)
- Presentational Components:データを参照し表示する描画処理
しかし、RSCではサーバー処理とクライアント処理が共存できないため、従来のパターンでは以下の問題が発生します。
- fetch処理と状態管理処理をContainerにまとめると、Server Componentにできない
- 提唱者のDan Abramov氏自身が既に非推奨としている
RSC時代に適応した新しいパターン
外部の Container/Presentational Pattern に関する記事を参考に、RSC に適応した Container/Presentational パターンを採用しました。
新しい責務分割:
- Container Components:データフェッチなどのサーバーサイド処理のみ(Server Component)
- Presentational Components:データフェッチを含まないShared ComponentsまたはClient Components
feature 内の具体的な構成
features/search/ ├── components/ │ ├── result/ # 検索結果表示機能 │ │ ├── result-container.tsx # Container Components (データフェッチ) │ │ └── result.tsx # Presentational Components (UI制御) │ └── condition/ # 検索条件機能 ├── hooks/ # カスタムフック ├── types/ # 型定義 ├── stores/ # グローバル状態管理 └── utils/ # 機能に限定したユーティリティ関数
この構成により、以下のメリットを実現できました。
- 明確な責務分離:データ取得とUI制御の関心事を分離
- テスタビリティ向上:Container Components でのデータ取得のテストと Presentational Components の描画処理のテストが容易
- Storybook対応:データに依存しないコンポーネントの開発・デザイン確認が可能
運用して見えた成果と課題
得られた成果
1. チーム開発の効率化
- 共通認識の確立:Server/Client Component の使い分けをドキュメント化し、チーム全体で統一した理解を構築
- 直感的な構造:Package by Feature により機能単位で開発しやすく、機能改修時の該当ファイルが探しやすい
- テスト・開発効率向上:API通信と描画処理の分離により、テストや Storybook の実装が容易
2. 開発スピードの向上
- 少ない人数で効率的に開発できる体制を構築
- 機能横断的な修正でも影響範囲が把握しやすい
直面した課題
1. 共通コンポーネントの配置問題
検索・会場詳細・特集など複数機能で利用するドメイン知識を持つコンポーネントや関数の配置場所に悩みました。大分類で整理している関係上、どこに置くかの判断が難しいケースがあります。
この対応として、コンポーネントを細分化してドメイン知識を持たない粒度にしてから共通コンポーネントに配置したり、複数サービスで利用するコンポーネントや関数は共通パッケージ化しています。
2. レビュー負荷
高速開発により1日当たりのPR数が多く、レビュー負荷が大きくなる傾向があります。ただし、影響範囲が明確なのでレビューはしやすい印象です。
まとめ
ぐるなびウエディングリニューアルプロジェクトでは、高速開発と保守性を両立するため、以下のアーキテクチャを構築しました。
- Next.js App Router + RSC:将来性と性能を両立
- Package by Feature:直感的で保守しやすいディレクトリ構成
- RSC時代のContainer/Presentational パターン:責務分離と開発効率の向上
この設計により、少人数のフロントエンドチームで毎週大量の PR を作成できる効率の高いフロントエンドのアーキテクチャを実現できました。
今後も技術の進歩に合わせてアーキテクチャを進化させ、より良い開発体験を追求していきます。
