Reactの勉強を兼ねてNext.jsでプログラミングスクールの口コミサイトを開発した。

https://develop.d3u1mwn0bgdpjl.amplifyapp.com/

採用した技術

  • フロントエンド: Next.js
  • バックエンド: AWS Amplify
  • CI/CD: Amplify Hosting

サイト構成

実運用を想定し、以下の2つのサイトを構築

  1. メインサイト: ユーザーが口コミの閲覧や投稿で利用するサイト
  2. 管理(Admin)サイト: サイトの管理者がスクール情報を登録したり、口コミ情報の公開設定などを行うサイト

1. メインサイト

トップページ

口コミ検索画面

スクールとコースの一覧を選択し、一致する口コミ一覧を表示。
また、スクールの公式サイトのリンクも管理サイトで登録しているため、
リンクがある場合は公式サイトに飛ばす。

ユーザーが多いサイトになれば、アフィリエイトリンクを貼って広告収益を得られるのだろうなと妄想中。

口コミ登録画面

口コミの投稿は全部で4つのプロセスで完了する。

  1. プロフィール入力
  2. 受講情報の登録
  3. 具体的なレビューの投稿
  4. 確認・送信

プロフィール入力では、ユーザーの属性を登録させる。
入力情報についてはブラウザのストレージ機能を使っているため、リロードしても保持される。
ただし、口コミ登録画面から離脱した場合はストレージのデータを初期化している。

受講情報の登録画面では、ユーザーが受講した「スクール」と「コース」並びに、「受講期間(ヶ月)」を登録する。
この画面の入力データもブラウザのストレージを使用して保持する。

口コミの投稿画面では以下の項目を登録する

  • 総合評価(1~5段階)
  • スクールを受講して得られた結果
    印象が良かったとか抽象的なものではなくて、実際に受講することでどうなれるのかを記載してもらう。
    そうすることで、受講を検討しているユーザーが未来の自分をイメージしやすい。
  • 口コミの詳細
    将来的にプログラミングスクールの運営者がこのサイトを見てカリキュラムの改善などができるように、ユーザーにはありのままを記載してもらう。
    また、スクールの「先輩」として「後輩」が一歩踏み出せるようなメッセージを期待。

確認画面では、これまで入力したデータを確認して問題がなければ送信する。

送信したデータはすぐに公開されるわけではなく、管理者が投稿内容を確認して問題がなければ公開設定を行う(後半で解説)。
悪ふざけやスクール(メンター個人を含む)を誹謗中傷する内容が投稿されないようにするため。

お問い合わせ画面

問い合わせされたデータはデータベースに保存され管理者に通知される。

2. 管理(Admin)サイト

認証画面

AWS Cognitoユーザープールで管理者権限のグループを作成し、管理者のみが認証できるように制御。
同時に、管理サイトは管理者権限のグループのユーザーしか使用できない。

以下の機能を実装。

  • ログイン
  • 会員登録
  • パスワード再設定

スクール情報管理画面

プログラミングスクールのマスタデータを登録するための機能

以下の機能を用意。

  • スクール情報の一括登録(インポート)機能
    csvデータなどに情報をまとめて登録できれば便利であるため開発
  • 登録済み情報のエクスポート機能
    データ編集についてもcsvで一元管理できれば楽だと思ったため開発。
    登録済みデータについてはidが付与されているため、再度インポートしたら更新処理が実行される仕様にした。
  • 個別データのCRUD

コース情報管理画面

プログラミングスクールに紐づくコース情報を登録するための機能

  • コース情報の一括登録(インポート)機能
    画面上部のセレクトボックスで選択中のスクールに紐づくコースとして登録される。
  • 登録済み情報のエクスポート機能
    登録済みデータについてはidが付与されているため、再度インポートしたら更新処理が実行される。
  • 一括削除
    スクール1 : コースNの構造になるため、スクールが休業した場合などはコースを一括削除できるようにした(個別削除は面倒)。
    画面上部のセレクトボックスで選択中のスクールが対象。
  • 個別データのCRUD

