Next.jsのReact EssentialsのServer Componentsの説明をオレオレ翻訳しながら読む

これの話

https://nextjs.org/docs/getting-started/react-essentials#server-components

正確さではなく(それならAIでいいよね)、自分の理解に引き寄せて翻訳してみる。

Server Components

Server and Client Components があるとサーバーとクライアントの両側にまたがるアプリケーションを作れます。クライアントサイドのリッチなインタラクティビティと伝統的なサーバーレンダリングのパフォーマンスを組み合わせることができます。

Thinking in Server Components

ReactはUI設計の考え方を変えました。同じようにReact Server Componentsは、サーバーとクライアントの両方を活かしたハイブリッドアプリケーションを作るための新しい考え方です。

Reactはこれまで全てをクライアントサイドでレンダリングしていました(SPAというやつです)。しかし、Server Componentsがあると目的に応じてどこでレンダリングするかを決められます。

たとえば、アプリケーションのpageを考えます。

ページをコンポーネントに分割すると、実は大多数のコンポーネントはインタラクティブではないです。つまりサーバーサイドでServer Componentとしてレンダリングできます。そしてその中にインタラクティブなクライアントコンポーネントを点在させるのです。これはNext.jsのサーバーファーストなアプローチと相性がいいです。

Why Server Components?

Server Componentsのメリットは何?という疑問が浮かぶでしょう。

Server Componentsはサーバーのインフラを活用しやすいです。たとえば、巨大な依存パッケージをクライアントに送信する必要がありません。こうなるとReactはPHPやRoRのよう(テンプレートエンジンのように?)に扱えます。

Server Componentsは初期ページ読み込みが早いです。バンドルサイズが小さくなります。根幹部分のクライアント側ランタイムはキャッシュ可能かつサイズの予測が可能で、アプリケーションが成長しても増えません。追加されるJavaScriptは、Client Componentsが使われたときだけ増えます(この辺あんまりわかってない)。

Next.jsでrouteが読み込まれたとき、初期HTMLがサーバーでレンダリングされます。このHTMLはブラウザで段階的に成長し、クライアントがアプリケーションを引き継ぎ、インタラクティビティが付加されます。このためのランタイムの読み込みは非同期的です。

Server Componentsに楽に移行できるように、App Router内のコンポーネントは全てデフォルトでServer Componentsにします。special filesやcolocated componentsも同様です。だからあなたは何もしなくてもServer Componentsを採用して優れたパフォーマンスを得られます。ここに use client directiveを使うことでClient Componentsをオプトインできます。

Client Components

Client Componentsを使うとクライアント側でのインタラクティビティを付加できます。これはNext.jsではサーバーでのpre-renderingとクライアント側でのhydrationで実現しています。Client ComponentsはこれまでのPage Routerと同じ動きです。

The "use client" directive

use client directiveはServer / Client Componentsの境界線を宣言するものです。

'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

"use client" はサーバーとクライアントのコードの間に配置します。ファイルの一番上、importsよりも上に書きます。"use client" が宣言されると、そのファイルがimportしている全てのモジュール(子コンポーネントなど)はクライアントバンドルの一部と認識されます。

デフォルトはServer Componentsなので、 "use client" 宣言がない限り全てはServer Component moduleに含まれます。

豆知識

  • Server Component moduleに含まれるComponentはサーバーでのみレンダリングされることが保証されます
  • Client Componentはクライアントでレンダリングされるものですが、Next.jsは事前にサーバーでレンダリングすることもあります
  • "use client" はファイルの一番上で宣言しなければなりません
  • "use client" は全てのファイルで宣言する必要はなく、Server Componentsとの境界でだけ宣言すれば十分です(そのファイルがimportしているファイルもクライアントバンドルになるので、"use client" が宣言されたファイルはentry pointとみなすことができます)

おわりに

Server Componentsが何なのか知りたくて調べていたんだけど、なんか概念的な記事しかなくてよくわからなくて、この記事もそうだった。完。でも概念はちょっとわかった。

goでmysqlにDATETIMEを入れるときにgo-mysql-driverとbunでタイムゾーンの扱いが違う

goのTimeは日時とタイムゾーンの情報を持っている。日本標準時のタイムゾーンを持ったTimeを作ってみる。

location, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
    log.Fatal(err)
}
t := time.Date(2019, 1, 2, 3, 4, 5, 0, location) // 2019年1月2日3時4分5秒

これをmysqlに突っ込む。mysqlとのコネクションはこんな感じ。

