🔖

Remixで個人開発したサービスの技術的変遷

2024/10/15に公開

まえがき

本記事では二年以上前にリリースをした「しおりモ!」というサービスについての技術的な変遷を書きます。
リリース当時の技術スタックについては以前記事を書いているので、そちらを参照してください。
https://zenn.dev/wancup/articles/20220808_introduce_shiorimo
以前の記事同様、各技術の詳細な説明や導入方法は省きます。実際に小さいサービスで使ってみた感想を記すことに重きを置きます。
なお前の記事から二年も経っているので、細かいところは色々忘れています、、。許して、、、。

今回言及する技術

名称 前回記事からの差分 概要 リンク
Remix v2にアップデート Reactフレームワーク https://github.com/remix-run/remix
Fly.io v2にアップデート PaaS https://fly.io/
Cloudflare 新規 ドメインレジストラ/DNS https://www.cloudflare.com/
Firestore 継続 BaaS https://firebase.google.com/docs/firestore
Firebase Authentication 継続 IDaaS https://firebase.google.com/docs/auth
Mantine v7にアップデート コンポーネントライブラリ https://github.com/mantinedev/mantine
Panda CSS 新規 スタイリングライブラリ https://github.com/chakra-ui/panda
Playwright CodeceptJSから移行 E2Eテストフレームワーク https://github.com/microsoft/playwright
Meilisearch 継続 全文検索エンジン https://github.com/meilisearch/meilisearch
Upstash 廃止 サーバレスデータプラットフォーム https://upstash.com/
Sentry 新規 モニタリングサービス https://sentry.io/
Renovate 新規(実際は継続) 依存関係自動更新ツール https://github.com/renovatebot/renovate

しおりモ!

まずは軽く今回の記事で対象とするサービスの紹介と、サービス自体が前回の記事からどう変わったかについて話します。

https://shiorimo.app

「しおりモ!」は本についての情報を扱うことに重点を置いたメモ帳サービスです。
良い感じにメモを記録できて、良い感じにメモを閲覧できることを目指しています。(つまり、自分が読書中とか本について振り返るときに使いやすいものを開発しているということです)

「しおりモ!」でメモを作成、検索する画面を収録したGIFアニメーション

ちなみに、自分は普段本を読みながら、気に入った文章をiPhoneのカメラで文字起こししたものを「しおりモ!」で記録しています。

前回の記事からの変更点

メモを追加・編集・削除・検索できるという、基本的な機能は前の記事から変わっていません。変更点と簡単なコメントを以下にあげます。

  • カスタムドメインへ移行
    • インフラがFly.ioに固定されるのを避けたかった
  • メモの追加を別ページに分割
    • 以前はメモ一覧ページ上のドロワーで追加できるようにしていた
    • ドロワー上でメモを追加することによるバグや使いづらさがあった
  • 全体的なデザイン・ロゴの調整
  • パン屑リストの追加
  • メモ作成・編集時のオートコンプリート機能の追加
  • 索引ページ(メモに記載している著者とかの一覧を見られる画面)の追加
  • 著者等でのメモの絞り込み機能の追加
  • 全メモのダウンロード機能の公開
    • 機能自体は結構前から実装していたが、正式に公開したのが最近
  • バージョン・最終リリース日の表記を追加
    • 個人開発系のサービスって管理が続いているのか不安になることがあるので

Remix

v2への移行

