【個人開発】開発者のためのソーシャル分報アプリ「Timesy」を支える技術

ソーシャル分報アプリ「Timesy」をリリースしたので、その背景と技術スタックについてご紹介したいと思います。

## アプリ紹介
https://www.timesy.dev

![スクリーンショット 2023-08-10 15.59.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169330/1b0c3f9f-56e6-8329-29ff-ff9c0fb707eb.png)


個人開発や趣味開発にも使えるオープンな分報（times）を使ってみたいと思ったのが、このアプリを開発したきっかけです。最初は、特定のトピックスに絞ったサーバーを建てられるように、分散型にしようかと考えたのですが、分散型ソフトウェアの管理の難しさを感じ、中央集権型に切り替えました。ただ、OSSの体裁は保っています。

https://github.com/moekidev/timesy

Zennにもスクラップがありますが、普段の開発からシームレスに特定のタスクのスレッドに移行したい、また他の人が覗きやすいようにオープンな形を取りたいと思い、マイクロブログのような構成を採用しました。

## Timesyの技術スタック

### インフラ
- [Heroku](https://heroku.com)
- [Cloudinary](https://cloudinary.com/)

### バックエンド
- [Ruby on Rails](https://rubyonrails.org/)

### フロントエンド
- [rails/importmap-rails](https://github.com/rails/importmap-rails)
- [Hotwire](https://hotwired.dev/)
- [Tailwind CSS](https://tailwindcss.com/)

## Heroku

Herokuにデプロイしています。https://render.com を利用していた時期があったのですが、デプロイの挙動が不安定であるような実感があったのでHerokuに戻ってしまいました。

## Ruby on Rails

今回は、シンプルなブログ型のサービスということで、CRUDとフォームの挙動が用意できればいいということと、オープンソースなので、できるだけ参入の間口を広げたいという狙いからRuby on Railsを利用しました。最初、Remixを採用して実装を進めていたのですが、画像のアップロード処理やメール配信などのバックエンド寄りの実装がかなり頑張らないといけない感じだということが分かり、ある程度進んだところでRailsに切り替えました。後述しますが、StimulusとTurboでのフォーム周りの実装がかなりスムーズだったのと、ActiveStorage・ActionMailerの便利さの恩恵を受けられた点が良かったです。

## Import Maps
Import Mapsは、外部サーバーからJavascriptのモジュールをimportすることができるWeb標準です。`<script type="importmap">`タグの中にモジュール名とその取得先URLを定義したJSONを埋め込むことで、importできます。

```html
<script type="importmap">
  {
    "imports": {
      "square": "./module/shapes/square.js",
      "circle": "https://example.com/shapes/circle.js"
    }
  }
</script>
```

```js
import { name as squareName, draw } from "square";
import { name as circleName } from "circle";
```

これは今年の3月からSafariが対応したことで、一般的なブラウザのすべてが対応したことになります。
Import Mapsはその特性上、いくつかのメリットとデメリットが存在しますが、それらを鑑みて、実験的に導入した次第です。正直、それほどフロントエンドのボリュームも多くないので、他の方法に簡単に移行できるはずです。

まず１つ目の特徴は、トランスパイラがないので、TypescirptとJSXが動きません（！）これは、プロジェクトの内容によっては致命的ですが、今回の場合は、StimulusでMarkdownなどのライブラリをHTMLにマウントする程度なのでそれほど心配はしていません。また、トランスパイルの必要がないので、開発のスピードは理想的です。

もう一つは、パッケージマネージャを使わないので、Dependabotなどの便利な管理システムを利用することができません。ただし、上記のimportmaps-railsは実質的にはパッケージマネージャのような挙動をするので半分は恩恵を受けられます。
その半分とは、パッケージの依存関係を自動で計算してくれる点です。

```
bin/importmap pin xxx
```

のように、コマンドを利用するとxxxが依存しているパッケージもまとめてImport MapsのJSONに吐き出してくれます。また、import先のURLも自動で取得してくれます。

恩恵を受けられないもう半分はDependabotです。こちらはDependabotの方にIssueがあるのですが、なにせ利用者が少なく進んでいないようです。もしかしたらコミットするかも。

https://github.com/rails/importmap-rails/issues/19

あとユーザーから見たパフォーマンスはちゃんと計測してません。いずれやってみようと思います。

## Hotwire

HotwireはRailsのチームが開発したJavascriptのフレームワークです。これは、Stimulus、TurboそしてStradaの3つのライブラリがまとまったもので、これらのうち前者２つを利用しました。というかStradaはモバイルアプリ向けのライブラリなので今回は関係ありませんでした。

### Turbo
Turboは、シングルページを実現するためのライブラリです。HTMLにパーツとなる箇所を定義し、Rails側に適切なRoutingを用意すると、HTMLの断片をやりとりし、フォームの送信やページのスクロールをきっかけとして、ページの一部だけを更新することが出来ます。

クライアントサイドでHTMLを組み立てる方式と見た目は変わりませんが、厳密に言うと、DBの状態変化のあとにHTMLが生成され、クライアントサイドに送信されるので、若干の遅延があります。ただそこまで体感として遅いという感じはしないので、大きな問題と捉えてません。それよりも、シングルページを実現するためだけに、コンポーネントのフレームワークやNode.jsを利用するほうが個人的には心理的な負担が大きかったです。これはあくまで個人的な感想です。

### Stimulus
Stimulusは、Controllerと呼ばれる、特定のUI制御の役割を担うクラスがあり、HTMLのカスタムデータ属性にこのControllerに対応する情報、例えば、Controllerで利用する値や、Elementの情報を定義することで、HTML上にControllerをマウントすることができます。

私の使い方では、ほとんどが、DOMContentLoadedのlistenや、Elementの取得と操作のためにしか使っていません。例えば、Markdownのライブラリを呼び出して、Controllerと対応しているElementにMarkdownを適応するといった様な感じです。

```js
export default class extends Controller {
  static values = { minHeight: String, placeholder: String }

  connect() {
    this.element.easyMDE = new EasyMDE({
      element: this.editorTarget,
      placeholder: this.placeholderValue,
      toolbar: false,
      status: false,
      tabSize: 2,
      minHeight: this.minHeightValue,
      spellChecker: false,
    });
  }
}
```

## Tailwind
CSS-in-JSが使えないので、消去法でTailwindになりました。CSSとの仕様のコンフリクトや、急激な仕様変更などいろいろ苦しめられたのでSassはもう使いたくありません。

## まとめ
非常にRailsに偏った構成になりましたが、Railsがターゲットにしているようなアプリケーションのど真ん中だったので必然的にRailsのチームが採用している技術がマッチしていたという感じだと思います。

## 宣伝
ぜひ使ってみてください！

https://www.timesy.dev/

https://github.com/moekidev/timesy