はじめに
初めての個人開発をしました。これまでプログラミングやWeb開発についてちまちま学んできましたが、どれも応用には至らない断片的な知識ばかりだったので改めて0から勉強し直すところから始めました。この記事では開発したサービスの紹介と今回の個人開発での取り組みや感想等について書いていこうと思います。
作ったサービス
※現在はレスポンシブ対応ができていません(今後対応)。
GitHub リポジトリ
サービスについて
サービス概要
復習スケジュール管理の負担を減らすためのWebアプリです。
好きな復習スケジュールのテンプレート(以降「パターン」と呼ぶ)を作成し、それらのパターンを使い分けることで学習の内容に合わせて個別の復習スケジュールを作成することができます。
このアプリはふろむだ (id:fromdusktildawn)氏の「最新研究からわかる 学習効率の高め方」で言及されている
カーピキー2008実験の《弱点勉強・全テスト》
と、安川 康介氏の「科学的根拠に基づく最高の勉強法」の復習タイミングに関する言及の
「等間隔か、徐々に延ばすのか」という問題は、僕の知る限り決着がついていません。そもそも、学習者の元々の知識量や勉強する内容の量や難しさ(忘れやすさ)などによって異なってくるのではないかと予想します。
(安川 康介. 科学的根拠に基づく最高の勉強法 (p. 76))
と
アクティブリコール+分散学習=連続的再学習
(安川 康介. 科学的根拠に基づく最高の勉強法 (p. 241))
の考え方を基準にしています。
アクティブリコール、分散学習、連続的再学習とは
アクティブリコールは同書籍の説明が分かりやすいため引用すると、
勉強したことや覚えたいことを、能動的に思い出すこと、記憶から引き出すこと
(安川 康介. 科学的根拠に基づく最高の勉強法 (p. 45))
とされています。
分散学習とは、「時間(日にち)をあけて繰り返し勉強すること」です。
連続的再学習とは「アクティブリコールと分散学習を組み合わせた学習法」のことです。
つまり、日にちをあけて繰り返し全テスト(思い出す)するという復習法を様々な復習タイミングのパターンで復習できるようにすることが本アプリの目的となっています。
また復習管理の労力を最小限にするために、ユーザーが主に記録するのは復習した内容の概要となっていて、あくまで"今日テストするのは何かを辞書的に確認する"という使い方を想定しているため、勉強した内容を詳細に記録しそれを見て復習する用途には向いていません(オプションとしてそういった詳細を記録する機能も用意し、自由度を確保しています)。
開発の背景
復習スケジュールは適切に組めばとても効果的です。しかし、スケジュールを組む以上それを管理する労力が少なからず発生します。
「複数の復習スケジュールを組む」、「復習済みかどうか判断する」、「サボってしまった時のスケジュールのズレを考慮する」 など、復習スケジュール管理には様々な要素が存在し、しっかり管理しようとするとそれなりに面倒です。この管理に割かれる労力が増え、返って復習効率が悪くなる可能性もあります。
同様のサービスはすでに多く存在しますが、人それぞれ使いやすいと感じる管理方法は異なります。当然自分に完璧にピッタリ合うサービスというのは中々存在しないためある程度の妥協は必要ですが、復習はこだわりたいなと思い当初はスプレッドシートで管理していました。
ただ、徐々に管理が大変になりGASで改良しないといけなくなったり、もう少し復習に特化したUIで復習したくなったこともあり、復習スケジュール管理に割かれる労力をできるだけ減らすことを目標に個人で開発することにしました。
解決したい課題
- スケジュール通りに復習しなかった時のリスケジュール作業が少し面倒で復習に集中しづらい
- 勉強の内容毎に異なる日付け間隔で復習スケジュールを組もうとすると管理が複雑で大変
- 復習スケジュール管理にできるだけワーキングメモリを消費したくない
解決方法
今回のような〇〇効率化系のアプリは何かしら機能が足りず不便な部分があったり、逆に機能を揃えすぎて扱いにくかったりといったことが個人的にありました。そのため、「復習管理に便利そうな機能を揃える」のではなく、「復習管理で不便な点を解消していく」というスタンスでUXを考えていくのが過剰な機能の実装を防ぎ、且つかゆいところに手が届くようなアプリを作るのに良いと思いました。
その結果、主に下記のような機能やUXを包括的に提供することで、こうしたスケジュール管理の負担を減らすことができると考えました。
- 自分好みのパターンを自作でき、それを使って自動で復習日をスケジューリングできる。
- 復習内容の答えまでは記入せず、単なるスケジューリングアプリという範囲に収まっている。
- 自分がその日、その復習物を復習したかチェックできる。
- 復習物をカテゴリー分けして登録でき、カテゴリー間での復習物の移動もできる。
- 復習日が今日のものを一覧表示できる。
- 復習をサボった時のスケジュールのズレを考慮して、自動で再スケジューリングしてくれる。
- 例えば、「1、3,5(日後)」 というパターンをもとにスケジューリングされた復習物Aの2回目(3日後)を完了チェックせずに日付を跨いだ時(この時、1回目(1日後)の復習日は復習済みという扱い)、1、4、5(日後) という形で再スケジューリングされる。
- また、復習したけど完了チェックをし忘れて再スケジューリングされてしまった場合に、日付を戻して完了チェックすることができるという機能も欲しかった。
- ダークテーマがある(目が疲れるから欲しかった)
- 多種多様なパターンやカテゴリーが存在する中で、シームレスに目的の復習スケジュールにアクセスでき、すぐに復習に取り組めるUX
- 一度、自分の型(パターンやカテゴリーなど)を自作し整えておけば、そのあとはそこにポンポン復習物を登録していくだけで復習スケジュールに意識を割かずにスケジュール管理ができるUX
サービスの構造
ユーザーが作成したパターンはこのボックスに適用することができます。
ボックスに復習物が登録されると、ボックスに適用されているパターンをもとに復習スケージュールが生成されます。
また、ホームに対して1つとカテゴリー毎に1つずつ「未分類復習物ボックス」というものが存在します。これはどのボックスに復習物を登録すべきか迷ったときに一時的に登録するためのボックスです。(このボックスにはパターンは適用不可で復習物毎に別々のパターンを適用可能。)
サービスの使い方(主要画面)
1. 復習物作成モーダル
新しい復習物を作成するための入力フォームです。
- 「復習物名」、「詳細(任意)」、「学習日」に加え、どの「カテゴリ」のどの「ボックス」に属させるかを選択します。所属するボックスが設定されると、そのボックスに紐づいた復習パターンが自動で適用されます。ただし、ユーザー作成系のボックスの場合はパターンはそのボックスが持つものに固定されます(未分類系ボックスの場合はパターン選択可能)。
- また、このモーダルのカテゴリとボックスの選択肢はどの画面からこのモーダルを開いたかに応じて初期値が自動で設定されます。
2. パターン作成モーダル
復習スケジュールをカスタマイズするための画面です。
- 「パターン名」と学習の優先度を示す「重み」を設定し、「ステップ」で何日後に復習するかを自由に定義することができます。例えば、「1日後、3日後、7日後...」といった具体的なステップを数値で入力して、独自の復習スケジュールのパターンを作成します。
3. カテゴリー内部画面
特定のカテゴリ(この例では「プログラミング」)に属するボックスの一覧画面です。
- 各ボックスには、含まれる全復習物数と、そのうち今日が復習日の復習物数が表示されています。
- 「ボックス作成」ボタンからこのカテゴリに新しいボックスを追加することができます(ボックス作成時には、「名前」 と 「パターン」 を設定する必要があります)。
- 画面上部に表示されているカテゴリータブとボックスタブでカテゴリー、ボックス間を移動することができます。
4. ボックス内部画面
特定のボックス(この例では「高頻度」)に含まれる復習物の一覧です。
- 「復習物を作成」ボタンからこのボックスに新しい復習物を追加することができます。
- 復習日が今日の復習物の左には、「完了/取消」ボタンが表示され、クリックすると状態を操作することができます。
- 全ての復習日が完了、もしくは復習物を強制完了(鉛筆マーククリックで表示される画面から可能)した場合、スケジュールテーブル上部辺りにある「完了済みを確認」ボタンをクリックしたときに表示される画面に移動します。そこではこれまで完了させてきた復習物一覧を確認することができます。
緑色の日付: 過去の完了済み復習日。
青色の日付(暗): 今日の復習(完了済み状態)。
青色の日付(明): 今日の復習(未完了状態)。クリックすると復習日変更画面が表示され、そこで復習日を過去に変更して完了状態にすることができます。(後続の復習日は復習日の変更幅に従って再スケジューリングされます。)
5. 今日の復習画面
今日の日付が復習日に設定されている復習物が一覧表示される画面です。
- ここでも復習の完了/取消を操作をすることができます。
- ボックスへ移動ボタンをクリックすると、そのときに開いている条件の画面へ移動することができます。
- 例:「全て-全て」ならホーム、「プログラミング-全て」ならプログラミングカテゴリー内部画面、「プログラミング-高頻度」ならプログラミング-高頻度ボックス内部画面へ移動。
- 画面上部に表示されているカテゴリータブとボックスタブでカテゴリー、ボックス毎の今日の復習画面を移動することができます。
6. ホーム画面
使用技術
概要
プログラミング言語
- Go
データベース
- PostgreSQL
フレームワーク/ライブラリなど
- labstack/echo
- sqlc
- golang-migrate/migrate
CI
- GitHub Actions
技術選定
何も知らない中で技術選定なんかしても意味ないかなとも思いましたが、全くの見当違いな技術を選んでてあとからリプレイス鬼大変みたいな事態は避けたかったので自分なりに納得感を持てるように調べました。
基本的にコミュニティが活溌かどうかを共通の基準として設定し、それで出た候補の中で別々で基準を設けて選びました。
Go
個人的に(AIが代替しなければ)一生使いたいアプリのつもりで開発を始めたので、選定基準として保守性の高さは重要な条件の一つでした。そこで大まかに以下の基準で考えました。
1. 静的型付け言語
2. 学習コストが高くない
3. 長い期間保守するのに向いていて、将来性が高い。
1. 静的型付け言語
この基準を設けた理由は以下です。
- 長く運用することが想定されるので、未来の自分が理解しやすいように型情報が欲しい
- 以下の理由でエラーはコンパイル時に検出したい
- 開発経験がほぼないのでコードのミスが多発することが想定される
- 時間の都合でテストケースを網羅できない可能性が高い
この時点で、ソフトウェア開発の情報量も考慮してGo・Java・Kotlin・Rust・C#の5つに絞りました。
2. 学習コストが高くない
開発経験の少なさから、なるべく学習コストの低い言語を使うことにしました。何を持って学習コストが低いとするかは難しいところでしたが、学習コストの低さに関して賛否両論が少なく文法がシンプルという特徴が気に入りGoとKotlinに絞りました。
また、Goが持つ誰が書いても似たような書き方になるという特徴も学習コストを下げる要素の一つとして魅力的に感じ、この時点でGoが良いなと思いました。
3. 長い期間保守するのに向いていて、将来性が高い。
GoはGo1の間は後方互換性が保たれているので、長くアプリを保守するのに向いていると思いました。また、将来性に関してもGoとKotlinはともに高く、長い期間活発なコミュニティが期待できるのも安心です。
以上の基準でGoにしました。
どの言語もまともに使ったことがないので言語選定は苦労しました。言語に限らず言えることだとは思いますが、次回からはある程度各言語の初歩的な土地勘くらいは知っておかないとなと思いました。
labstack/echo
なるべくGoの記法に慣れたかったのでBeegoのようなフルスタックフレームワークは候補から除外し、コミュニティの活発さでEcho・Gin・Fiberの3つを候補にしました。
このアプリは長く使う予定だったので将来の変更に対応できて機能不足になりにくいフレームワークが良いと思い、薄いけどHTTP周りの機能が豊富で拡張性が高い点が気に入りEchoを使うことにしました。
企業が運営しているという点も大きいです。
sqlc
コミュニティが現在も活発なDB操作系のライブラリとして以下を候補にしました。
- sqlc
- GORM
- ent
- uptrace/bun
SQL素人なので、できるだけブラックボックス化を避けるためにSQLファーストなライブラリが良いと思いsqlcかBunに絞りました。
Bunのクエリビルダーは生SQLに近く、Goコードからクエリを生成できたり他にも機能が揃っていて最高のORMなのですが、以下の理由でsqlcにしました。
- SQLの理解が浅いとミスではなく普通に理解不足でSQLを書き間違える気がしたのでスキーマやクエリをちゃんと書かないとエラーになるsqlcの方がブラックボックス化しにくくてトラブルシューティングしやすそう。
- sqlcの場合、生SQLと標準パッケージのみでコードが構成されるのでいざ別のライブラリに差し替えるとなったときに楽そう。(独自記法多めなライブラリからの差し替えだと同じふるまいを再現するのに認知不可が高そう)
- sqlcはあくまでコード生成ツールなので、サードパーティ製のOSSに直接依存せずに済むというのも気が楽。
デメリットとしては動的クエリができないことがよく挙げられている印象でした。このアプリでクエリの分岐が起こりそうな機能として復習物の一覧取得がありますが、この復習物が属するボックスは「未分類復習物ボックス」と「通常のボックス」の2種類に分けられ、多くても分岐は2、3個で済むことが想定されたので問題ないと思い最終的にsqlcにしました。
結果的に、生SQLを書いたりsqlcが生成したDBアクセスメソッドを呼び出す処理は別途実装する必要があったりと多少苦労しましたが、ブラックボックス化を防ぎながら型安全に開発が進められたので良かったです。
PostgreSQL
DB操作系のライブラリとの相性が主な判断規準でした。DBそのものに対しては以下の基準で考えていました。
- パフォーマンスは重要ではなく、主キーの順序保証も求めていない
- アプリの機能としてソートが必要な場合は別途専用カラムを追加したり、監査用にソートしたい場合は
created_at
/updated_at
を使うようにして主キーに役割を集中させすぎないようにしたい。
- アプリの機能としてソートが必要な場合は別途専用カラムを追加したり、監査用にソートしたい場合は
- テーブル毎に主キーの扱いが異なるのを避けるために主キーの型は統一したい
DBに関する知識が無さすぎる&単なる小規模なWebアプリということもあり、正直連番でもUUIDでもどちらでも変わらなそうな印象でした。
ただ、「MySQLのLOAD DATA
でバルクインサートするとエラーが発生した行は無視され、正常な行だけが登録されてデータが不整合になる」という問題があるようだったのでPostgreSQLにしました。
アプリの機能でバルクインサートが必要な部分があったのですが、sqlcのcopyfrom
(バルクインサート用のアノテーション)はMySQLの場合はLOAD DATAが使われて上記の問題が発生するようでした。
公式では「トランザクション内でこれを実行してSHOW WARNINGS
でエラーを拾ってロールバックすれば解決」と記載がありましたが、PostgreSQLならそういった処理を一切書かなくて済むならこっちにしようということでPostgreSQLにしました。
代わりにMySQLのON UPDATE CURRENT_TIMESTAMP
は使えなくなりましたが、自動更新対象のupdated_at
はアプリでは使わずDB側だけの関心事として考えていたので、自動更新用のトリガーの記述が増える分には問題ないと思いました。
DBドライバーはsqlc公式推奨のpgxにしました。
golang-migrate/migrate
学習コストもそこまで高くなさそうだったのであとあと助けになりそうなマイグレーションのバージョン管理をすることにしました。
候補はGoose・golang-migrate・Atlasの3つでした。
Atlasで最終的なスキーマ構造だけ定義→atlas migrate diff
で差分を考慮したDDLを生成→sqlc generate
と開発体験は良さそうでしたが、前述した通りDDLを書いてSQLに慣れた方が保守しやすいと思いAtlasは候補から外しました。
Gooseとgolang-migrateは正直違いがあまり感じられなかったので、日本語の情報が充実しているgolang-migrateにしました。
GitHub Actions
CIは無料枠で十分だったのでGithub Actionsにしました。自動デプロイはバックエンドはRender、フロントエンドはVercelの標準機能にしました。
ER図
- DBアクセス前にJWT認証は済ませてありますが、ユーザー識別用に
users.id
を全てのテーブルから外部キー参照させてクエリのWEHERE句で防御的に使うようにしました。 - 上記の理由やJOINクエリ回避の都合で一部カラムが冗長となっていて第2正規形と第3正規形に違反しているテーブルがありますが、全て親が変更されることのないカラムで挿入時以外に不整合のリスクがないためこのようにしました。ただし、現状の実装のままだと挿入時だけはアプリ側のロジックで整合性を担保しながら保守する必要があるため、今後セキュリティの勉強とJOINクエリの効果的な導入を進めていき最終的には第3正規形にしていく予定です。
- 復習物(review_items)が完了状態かどうかは、紐づいている復習日(review_dates)のisCompletedが全てtrueかどうかで判別できますが、「強制完了」という概念を表現するにはreview_datesの状況によらずreview_items自身で判別できる必要があったので、review_itemsテーブルに完了ステータスを示すisFinishedというカラムを設けました。
アーキテクチャ設計
アーキテクチャ
以下のようにドメイン層を中心に内側へ依存する4層クリーンアーキテクチャで実装しました。
コントローラー層
↓
ユースケース層
↓
ドメイン層
↑
インフラ層
「パターン&パターンステップ」や「復習物&復習日」はそれぞれ同じトランザクション内で更新したかったので同じ集約にしました。
アプリのどういう部分がコアなのか分かりやすいように、そういった処理(復習日生成など)はドメイン層に置きました。
クリーンアーキテクチャのデメリットの一つである実装に時間がかかる点は、既存のコードとほぼ似たコードになりそうな部分をGitHub Copilotに生成してもらうことで解消しました。
ディレクトリ構造
.
├── cmd
│ ├── api
│ │ └── main.go // アプリのエントリーポイント(DB接続、DI含む)
│ └── batch
│ └── main.go // バッチ処理のエントリーポイント(DB接続、DI、定期実行スケジューラの設定含む)
├── router
│ └── router.go // APIのエンドポイント定義、ミドルウェア設定
├── controller // HTTPリクエスト処理、ユースケース呼び出し
│ ├── box
│ ├── category
│ ├── item
│ ├── pattern
│ └── user
├── usecase // ビジネスルールに従ったオブジェクトの生成・操作・永続化メソッドの呼び出し
│ ├── batch
│ ├── box
│ ├── category
│ ├── item
│ ├── pattern
│ ├── transaction
│ └── user
├── domain // 各ドメインのエンティティ、ファクトリ関数、バリデーション、ビジネスロジックを定義
│ ├── box
│ ├── category
│ ├── item
│ ├── pattern
│ └── user
├── infrastructure
│ ├── auth
│ │ └── jwt_generator.go // JWT生成
│ ├── mailer
│ │ └── smtp_mailer.go // アカウント作成時の認証用メール送信
│ ├── db
│ │ ├── connect_db.go // DB接続
│ │ ├── dbgen
│ │ │ ├── *.sql.go // クエリメソッドの実装(sqlc生成)
│ │ │ ├── copyfrom.go // バルクインサートの実装(sqlc生成)
│ │ │ ├── db.go // pgxのDB操作メソッドを定義したインターフェース(sqlc生成)
│ │ │ ├── models.go // DBのテーブル構造に対応するGoの構造体(sqlc生成)
│ │ │ ├── querier.go // 全てのクエリメソッドを定義したインターフェース(sqlc生成)
│ │ │ └── queries.go // Queries型にCopyFromメソッドをラッピング
│ │ └── query
│ │ └── *.sql // クエリ定義
│ └── repository
│ └── *_repository.go // domain層で定義されたリポジトリインターフェースの実装
├── migrations
│ ├── *.down.sql // マイグレーション用ファイル
│ └── *.up.sql // マイグレーション用ファイル
├── sqlc.yaml // sqlcの設定
├── go.mod
├── go.sum
└── docker-compose.yml // ローカル開発環境の構築用
開発の流れ
開発は以下の様な流れで進めました。
1. アイデアの目的を整理
2. ユースケースを考える
3. 画面遷移図とUIデザインを考える
4. 機能を洗い出してAPI設計
5. DB設計
6. アーキテクチャ設計
7. 実装
8. 設計の見落としがあり次第、1〜7の流れで追加実装
1. アイデアの目的を整理
アイデア自体に価値がないと元も子もないので慎重に行いました。具体的には、どのような課題なのかやなぜ解決したいのかなどを徹底的に整理しました。
3. ユースケースを考える
復習スケジュール管理を楽にするのが根本の目的なので、どういう状況なら復習スケジュールにストレスを感じなくて済むかを考えながら、最低限必要なユースケースと必須ではないけどあったら良さそうなユースケースの2種類を整理しました。
- 例:復習物ボックス画面を表示中:復習物を作成できる。復習物作成時は開いている復習物ボックスの状態が選択肢に自動設定される。作成された復習物が持つ学習日をもとに、その復習物ボックスに適用されている復習パターンで復習日を算出し表示し、ユーザーは復習日を確認できる。
3. 画面遷移図とUIデザインを考える
これをせずに機能やらAPIやらを考えるとそっちの都合に引っ張られて本来実現したい快適さに影響しそうだったので、先に画面遷移図とUIデザインしてそれをもとに設計する方針にしました。
以下がその時の画面遷移図とUIデザインです。結果的にスムーズにその後の設計ができたのでやって良かったです。
4. 機能を洗い出してAPI設計
3で作った内容をもとに、APIが必要な部分とそうでない部分に分けて機能を洗い出しました。そしてAPIが必要な機能それぞれには、HTTPメソッドとURI、概要説明、リクエスト内容、レスポンス内容を整理しました。
URIがDBのスキーマ構造に影響されないようにとAPI設計を先に行ったのですが、結果的に似たような感じになったので中々難しいなと思いました。
5. DB設計
エンティティ同士の関係や実際に実行したいクエリを参考に設計しました。
根本は同じ概念だけど場合によって複数の概念に分岐するエンティティをテーブル定義として落とし込むのが難しかったです。(完了/未完了ボックス、通常/未分類ボックスなど)
6. アーキテクチャ設計
本当に何もわからなかったのでとりあえず評判が良い設計関連の書籍を読んだりしました。先人のおかげでベストプラクティスには簡単にアクセスできますが、なぜベストなのかに共感して応用できるような知識がなかったので、一応納得感は求めつつもある程度割り切ってアーキテクチャは決めました。
7. 実装
バックエンド
ドメイン層の実装をまとめて行い、その後はpackage毎にインフラ層、ユースケース層、コントローラー層の順で実装していきました。
インフラ層では、クエリをローカルDBで試して意図した結果が返ってきたらsqlc generateし、セットでリポジトリインターフェースの具体的な実装をしていきました。
テストは基本的にTDTで行っています。リポジトリのテストではdockertestでテスト用DBを用意し、テストデータはtestfixturesで挿入して結合テストをしています。
リポジトリ以外は基本テストは並列実行し、ユースケース層は現在は正常系のみテストしました(リポジトリとドメイン層の一部はuber-go/gomockでモック化し実行順序、呼び出し回数をテスト)。
フロントエンド
フロントエンドは流石に時間かかると思いClaude Codeを課金しました。ゼロイチの生成が微妙という意見を見かけたので、最初は要件モリモリにしたプロンプトで一度Geminiで全体コードを生成し、それをClaude Codeでいじる感じで開発していきました。
工夫したこと
スケジュールをこなすことを目的化させない仕様
復習をサボった時、1日ズレたけど既存のスケジュールのまま復習しちゃおうというのはよくあることですが、それだと復習スケジュールをこなすことが目的になっていて復習スケジュールを管理する意味が薄れてしまいます。また、ズレたことによってわずかながらモチベ低下に影響することもあります。
そのため、モチベも失わず適切なタイミングで復習できるように、サボった場合は日付けを跨いだときに後続の復習日が全て自動でプラス1日される機能を実装しました(将来的に今日の復習をメール通知する機能を実装予定のため、アクセス時更新ではなく日付けを跨いだ時に更新するようにしています)。各タイムゾーンでは最小15分単位で日付けが変わっているため、この機能は15分毎に定期実行しています。
復習したものの完了し忘れて復習日がプラス1日されてしまった場合でも、日にちを戻して自動でリスケジュールしてくれる機能も追加しました。
また、復習する必要のなくなったスケジュールは強制完了して記録として残せるようにしました。
復習に集中できる仕様を意識した
復習をサボった時のリスケジュール作業を自動化、カテゴリーやボックス間をシームレスに移動できるように移動用のタブバーを実装、サクッとスケジューリングできる体験の提供などで、可能な限り復習に集中できるような仕様を意識しました。
このアプリを使うことによる負担をなるべく減らし、他のサービスとの共存を意識した
冒頭でも述べましたが、このアプリは"今日テストするのは何かを辞書的に確認する"という使い方を想定しています。そのため、このアプリでは学習内容を一言にまとめて復習物名として設定するような仕様になっています。この仕様は復習物作成時の労力を減らす目的もありますが、他サービスとの共存も意識しています。
ユーザーはそれぞれNotionやEvernoteなど、学んだことを記録するサービスをすでに使っていることが多いです。今の時代、ただでさえ日常的に使うツールが多い中で新たに一個いつメンを増やすというのは中々大変ですし、せっかく別のサービスで復習しやすくまとめたのにもかかわらず、それを二重に復習アプリに記録するのは勿体ないです。そのため、労力を減らすだけでなく特定の用途に特化し他のサービスを邪魔しないようなアプリである必要がありました。そういったことを考えた結果、このアプリには答えまで記入はさせず、何を勉強するのかという辞書的な使い方に抑えるのが良いと思いこの仕様にしました。
また、復習物に「詳細」という機能を追加し、そこに答えが乗っている他サービスのノート(もしくは記事)のリンクを貼れるようにして答えを確認しやすくしました(勿論答えを貼るという使い方も可能です)。ただ、この「詳細」は復習物作成時にセットで記入するという労力があるため、任意の機能として実装しました。将来的にはここの労力も解消したいです。
アカウント登録時の初回認証用に使うテーブルを切り出してデータ量削減
- email_verificationsは認証時に一時的に使う認証コードと認証期限を管理するために作りました。認証が完了した場合、データは不要になり毎回削除するためusersテーブルから別テーブルとして切り出し余計なデータ量を減らすようにしました。
N+1問題の回避
親テーブルと子テーブルから共通のカラム(user_id)で一括取得し、アプリ側のロジックで整理するようにして回避しました。
余計なクエリの実行を避ける
ユースケース層のメソッドで、本物のデータを確認して分岐するのではなく、クライアントから来たデータでフラグを多用して分岐することで一度のリクエストで行われるクエリの実行回数を減らしました。
反省点
どういう復習スケージュール管理の体験が求められているのかちゃんと調査するべきだった
おおまかにUX→UI→機能の順で考えていったのですが、なるべく万人が使うことを意識していたとしてもスタート地点のUX、UIが全て主観で出来上がっているので誰にも刺さらない可能性が高いです。
個人的にスプレッドシートに代わる復習管理アプリを早く使いたかったということもあり、似たアプリの調査をしただけで設計に進んでしまいましたが、本来ならしっかりアンケート等を実施したりフィードバックを貰った方がより良いアプリになっていたと思います。
見積もりが甘かった
当初、開発期間は1ヶ月半くらいで作れるだろうと考えていたのですが、結果的に3ヶ月くらいかかったので流石に見積もりが甘すぎました。単なる個人開発なので何か問題になるというわけではないのですが、誰かと何か開発するときに正確に見積もれないのは怖いので改善しなければいけないなと感じました。
次回からはタスクを細分化し全て書き出した上で見積もったりたくさん個人開発して見積もりの場数を増やそうと思いました。
必要なタスクの取捨選択ができていなかった
このアプリを作る上で不必要もしくは優先順位の低すぎるタスクに取り掛かってしまい、余計なタスクを増やしてしまいました。そもそも見積もりに含めるタスクが必要なものでなければ元も子もないです。
次回からはまずアプリが提供したい価値が何かを考えてから、逆算して必要なドメイン知識や技術を整理しようと思いました。
サーバーのリソースをちゃんと考慮していない
今回はクエリを減らそうとか遅くならないようにしようとばかり考えていて、CPUやメモリのリソースの消費量を考慮していませんでした。
結果的に効果がほぼ無かったりパフォーマンスが悪くなったら意味ないので、次回からは最初は第3正規形まで正規化したDBモデルを基本にして、ちゃんと計測してパフォーマンスが問題になってから最適化する順番で開発しようと思いました。
実装前にテストケースを整理するべきだった
今回は大体の実装が終わったあとにテストコードを書いたのですが、正常系と異常系のテストケースは実装前に書き出しておけばより開発がスムーズだと思いました。
開発途中、特に分岐の多いメソッドに関してブラウザで実行して初めて気づくサーバーエラーがいくつかありました。気づく頃には最初に考えていた全シナリオを純度100%で思い出すのが難しく、思い出したり全シナリオを再度整理する作業が必要でした。設計当初考えていたシナリオはドキュメントとしてまとめてはありましたが、開発が進む中で考慮不足だったシナリオが出てきた時に都度ドキュメントに追加していく作業を怠っていたのがだめでした。
次回からはドキュメントをしっかり更新するか、最初からテストコードを書いてそこにテストケースを追加していくようにしてテストファイルをドキュメント化するのが開発しやすそうだと思いました。
今後の展望
- 今日の復習一覧のメール通知機能の追加
- 生成AIを使った、ユーザーが貼り付けた文章から復習物を分割抽出して、復習物として一括作成できる機能
- 学習内容を特定のノートアプリ等(ここではNotionを例に扱う)に記録しているユーザーの場合、Notion APIの更新履歴情報からどういった内容を記録したかを取得→復習物として自動作成し、ユーザーアクセス時に「自動作成された復習物」を一覧表示し、取捨選択できるようにする機能の追加
- 昨日以前の完了済み復習物を未完了にして今日に戻せる機能の追加
- あるボックスに対して、完了済み復習物/復習日が一つもない場合にボックスに設定されているパターンを変更できる機能の追加
- 復習物一覧やパターン一覧など、並び替え機能のある画面の並び替えのデフォルト状態設定をユーザーが設定できる機能の実装
- モチベアップが促されるような過去の復習記録を見られるページの追加
- ドラッグ&ドロップで復習物をボックス間で移動させられる機能の追加
- 復習物の「詳細」で画像やハイパーリンクに対応する(現在はテキストのみ)
- 復習物の一括完了/削除/移動機能の追加
- パスワードを忘れた時用のパスワード変更機能の追加
- レスポンシブ対応
感想
初めてのちゃんとした個人開発で色々と大変でしたが、個人的に欲しかったアプリが作れたので大満足です。
この開発ではAIにたくさん質問したのですが、上のレイヤーから下げて聞いていくとどうしても見えない情報に気づかずに的外れなトレードオフに納得してしまうみたいなことがあるので、やはり低レイヤーから理解してAIの出力を自分で評価できる能力は大事だなと痛感しました。
今後は開発の場数を増やしながらよりCSの勉強に力を入れようと思いました!
また今回は復習のアプリなので作りやすかったですが、結局自分の知ってる範囲内でしかアイデアが湧かない感が否めないので技術だけでなくドメイン知識もたくさん集めていきたいなと思いました!