🎫

Next.js13のapp directoryでチケット共有アプリを作ってみた

2023/05/07に公開

はじめに

今回GWという時間を利用して、Katackyというチケット共有アプリを開発しました。

アプリ概要

https://katacky.xyz/

ソースはこちら
https://github.com/skyt-a/Katacky

家族やカップル、友達の間で使えるオリジナルチケットを作成、譲渡、使用できるWebアプリです。
スケジュール設定をすることであるユーザーに月に2枚チケットを配布するみたいなこともできます。
サービス名のKatackyというのはこういう身内だけで運用するチケットといえば、肩叩き券だなと思ったのでそこから考えました。
ちなみにKatackyのロゴはBRANDMARKというサイトで作成したものです。


こんな感じのチケットが作れます

なぜ作ったのか

彼女とダイエットに勤しんでいまして、ただ月に何回かはチートデイ的に1食好きなものを食べる権利をお互い持っているのですが(彼女は2回、私は1回)、この権利を行使したことをDiscordに記録していました。
これアプリ化したいなーと思っていたので今回作ってみました。

技術スタック

フロントエンド + APIサーバー

アプリを作るにあたって、フレームワーク・UIライブラリを何にするかは悩みましたが、今回はNext.js 13で登場したapp directoryを使ってみたいなと思ったのでNext.jsに決定しました。
RemixNuxtSolid.jsSvelteKitあたりが他の候補でした)
ちょうどGW中にGAになりましたね。
https://nextjs.org/blog/next-13-4
またAPIサーバーとフロントのインターフェースにはtRPCを採用しています。
2023/05/28追記
Next.jsのServer Actionが発表されたことを受けて、Query系のAPIはServerComponent内で呼出し、Mutation系はServerActionを使用することで、tRPCを導入する理由の一つだったフロントエンドのコードベースの中からシームレスにバックエンドAPIを呼び出すという要件を満たせるようになったので現在はtRPCは使用していません。

スタイリング・UIフレームワーク

普段は個人的にemotionstyled-componentsといったCSS in JS系のライブラリを使うことが多いのですが、今回app directory(React Server Component)を使うということでこれらのライブラリは相性が悪いため、初めて巷で話題のTailwindCSSに触れてみることにしました。
またUIフレームワークとしてshadcn/uiというフレームワークを採用しています。
これはRadix UIというHeadlessなUIフレームワークとTailwindCSSを組み合わせたもので、カスタマイズが容易そうだったので採用しました。
Mantineも良さそうだったんですが、emotion依存が気になったので今回は見送りました。
(ただMantineは今後emotion依存をやめる可能性があるようです: 参考)

認証

認証はFirebase Authのメール+パスワード認証を採用しました。(今後他のプロバイダーも追加していく予定です)
またNext.jsと合わせて簡単に認証情報を利用するためにAuth.js(NextAuth.js)も採用しています。

ホスティング

ホスティング先はNext.jsとの相性の良さからVercelにしました。
Firebaseを使っているので、Firebase Hostingもありかなと思ったんですが、Next.jsのホスティング先としてはまだまだ安定版ではないようなので今回は見送りました。

データベース

今回はPlanetScaleを採用しています。
Firebaseに依存しているからFireStoreもありかなと思ったんですが、個人的にはRDBの方が好みなので今回は採用を見送りました。
ちなみに開発当初はFirebaseではなく、Supabaseを使っていて、DBもSupabasePostgreSQLを使用していたのですが、後述するFirebase Cloud Messagingが使いたくなった関係でSupabase依存をやめ、PlanetScaleへ移行することになりました(Authも合わせて移行しました)。
技術選定の前にちゃんと要件固めとかないと。。。
またORマッパーとしてPrismaを採用しています。

結果的にT3Stackと呼ばれる構成になりました。

こだわりポイント

グループ登録をQRコードでできるようにした

