【個人開発】蔵書管理を楽にするためのスマホアプリを作りました

# 「あの漫画、何巻まで持ってたっけ？」をなくしたい
電子書籍が増えてきて、漫画もスマホで読むことも増えましたが、やはり紙で持っておきたいものもいくつかあります。
紙で持っている漫画でいつも困るのが、「あの漫画、何巻まで持ってたっけ？」を忘れることです。

家の中にいるときであれば、本棚まで行けば確認できます。
しかし、ふと立ち寄った本屋で、「そういえばあれの新刊出てるかな？」と見たときに、家に何巻まであるのかは確認できません。
実際、それで過去に同じ巻を２冊買ってしまったこともあります。

スマホに記録しておけばいいのですが、メモ帳とかだと埋もれてしまいがちです。
簡単に記録できて、さっと確認できるアプリが欲しいなぁと思っていました。
検索するとそういった用途のアプリがいくつかありましたが、ちょっと自分のイメージと違うので、作ることにしました。

# Libra Mate
作ったのはLibra Mate（リブラ・メイト）というアプリになります。

[iPhone版](https://apps.apple.com/us/app/libra-mate-%E6%9C%AC-%E3%82%B3%E3%83%9F%E3%83%83%E3%82%AF%E7%AE%A1%E7%90%86/id6448055156)

[Android版](https://play.google.com/store/apps/details?id=com.apps.hal.libramate)

基本機能は以下の通り

1. 本の登録（手動・バーコード・WEB検索の３パターン）
1. 続き物をシリーズとして管理（コミックのような単純な連番のものは自動でシリーズとして登録。手動でまとめることも可能）
1. 新刊が出ている場合の通知（半Pull型。Push型は今後実装予定）
1. Spreadsheetへの自動連携
1. 複数端末間での同期

:::note info
2024/3/28
スマートフォンだけでなく、タブレットでも利用できるようになりました！
:::

## １．本の登録
本の登録は手動・バーコード・WEB検索の３パターンから可能です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/53b43f18-e6dd-f421-c3a9-e2e73fa8785b.png" width="240px" />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/60814ba2-d5a3-e3aa-1160-3de73b67c9d6.png" width="240px" />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/3dbf1ecc-3ad8-b9ac-9789-8d4abff70249.png" width="240px" />

手動は面倒なのでおすすめしません。
バーコードでもWEB検索で見つからなかった場合に使ってください。

バーコードは本の裏にあるバーコードのうち、上のバーコード（ISBN）で検索します。
操作感はこんな感じです。

https://twitter.com/HAL1986____/status/1651599512593661959?s=20

WEB検索は現状楽天Books APIからの取得に頼っています。
そのため、楽天ブックスで取り扱いのない本は出てきません。
Amazonも対応したいんですが、AmazonのAPIの利用のためにアフィリエイトの実績が必要で、まだそこがクリアできていません。
クリアできたら組み込んでいく予定です。

## ２． 続き物をシリーズとして管理
このアプリのメインと言える機能です。
漫画などは何巻か続きものとして刊行されることが多く、それらを１冊ずつ登録してしまうとその漫画の巻違いで埋め尽くされてしまうので、管理がちょっと煩雑になります。
そのため、シリーズとしてまとめて管理できるようにしています。

基本的には以下のような要領でシリーズとして判定します。
要は正規表現です。

* タイトル部分 ＋ 空白 ＋ 数字
* タイトル部分 ＋ "（" ＋ 数字 ＋ "）"
* タイトル部分 ＋ 空白 ＋ 数字 ＋ 空白 ＋ "〜" ＋ サブタイトル "〜"

あとは上中下巻とかはなるべく対応できるように正規表現で抽出してます。
ただ、タイトルのあとに空白が入ってない場合とかだと、一部の巻だけ表記が違うためにシリーズとしてまとめられない場合があります。
その場合は手動でシリーズとして追加してあげる必要があります。

「この本がシリーズとして判定されなかった」とか情報があれば教えてもらえると助かります。
もっとシリーズ物の表記を正規化してほしいんですけどね・・・。

## ３．新刊が出ている場合の通知
半Pull型で新刊の通知も表示します。
「半Pull」と言っているのは、「アプリの対象画面を開くことで新刊の通知を表示する」という仕組みのためです。
Push型の場合はアプリを開かなくても通知が届きますが、その辺は未実装です。
今後追加していきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/68494cef-f90e-ba65-7e2e-9826d8bdac4a.png" width="300px" />

こんな感じで、シリーズの一覧画面を開いた際に「まだ持ってない巻」が見つかったら通知を表示してくれます。
これの便利なところは、ここで表示されている本を持っている場合は、「持ってる」をタップするだけでリストに追加できる点です。
バーコードを読む必要すらありません。

## ４． Spreadsheetへの自動連携
最後に、Spreadsheetへの自動連携です。
こちらは、設定画面で連携の設定をしておくと、アプリに登録した本をSpreadsheetに転記してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571256/ef0f2c53-d5ca-b72d-343f-9c296dcb64f1.png)

転記する項目はカラム位置含めて固定になってしまうので、不要な項目は非表示にするなりしてください。
また、今の項目数から増える可能性があるので、後ろのカラムを使ってしまうとあとで上書きされる可能性があります。
独自項目を足したい場合は、別シートを使うなどしていただくのが安全です。

ちなみに、Spreadsheetに書き込んでもアプリ側には反映されません。
あくまでもアプリ→Spreadsheetの一方通行です。

## ５． 複数端末間での同期
これは正直そんなに必須ではなかったんですが、Spreadsheetへの書き込み機能実現のためにFirestoreが必要だったため、併せて複数端末間での同期機能をつけました。
iPone / Androidの両方で、データの同期・即時反映が可能です。

データの同期のためにはAppleまたはGoogleでのサインインが必要です。
Android端末はAppleでのサインインが使えないので、iPhoneとAndroidで同期したいときはGoogleサインインを使ってください。

なお、Apple / Googleサインインを使用しなくてもアプリは利用可能（匿名認証を利用）ですが、同期機能は使えなくなります。
その場合も、Spreadsheetへの書き込みは使えます。

# 技術的な話
技術的には以下のものを使っています。

* React Native + Expo
* Firebase Firestore
* Firebase Functions

自分が作るアプリはだいたいこのスタックです。
また、現在スマホアプリのみのリリースとなっていますが、WEB版の作成も検討しています。
その際もReact Native + Expoで作れたらいいなと思っています。

## 端末間同期機能実現とサーバー代節約のための対応

今回、バックエンドにFirebaseを使っているため、あまり無計画にリソースを使うとお金がかさんでしまいます。
functionsの方はまぁこのアプリの特性上そんな頻繁に更新されるものでもないのであまり問題はありません。
firestoreの方は以下のような課金になります。

> * 読み取り、書き込み、削除を行うドキュメントの数。
> * 集約クエリにより照合されたインデックス エントリの数。クエリにより照合されたインデックス エントリのバッチ（1,000 個まで）ごとに 1 回のドキュメントの読み取りとして課金されます。
> * データベースにより使用されるストレージの容量（メタデータとインデックスのオーバーヘッドを含む）。
> * ネットワーク帯域幅の使用量。

https://firebase.google.com/docs/firestore/pricing?hl=ja#pricing_overview

今回、１冊１ドキュメントとしてデータを保持するようにしたため、一番影響するのは読み取りのドキュメントの数になりそうです。
読み取りは１日５万回まで無料枠がありますが、アプリの起動するたびにデータを取りに行ったのでは、簡単に超えてしまいます。
１００冊くらいの登録だとしても、５００人もいれば超えますし、一人が何回か起動したらもっと少ない人数でも超えますね。

これではfirestore代だけで大変なことになってしまうので、なるべく読み取りの回数を減らすことにしました。

## ローカルDBとfirestoreとの同期
仕組みは簡単です。

1. firestoreのデータを取得したら一度ローカルのDB（SQLite）に登録する
1. アプリの中での参照はローカルDBから常に行う
1. firestoreのSnapshotでデータの変更を`where updatedAt > lastGetAt`でListenする（最後に取得した日時以降の変更を取得する）
1. 変更があったときだけ新規データを取得してローカルDBを更新する
1. データの更新時はfirestore側の更新処理を呼び、`updatedAt`カラムを現在日時で更新する（更新されると３、４の仕組みでローカルのDBが更新される）

更新のフローはReduxのような１方向になるようにしています。

この仕組みで行けば、データの読み取りは都度変更があったものだけになるため、件数を大幅に減らせます。
また、基本的にはローカルにデータを保持するので、オフライン状態でも参照は可能になります。
firestore側の更新がうまく行かなかった場合（ネットワークの問題とか）も、firestoreのSDK機能でオフライン更新を処理してくれるので、ある程度は問題ないはずです。

### ローカルDB・firestore同期のメリット
この手法のメリットは以下のとおりです。

* サーバー費用を抑えられる
* firestoreのリアルタイムアップデート（onSnapshot）の恩恵を受けられる
* firestoreだとやりづらい集計処理などがやりやすくなる（SQLでできる）

特に集計処理はfirestoreのみでやろうとすると読み取りが増えがちですが、ローカルのDBであれば気兼ねなく集計できますし、集計処理が得意なSQLの機能をフル活用することも可能です。

### ローカルDB・firestore同期のデメリット
逆にこの手法のデメリットはこんなところでしょうか。

* データストアが２つある状態になるので更新タイミングなどの複雑性が増す
* RDBの制約に縛られるため、firestoreの自由なスキーマ設計が使いにくくなる

firestoreとローカルDBの両方を管理しなければならないので、実装も手間ですし、微妙に発生するタイムラグなどをどうハンドリングするかなど、複雑性は増します。
また、SQLiteなどを使う場合、RDBの制約に縛られるため、firestoreで使えるようなMapや配列がそのままでは使えなかったりします。
このあたりを犠牲にしても、メリットが大きいと考えられるときは採用を検討しても良さそうです。

# 最後に
アプリの感想や要望などは、QiitaでもTwitterでも構いませんのでお送りいただけると嬉しいです。
特に追加機能の要望については、まだ少し追加する予定はあるものの、みなさんがどんな使い方をしたいのかによって増やしていきたいと思っているので、教えていただけると助かります。

Twitterは[こちら](https://twitter.com/HAL1986____)

あと、最近はFlutterアプリが多いように思えますが、React Nativeもとっても良い感じなので、興味持った方はやってみてください。
Expo使った開発は体験が最高ですよ。

https://reactnative.dev/

https://expo.dev/