レビュー情報管理画面

メインサイトでユーザーが投稿した口コミ情報を管理するための機能。

口コミはユーザー投稿後に即公開ではなく、サイト管理者が審査した上で掲載に値するものを公開するようにしている。

また、投稿したユーザー情報を確認できる画面を用意。

フロントエンド設計について

主要ライブラリ

ライブラリバージョン範囲使用目的
react^17.0.0 || ^18.0.0
next14.0.3サーバーサイドレンダリングをサポートするReactフレームワーク。高速なページロードとSEO最適化を提供。
@mui/material / @mui/icons-material5.14.18Material Designコンポーネントの実装を提供。
aws-amplify / @aws-amplify/adapter-nextjs6.0.3 / 1.0.3認証、API、ストレージなどのAWSサービスにアクセス。
react-hook-form7.48.2効率的なフォーム処理とバリデーションに使用する。

コンポーネント設計

コンポーネントの種類責務依存関係詳細説明
Pageルーティング管理Paneナビゲーションとルーティングに集中。React Routerなどのルーティングライブラリと統合し、ルートパラメータの管理を担当。

パス: src/pages
Paneレイアウト管理、UIの構成Pageページの主要なレイアウトとUI構造を担当。ビジネスロジックやAPI通信はカスタムフックに委ねる。
SectionUIの細分化とプレゼンテーションロジックの管理PaneUI要素をより小さく集中的な単位に分割し、フォームや特定のUIセクションの表示を担当。API通信は行わない。

例)ヘッダー、フッター、フォームなど
Partsテキストフィールドやカード要素などのパーツ単位の表示Section管理が煩雑になるため使用しない。@mui/materialのUIコンポーネントをセクション内で使用する。

使用例

  • Page: LoginPage/login パスに対応し、LoginPane を表示する。ルーティングロジックのみを持ち、ページ固有のデータ処理は含まない。
  • Pane: LoginPane はログインフォームとその周辺のレイアウトを管理し、useLogin カスタムフックを使用して認証プロセスを制御する。
  • Section: LoginFormSection はログインフォームのUI要素を担当し、フォームの入力フィールドと送信ボタンをレンダリングする。
  • Parts: <TextField /> はテキストの入力管理を担当する

Hooks

UIとビジネスロジックの分離を明確にするために、ReactのHooksを使用する。

https://ja.react.dev/learn/reusing-logic-with-custom-hooks

ディレクトリ責務説明
hooks/apiAPI通信に関連するロジックAPIエンドポイントへのリクエストやレスポンス処理など、外部APIとの通信を担当するカスタムフックを提供。
hooks/utils汎用的なユーティリティ機能様々なコンポーネントで再利用可能な汎用的な機能(バリデーション、データ変換など)を提供するカスタムフックを格納。
hooks/serverサーバーサイドレンダリングデータ取得系などサーバーサイドレンダリングで使用するロジックを定義
components/hooks特定のコンポーネントに関連するビジネスロジック特定のコンポーネントやページに特化したロジック(状態管理、イベントハンドリングなど)を担うカスタムフックを管理。

Middleware

管理(Admin)サイトでは認証が必要なため、ページ遷移ごとにユーザーが認証済みであるかどうかをMiddlewareで確認し、未認証の場合はログイン画面にリダイレクトする処理を実行している。

詳しくは、以下の記事を参照。

https://blog.freelance321.com/next-js/next-js-development-site/

状態管理

アプリケーション全体でデータのグローバルアクセスを行うために、Context APIを使用する。

https://ja.react.dev/learn/passing-data-deeply-with-context

管理している状態

状態の種類ファイルパス説明
ローディング状態src/contexts/LoadingContext.tsxアプリケーションのローディング状態を管理。非同期処理中などのUI表示に使用。
アラートメッセージsrc/contexts/MessageAlertContext.tsxユーザーに通知するメッセージをアプリケーション全体で管理。エラーや通知など。