このアプリではグループという概念があり、ユーザーは同じグループのユーザーにのみチケットを譲渡できるようになっています。
このグループはユーザーが自由に作成できるのですが、他のユーザーが作ったグループにどうやって参加できるようにするか悩みました。
メールアドレスを入力させて招待メールを送る動線も考えたのですが、アプリの想定用途的にも家族やカップルなど直接顔を合わせるようなメンバーでグループを作ることが考えられるので、この動線ではユーザーにとって少しめんどくさいだろうなと考えました。
LINEの友達登録のようなQRコードでの登録の方が今回のアプリ的にはベターだと考えられるので、今回はこちらを採用しました。
QRコードの読み取りはjsqr、作成・表示はnext-qrcodeを使用しています。

チケットの受け取りをプッシュ通知するようにした

Katackyではチケット受取時のプッシュ通知に対応しています。
これまでWebのプッシュ通知はiOSでは対応していなかったのですが、ついにiOS 16.4でSafariでもプッシュ通知が利用できるようになったこともあって実装にチャレンジしました。
プッシュ通知の送信はFirebase Cloud Messagingを使用して行っています。

PWA対応

プッシュ通知を実現したかったこともあり、PWAに対応しています。
ぜひホーム画面に追加して使ってみてください!

ちゃんとアイコンも表示されます

チケットをスケジュール発行できるようにした

私と彼女の要件だと月に1回チケットを私は1枚、彼女は2枚発行する必要があるのですが、毎月手作業でやるのは面倒くさいと思ったのでスケジューリング機能をつけて自動化しました。
スケジューリングはVercelを使っていることもあり、Vercel Cronを採用してみました。
実行したいGET API(今回はRoute Handlerを使用しています)とvercel.jsonを用意し、以下のように記載することで簡単にスケジューリングできました。

vercel.json
{
  "crons": [
    {
      "path": "/api/ticket/schedule",
      "schedule": "0 20 * * *" // 毎日AM5:00に↑のAPIが呼び出される(Vercel CronはUTC基準なので注意)
    }
  ]
}

ただ↓にあるように
https://vercel.com/blog/cron-jobs#limits-of-cron-jobs-and-vercel-functions

While in beta, Vercel Cron Jobs are free on all plans. However, it'll be a paid feature for general availability.

とのことなので、ベータが外れた暁には無料で使えなくなる可能性が高そうですね。

開発してみて感じたこと

app directory(Next.js)

app directoryについての詳しい説明は他に素晴らしい記事がいくらでもあるので割愛します。
とりあえず公式ドキュメントに一通り目を通して、あとはトライアンドエラーの精神で頑張ろうと意気込みました。
正直まだまだちゃんと使いこなしているとはいえず、どういう設計がうまく合うのかはこれからも模索していく必要がありそうです。
サーバーサイドのキャッシュ戦略がまだよくわかっていないところもあるのでちゃんとキャッチアップしたいですね。

Server Component内で非同期処理をasync-awaitで書けるのは素晴らしい

こんな感じで書けて非常に直観的。個人的にはgetServerSidePropsの中で書くよりもぱっと見でやりたいことがわかりやすいです。

