ウェブページ管理ツール「Chronoter(クロノーター)」をリリースしました【個人開発】

公開されているWebページのスナップショットの記録とドキュメント管理ができるサービス「Chronoter(クロノーター）」のβ版をリリースしました！

構想は1年ほど温めていたのですが、
ここ半年ほどかけて一人で開発し、MVPと呼べるレベルのものが出来上がりました。
サービスと利用している技術についてご紹介します！
ぜひ興味を持っていただけたら、実際に使ってみて感想などいただけると嬉しいです。

## リリースしたサービス

https://chronoter.com/ja

### できること
#### 1. ウェブサイトのスナップショットを保存する
公開されているWebページの情報を保存して、いつでも再現できるようになります。
ページの内容が更新されたり、公開が終了した場合でも、スナップショット撮影時点での状態を確認可能です。
![スクリーンショット 2024-06-13 18.55.07（3）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176054/2db4830f-5760-b59b-8a83-5f2e49bde554.png)


#### 2. 保存したスナップショットにドキュメントやコメントを追加する
スナップショットに対してMarkdown形式でドキュメントやコメントを追加できます。
ページに関する仕様やメモなどを記録するのに役立ちます。
特定の要素に対してコメントを残すこともでき、変更箇所の指示にも使えます。

![soai2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176054/836beb83-a16a-ad35-30d5-eb7e94fcc661.png)


#### 3. スナップショットに差分が発生したら差分箇所を自動で検出する
一度スナップショットを保存したページの内容が更新されている場合、再度スナップショットを取得すると前回との差分を検出し、どこがどのように変わったのかを可視化します。
![sozai3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176054/6f39e541-2241-f888-a318-ffd72e0fff0a.png)


### コンセプト、ターゲット
メインターゲットはウェブサイトの開発・運営を行っている個人やチームです。
チームに新しく入った人や開発者以外の人がサイトを理解する手助けになる「ウェブサイトの説明書」のような存在を目指しています。

- コード以外に現在のサイト仕様を把握できる資料がない
- サイトの仕様書が開発当初のまま更新されてない
- WordpressやCMSツールで作成したサイトが適切に資料化されていない
- 担当者の入れ替わりが激しく、次の担当者がサイトの仕様を把握するのに時間がかかる
- エンジニア向けの資料はあるが、それ以外のメンバー向けの資料がない
- 他社から移管することになったサイトについて、受け渡しされるのは生のソースコードだけで資料がなく困る
- 仕様はそのままでサイトの技術を新しくしたいが、仕様書がない

こんな課題をChronoterで解決したいと思い作成しました！

## Chronoterの技術周り

### 動作環境
- フロント
    - React, Nextjs(App Router)
    - TypeScript
    - Tailwind
    - shadcn/ui
    - Storybook
    - Jest
    - Pretiier
- バックエンド
    - Python, FastAPI
    - black, flake8, mypy, pytest
    - MySQL
    - nginx
    - Redis
- インフラ
    - Docker
    - AWS(ECS, CloudFront, RDS, S3, SES, Route 53, ElastiCacheなど)
    - Sentry
- その他
    - CI/CD
        - GitHub Actions
    - モニタリング
        - Sentry

### フロントの技術選定
仕事でフロント開発を主力としていたため、昨今のトレンドと自分のスキルを考慮して、最も労力がかからない慣れた構成にしました。

後述するFastAPIでOpenAPIのSchemaが自動生成されるので[OpenAPI TypeScript](https://openapi-ts.pages.dev/)を使ってそこから型生成し、[OpenAPI fetch](https://openapi-ts.pages.dev/openapi-fetch/)でAPIリクエストを行うという構成だと非常に開発体験が良いのでおすすめです。

基本的にログインして使うサービスということもあり、SSRやSSGを考慮する必要がないのでNodejsサーバーを立てる手間を省くためにNextjsの[Static Export](https://nextjs.org/docs/app/building-your-application/deploying/static-exports)を利用して静的出力したものをCloudFrontで配信しています。
多言語化対応の際にCloudFrontでパス調整に少し苦労しましたが、それ以外は大きなつまずきはありませんでした。

状態管理ツールについては、最近はRedux、Recoil、Jotaiなど利用するメリットを感じられないため、どうしてもページをまたがる状態管理が必要な場合はuseContextで済ませています。

UIフレームワークはshadcn/uiを採用しました。
車輪の再開発を避けつつ個性も出していけますし、秩序と自由度を両立できて、昨今フロント開発していて感じていた痛みをかなり解消できて嬉しいです。
(BootstrapやMaterial UIと比べると、デフォルトの色パターンが少ないのだけ最初少し気になりました。）

基本利用はPCをターゲットにしていますが、メイン画面のレスポンシブ対応も将来的に実施予定です。

### バックエンドの技術選定
仕事ではPHPとRubyの経験が長いのですが、今回はサービス内容との相性を考え、Pythonを選択しました。

ここ数年ずっとOpenAPIを使ったSchema駆動開発に慣れていたので、いつも通りOpenAPI作成しようと思ったのですが、APIフレームワークにOpenAPI生成機能がセットで付いてくるFastAPIに魅了されてそれを採用しました。

いつもだとOpenAPIの更新をAPIの実態とどう追従させていくかが課題でしたが、FastAPIだと実装を変えたら自動でSchemaも更新されてるので管理する必要がなくストレスフリーです。
Schemaが自動生成されるだけじゃなく、Swagger UIによるAPI動作確認のためのGUIツールまでセットで付いてきたのでPostmanやApidogを利用してAPIリクエストを行う必要もなく、色々完結してくれて非常に開発体験が良かったです。

TypeScriptでのフロント開発に慣れた身としてはバックエンドでも型補完を効かせたくなり、Pythonでもtestやci、formatterなど駆使してなるべく型厳格にできたのでPHPやRubyより書きやすく感じました。

### インフラの技術選定
インフラは正直専門外で普段仕事ではあまり触っておらず、付け焼き刃感があるのですが、とりあえず最初は動けばOKの理論で構成してみました。
現状はコスト削減のためにいろんなスペックを落としているので運用の様子見て調整していきます。（そのために動作遅いとこなどありますがご容赦ください...）

### 生成AIによる個人開発の変化
フロント、バック、インフラ、設計からテストまで全てを一人でやらないといけないのが個人開発の大変なところであるのは周知の通りですが、昨今の生成AIの発達により、その負担もかなり楽になったと感じました。
私の技術力ではスペシャリストにはとても敵いませんが、どの領域も多少は仕事で触っていたため、大まかな実装方針は自分で考えられます
そうなるとChatGPTやGitHub Copilotの存在がありがたく、これまでだったら方針を考えた上で、地道に言語やライブラリなどを学んで書いていくしかなかったのですが、その学んで書くという時間をまるっとAIに任せてすっ飛ばせるようになります。
AIが出力してくれたコードが自分の書きたかったコードになっているか読んで確認して、微調整してを繰り返していくだけで、それなりにしっかりした機能を次々追加していけるようになるのはちょっと前までの個人開発体験と比べて革命的に変わりました。

### サービスの今後について
まだまだうまくスナップショットの保存や表示ができないサイトもあるので、随時フィードバックをいただきながら対応できるサイトの種類や技術を増やしていこうと考えています。
正式版リリース時にはチーム管理、ユーザー招待機能やLLMとの連携機能も追加予定です。
そのほか欲しい機能の要望などありましたらぜひ私のtwitterやサイトの問い合わせフォームからご意見ください！

https://x.com/nyama_111