cfg := mysql.Config{
    User:      "root",
    Passwd:    "password",
    Net:       "tcp",
    Addr:      "127.0.0.1:4306",
    DBName:    "time",
    ParseTime: true,
}

Locは指定していない。その場合UTCになる。

https://github.com/go-sql-driver/mysql/blob/191a7c4c519ef60cf3e8656fde8728eee9194308/dsn.go#L73

// NewConfig creates a new Config and sets default values.
func NewConfig() *Config {
    return &Config{
        Collation:            defaultCollation,
        Loc:                  time.UTC,
        MaxAllowedPacket:     defaultMaxAllowedPacket,
        Logger:               defaultLogger,
        AllowNativePasswords: true,
        CheckConnLiveness:    true,
    }
}

go-mysql-driverで生のSQLを書いてmysqlにINSERTする

db.Exec("INSERT INTO `time` (`id`, `time`) VALUES (?, ?);", "Asia/Tokyo 2019-01-02T03:04:05", t)

このとき発行されるクエリをgeneral_logで確認すると、この段階でUTCの 2019-01-01 18:04:05 に変換されている。

2023-05-06T14:33:27.581647Z     9 Execute   INSERT INTO `time` (`id`, `time`) VALUES ('Asia/Tokyo 2019-01-02T03:04:05', '2019-01-01 18:04:05')

これはgo-mysql-driverがTimeをシリアライズする前にInでタイムゾーンをUTCに変換しているからだ。

https://github.com/go-sql-driver/mysql/blob/191a7c4c519ef60cf3e8656fde8728eee9194308/packets.go#L1119

b, err = appendDateTime(b, v.In(mc.cfg.Loc))

ではbunでTimeをINSERTするとどうなるか

type BTime struct {
    bun.BaseModel `bun:"time2"`
    ID            string
    Time          time.Time
}
bundb.NewInsert().Model(&BTime{ID: "BUN Asia/Tokyo 2019-01-02T03:04:05", Time: t}).Exec(ctx)

このとき発行されるクエリでは、Timeに設定されているタイムゾーンを無視して 2019-01-02 03:04:05 とシリアライズしている。

2023-05-06T14:41:39.343913Z    10 Query INSERT INTO `time2` (`id`, `time`) VALUES ('BUN Asia/Tokyo 2019-01-02T03:04:05', '2019-01-02 03:04:05')

どうやらこれは意図的な変更の結果らしい。mysqlやgo-mysql-driverなどと二重変換してしまって正常に動作しないという問題があったようだ(←わかってない)。

https://github.com/uptrace/bun/issues/168

とにかく、じゃあどうやれば安心してDATETIMEを扱えるんだよという話になるんだけど、mysqlとまたがる範囲で暗黙的な変換が入ると状態の把握が難しくなるので、goのアプリケーション側で確実にUTCにしちゃってからORMなりクエリビルダーなりに放り込むことにした。そうすれば少なくともバグったときにfmt.Printfでなんとかなる。

goとmysqlの間で苦しんでる人はたくさんいた(類似記事が多い)がbunの話してる人は全然いなかったのでテキトーに書いてみた。

おまけ dockerのmysqlでgeneral_logを見る

適当なファイルに以下を書いておいて

[mysqld]
general_log=1

そのファイルが入ったディレクトリを、コンテナの/etc/mysql/conf.dにマウントする

-v /path/to/cnf-dir/:/etc/mysql/conf.d

コンテナ起動後にコンテナに入ってファイルの場所を探してtailする

nerdctl exec -it <container_id> /bin/sh
mysql -u root --host 127.0.0.1 -p
> SHOW VARIABLES LIKE '%general_log%';
> exit;
tail /path/to/general.log

なんで general_log_file 使わないの?

なんか general_log_file=/var/log/mysql/general.log するとmysqlの設定値はそこになるんだけどファイルが作られないんだよね。パーミッションの問題とかあるのかな。

上記の方法でやるとログファイルは /var/lib/mysql/hoge.log に生える。

Ubuntu 23.04への式年遷宮 トラブル録

普段はLTSしか使わないのだが、なんかやりたい気持ちになったので珍しく奇数系を入れてみた。

https://www.ubuntulinux.jp/News/ubuntu2304-ja-remix

をUSBメモリに焼いて(今はUbuntu公式でもEtcher使えって書いてるんですね、昔はUbuntuにUbuntuのイメージを焼くためのソフトウェアが付属していた気がするが)、PC再起動。