例) アラートメッセージ

どの画面でも画面上部に固定で表示される。

プロバイダーの定義と呼び出し方

LoadingContextの場合、以下のように定義して状態管理を行なっている。

import React, { createContext, useContext, useState, ReactNode } from "react";
import Router from "next/router";

// コンテキストで使用されるプロパティの型定義
interface LoadingContextPropsType {
  isLoading: boolean;
  setLoading: (isLoading: boolean) => void;
}

// コンテキストの作成。初期値はundefined
const LoadingContext = createContext<LoadingContextPropsType | undefined>(
  undefined
);

// プロバイダーのプロパティの型定義(子コンポーネントを受け取る)
interface LoadingProviderProps {
  children: ReactNode;
}

// ローディング用のプロバイダーコンポーネント
export const LoadingProvider: React.FC<LoadingProviderProps> = ({
  children,
}) => {
  const [isLoading, setLoading] = useState(false);

  return (
    <LoadingContext.Provider value={{ isLoading, setLoading }}>
      {children}
    </LoadingContext.Provider>
  );
};

// カスタムフック:コンテキストを使用してローディングの状態にアクセス
export const useLoading = (): LoadingContextPropsType => {
  const context = useContext(LoadingContext);
  // コンテキストがLoadingProvider内で使用されていない場合はエラーをスロー
  if (!context) {
    throw new Error("useLoading must be used within a LoadingProvider");
  }
  // コンテキストの値を返す
  return context;
};

プロバイダー内部でカスタムフック`useLoading`を定義しているので、各コンポーネントでhookを呼び出し、ローディング状態をコントロールできるようにした。

例えば、ログイン画面での処理の場合、
プロバイダーで定義したカスタムフックを呼び出し、setAlertMessage、setLoadingという関数を使用して状態の更新を行なっている。

import { useLoading } from "@/contexts/LoadingContext";
import { useMessageAlert } from "@/contexts/MessageAlertContext";
import useAuth from "@/hooks/api/useAuth";
import { AuthLoginFormType } from "@/types/FormType";
import { SubmitHandler } from "react-hook-form";

const useLogin = () => {
  const { setAlertMessage } = useMessageAlert();
  const { setLoading } = useLoading();
  const { apiSignin } = useAuth();

  // サインイン完了後の処理
  const handleAfterSignIn = async () => {
    // 処理を書く
  };

  /**
   * サインイン 送信処理
   * @param data AuthLoginFormType
   */
  const login: SubmitHandler<AuthLoginFormType> = async (data) => {
    setLoading(true);
    try {
      const { isSignedIn, nextStep } = await apiSignin(data);
      // サインイン完了
      if (isSignedIn) {
        await handleAfterSignIn();
      }
    } catch (error) {
      console.error(error);
      setAlertMessage({
        type: "error",
        message: "認証に失敗しました。",
      });
    } finally {
      setLoading(false);
    }
  };

  return {
    login,
    handleAfterSignIn,
  };
};

export default useLogin;

コンポーネントツリー内での使用

  • アプリケーションのルート(**src/pages/_app.tsx**)で各コンテキストプロバイダーをラップする。
  • これにより、アプリケーションのどのコンポーネントからでも、これらの状態にアクセスし、更新することが可能になる。
import type { AppProps } from "next/app";
import { MessageAlertProvider } from "@/contexts/MessageAlertContext";
import { UserProvider } from "@/contexts/UserContext";
import { LoadingProvider } from "@/contexts/LoadingContext";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <LoadingProvider>
      <UserProvider>
        <MessageAlertProvider>
          <Component {...pageProps} />
        </MessageAlertProvider>
      </UserProvider>
    </LoadingProvider>
  );
}

注意点

現時点でContext APIはグローバルな状態管理にのみ使用し、コンポーネント間(Pane→Section)のデータ共有についてはpropsを使用する。

カテゴリー: Next.js