🦄

インクロフトを支える技術

2023/11/16に公開

インクロフトというマンガ投稿サービスを作りました。読者と作者にとって使いやすく、投稿しやすいサービスにしていきます。

https://zenn.dev/itome/articles/53f64e88956ca8

先の投稿でインクロフトを作った理由や実現したかったことを書いたのですが、この投稿ではそれらを実現するために選んだ技術構成と工夫した点について紹介していきます。

Cloudflare

インクロフトの各種機能は全てCloudflare上で動いています。様々なサービスを使っていますが、総じて他のクラウドに比べてコストが安く、また安い理由も腑に落ちるのがよかったです。

Cloudflare Workers

サーバーレスのアプリケーション基盤で、機能としてはGCPのCloud FunctionsやAWSのLambdaなどに近いですが、コールドスタートが爆速(0ms)なのとコストが他に比べて安いのが特徴です。
また、エッジで実行されるため地理的に近いサーバーのリソースを使うことができるのも魅了でした。

https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/

詳細な説明は本家のブログに譲りますが、Nodejsの代わりにV8エンジンのIsolate(Chromeでタブごとに別々の環境を立ち上げる技術)を使うことで、Javascriptコードの呼び出しにかかるオーバーヘッドを短く(5msくらい)するという仕組みです。

さらにTCPの3-way-handshakeの1回目の通信時にあらかじめWorkerを立ち上げておくことによって、5msあったオーバーヘッドを実質0msにしているようです。めちゃくちゃすごいですね。

注意点として、Node.jsではないのでNode.jsに用意されている関数を使うことはできません。代わりにWeb standards APIを使ってJavascriptを書くことになります。最初は戸惑いますが、Service Workersで書けることは大体書けるので、そのイメージで書いていくとすぐに慣れることができました。

ほぼ同等の機能をWebサイトのホスティングに特化して提供しているCloudflare Pagesというサービスもあります。
実は最初はCloudflare Pagesで開発を進めていたのですが、後述するCloudflare ImagesをWorkerから利用するためのfetchAPI拡張がなぜかCloudflare Pagesでは動かなかったので、Cloudlare Workersを使うことにしました。

D1 Database

https://www.cloudflare.com/ja-jp/developer-platform/d1/

データベースにはD1を利用しました。
使い心地としてはWorkersから簡単に使えるSQLiteという感じなのですが、エッジで実行されているWorkersの近くにリードレプリカを配置してくれて、エッジ間で自動同期してくれるかなり特徴的なデータベースです。

メリットはなんといってもコストで、1000000行の読み取りに対して$0.001しかかかりません。

個人開発のコストはDB次第でも言われているようにデータベース(特にSQL)はコストが大きくなので、これは個人で運営しているサービスにとってとてもありがたいです。浮いた分のお金はユーザー還元に回すことができます。

注意点としては、D1自体がまだパブリックベータなので決済などの重要な情報の扱いには慎重になる必要があります。
またwriteは分散されないっぽいので、書き込みがヘビーな用途で使う場合はパフォーマンステストをしっかり行う必要がありそうです。

Cloudflare Workers KV

https://www.cloudflare.com/ja-jp/developer-platform/workers-kv/

D1がSQLデータベースならこちらはRedisのようなキーバリューストアです。D1よりさらに高速にアクセスできるので、キャッシュの保存や一時的なデータ置き場として使えます。

インクロフトではユーザーの認証情報の保存と、Remixが生成する静的ファイルの置き場として使っています。

Cloudflare R2

インクロフトのマンガ画像、サムネイル、プロフィールアイコンなどは全てR2に保存しています。R2はAWS S3互換のオブジェクトストレージで、なんとデータのegressが無料です。

画像などのデータサイズの大きいファイルの配信は帯域にかかる費用も馬鹿にできないので、これは大きなメリットです。

Cloudflare Image Resizing

https://developers.cloudflare.com/images/image-resizing/

ユーザーがアップロードする画像には様々なサイズやフォーマットを許容しているので、Image Resizingを使って最適化しています。

マンガの画像にはURLの有効期限をつけたり、読めないようにBlurをかけたり、購入したユーザーIDを埋め込んだりする必要があるため、直接URL指定で利用するのではなく、HMACで暗号化したURLをCloudflare Workersで検証してWorkers経由でImage Resizingを利用しています。

Workersのfetch関数はcf拡張のパラメータを渡すことができるようになっており、ここに適用したいオプションを書くことで、Image Resizingをおこなってくれます。

fetch(imageURL, {
  cf: {
    image: {
      fit: "scale-down",
      width: 800,
      height: 600
    }
  }
})

R2のパブリックアクセスを無効にしているとそのままではfetchでアクセスできないので、S3の仕様に従って署名付きURLを都度作成して、そのURL経由でアクセスすることで回避しました。こういった処理ができるのもS3互換の便利なところだと思います。