インストールの手順については、特に変化はなさそう。今回はLVM使用、ディスク暗号化をつけてみた。

gnome-text-editorで日本語入力できない

毎度の鬼門(と言いつつ、熱心な日本Ubuntuユーザーが結構情報を残してくれているので言うほどではない)。こちらのページが参考になった。

https://www.kkaneko.jp/tools/server/gnome_ja_input_method.html

注意点として、23.04では(正確には22.10から)はgeditに代わってgnome-text-editorというソフトウェアが入っている。こいつがfcitxとの相性が悪いのか、日本語変換が一切効かない(日本語入力モードに入れない)。シェルから開いてみるとこんなエラーメッセージが残されていた。

(gnome-text-editor:9858): Gtk-WARNING **: 03:13:56.134: No IM module matching GTK_IM_MODULE=fcitx found

意味はよくわからないのだが、fcitxを使えと言われているがgnome-text-editorがそれを理解できていないように見える。実際にはgnome-text-editorのリポジトリにこのようなエラーメッセージはなさそうなので、GTK自体に何らかの問題があるのかもしれない。

20230507追記

fcitxじゃなくてfcitx5を入れると問題が起きなかった。

vscodeで日本語入力時に確定するまで文字が表示されない

確定するまでは変換ボックスを見ないと入力中の文字が確認できず、Enterで確定すると一気にエディタ上に文字が追加される状態になった。これはUbuntu Softwareからデフォルトのsnapで入れていたのが悪いので、消してdebで入れ直したら直った。

bluetoothヘッドホンで音が鳴らない・途切れる・汚い

Linuxでbluetoothヘッドホンを使うのはコツがいる。僕が使ってるのはWH-1000XM4ね。

まず音が途切れるのはドライバのバッファリングが下手くそなせいなので、pavucontrol→出力デバイス→WH-1000XM4→高度な設定でLatency offsetを10msくらいにしてやる。ほんの少しでも設定してやるといい感じになる。

音が汚いのはコーデックがHSPになっているから。bluetoothヘッドホンの通信コーデックにはA2DPとHSPがあり、前者は聞く専用で高音質、後者はヘッドホンの付属マイクからの音声入力も可能だが音質は悪い。なのでA2DPに設定してやることで解決(これはbluemanからでも標準のbluetooth設定からでも、たぶんCLIからでもできる)。

音が鳴らないのは今回初めて遭遇した。HSPでの接続は可能だったがA2DPが選べなかったり、A2DPにすると音が鳴らなくなったりした。これは理由が不明だが、ペアリング直後にbluemanからA2DPに設定してやることで解決した。

ストレージデバイスのマウント

今回はなるべくGUIでやっちゃうのを目標にした。普段はストレージデバイスのマウントは/etc/fstabを手で編集してやっているが、今回は「ディスク」から。デバイス→パーティションを選択して、歯車マークのメニューから「マウントオプションを編集」でGUIからマウントを設定できる。ここで設定した内容がfstabに書き込まれていた。

やはり奇数系のUbuntuを使うのはエキサイティングだ。時間のあるときに限るが、楽しいんだこれが。

TypeScriptの引数を分割代入したときの ‘A’ is assignable to the constraint of type ‘T’, but ‘T’ could be instantiated with a different subtype of constraint ‘B’ みたいなエラー

T型の引数を受け取ってそのまま返す関数だが、返り値の型をTにすると型エラーになる。

const f1 = <T extends { user: { id: number } }>({ user, ...rest }: T): T => {
  return {
    user,
    ...rest,
  }
}
Type '{ id: number; } & Omit<T, "id">' is not assignable to type 'T'.
  '{ id: number; } & Omit<T, "id">' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ id: number; }'.(2322)

このエラーメッセージが難しい。 T extends { user: { id: number } } に注目する。extendsだからTはたとえば

type T1 = {
  user: {
    id: number;
    name: string;
  }
}

かもしれない(狭い型)。そうだとしても、関数内でのuserの型は { id: number } だ。nameがあることにはならない。TypeScriptならわかるかと思ったがわからないらしい。

Tに何を渡されようと返り値のuserの型は { id: number } (広い型)なので、それを T["user"] (狭い型)に渡しても要求を満たせないことがある。だから型エラーになる。

という話はここに書いてあるのだが
https://github.com/microsoft/TypeScript/issues/33579#issuecomment-534789400

このように書くと引数の型を維持できる。

