CloudRun + Go + Next.js で画像閲覧に特化した Twitter クライアントを作ったはなし
zenn 初投稿になります、kimihiro_n です。
先日リリースした個人開発のアプリケーションのはなしをしたいと思います。
作ったもの
最初に宣伝かねて作ったものの紹介を。
イラスト投下閲覧用に Twitter を使っているのですが、公式 Twitter だとイラスト以外の投稿もたくさん混ざって追いづらかったり、最適化の影響で見逃してしまうツイートがあったりと不便さを感じていました。
Twitter API を使ってこの辺いい感じにフィルタリングしたら快適になるんじゃないかと思い Web アプリケーションとして作ってみることにしました。
自分のタイムラインやリストをフィルタリングして表示するだけのアプリケーションですが Twitter で絵師さんを追いたいみたいな用途だと便利に使えると思うので是非試してみていただければと。
後述しますが PWA(Progressive web apps) にも対応してるのでPCやスマホでアプリケーションとしてインストールすることもできます。
ログインするとこんな感じで閲覧できます。
宣伝おわり。
構成
アプリケーションの構成としては以下で作りました。
- サーバーサイド
- 言語: Go
- Web Framework: gin
- クライアント
- 言語: TypeScript
- Framework: Next.js
- CSS Framework: Tailwind CSS
- インフラ:
- Cloud Run
- Database: Datastore
サーバーサイドは Twitter の API をラップするくらいの役割だったので、慣れてる技術を使ってサクッと実装しました。
今回個人的に挑戦してみたのは「Next.js」「TailWind CSS」のフロントまわりです。React 自体は結構触ってますが Next.js をがっつり使うのは初めてでした。
またインフラの CloudRun 単体でアプリケーションとして公開するのも初です。
API
API で行ってるのは
- ログイン管理
- Twitter API のラップ
くらいです。
サーバーで特別処理しなくてはいけないのは Twitter の認証情報を保存するくらいなので慣れた構成で作ってしまいました。
データベースは Datastore を利用しています。
これも慣れているというのと維持費用が安いという理由で使いました。
Go から Datastore 使うはなしはこの記事とかにまとめています。
今回最終的には App Engine ではなく Cloud Run でデプロイすることにしましたが、App Engine 自体に特別な依存がなくなってきているのでインフラの構成を事前に決めずに実装を進めることができました。
Front
今回のメインのフロントについて。
Next.js
Next.js という React の フレームワークを使って開発しました。社内勉強会で少し触ってホットリロードが便利くらいの認識だったのですが、初期の開発環境構築が簡単だったり、PWA 対応もプラグインで対応できるみたいな点で採用してみることにしました。
使ってみてやはり 高速なホットリロード と設定ファイルあまりいじらなくてもいい点はとても開発体験がよかったです。CDNで提供されているライブラリだけでページを作るのと比べて、フロントでビルド環境を整えて Framework を導入するというのは結構腰が重い作業だと思ってたのですが、Next.js はコマンド1つで準備が整って開発スタートできるのでありがたかったです。
開発始めてからもホットリロードで即時ブラウザに反映されるのでレイアウト作業がとても捗りました。ちょっとしたページを作るとかでも全然採用する価値がありそうです。
React もフックという機構が導入されてからはだいぶ書きやすくなりました。以前の React はコンポーネントが状態を持つなら全部 Root から状態を受け渡していくように書く必要がありましたが、フックを利用すればコンポーネント内で利用する状態を宣言して安全に扱うことができるので書きやすいです。
SWR
Twitter のタイムラインを取ってきて表示する部分はSWR というライブラリを使いました。
タイムラインみたいな時系列データを表示する場合
- ローディング状態の管理
- エラー発生時の処理
- 過剰にAPIへアクセスしないようキャッシュ
- 最新のアイテムを取得してリフレッシュ
- アイテムの一部をキーにページング
みたいにやることが多くて頭が痛くなるのですが、このライブラリを導入したらスマートに処理を記述できて感動しました。
特にページングの処理は恩恵が大きかったです。時系列データを扱う場合、先頭のデータよりも新しいデータというのがどんどん入ってきます。なので現在を基準に1ページ目、2ページ目みたいなページングをするとどんどん開始時点がずれておかしな事になってきます。
それを防ぐためにアイテムのIDや作成日時などをキーにしてそれより新しい(or 古い)データを取得する方式のページングが必要になります。これを React のコンポーネントの中で…となると状態の管理がまあ大変です。
そこでこの SWR なのですが、なんとキーベースのページングの書き方が公式ドキュメントとしても載っています。
とても心強いですね。
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null // reached the end
return `/users?page=${pageIndex}&limit=10` // SWR key
}
function App () {
const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
if (!data) return 'loading'
…
useSWRInfinite
という関数が用意されていて、前回の取得データ(previousPageData)から動的に key を決める関数を渡すことで次ページの取得パラメータを調整することができます。キーベースのページングに特化したユーティリティ関数という感じではなくいろいろな取得パターンに対応できそうな API 設計されていて感動しました。
Tailwind CSS
今回 CSS でのデザイン組みは Tailwind CSS というフレームワークを使ってみました。
Bootstrap や Semantic UI みたいなものと比較するともっと CSS に近いタイプのフレームワークでした。
たとえばこういうボタンを作るとき、他のフレームワークだと class="ui primary button"
みたいにどういうボタンかを記述すればボタンっぽく表示されます。
しかし Tailwind CSS の場合
<!-- https://v1.tailwindcss.com/components/buttons -->
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Button
</button>
みたいな記述になります。
Tailwind CSS にはボタンというパーツは用意されておらず
- bg-blue-500: 背景をblue-500に
- hover:bg-blue-700: ホバー時は背景をblue-700に
- text-white: 文字色を白に
- font-bold: 文字のウェイトを bold に
- py-2: 上下の padding を 2px に
- px-4: 左右の padding を 4px に
- rounded: 角を丸く
というのを全部クラスに記述します。ほぼ CSS 書いてる感じですね。
クラス名の記述も呪文みたいに長いし、このフレームワーク本当に幸せなのだろうか…みたいに感じていました。CSS本来の命名とも変わってるものもあり覚えることも多いです。
ただ書き方に慣れてくると思い通りのレイアウトへサクサク書いていく事ができますし、ブラウザの互換性とかについても注意を払わなくていいので利便性の方が上回ってきました。HTML(JSX)内にずらずら書かなくてはいけない問題も、@apply という機能で CSS のように名前を付けてまとめられるみたいです。今回は見送りましたが。
Tailwind CSS を使ってて特にいいと思ったのは、パーツの再利用性が高い点ですね。さっきも書いたとおり Tailwind にはボタンみたいなコンポーネントは用意されていないのでクラス名を組み合わせて作る必要があるのですが、必ずしも自分で作らなくても大丈夫です。
Tailwind CSS Components という任意投稿形式のレポジトリが用意されており、ここからコンポーネントを見つけてコードを使うことができます。コードを持ってくるときも HTML のクラス名に必要な情報が記述されてるのでそのままコピペでいい感じに利用ができます。
CSSとHTMLのセットで公開されているコンポーネントもよくありますが、CSS だと div の階層に依存した記述があったりしてコンポーネントの部分だけ抜き出すとうまく動かなかったりします。しかし Tailwind で記述されたものは階層依存がほぼないのでスッとはまってくれます。ランディングページもいろいろパーツを借りてきて作りました。
デザイナーさんがちゃんとついているプロダクトで Tailwind CSS を入れるのは旨みが少なそうですが、個人開発でプロトタイプ兼ねて作るみたいなときは便利なフレームワークでした。
PWA 化
対応する PC やスマホでサイトを開くと、インストールを促すダイアログがでるようになってます。インストールすると普通のアプリのように一覧へ登録され、ブラウザを意識せずにサイトを使う事ができます。
npm install
して少しの設定と必要なリソースを配置するだけで PWA 化することができます。
アイコン類は PWA Manifest Generator みたいなのでググって出てきたツールで作りました。(具体的にどれ使ったか忘れました)
サクッと PWA 化対応できてすごく便利でした。
実際 Andorid スマートフォンにいれてみるとサクサク動きネイティブアプリとそこまで遜色ない印象を受けました。Pull To Refresh とかを自前で実装するとよりそれっぽくなります。
インフラ
開発段階では App Engine か Cloud Run どちらにデプロイするかまでは決めないまま開発すすめてました。
月数十円くらいで運用したいという思いから Datastore + (App Engine or Cloud Run) にすることは決めてたのですが、肝心のアプリケーション環境については決めかねてました。
App Engine であれば静的ファイルの配信機能がありますが、Cloud Run だと静的ファイルの配信も含めて 1コンテナにする必要があります。一方コンテナ内のカスタマイズとしては Cloud Run のほうが優れており、App Engine だと決められた方式で Go を動かす必要があります。
.dev
ドメインを購入したことで解決しました。どちらの環境にもカスタムドメインを振ることができます。
開発環境
Docker Compose を使ってローカルだけで動く環境を作りました。
Datastore もエミュレータが公式の SDK に入っているのでそれを使っています。
あとは開発環境用に Nginx を追加で導入しています。
この Nginx は Go でつくった API と Next.js の開発サーバー両方をリバースプロキシして、http://localhost:8000 の1つのポートから両方にアクセス出来るようにするためのものです。
こうすることで Next.js や Go 側のホットリロードを生かしたまま開発を進めることができます。ちなみに Go のホットリロードには Air というツールを使ってます。
実際の docker-compose.yml はこんな感じです。
必要な環境変数だけセットして docker-compose up すれば即動かせるようになっています。
本番環境
Cloud Run VS App Engine のはなしですが、結局 CI/CD 組んで Next.js をビルドするのが面倒ということで App Engine をあきらめ Cloud Run へデプロイすることにしました。
Docker のマルチビルドを使って Next.js および Goビルドを行い、成果物だけを最終イメージに乗せることで軽量なイメージを作ることができます。
圧縮状態でなんと 8MB。
ランタイムが必要な Python とかと比較して圧倒的に軽いですね。
Go の本番イメージはこの記事を参考にさせて頂きました。
1つのイメージにしてデプロイする必要があるので、Next.js のビルド成果物や静的ファイル配信は Go のアプリケーション経由で配信するようにしています。
あと Next.js のアプリケーションとして動かすために
-
/
へ来たら index.html のファイルの中身を返す -
/hoge
へ 来たら /hoge.html のファイルの中身を返す
のようなルーティングを自前で行ってあげる必要があります。
for _, file := range dirwalk(env.StaticDir) {
alias := strings.Replace(file, env.StaticDir, "", 1)
if strings.HasSuffix(file, ".html") {
alias = alias[:len(alias) - 5]
}
if alias == "/index" {
alias = "/"
}
g.StaticFile(alias, file)
}
ページが増える度 Go 側に手をいれるのも手間なので、Nginx の try_files みたいな仕組みを Go で実装してルーティングするようにしました。
アイコン
最後のおまけとしてアイコンのはなしも。
iPad の ibisPaint というアプリでお絵かきして作りました。ここ数年イラストの練習してるお陰でロゴみたいなのもパッと作れるようになってきました。
「アプリ開発は総合格闘技」と言われるように何でも自分で出来るようになってくると楽しいですね。
よかったら emiru も是非使ってみてください!
Discussion