Remix

https://remix.run/

フロントエンドとWebサーバーはRemixで作成しました。Remixはreact-routerの作者の方が作ったReactのフレームワークで、Next.jsのようにファイルベースのルーティングができるのが特徴です。
また、当初からCloudflare Workers上で動くことを売りの一つにしており、今回はCloudflare Workersとの相性の良さで選択しました。

"Cloudflare上で動くNext.js"くらいの認識で使い始めましたが、使っていくうちに全く別の思想をベースに作られたものだと気づきました。

特にForm関連の公式ドキュメントを読んだ時に両者の違いが明確です。

https://nextjs.org/docs/pages/building-your-application/data-fetching/forms-and-mutations

https://remix.run/docs/en/main/components/form

Next.jsはFormonSubmitをJavascriptで処理してる一方で、RemixのFormactionmethodなどの引数を使ってJavascriptなしでフォームの送信を実装しています。

どちらも一長一短ではありますが、Remixの方がWeb標準に近く、Javascriptが使えない環境でも問題なく動きます。

個人的には、管理画面などの複雑なフォームを必要とする場合はNext.js的なやり方の方が自由度が高く、ログイン画面のようなシンプルなフォームはRemix的なやり方の方がシンプルで向いていると感じました。

shadcn-ui

UIライブラリにはshadcn-uiを使っています。既存のライブラリとは違ってnpm経由でインストールせず、ドキュメントに書いてあるコードを自分のプロジェクトにコピペして使ってねという方針のライブラリです。

不要なコンポーネントをインストールする必要がないですし、挙動を少し変えたいときはコピペしてきたコードを変更すればいいだけなのでとても便利です。

用意されているコンポーネントも多過ぎず少な過ぎず、radix-uiを使っていてアクセシビリティにも気を配られているので、お気に入りのUIライブラリになりました。

その他使っているライブラリ

利用ライブラリの選定基準はとにかく軽いことです。Cloudflare Workers上で動かす必要があるのでバンドルサイズができるだけ小さいことはもちろん、読者がSNSから開いてさっと読む体験を邪魔しないためにパフォーマンスにも気をつけています。

  • valibot
  • date-fns
  • dnd-kit
  • embla-carousel

Stripe

決済にはお馴染みのStripeを使っています。1人で開発・運用をしているので、できるだけ複雑にならないようにしています。

Stripe Checkout

https://stripe.com/jp/payments/checkout

これまでStripeを使うときはStripe Elementsを使ってクレジットカードの入力フォームを作って、保存したカードを使ってWebサイト上で決済を行うことが多かったのですが、今回はStripe Checkoutを使うことにしました。これまでなんとなくユーザーの手間が多そうな印象だったので使っていなかったのですが、

  • Webサイト側にはリンクとリダイレクトを設定するだけ
  • 決済のページには料金と商品情報がわかりやすく表示される
  • Google Pay/Apple Payなどにも対応
  • クレジットカードは保存しておいて次回利用も可能
  • Stripe Linkを有効にすれば他のストアで保存した決済情報も利用できる

などユーザーの購入体験を向上させながら運用コストを劇的に減らすことができました。

Stripe Connect

https://stripe.com/jp/connect

有料販売の売り上げを作者に振り込むフローにはStripe Connectを使っています。
振り込みごとに250円+振込額の0.25%と、当月中に振り込みのあったアカウントごとに200円の利用料がかかりますが、振込先の登録と本人確認などを全て任せることができるので、採用してよかったです。
運用コストが下がる分でStripeに払う手数料が相殺できそうなので、Stripe Connectにかかる手数料に関しては一律でインクロフト側が負担するようにしました。

購入した書籍のダウンロード機能

インクロフトの特徴の一つに購入した書籍をhtmlの単体ファイルとしてリーダーごとダウンロードできる機能というものがあります。サンプルファイルを用意したので下のGistをダウンロードしてお手元の端末で開いてみてください。

https://gist.github.com/itome/bfc2f1eeba480f204443627b0f7cb4dd

画像ファイルをbase64エンコードしたものを直接htmlに埋め込んでダウンロードしています。
Reactで書かれたリーダーを1つのhtmlにまとめるために、viteで別プロジェクトを作って、
vite-plugin-singlefileを使ってビルドしています。

まとめ

多くのマンガサービスがある中で使ってもらうために、とにかく軽量さと軽快さを意識して開発をしました。
その上でサービスを長く続けるために、運用と金銭的なコストができるだけ少なくなるような技術選定をしています。その結果当初よりも余裕ができた分に関しては、手数料の変更などでユーザーに還元していく予定です。

まだ始まったばかりのサービスですが、使っていて気になるところがあれば@itometeamまでご連絡ください。

Discussion