Remixはv1 -> v2で結構色々な変更がありました。
基本的にはドキュメント( https://remix.run/docs/en/main/start/v2 )の通りに移行しただけなのですが、Fleature Flagでのバージョン移行体験は結構良かったです。
依存はv1のままひとつずつFeatureFlagでv2の機能を有効化して、修正すべきところを変えて、動作をチェックする。これを繰り返すだけで概ねv2への移行準備ができました。
ただし、それでも自分がバージョン更新するときに一点だけ困ったところがありました。v2ではserverModuleFormat設定値のデフォルト値がesmになったことです。v2への更新作業をしていたときはまだ過渡期という感じだっただめ、一部ライブラリが動かなかったりしました。しかし、最近はライブラリ側でのesm対応が進んでいるので今は大体問題は無いようにも思います。

Vite版への移行

v2へのアップデートの目玉機能のひとつとして、Viteとの統合がありました。
v1(Classic Remix compiler)時代はビルドプロセスに手を入れることが大変だった( https://github.com/aiji42/remix-esbuild-override )ので、Viteを使うようになったことで他のツール類との互換性も上がっているのではないかと思うと嬉しいですね。ただ、「しおりモ!」ではそんなに複雑なことをやっていないので実際のところは未知数です。
Vite版とClassic版との大きな違いは.serverモジュール( https://remix.run/docs/en/main/file-conventions/-server )の有無だと思っています。
Classic版ではserverで使うモジュールをhoge.server.tsのようにファイル名で管理していましたが、Vite版ではそれに加えて.serverというディレクトリ以下はすべてserver-onlyなモジュールとして扱えるようになりました。
実はこれに合わせて大きくディレクトリ構成を変えたのが一番大変でした。ただ、そのおげでコードの見通しはかなり良くなりました。

ディレクトリ構成

ディレクトリ構成についてはクラインアトとサーバ側の処理が分かれていること、テストしやすいようにDIできること、くらいだけを考えています。
細かいところを除いた大まかな構成は以下になります。なお、命名とかあまり慣習に沿えていないと思うので参考程度に留めてください。

.
├─ app/
│  ├─ .server/ # サーバ側の処理
│  │  ├─ infrastructures/ # 実際のDBアクセスなど
│  │  ├─ irepositories/ # infrastructuresのインターフェイス
│  │  └─ services/ # 実際の色々な処理
│  ├─ config/ # 定数とか
│  ├─ data/ # クライアント-サーバ間で共有したい型
│  ├─ features/ # クライアント側の処理やReactコンポーネント
│  ├─ routes/
│  ├─ entry.client.tsx
│  ├─ entry.server.tsx
│  └─ root.tsx
└─ package.json

もともとはサーバの処理もfeaturesディレクトリ内に入れてファイル名で管理していたのですが、どうにもごちゃっとして分かりづらいなぁと思っていました。
サーバ側の処理をすべて.serverディレクトリにまとめるだけでだいぶ分かりやすくなりました。
小さいサービスなのでこれくらいで十分扱いやすいのですが、プロジェクトによってはもっとちゃんとしたアーキテクチャを採用すると良いと思います。

ルーティング

Remixはv2でファイルベースルーティングのデフォルトの挙動が大きく変わりました。
https://remix.run/docs/en/main/start/v2#upgrading-to-the-new-convention
もともとはディレクトリの構造がそのままルーティングのパスに対応していたのですが、v2ではディレクトリ名(あるいはファイル名)にパスの情報をすべて含めるようになりました。
一見すると名前が長くなって見づらくなったように感じたのですが、実際に使ってみるとかなり開発体験が良かったです。
まず、ファイルの階層が同じところに並ぶので、ファイル管理ソフトやIDEでファイルを眺めるだけで実際のサイトのページ構成を概観できます。
そして、親のページレイアウトを子のページに適用するかを名前で管理できるので、かなり柔軟にレイアウトを適用できます。
以前別のフレームワークを使っていたときには、レイアウトを分けるためにはディレクトリレベルで分ける必要がありました。そうすると全体の見通しが悪くなるなぁと感じていたので、Remixのようにレイアウトとパスの管理をそれぞれで柔軟に設定できるのには感動しました。まぁ、そもそもURLの設計とレイアウトの設計が一致しているのが理想ではあるのでしょうが、、。
ちなみに、「しおりモ!」での実際のルーティングディレクトリは以下のようになっています。
routesディレクトリ内の同階層にすべてのページのディレクトリが並んでいる
これを見るだけでサイトの全体像が分かると思うので、新しく開発者が入るときにも嬉しい作りだなと思います。
なお、remix-flat-routes( https://github.com/kiliman/remix-flat-routes )あたりを導入すると、ディレクトリのネストとファイル名でのパス管理をさらに組み合わせたりできるようです。個人的にはページ数が膨大にならない限りデフォルトのRemixだけで十分だと思っていますが、巨大なサイトを構築するとかであればこのライブラリの導入を検討すると良いかもしれません。

ViewTransition

RemixのViewTransitionがv2.13.0で安定化( https://github.com/remix-run/remix/blob/main/CHANGELOG.md#v2130 )しました。
「しおりモ!」ではメモの一覧画面から詳細画面への遷移で使用しています。
しかし、実はこれを実装した後(unstableのころに実装はしていました)にメインブラウザをFirefoxに移行してしまったので、自分自身は恩恵に預かれていません、、。
ViewTransitionでページ間の要素の移動をアニメーションさせると、(もちろんデザインによるところはありますが)結構大きく画面が動いてしまいます。
そのため、prefers-reduced-motionを設定しているユーザにはなるべく激しい動きを無効にしたいところです。
全体を一括で管理したいという気持ちがあったので、自分は今のところCSSでanimation-durationanimation-fill-modeを設定して擬似的にアニメーションを無効化しています。

@media (prefers-reduced-motion) {
  ::view-transition-group(*) {
    animation-duration: 0s;
    animation-fill-mode: none;
  }
}

ただ、これは正確にはアニメーションを無効化しているわではないので、ちゃんとwindow.matchMediaでprefersの設定を調べてVirewTransitionの有効/無効を切り変えたほうが良いのかなぁという気持ちも少しあります。

eslint-config

いつの間にか@remix-run/eslint-configは非推奨になっていたようです。

https://github.com/remix-run/remix/pull/7597
https://github.com/remix-run/remix/discussions/9288

どこかでアナウンスがあったのか分かりませんが、自分はなかなか気づきませんでした。ESLint v9対応について調べているときに知ったので、そのタイミングで依存を外しました。

Fly.io

v2

Fly.ioも一年以上前にアップデートがありました。
手動アップデートできるというアナウンスは見ていたのですが、面倒だからと後回しにしていたら自動でバージョンアップしてくれました。
https://community.fly.io/t/get-in-losers-were-getting-off-nomad/12914
アップデートしたから好きなタイミングで設定ファイルの同期を取ってね、問題あったら連絡してね、という感じのメールが来た感じです。
ただ、タイミングの問題かもしれませんが、Volumeマウントしているアプリは自動でアップデートしてもらえませんでした。Volumeマウントしていると少しダウンタイムが発生するとのことだったので、その影響かなと想像しています。
なお、その手動アップデート自体もコマンドを打ったら終わりというくらいには簡単だったと記憶しています。

リージョンごとの課金

https://community.fly.io/t/region-specific-machines-pricing/20690

マシンを起動しているリージョンごとに課金額が変わるようになりました。
「しおりモ!」は東京リージョンで動かしているので、課金額が少し増えました。その関係で無料枠を超えてしまったので、設定を調整しようかと悩み中です。

swapの設定

デフォルトではswapメモリの設定が0なので、たまに割り当てているメモリを超過してmeilisearchを動かしているインスタンスが落ちることがありました。
https://fly.io/docs/reference/configuration/#swap_size_mb-option
まぁmeilisearchを要求メモリより小さいサイズで動かそうとしていたのが悪いのですが、、。
OOMが発生していることに気付いてからはswapを設定するようにしました。
なお、今はRAMの設定自体も増やしているので大分安定しているかと思います。

Cloudflare

最初はFly.ioがデフォルトで用意してくれるドメインを使っていたのですが、一年以上開発を続けられたので独自ドメインでホストすることにしました。
ドメインはCloudflareで購入し、DNSもそのままCloudflareを使っています。

証明書

Fly.ioでカスタムドメインを設定して証明書を発行する際、Cloudflare DNS上でプロキシを設定しているとIPでのドメイン確認に失敗します。その場合はDNS-01チャレンジでドメインを検証すれば証明書が発行できます。

https://fly.io/docs/networking/custom-domain/#optional-validate-with-an-acme-dns-01-challenge

Firestore

定期バックアップ

まだプレビュー版の機能ですが、Firestoreにはバックアップ関係の機能があるのでこちらを利用して定期バックアップを取っています。
dailyとweeklyのバックアップを設定しています。

https://firebase.google.com/docs/firestore/backups?hl=ja

それに加えて一応PITRも有効化しています。

https://firebase.google.com/docs/firestore/pitr?hl=ja

ここらへんは課金額とかを見ながら適宜調整していくつもりです。
まだプレビューとはいえ、こういった設定をサービス側で簡単に管理できるのはありがたいです。

IaC

最初はコンソールで設定をポチポチやっていたのですが、設定項目が増えていきそうだったのでコードで管理するようにしました。
今回はOpenTofuを使って管理しています。
導入の手順については以下の記事をほぼそのまま動かした感じです。詳細な説明でかなり助かりました。

https://zenn.dev/sun_asterisk/articles/manage-existing-firebase-by-terraform

Group By

メモの索引ページを作るにあたって、すべてのメモから著者などのメモに紐づいている情報を抽出する必要がありました。
たとえば、メモ1とメモ2は著者Hoge、メモ3は著者Fugaであれば、索引にはHogeとFugaを表示したいわけです。
SQLだとGroup Byで大体済みそうな気がするのですが、Firestoreだとこの実装に困りました。
集計クエリ(count)を使えば特定の著者に属するメモが幾つあるかは計測できそうですが、そもそもの著者の一覧を取得するにはどうすれば良いのだろうかと。
もちろん、全件取得をしてアプリ側で集計処理をおこなえば実現はできます。しかし、毎回全件取得するのは負荷的にも従量課金的にもなるべく避けたいです。
今のところ、通常のメモとは別に集計情報を保存しておいて、メモの更新時にはその集計情報も更新するように実装しています。
しかし、この場合集計処理にバグがあったりするとすぐに不整合な値を記録することになりますし、そもそもDB設計としての気持ち悪さがあります(RDBならまず避けるでしょう)。
なにかうまい方法はないものかと悩んでいるのですが、答えは出ていません、、。

Firebase Authentication

もともとはGoogleアカウントでのログインの際にsignInWithRedirectを使っていたのですが、Safari 16.1以降はその操作がブロックされるようになってしまいました。

https://firebase.google.com/docs/auth/web/redirect-best-practices?hl=ja

対応方法はいくつかあるようですが、自分は多分一番手軽なsignInWithPopupを使うように修正しました。

Emulator

前回の記事でも書きましたが、やっぱりFirebaseはEmulatorが便利ですね。
ローカルやCI上でもGoogleアカウントでのログインを殆ど本番環境と同じような流れで実施できるので、開発が楽です。
なお、Emulatorは簡単なDockerfileを書いてdocker上で動かすようにしています。

FROM node:20-bookworm-slim

RUN apt-get update && \
    apt-get install -y openjdk-17-jre-headless
RUN npm install -g firebase-tools@13.20.2

WORKDIR /app

# ↓のXXXは記事用に仮で置いています
CMD ["firebase", "emulators:start", "--project", "XXX", "--import=./XXX", "--export-on-exit"]

サーバを再起動してもデータが消えないよう、起動オプションにimportexport-on-exitを指定しています。
https://firebase.google.com/docs/emulator-suite/connect_firestore?hl=ja#import_and_export_data

Mantine

Mantineはv7で一度Emotionの依存を外したので、sx propsでのスタイル指定ができなくなりました。そのため、後述するPanda CSSを導入しました。
なお、v7.9で@mantine/emotionというパッケージが作成されたので、今はv6までのようにEmotionを使ってスタイルを指定することもできるようです。

https://mantine.dev/guides/6x-to-7x/
https://mantine.dev/styles/emotion/

sx propsでのスタイル指定からclassName/classNamesでの指定に移行したわけですが、Panda CSSが書きやすいこともあって、書き心地はそんなに変わっていません。移行自体もコピペと機械的な変換で済むところが多く、修正範囲のわりには楽だった印象です。
この変更のおかげもあって、MantineはHeadless UIライブラリとしても使えるようになりました。Mantineはコンポーネント数が多くて細かいところに手が届くので、スタイリングの自由まで手に入ったのは嬉しく思います。

Panda CSS

Mantineをv7にアップデートするにあたってスタイリングをどうするか迷ったのですが、自分はCSS-in-JSが好きなのでPanda CSSを選びました。
まだv1には至っていませんが、最近は安定性も大分増してきたように感じます。
多分一番多用するcssという関数の戻り値は単なる文字列なので、Headlessライブラリとの相性がかなり良いです。
また、cvasvaというjoe-bell/cvaインスパイアの便利関数もあり、コンポーネントのバリアントのスタイルを定義するのも楽です。

https://panda-css.com/docs/concepts/recipes
https://panda-css.com/docs/concepts/slot-recipes

スタイル生成

Panda CSSはスタイル生成の仕方をPostCSS、Lightning CSS、CLIから選べます。
最初はデフォルトのままPostCSSを使っていたのですが、たまにスタイルがうまく生成されないことがありました。
サーバを再起動すると正常に動いたりしたのですが、原因もよく分からず、、。多分設定まわりでどこか間違えていたとか他のライブラリと競合していたとかだと思うのですが、、。
というわけで途中からCLIで手動でスタイルを生成するようにしました。CLIが用意されていると汎用性高くて良いですね。

Playwright

前回の記事を書いたときはE2EテストにCodeceptJSを使っていたのですが、途中からPlaywrightに移行しました。
とは言えCodeceptJSの裏側ではPlaywrightを使うようにしていたので、CodeceptJSのレイヤーを削除したという表現が適切かもしれません。
移行した理由はPlaywrightのAPIを直接触りたいと思うことがちょくちょくあったためです。
CodeceptJSは記法が英語っぽくて読みやすかったり、awaitを毎回書かなくて良かったりと便利な点もあったのですが、プログラマーが書く分には直接Playwrightを触ったほうが楽かもしれません。

Fixtures

PlaywrightはFixturesという機能がかなり便利です。

https://playwright.dev/docs/test-fixtures

特定の機能を動かすときに、特定の前処理と後処理が自動で走るように書くことができます。
「しおりモ!」ではテストケースを動かすときに、前処理としてユーザ作成、後処理としてユーザの削除をおこなうようにしています。
また、前処理や後処理を書かずとも、単純にテスト用の共通処理を置くところとしても便利です。

Meilisearch

Meilisearchは前回の記事に引き続き、セルフホストしたものを全文検索用に使っています。

日本語対応

Meilisearchはv1.10で検索する際の言語を指定できるようになったので、日本語で使うのが少し楽になりました。
前の記事で書いていたように、以前の通常のイメージは日本語の検索に少し問題があり、日本語用の特別なイメージを使う必要がありました。
以下のDiscussionに詳しいです。

https://github.com/orgs/meilisearch/discussions/532

バージョン更新

前回の記事でも書いたバージョンアップ時に毎回データのダンプと読み込みが必要という問題は解消されていません。というより解消するつもりが当分は無さそうです。

https://github.com/meilisearch/meilisearch/issues/2570#issuecomment-1849782879

MeilisearchのCloud版は最低金額でも高くて個人では導入しづらいんですよね。
バージョンアップが少しでもやりやすくなるよう公式ドキュメントの手順( https://www.meilisearch.com/docs/learn/update_and_migration/updating#updating-a-self-hosted-meilisearch-instance )を動かすためのスクリプトみたいなのは書きました。
しかし、結局ダンプデータが必要ということはダンプ後にデータ更新がないようにする必要があります。そのため、短い時間とはいえバージョン更新の度にダウンタイムが発生してしまいます。
業務で使うとなると結構致命的な感じがするのですが、皆さんはどのように回避されているのでしょうか、、。

なお、この点以外はとても使いやすく、動作も早いのでかなり気に入っています。

Upstash

前回の記事ではセッション管理用のRedisサーバとしてUpstashを使っていると書いたのですが、結局使うのを止めました。
Upstashに不満があったわけではなく、サービスの規模的にRedisサーバが不要だなと考えたためです。
また別の機会に使いたいなと思っています。

Sentry

エラー監視とCSPのレポート用にSentryを導入しました。
最初はhighlight.ioを導入してみたのですが、SessionReplayを無効化しても常に通信が発生していたので、今回のサービスには過剰だなと思ってSentryにしました。ただ、highlight.ioもちゃんとすべてのオプションを調べたわけではないので、もしかしたら自分の調査不足かもしれません。

https://www.highlight.io/

なお、highlight.ioはフロントのエラーとバックエンドのログを紐づけて確認できるので、フルスタックフレームワークとはかなり相性が良いなと感じました。こちらもまだどこかの機会で使ってみたいなぁとは思っています。

CSP Reporting

まだプレビュー版の機能のようですが、SentryではCSP(Content Security Policy)のレポートも収集できます。

https://docs.sentry.io/security-legal-pii/security/security-policy-reporting/#content-security-policy

Sentryの画面ではプロジェクト > 設定 > セキュリティヘッダー > Content Security Policy(CSP)からエンドポイントの確認や追加の設定ができます。
実際にCSPに違反した処理が発生すると以下のような課題が起票されます。

CSPの違反内容をタイトルとした課題がSentryに起票される。通常の課題と同様のユーザ情報に加え、CSPに関する情報も課題内に記載されている。

ただし、課題はUnhandledとして起票されるわけではないようで、プロジェクト詳細の課題表示欄のタブをUnhandledではなく新しい課題すべての課題に切り変える必要があります。

Renovate

実は開発を始めた初期段階から導入していたので、前回の記事を執筆したときにもRenovateを使っていました。しかし、前回の記事では書くのを忘れていたので今回始めて言及します。
最初は個人開発なのでRenovateのプルリクエストを確認する手間を減らそと思っていました。
そのため、プルリクエストは週に一回だけ、かつマイナーバージョンとパッチバージョンの更新はまとめるように設定していました。
しかし、実際に運用してみると、マイナー/パッチバージョンの更新でもときどきテストが落ちることがありました。そういったときにどのライブラリが原因なのかを確認し、それ以外をアップデートするという作業がかなり手間でした。
また、複数のライブラリの更新がまとまっているため、更新内容の確認もそれなりに大変でした。
そこで、途中からはプルリクエストをまとめず、更新の度にプルリクエストを作成するようにしました。
テストが落ちても原因が明確になり、プルリクエストごとの更新内容も少なくなったので、かなり管理が楽になりました。
プルリクエストの回数自体は増えたものの、「しおりモ!」の規模では数日に一回くらい軽く確認してマージしていくだけで十分です。今のところライブラリの更新内容を把握しておきたいので自動マージを無効にしていますが、さらに手間を減らすには有効化を検討するのもありだと思っています。

細々とした話

コードフォーマッタ

prettierからdprintに移行しました。
フォーマットする対象言語をプラグイン形式で追加していくというdprintの設計が気に入っています。

ロジックテスト

vitestを使っています。

CI/CD

GitHub Actionsを使っています。

gitmoji

コミット履歴を見るときに絵文字があると楽しい気分になるので、コミットメッセージのプレフィクスにはgitmojiを採用しています。

今作るとしたら

最後に、今改めて「しおりモ!」を作り始めるとしたらどんな技術スタックになりそうかを考えてみます。

個人開発の場合、多分現状と大差ない選択をすると思います。
変えるとしたら、Remixを別の使ったことがないフレームワークにするくらいでしょうか。
それもRemixが嫌だからというわけではなく、単純に触ったことのないフレームワークをいじりたいという気持ちからです。
前述したように、Remix v2のファイル名でルーティングを表すやり方とかかなり好きなので、全体としてポジティブな気持ちが強いです。

しかし、もし仕事で開発するとしたら、Firestoreは別の技術を使うかもしれません。
前述したGroup Byみたいなことをどう実践すれば良いのだろうかとか、やっぱりRDBのほうが各種プラクティスが(ある程度)確立されていて設計しやすいのが理由です。Firestoreはクライント側から直接アクセスする場合などは強力だと思いますが、「しおりモ!」ではそういった使い方をしていないのでよりそう感じるのかもしれません。
ただし、DBの管理はそれはそれでかなりのコストだと思っているので、個人開発ならメリットの方が上回るかなというふうには判断しています。
なお、もしFirestoreを止めるとしたら、Firebase Authenticationもあわせて再考するかもしれません。
前述したようにバージョンアップ時の話があるので、要件次第ではmeilisearchの選択も変わるかも、、?

おわりに

と色々書きましたが、概ね良い開発ができているかなと思っています。
自分自身が日々使っているので、幸い開発のモチベーションも続いています。まぁ開発が楽しくて読書量が落ちている疑惑があるのを喜ぶべきかは難しいところではありますが、、。
実装したい機能はまだまだあるので、ゆっくりでも開発は続けていくつもりです。

GitHubで編集を提案

Discussion