キーボードの打鍵音を動画投稿できるウェブアプリを作った

こんにちは！

個人開発でウェブアプリを作れるようになったら楽しそうという理由と、コードを書くことで価値を自分から生み出せるのが好きで、2023年の7月から個人開発をはじめました。

## 最初に取り組んだアダルトサイト自動化

最初に作ったのは、某エロタレのコピーサイトです。3ヶ月で作りました。

[Githubソースコード](https://github.com/tomato-ga/Next-rss2/tree/main)

### 技術スタック

- Next.js 13 Pages Router(JavaScript)
- FastAPI
- Go(RSSパース)
- PostgreSQL

アダルトなら需要はなくならないし、自分も興味あるしという理由から、AWS Lightsail(1GB)2台でフロントをNext.js、バックエンドでAPIをPython FastAPI、DBをPostgres、RSS処理をGo(速いから)で構築していました。Next.jsでAPI生やせることなどは知っていても、自分が作れる構成を考えるとこのような形になっていました。

みるみるデータが貯まっていき一応の完成は見せ、個人的にも出会えなかった動画に出会える楽しさから一定の満足度は見せていましたし、アクセスも良好でした。

ですが、RDBの特性がわかっておらず、稼働開始1ヶ月で50万レコードが登録され、DBが頻繁に落ちている状況に。なんでDBが落ちるのかもわかっておらず、DBスペックの重要性も認識できていませんでした。

ここでクラウドのマネージドDBの重要性を理解し始めたのですが、AWS RDSや当時は無料だったPlanetScaleなどに移行することも考えましたが、移行後のコストや、バックエンド入れ替え・毎月かかるコスト、そもそもの社会理念などを考えると、いい大人がアダルトやることに抵抗が出てきて、継続は困難だと考え廃止しました。

1つ目の個人開発でNext.js Pages Routerの特徴を大枠理解し、DB運用の見積もりが甘かった反省から、DynamoDBで二つ目のアプリを作りました。

## アマゾンのセール情報掲載サイト

二つ目はAmazonのセール情報を毎日商品ごとに取得（1000件くらい）し、それをすべて個別ページにして掲載するセール情報まとめウェブアプリです。

[Githubソースコード](https://github.com/tomato-ga/otoku-deal-pub)

### 技術スタック

- Next.js 13 Pages Router(JavaScript)
- Playright(スクレイピング)
- AWS DynamoDB
- Vercel
- Vercel Postgres(ブログ用)

こちらのアプリもJavaScriptで構築していきました。

初期開発の時点では、Chrome拡張機能を作って、Amazonを開いたら、タイムセール情報を自動でクローリングしていくようにしていたのを、Node.jsでPlayrightを使ってサーバー上で動作させるようにしてページ情報を取得してDynamoDBに保存していました。

DynamoDBは、一つ目に作ったウェブアプリで構築した自前サーバーPostgresとは比較できないほど快適なレスポンスを提供してくれたのですが、NoSQLのテーブル設計が独特で、DynamoDBの扱いに慣れるまでが一番時間がかかったように思います。

このウェブアプリはVPSから脱却して、Vercel Proでデプロイしていました。

稼働は問題なく続けられていたのですが、GoogleのSEOアップデートにより、インデックスされない問題が発生。

「あぁこういうアプリは今の時代には合っていないし難しいんだな」と感じて閉鎖しました。

あと、DynamoDBやCloudWatchの管理する手間を考えると、一度閉じてしまって新しいものを作りたいと思ったのです。

ここまで、自分が興味を少しは持てて、作れそうなものを軸にJavaScript主体で開発していましたが、3つ目のアプリ開発に移行する前にTypeScriptの勉強をはじめました。

プログラミングはPythonのスクレイピング・データ分析から入った人間です。

同じ動的型付け言語とはいえ、JavaScriptのコードは書きづらいと今でも思っていますが、TypeScriptを使い始めてから景色がガラッと変わってきたと思います。

複雑なジェネリクスはまだまだ書けませんが、型の恩恵を受けられるメリットはありがたいですね。

今回作ったのは、自分の興味が強く、世の中にまだ存在していないだろう「キーボードの打鍵音」に特化したニッチなウェブアプリです。

## キーボードの音を動画で投稿するサイト

### 技術スタック

- Next.js 14 App Router + TypeScript
- Prisma
- NextAuth

### インフラ

- Vercel Pro
- Supabase (Postgresのみ)
- Cloudflare R2

## 開発補助AIツール

- Claude Opus
- GPT-4

なぜ、このウェブアプリを作りたいと思ったかというと、元々ガジェットが好きだったことと、最近のメカニカルキーボードはカスタマイズが楽しく、海外メーカーの勢いを感じる分野だからです。

Nuphyというメーカーの打鍵音をたまたまYouTubeで聞いたことで、メカニカルキーボードというジャンルが盛り上がっていることを知り、YouTubeでキーボードの気持ちいい打鍵音を探しては再生を繰り返す日々を過ごしていました。

YouTubeのキーボードと同じ構成でパーツ購入したら、同じ打鍵音を聞けるのか…という夢見る少年の心でYouTubeを漁っていましたね。

気づいたらメカニカルキーボード1台で、8-9万円(円安効果アリ)のカスタムをしているほど楽しめたので、せっかくなら「キーボードの音」をテーマにしたウェブアプリを作ろうと思ったのです。100%自己満足優先の開発です。

2024年3月末から開発をはじめ、Wakatimeで開発時間を計測しているのですが、だいたい平均して1日2-3時間を充てて5月GW最終日にはおおよそ完成しました。

あと、Claude OpusがGPT-4より高速で頭がいいので、わからないことを解決しやすくなったのは時間短縮に効いているように思います。

ウェブアプリはまだまだ完成していませんが、技術的にはひとまず初歩的なCRUD操作をPrismaを通じてできるようになったことで、NoSQLに戻れない体になったと思います。

Next.js App Routerは、書籍の実践Next.jsの内容を取り入れながら開発しています。書籍ではキャッシュ周りが参考になりますし、書籍読んでから公式ドキュメントに入ると理解しやすいのでおすすめです。

Next.jsはPages RouterからApp Routerへ移行すると、サーバーコンポーネントの扱いが面倒だなと思ってしまいますが、慣れれば扱いやすいですし、型の解決にハマらなければTypeScriptありがたいと思う日々であります。

はじめてログイン機能をNextAuthで実装してみましたが、ユーザー認証とDB連携、ユーザーと投稿内容の設計など、一見するとシンプルで単純そうに見えるものをゼロから開発したことで、「そんな簡単じゃないんだな」と実感できたことが一番の収穫でした。

また、動画を主に扱うことで、転送量の問題にぶち当たりました。

Vercel Proプランは、転送量が1TBまで使えて、それ以上は従量課金となり100GBで40ドルかかる料金体系でした。（価格改定が行われた模様 https://vercel.com/docs/pricing#regional-pricing ）

動画データをVercel経由でR2にアップロードすると、テストでアップロードしているだけで1日500MB以上転送量が必要でした。

それに、Server Action経由だと、[Next.jsで4.5MB制限の壁](https://vercel.com/guides/how-to-bypass-vercel-body-size-limit-serverless-functions)が存在していて、やべっ、これ転送量おおすぎだよな...？ってびびり散らかしていました。

10MBの動画すらアップロードできないし、どうすんのおれ。みたいな悩みをGW夜中に抱えていたことを思い出します。

問題解決のため転送量無制限の国内VPSにデプロイすることを考えましたが、結果として署名付きURL発行をCloudflare R2で行い、Vercelを経由しないファイルアップロードを実装できたことで、転送量問題は解決できました。

考えてもいないところでコストかかりそうだと頭抱えてしまいますね。

そんなところで、公開しているのが[keyboard-sound.net](https://keyboard-sound.net/)です。

2個目のウェブアプリ開発からVercel Proを使い続けていて、Supabaseは無料枠ですし、追加コストは発生してないという都合の良い認識で公開しています。

誰でもキーボードの音を投稿できますので、ぜひいろんな音を聞かせてください。

もちろん、自作キーボードでもいいし、カスタムしたキーボードでもいいし、メーカーさんの新商品アピールの場として使ってもらっても問題ありません。

次回は設計周りを整理して記事にしようと思います。