const f2 = <T extends { user: { id: number } }>({ user, ...rest }: T): Omit<T, "user"> & { user: T["user"] } => {
  return {
    user,
    ...rest,
  }
}

const b = f2({user:{id: 1, name: "chao", premium: false}})
const p = b.user.premium // アクセス可能

TypeScriptくんは書き方によって推論ができたりできなかったりする。かわいい。

サーバーゼニ節約&kubernetes入門

ふと毎月のサブスク費用を確認したらAWSとさくらインターネットで月額5000円くらいかかっていて、いくらなんでも辛いのでこれを圧縮する。

原因は必要以上に多くのサーバーを稼働していること。現在稼働しているのは3つ。

さくらインターネット 2GB

  • 1738円/月
  • 2017年から借りており、中途半端な技術的挑戦の残骸が最も多い
  • 金額に対するスペックが良い

積載物

AWS EC2 t2.micro

  • 1544円/月

積載物

AWS EC2 t4g.micro

  • 1076円/月
  • AWS謹製のGravitonプロセッサなので安くて高性能。ただしArm。

積載物

  • gotosocialインスタンス

移行計画

どうしたものか…

とりあえずEC2は新世代ほど高コスパの原則があり、t2.microはt4g.microに移行すべき。自分の学習を考えるとさくらはやめてEC2に揃えたい。全部t4g.microに相乗りを狙ってみようか。

そしてどうせならkubernetesに全部載せてみたい。複数のアプリケーションを相乗りさせるときのトラフィック管理をこれまではjwilder/nginx-proxyでやっていたが、もうちょっとナウいやつに移行して監視とかも統一的にやりたい。

自分用ActivityPubインスタンスとしてGoToSocialを立てた

ツイッターがいよいよヤバそうなのでActivityPubのインスタンスを立てたくなった。まずMastodonを検討したが、リソースの要求が高いので諦めた。不慣れなRoRだしTypeScriptじゃないし構成も複雑なので個人で管理・改造するのは負担が大きいというのもある。

改めてActivityPub実装の中で軽量なものを探したところGoToSocialが良さそうだったので立てた。AWS EC2のt4g.microで現状問題なく稼働している。と言ってもまだフォローが少ないからかもしれない。ActivityPub、というか分散型SNSという仕組み上フォロー関係が増えると加速度的に通信が増大していくはず。

GoToSocial

GoToSocialはGoで書かれている。シングルバイナリで吐かれるアプリケーションとsqliteで動き、公式に提供されるdocker-compose.ymlを少し調整してリバースプロキシとしてnginxを立てるだけで動かすことができた。

現段階ではアルファリリースという位置づけであるが、大部分の基本的な機能に問題はない。今自分が把握している問題は以下の2つだ。

  • 動画が表示されない
  • notestockが利用できない
    • 利用を開始するためのnotestockのbotアカウントへのメッセージ送信がエラーで失敗する
    • mastodonには送れるのでnotestock側に何か問題があるのではと思っているが、notestockのソースコードは公開されていないので詳細不明

フロントエンドをほとんど持たず、サードパーティのクライアントから叩かれるAPIサーバーに専念している。推奨されているクライアントはpinaforeだが、pinaforeは2023年1月に開発の停止が発表された。webクライアントに限ればこれが最も盛んに開発されていたので先行きが不安だ(しかし作者自身が述べているようにpinaforeもだいぶ技術スタックが尖っていて将来性は怪しかった…)。ActivityPubデビューする人は最初にサーバーに登録してからクライアントを導入するだろうが、GoToSocialはユーザー登録用のフロントエンドも持っていないので一般に公開するにはハードルが高めかなと思う。自分はdockerコンテナに入ってCLIからアカウントを作成した。

自分でちょっとした改造を試みたときにdocker buildが大量のメモリを食ってEC2が落ちた(2GBでは足りなかった)。Dockerfileからswagger関連の処理をゴリッと消してやると直る。

Raspberry Pi Zero WHで気温センサBME280を動かすときの罠

前提

https://github.com/SWITCHSCIENCE/samplecodes/tree/master/BME280/Python27 を動かすときに。

最初に結論

  • 依存パッケージはsudoで入れよ
    sudo pip install -r requirements.txt
  • I2Cを有効化せよ
    sudo raspi-config
  • プログラムはsudoで実行せよ
    sudo python bme280_sample.py

以下は蛇足。どういう経緯で結論に至ったか。

ImportError: No module named smbus2