export const Profile = async () => {
  const user = await getUserInfo();
  if (!user) {
    return null;
  }
  ...

状態管理やデータキャッシュ周りはクライアントからサーバーサイドに寄せるような構成が主流になりそうな雰囲気

基本的にAPIからのデータのfetchもServerComponent内でやることが多く、ReactQuerySWRでやっていたようなクライアントでのデータキャッシュみたいな機構がそこまで必要じゃなくなって来るのかなと感じました。
今回ぐらいのシンプルなアプリだと管理したいようなステートもそこまで多くなく、今回は特にクライアント側の状態管理系のライブラリは使用していません。

バグのような挙動もいくつか見られた

ベータ版(GW中にGAになりましたが)ということもあり、ところどころ動作が不安定になることがあります。
謎にエラーが発生したと思いきや、何もせずにもう一度ページを開くと直ったり、実装が悪いのか何かしらのバグなのかわからない状態で開発を進めるシーンが何回かありました。
ただこれは今後改善されていくはずなので安定するのを期待して待ちたいところです。
いくつか気になった事象は後述しています。

周辺ライブラリがまだ対応できていないことが多い

これも前述した通りまだまだ出たばかりなので仕方のないことですが、周辺ライブラリが対応できないことが多く、公式のドキュメント通りに実装してもうまく動かないことが何回かありました。
例えばtRPCは公式のインテグレーションガイド通りに進めると、クライアントからのAPI呼び出しでエラーが起きました。
根本的な原因はわからないですが、今回はcreateNextApiHandlerを使うのでなく、以下のようにfetchRequestHandlerを使うことでなんとか動くようになりました。

app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "~/servers";

const handler = (request: Request) => {
  return fetchRequestHandler({
    endpoint: "/api/trpc",
    router: appRouter,
    ...
  });
};

export const GET = handler;
export const POST = handler;

TailwindCSS

開発前には個人的にはclass属性がやたら長くなることと、実際のCSSのプロパティとTailwindCSSで設定するクラス名に結構ギャップがあって都度調べないといけないところが気になっていました。
ただ実際に開発で使ってみると見えていなかったいいところもありました。

簡単に所感

Reactのようなコンポーネント単位でスタイルが閉じるような環境ではclass属性の長さはあまり気にならない
VSCodeの拡張機能である程度補完は効く(なぜかたまに効かないことがある?)
・ダークモード対応が楽!
・今回あまり使いこなせていないが、デザインシステムをかっちり構築したいようなアプリではかなり重宝しそう

開発中遭遇したエラーや問題

開発中遭遇したエラーや問題をいくつか書いておきます。
同じ事象に遭遇した方の助けになれば幸いです。

You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.

何回も見たエラーです。
サーバーコンポーネントでクライアントコンポーネントでしか使えないモジュールをimportした時に発生します。
特に考えさせられたのはutilファイルみたいなものを作成しようとしたとき。
個人的にはhooksと純粋な関数や定数みたいな分け方より、utils/form.tsみたいな機能ベースでファイルを作ることが多いんですが、こうするとサーバーコンポーネントでも使えるモジュールとクライアントコンポーネントでしか使えないモジュール(hooksなど)が混在してしまい、よくこのエラーで怒られていました。
共通モジュールはクライアントでしか使えないものとそれ以外でファイルベースで分離することが求められそうです
これに限らず今までのやり方をそのまま踏襲すると問題が起きるので、app dirの気持ちになることが大事ですね。。。

unhandledRejection: Error [FirebaseError]: Messaging: This browser doesn't support the API's required to use the Firebase SDK. (messaging/unsupported-browser).

Firebase Cloud Messaging関連のモジュール実装時に発生した問題です。
サーバーサイドでのレンダリング時にFirebase Cloud Messagingが要求するAPIがブラウザにないぞと怒られています(当然ですね)。
isSupportedという関数が用意されているのでこれを使用して回避します。
戻り値がPromiseなので注意。

@supabase/auth-helpersのcreateRouteHandlerSupabaseClientでセッションが取得できない

これはQiitaの方で書きました
https://qiita.com/sky_t/items/e41e46ea071a09c8dce1
が、結局supabaseはやめてfirebaseに切り替えました。

TypedRouteが正しいパスを指定してもエラーになる

Next.js@13.2Typed Linkがベータ提供されるようになったので嬉々として利用していたんですが、たまに正しいパスを指定しているのにTypeScriptエラーが出ることがありました。
この場合はVSCodeのTypeScriptサーバーを再起動すると直りました。まだベータだからなのか私の環境に問題があるのかは不明です。
機能としては素晴らしいので今後のアップデートに期待です。

おわりに

app directoryは個人的には総合的に見れば開発体験上がった感覚ですし、GAになったことで今後さらに重要になっていくことでしょう。
最近のアップデートを見るに力の入れようが尋常じゃないように見えるので今後どんな機能がリリースされるのか楽しみですね。

Katackyの開発に関しては、まだいくつか見えてるバグはあるんですが、ゴールデンウィークの短い期間での実装でなんとか形にできてよかったです(WakaTimeによると大体40時間くらいで実装できたみたいです)。
この後は時間見つけてバグfixや機能追加、あと前から使ってみたかったAstroでLP作ってみたりしていきたいです。
なんとかNext.js側の今後のアップデートにもついていきたいところです。

Katacky、よければ使ってみてください!
https://katacky.xyz/

GitHubで編集を提案

Discussion