1. はじめに
ある日ふとサークルのメンバーと、何かひとつプロダクト作ってみたいねーという話になり、折角なら自分たちの経験から何か役に立つものを作ってみたいねーという話になり、その結果学内知恵袋サイトをつくることになりました。
学内知恵袋サイトにしたのは、大学内部の情報や経験を共有したり先達者に気軽に尋ねたりできるようなサイトがないと感じたためです。
私自身、長期の交換留学にむけて準備を進めていく際に、留学先の寮は何を選べばいいのかとか留学する間就活はどうしようかとか単位互換はどれくらい保証されるのかとか、聞きたいことが多かった割に、それぞれの情報について担当する部署が違ったりそもそも返答を得られなかったりしました。
留学課、キャリア支援課、学務課を梯子して情報を集めたり、どうしても分からなかったら同じ海外大学に留学していた方にコンタクトを取って生活の様子や申請準備の進め方を聞いたりなど、縦割りも相まってとかく情報を集めるのが大変でした。
そういう切実な経験もあり、何か作れるならそういう、情報や経験を共有できるサイトを作りたいと思い、今回のプロダクトを作りました。
どれくらい運用するかは未定ですし、HobbyプランでVercelにデプロイしているのでアプリがそこまでスケールできるわけでもないのですが、メンバーと共同でひとつプロダクトを作り上げたというのは資産になると思い、折角ならとここで紹介する次第です。
この記事は、私のポートフォリオとして使うことを想定して書いています。
詳細な技術的解説はあまりする予定ではないので、Next.jsの新機能を活かした開発をしてみたいという方は、別の記事を当たってください。
以下の二つは今回のプロダクトをつくるうえで非常に参考になりました。
謝意も込めてここで紹介させていただきます。
目次
2. アプリの概要
先述した通り、フロントエンドはNext.js、サーバーサイドはDjangoを使っています。
私はNext.jsでフロントエンドを担当しました。
「学内」に限定するため、アカウント作成時は大学のメールアドレスを使った登録を求め、確認メールを送ることによってユーザーのメールアドレスが有効かどうか確認しています。
全体のイメージとしては以下のような感じです。
3. フロントエンドフレームワーク一覧
フロントエンドのフレームワークとしては、Next.js以外に以下のようなものを使いました。
- zod
- 値のバリデーションのために利用しました。
- mui
- CSSスタイル適用の効率化のために利用しました。
- tailwindcss
- muiでは難しい細かいスタイルの部分を編集するために利用しました。
- jotai
- グローバルのステート管理のために利用しました。
- nodemailer
- ユーザーのメールを認証するために使いました。
- typescript(開発環境のみ)
- 変数やオブジェクトの型定義のために利用しました。
実は、今年の一月くらいから、Qiita記事の更新をしていました。
これは、開発を進めていく中で自分なりに工夫したことや調べたことをまとめ、後から振り返られるように、再現性を確保するために記事を書いていた面もあります。
例えば、Next.jsのサーバーアクションは多用していたため、それを共通化する処理を書いたり、nodemailerを利用したメール認証機能を実装したり、クエリーパラメータを利用して、クライアントコンポーネントで受け取った入力値をサーバーコンポーネントに渡す機能を実装したりしました。その結果はQiitaの記事という形でもアウトプットしています。よろしければぜひご一読ください。
以下では、このアプリの各ページの紹介をします。
機能面については、実装面における機微に触れない程度にお話ししようと思います。
4. ルートパス
ここでは、トップページの紹介をします。
投稿されてある質問板の一覧を表示するようにしており、見た目もシンプルなものです。
ここでは質問板一覧やページネーション、検索機能の様子をお見せします。
4-1. トップページ
トップページの画面表示は以下のようになっています。
ページ下部の部分です。
ページネーション機能が付いています。今は一ページだけですが、質問が増えていった場合、10枚の質問板ごとにトップページに表示されるようになっています。
また、二番目の質問板を見てもらえばわかるのですが、マウスホバー時、板が大きくなっている、膨らんで見えるようになっています。
膨らむときはゆっくり、萎むときは少し早くしており、ちょっとした拘りポイントです。
この挙動はMUIを使って実装しています。
コードは以下の通りです。
<Card
sx={{
borderRadius: "5px",
boxShadow: 3,
width: "60%",
marginX: "auto",
transition: "transform",
transitionDuration: "700ms",
"&:hover": { transitionDuration: "1000ms", transform: "scale(1.05)" },
"@media screen and (max-width:640px)": {
width: "90%",
},
}}
>
<CardHeader />
<CardContent sx={{ paddingY: 0 }}>
<Typography>Contents...</Typography>
</CardContent>
</Card>
4-2. 検索機能
検索機能実行時の画面の様子です。
検索バーに文字を打ち込んで検索をかけるとクエリーパラメータに反映され、トップページに表示されてある質問一覧が更新されます。
トップページとコンテンツとしての質問板もサーバーコンポーネントで作られていますが、searchParamsというDynamic functionの機能を用いることで、サーバーコンポーネントが静的な状態から動的な状態になっています。このおかげで、検索に反応して即座にページを更新してくれるわけです。
この機能の詳しい話はこちらでもしています。よろしければどうぞ。
5. authパス
これは、ログイン機能やアカウント登録処理を担うルーターです。
例えば、サインインページの様子は以下のようになっています。
フォームの入力値をNext.jsのWebサーバーに送るのはFormActionの機能を用いており、また、サーバー側で受け取った値のバリデーションを行うのはTypeScriptのzodを用いています。
少ない記述量で値のバリデーションが可能なので、zodは凄く便利です。
5-1. サインアップ
サインアップは以下のようになっています。
もしかしたらすぐに察せられるかもしれませんが、フォームのUIは再利用しています。入力フォームのタイトルとInputフィールドでひとつのコンポーネントにしており、親コンポーネントの側でフォームのタイトルを配列として渡すことで簡単に再利用できています。
この手法の詳しい話はこちらでもしていますので、よろしければどうぞ。
5-2. パスワードリセット
パスワードをリセットする処理は、MuiのStepper コンポーネントを利用して実装しています。
デフォルトのStepperコンポーネントには、処理が完了した場合は自動的に次のページへ進んだり、エラーが出た時は次には進めないようにしたりする機能が付いていません。
リセットパスワードの成功時と失敗時の挙動を制御する方法としては、グローバルステートを用いたり、ローカルストレージを利用したりする方法があると思います。
ローカルストレージに保存された値を編集されて次のページに行けたとしても、メールアドレス未認証ではプロセスが進まないようにサーバー側で制御しているのでセキュリティ上のリスクはないと判断し、今回はローカルストレージで成否情報の管理を行っています。
この挙動の管理も、今回工夫したポイントの一つです。
1.メールアドレス入力
2.認証コード入力
3.新規パスワード入力
4.エラー発生時の画面。上にもエラーが表示され、次の画面には行けない。
6. userパス
このパスは、認証を通らないと閲覧できない画面や、操作できない機能を集めています。
具体的に言えば、マイページと質問投稿画面のことです。
まずはアプリ共通の認証をつくる方法から見ていくことにしましょう。
6-1. middleware
middlwareはルートディレクトリーに配置しています。
srcディレクトリー配下にappディレクトリーを配置しているのであれば、appの中ではなくsrcディレクトリー直下にmiddlewareを置いています。
import { NextRequest, NextResponse } from "next/server";
export async function middleware(req: NextRequest) {
const isLoggedIn = !!req.cookies.get("access");
const isOnUserPage = req.nextUrl.pathname.startsWith("/user");
if (isOnUserPage) {
if (isLoggedIn) return NextResponse.next();
return NextResponse.redirect(new URL("/auth/signIn", req.url));
}
}
export const config = {
mather: ["/user:path*"],
};
userパス全体に認証をかけようとしていることがわかると思います。
もし、既にログインを済ませているのであれば、アクセストークンを持っていることになるので、そのまま素通り(NextResponse.next()
)させています。
6-2. My Page
ここで自分が投稿した質問や回答、評価した項目の一覧を見ることができます。
ユーザー情報やその編集機能は左側にまとめて配置しています。
また、この画面からでもそれぞれの質問ページに飛ぶことができますし、質問や回答を消すこともできます。 削除の確認画面はダイアログ表示を利用しています。
退会機能が見つけにくい仕様はダークパターンとして有名です。
だからと言ってマイページの見えやすいところに退会ボタンを配置してよいのかと言われれば微妙ですが、ユーザーが探しまくる羽目になるよりかはマシかなとは思います。
6-3. 質問投稿
カテゴリーを選ぶUIがちょっと貧弱なので、これはもうちょっと改善の余地があったなと思いますが、必要な機能は備えているのでひとまずはこれでリリースしています。
公開設定も選択することができ、この閲覧制限機能は、大学メールの@以降の部分が先生や生徒によって一定の形に定められていることに基づいています。
閲覧制限機能はサーバーを担当してくれた相方が、サーバー側の処理として実装してくれました。
7. questionsパス
ルートページの質問板をクリックすれば、その質問の詳しい中身と、コメントや回答がつけられたページに行くことができます。
これはNext.jsの動的ルーティング機能を利用しており、/questions/2
のように、URLの末尾に数値が付け加えられた形になっています。
ここでは、ページ全体に対して認証機能を追加するのではなく、グローバルなユーザーデータと質問・回答者のユーザーデータを突き合わせて、其々の機能ごとに編集権限を付与しています。
例えば、回答や質問・コメントは、その作成者しか編集・削除できないのはもちろん、質問に対する評価は質問作成者以外のログイン済みユーザーが、回答に対する評価は回答作成者以外のログイン済ユーザーがつけられるようにしています。
また、コメントに関して言えば、質問に対するコメントは誰でも付けられる一方、回答に対するコメントは質問者と回答者しか付けられません。
回答に対するコメントは、例えば以下のようになっています。
コメントフォームを開いたり閉じたり、コメント一覧を開いたり閉じたりもできるようになっています。
なお、通報機能はログイン済ユーザーであれば誰でもできます。
8. おわりに
反省点としては、折角フロントエンドをやるのでもっとUIに拘ってもよかったという点です。
質問作成ページのカテゴリーの一覧や、質問表示ページの評価ボタンのUIなど、もうちょっと改善できそうな点がちょくちょくありました。
Django側とのWebAPI通信機能を完成させることばかりに重きを置いて、UI側にはあまり時間を割かなかったことも原因としてあります。実際、Muiを使っていたこともあり大まかなUIは完成スピードが速かったのですが、Muiのコンポーネントが綺麗なぶん、自作のコンポーネントやボタンの配置などで自分のセンスのなさが出てきてしまいました。もし次フロントエンドをやるときは、もっとUI/UXについての勉強も進めたいと思いました。
別の共同開発案件も近々始まるので、留学までの期間、追い込みをかけて技術力を高めていきたいと思います。