pip install -r requirements.txt

すれば済む話に見えるが、実は

sudo pip install -r requirements.txt

の方がいい。理由は後述。

OSError: [Errno 2] No such file or directory: '/dev/i2c-1'

pip install requirements.txt
python bme280_sample.py

するとこうなる。

理由はデフォルトでI2Cが無効化されているからで、解決策は Check! Raspberry Pi で I2C を使いたい(トラブルシューティング)にある通り

sudo raspi-config

だ。なお上記記事は2016年のもので、2020年現在(Raspberry Pi OS 5.4.79+)では若干違う。I2Cの設定は 3 Interface Options の中にある。

さて、I2Cを有効化して改めて

python bme280_sample.py

するとどうなるかというと、こうなる

OSError: [Errno 13] Permission denied: &#039;/dev/i2c-1&#039;

じゃあsudoにすりゃいいんだろと思って

sudo python bme280_sample.py

すると

ImportError: No module named smbus2

になる。

pythonのモジュールシステムにはあまり詳しくないが、sudoで起動するpythonは別の場所から依存パッケージを探してるのかな?というわけで

sudo pip install -r requirements.txt

すると動くようになった。

20200426

食事

朝なし昼スパゲッティ夜カレー

住居とは

外出をしない土日だった。自宅にこもっていても生存できる、それはつまり、この狭い部屋に人間が生きるために必要な機能を密集させ、さらにそのために必要なリソースを逐一引いてきているということだ。想像するだけですごい手間だ。共通化が禁じられたプログラミングのようなものだ。

そこまでやらなくてもいいんじゃないかと思う。たとえば風呂とトイレを集約するだけで配管と掃除の手間は激減するのではないか。その代わり排泄と入浴のタイミングが完全に自由ではなくなるが、それほど重大なこととは思わない。家賃が安くなるのならばそうしてほしい。

ScalaでDBを使いたい

温度管理サーバーは現状データをCSVに保存しているが、ダサいのと勉強のためにちゃんとDBを使いたい。ということでslickを導入しようとしている。今はチュートリアルをやっている。

たぶんapplication.confのurlを適切に設定すれば自分が用意したDBに接続できるはずで、docker-composeとかで用意すればいいかな。

同時に開発環境も整備した。これまではdockerコンテナの中で動かしていたが、それをやめてマシン上にJavaとsbtを入れた。このとき両者のバージョンを正確にこれまでのものと合わせないと依存関係が解決できずvscodeのmetals拡張が壊れる。sdkmanを使うと楽。

iceiceiceのDota2

今日はBTS Pro Series: Southeast AsiaというDota2の大会の決勝があった。名前の通り東南アジアのプロチームが参加する大会だ。決勝戦はFnatic vs TNC。僕はFnaticのiceiceiceのプレーを見ていた。

決勝戦第3試合の彼のNature's Prophetの動きが素晴らしかった。Nature's Prophetは一切の下準備なしにマップの任意の場所にテレポートするスキルを持つ、ある意味Dota2のルールを逸脱したヒーローだ。iceiceiceはこのヒーローの性能をフル活用してマップを縦横無尽に移動して支配し、チームを勝利に導いた。

人間性能も凄い。攻撃動作によって透明状態を解除しようとしたときに敵が視界に入り、瞬時に反応して攻撃動作をキャンセルするシーンがあった。iceiceiceは29歳、プロゲーマーとしてはかなり年長なのだがすごい反応速度だ。

20200414

食事

朝なし
昼我流ペペロンチーノ
夜レーズンパン・カントリーマアム・ヨーグルト

house-temperature-core

Github Actionsによる自動デプロイが完成した。

https://github.com/chao7150/house-temperature-core/blob/master/.github/workflows/main.yml

  1. docker build & docker push
  2. docker-compose.ymlをscp
  3. sshしてdocker-compose up

という3段構成になっている。リモート側のディレクトリのオーナーがなぜかrootになっていてscpの書き込みに失敗し、しかもそれがエラーメッセージからわかりにくかったのですこし手間取ってしまった。

Scala+Playのproduction buildは速い

https://github.com/chao7150/house-temperature-core の話。

以前droneで自動テスト(sbt test)を試みたときはScalaやsbtのダウンロードでとんでもない時間(30分以上)がかかってしまって実用的ではなかったのだが、今回Github Actionsでsbt distを実行したところ1分くらいで終わった。何も調べてはいないが、とりあえず経験知の共有として。