emahiro/b.log

Drastically Repeat Yourself !!!!

time.Now() を使ってテスト結果の比較をするときにCreatedAt に幅を持たせる

知らなかったので備忘録です。

Overview

go-cmp を使用して DB の Insert のテストを記述するとき time.Now() を使ってしまうと生成したデータと比較対象のデータでレコードの作成時刻( created_at )で誤差レベルの Diff が生じてしまいます。
※ 生成時刻の time.Now() と比較したい対象(Goでは慣習的に want を使うことが多い)が生成されるタイミングの time.Now() で Call されるタイミングが微妙に異なるから(と言っても ns, ms レベル)

どうやるのか

以下の zenn のエントリに書いてあるとおりなのですが、go-cmp の cmpopts.EquateApproxTime という option を利用します。

zenn.dev

option の引数に指定した duration の分は誤差が合っても同じものとして判定される( Diff の判定をゆるくする)ので Insert や Update のテストで利用することができます。

SeeAlso

pkg.go.dev

次世代ターミナル「Warp」を試したらとても便利だった

Overview

最近 Warp というターミナルエミュレーターを使い始めてみて、これがとても便利だったのでその紹介エントリです。

ちなみにこのターミナルエミュレーターを知ったのは以下のツイートです。

Warp とは

21 世紀のターミナルというなかなか大胆なキャッチフレーズを使っているターミナルエミュレーターです。

www.warp.dev

なお、個人利用であれば現時点では「無料」です。

www.warp.dev

GitHub で keyset や theme も公開されていますし、Document も公開されているので基本的な使い方に迷うことはありません。
Wezterm というターミナルエミュレーターも試したことがありますが、最近のターミナルエミュレーター系のツールはドキュメントがちゃんと揃えてあっていいですね。

github.com

docs.warp.dev

便利だったポイント

まだ使い始めたばかりですべてわかってるわけではないですが以下の3つの点は非常に便利です。

  • Fig を使っていたときのような引数の Suggestion があること(特にブランチ移動したいときに git branch + tab で local のブランチを補完してくれるのは役に立ちます。)
  • コマンドの実行結果の区画をそのまま Clipboard にコピーできること。あるコマンドの実行結果部分を Drag & Drop しなくて良くなります。
  • 生成AI系機能。
    • Warp AI という機能があって、今からどんなコマンドを実行したいのか、ということを説明するとそれを実行するためにコマンドを教えてくれます。ChatGPT がターミナルに組み込まれているようなものです。また、実行したコマンドがちょっと間違っていた( git を typo して gti と叩いてしまう、みたいなやつ)場合に、「このコマンドを叩きたかった?」ということを Suggestion してくれます。

追加でこれは個人の使い方に依存する部分ですが、VSCode とのインテグレーションがあります。
詳しくは以下の動画を見てほしいのですが、例えば Go のテストコマンドを実行したときに、失敗したり、エラーが発生したファイル+行数の表示がスタックトレース等に表示されたときに、予め VSCode と連携しておくと、そのファイル名をクリックするだけで VSCode を立ち上げて(すでに立ち上がっている場合には VSCode のウィンドウがアクティブになり)、該当のファイルが Open されます。これは iTerm でも Wezterm デモできなかったことで、VSCode 組み込みのターミナルのみで実現できたことでした。
独立したターミナルエミュレーターからこのアクションが実行されるというのは、普段 VSCode を利用しているユーザーとしては非常に嬉しい機能です。

www.warp.dev

そもそも Warp を使い始めた理由

自分のターミナルエミュレーター遍歴は、

純正 -> iTerm2 -> iTerm2+Fig -> Wezterm

というものなのですが、iTerm の起動が遅くなり、最近話題だった Rust 製のターミナルエミュレーターとしてドキュメントも充実していた Wezterm を使い始めており一定満足していたのですが、Fig を使っていた頃の自動でコマンドを保管してくれる UX の再現を諦めていたので、それを再現したいと思っていたところ、Warp だと iTerm2+Fig を使っていたときに近いものが得られました。

ただ、Fig を使っているとただでさえ重たい iTerm が更に重たくなったことと、Fig のような追加ツールを入れるのが後々のPC設定のポータビリティ性を考えると面倒くさいなと思ったので外してました。(それでも基本的なコマンドしかいつも使わないので困ることはなかったのですが。)

また、各種シェルスクリプトのコマンドの option 等々を覚えるのが苦手だったのもあり、Warp に実装されてる「やりたいことを説明するとコマンドを生成して提案してくれる、いわゆる生成系的な振る舞い」が思いの外便利ということにも気づきました。引数の option とか毎回ググってますからね。。。
生成系AI、本当にエンジニアリングの作業効率に寄与しすぎててすごいです(語彙力)

まとめ

まだ使い始めたばかりですが、特段凝った設定なしにかなり自分のやりたいことができますし、何よりブラウザでググらずとも良い、という体験が一層自分のターミナル体験を良くしてくれている実感があります。
使いながら色々探してみたいと思います。

Go で Response Header を差し込む

Overview

大した話じゃないですが久しぶりに http を色々ゴニョゴニョするコードを書いていたら忘れていたので備忘録

Response Header を差し込む

※ サンプルは httptest の Server を立ち上げています。

// application/json を Response Header の Content-Type に差し込む
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusOK)
                _, _ = w.Write([]byte(jsonString))
}))

ただし、WriteHeader関数最後に呼ばないと Response Header の情報が上書きされてしまいます。

以下のサンプルでは application/json が指定されず、 plain/text が Response Header の Content-Type に指定されてしまいます。

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
                w.WriteHeader(http.StatusOK)
                w.Header().Set("Content-Type", "application/json")
                _, _ = w.Write([]byte(jsonString))
}))

これは WriteHeader の副作用によるもので公式のドキュメントにも記載されています。

// Changing the header map after a call to WriteHeader (or
// Write) has no effect unless the HTTP status code was of the
// 1xx class or the modified headers are trailers.

ref: https://pkg.go.dev/net/http#ResponseWriter

このため、Content-Type を指定して mock server 等を立ち上げたいときには、この振る舞いに注意することが必要になります。

余談

ResponseHeader を取り出す

これも大した話じゃないですが忘れてたので。以下のサンプルは Response Header の Content-Type を取り出します。

resp.Header.Get("Content-Type")

ちなみに以下でも取り出せますが、返り値が配列で返ってきます。
※ Go の http.Response の場合、Content-Type にはデフォルトで []string{"application/json;", "charset=utf-8"} 等 string の slice の状態で Header の map が格納されてるため。

GitHub Actions 上で Multi Platform 対応した Image を作る

Overview

GitHub Actions 上で Multi Platform に対応した Docker Image を作成して Docker Hub に push するまでの備忘録です。

GitHub 上で Multi Platform 対応のビルドをする

公式に手順が載っているのでこれをベースに Actions を構築します。

docs.docker.com

Docker build に渡す引数

path context をカスタマイズしたいときは以下の一覧に載ってるものを使います。

github.com

Docker Hub 側の設定

Github の Secret に DockerHub で生成したトークンを使用するので DockerHub 側でトークンを生成する必要があります。

設定方法は以下のように DockerHub の Account Setting の画面から行います。

実際の設定ファイル

1つ前のエントリで RedisCluster のイメージを作った話をしましたが、今回はそれを GitHub Actions 上でマルチプラットフォームに対応させて DockerHub まで Push するようにした設定ファイルは以下です。

name: multi platform build

on:
  push:
    branches:
      - "master"

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}
      - name: Build and push for Redis@6.2.11
        uses: docker/build-push-action@v4
        with:
          context: .
          build-args: redis_version=6.2.11
          platforms: linux/amd64,linux/arm64
          push: true
          tags: emahiro/redis-cluster:6.2.11

github.com

See Also

gokzen/redis-cluster を使って protected mode "no" で Redis Cluster を起動するまで

※ このエントリはある特定の Docker Iamge を利用して Github Actions で Redis Cluster を起動してアクセスできるようにするものです。他の Image を使っていたりする場合には特に有益な情報はありません。

Overview

  • grokzen/redis-cluster を利用して Redis Cluster にアクセスし、Go のテストができるようにします。
  • Go のテストには go-redis を利用します。

hub.docker.com

背景・前提

商用環境/検証環境を除く local 環境と test (CI込)環境において、ずっと Redis のコンテナと Redis Cluster を両方立ち上げていたのですが、すでに商用環境を始め remote の環境では Redis が Cluster mode で利用されており、すべての環境で統一して Redis Cluster を利用する方向に統一するために既存の Redis コンテナを削除したかったことが背景です。
ただし、前提として local 及び test 環境においては Redis Cluster を立ち上げておきながら、通常モードの Redis (Master1本構成)に接続しており、CIがこの通常モードの Redis に依存していたので、通常モードの Redis を引き剥がす際に CI の環境でも Redis Cluster を利用するように設定し直す必要がありました。

はまったところ

大まかに2つです。

  1. Redis Cluster の protected mode の問題。
  2. Redis Cluster 利用に合わせて Go のコードを修正。

Redis Cluster の protected mode の問題

  • Redis の protected-mode の設定 grokzen san の作ったイメージでは環境変数等でアクセスできず、GitHub Actions 上でこのイメージをベースとした Redis Cluster を立ち上げても Redis コマンドの実行を拒否されてしまう。
  • 以下の discussions でも話にはあがっているものの、ワークアラウンドがあるだけで、現状このイメージを使い続ける場合、自前でこの protected mode を off にする必要がある。

github.com

以下で issue も起票はされているが対応は未定らしい。
作者自身もイメージコピーして独自で対応してほしいとコメントしている。

github.com

対応Log

  • grokzen/redis-cluster を指定して GitHub Actions を起動するときに、 protected mode を no にする方法を調べる => 環境変数などは用意されておらず、issues でも対応予定はないっぽくワークアラウンドが提示されていた。
  • 他の image を使うことを検討する => 不思議なことに grokzen/redis-cluster 以外に割と使い勝手のいい image が存在しない。いくつか redis-cluster のコンテナを起動できるイメージはあるものの、どれも既存の設定を大きく変更する必要があり、作業量的にちょっとめんどくさかった。 grokzen/redis-cluster がそこまで利用しやすいのかと言われるとそうじゃないのかもしれませんが、Actions の設定項目の少なさはやはり魅力的で(他の docker hub に公開されている Redis Cluster を立ち上げる image 類は Actions にいくつか Redis のコンテナを立ち上げる必要があり、全部を記述するのが若干面倒。
  • groken/redis-cluster が立ち上がるときに実行してる設定ファイル (*.tmpl ファイル)を protected mode を no にするファイルを自前で用意して Volume をマウントする。=> GitHub Actions で service のコンテナにリポジトリのファイルをマウントしようとするとエラーになる で書かれてる内容と同じエラーに遭遇してマウントできなかったので諦める。
  • Redis Cluster のコンテナのコンテナを立ち上げてそのコンテナに向けて Redis のコマンドで操作することを試みる => GitHub Actions の環境そのままでは Redis Cluster の中に入れない + 仮に protected mode を更新できたとしても立ち上げてる Cluster 分設定を更新する必要があるのでめんどい(手順として壊れやすい ex ポート番号が変わったときに CI が全死)
  • Actions の内部で Redis Cluster のコンテナを操作することは難しかったので、取りうる方法としては以下の2つに限定される。
    • GitHub Actions の内部で Docker Image をビルドする => かなり時間がかかる。
    • grokzen/redis-cluster を fork して protected mode を on にして Redis Cluster を立ち上げる image を 自前でビルドする (結果としてこれを採用) => なお、そのまま M1 で Docker Image をビルドすると arm64 向けの Image がビルドされ GitHub Actions では動かない(GitHub Actions は何も設定しないと amd64 で動いているため)ので M1 でビルドするときは Platform を指定することで対応(Redis をそのままビルドするので M1 Max でも10minくらいかかりました。)
      • 実際に叩いたコマンドはこちら -> docker build --platform linux/amd64 . -t ${image_name}:${image_tag}
    • ビルドするときに最初は脳死で Redis バージョンを 7系にしてビルドしていましたが、6系と7系で API の互換がなく、コマンドの実行でエラーになる事がわかったのでビルドするときの大本の Redis のバージョンを 6系にしてビルドして利用しています。実際に自分が遭遇したエラーは issues にも記載されてます(ここで 6系にするワークアラウンドの話が出てるのが面白い...。)

github.com

そして出来上がったのが以下。
基本的な使い方は本家の設定と同じなので設定方法は割愛。

hub.docker.com ref: https://hub.docker.com/repository/docker/emahiro/redis-cluster/general

※業務で利用するものでもあるので、ちゃんとメンテをし続けるものは業務で使用している ECR に移行予定。特にこのイメージをちゃんとメンテしていくモチベーションは有りません。

ちなみに最後の最後で気づきましたが、 protected mode は Redis 7 系以降の機能なので、最初から Cluster mode で起動する Redis を 6 系にしていればよかったかもしれません。(つまい grokzen/redis-cluster の version を最新にしない、という選択肢です。)

go-redis の実装修正

大した話では有りませんが、もともと CI の環境では Cluster mode ではない Redis を使っていたので、Redis を利用してる実装のテストをする際にはテスト前後で Redis の中データを Flush する処理を追加しました。
ただし、Redis Cluster を起動するようになったので、ある node に対して FlushAll コマンドとを実行してもその node 上でデータをフラッシュしたことにしかならず、Cluster に所属する node 全体に FlushAll を実行しているわけではなかったので、 あるテストに置いてデータを事前に削除しても他のテストではまだ削除がされていない Node を引き当てて、データが残ってる状態でテストが実行されて既存のテストが壊れる、という事象が発生。
そのためもともと FlushAll を実行していた Go のテストのヘルパー関数の中で Node 全体のデーターを削除するように変更しました。すべての node に対してコマンドを実行する場合には https://pkg.go.dev/github.com/go-redis/redis/v8#ClusterClient.ForEachMaster を利用します。
ちなみにこのコマンドは以下の issues ではなされてる内容から生まれたもののようです。

github.com

まとめ

ただの備忘録ですが、Redis の最新バージョンでデフォルト ON になるセキュリティ系の設定を OFF にしたいだけのために結構回り道をしました。特に誰の約にも立つことはないと思いますが自分がいつか思い出したときのために書き残しておきます。

Mac ✕ OBS でゲーム配信をする

Overview

キャプチャボードを手に入れたので OBS を入れた Macで Switch のゲームをキャプチャして Youtube にアップロードするまでの手順を備忘録として記載します。

用意したもの

今回は同僚に以下の映像キャプチャを借りたのでこちらを使っていきます。

その他

  • Mac (何でも良いと思います。ただ、あとから気づきましたが、音声の取り込みが Mac の場合はひと手間かかるのでできれば PC は Windows が良いと思います。)
  • OBS

配線図

ざっくり以下のような構成で配信できます。

OBS の設定周り

ソースにキャプチャを加える。

ソースに「映像キャプチャデバイス」と「音声入力」を追加します。

このとき映像キャプチャデバイスの「デバイス」で 「Live Gamer Ultra」が表示されてるのでこれを指定すると Switch の画像が OBS に取り込まれるようになります。
キャプチャデバイスは映像だけでなく音声もキャプチャしてくれるので、「音声入力」のソースを追加し、同じく、「Live Gamer Ultra」を指定します。

映像キャプチャデバイスでは以下のようにフレームレートをプリセットではなくカスタムで 60 を指定しておくと良いです。

また、いかにも記載してますが、映像キャプチャデバイスでバッファリングを許可すると音ズレが発生します。

まとめ

結構簡単に画像がキャプチャして他の PF にアップロードできました。

せっかくなのでスプラのプレイ動画をあげるだけのゲームチャンネルを開設しました。

www.youtube.com

その他

その他に行った設定を箇条書きで追記します。

  • 設定 -> 出力タブの「録画」の項目の中で録画フォーマットを変更できるので mp4 にしておくのが無難。
  • 設定 -> 映像タブの出力解像度を 1920✕1080 に。

ハマったところ

「映像キャプチャデバイス」のバッファリングを ON にすると音ズレが起きる

書いてあるとおりなのですが、キャプチャボードのバッファリング設定を ON にすると映像の中で音ズレが発生しました。バッファリング設定はしなくても良いです。

Mac は音声入力がしづらい。

BlackHole などの仮想音声デバイスを使わないと Mac はゲームとPC の両方の音源を OBS に取り込むことはできません。
結構ややこしいので自分は一旦PCの音声をゲームの録画と合わせて配信するのはやめました。

Go1.20 から入った errors.Join が実は便利そうだった

Overview

  • Go 1.20 から errors パッケージに導入された errors.Join は複数のエラーを詰め込んでも詰め込んだエラーそれぞれを Unwrap して取り出せる。
  • fmt.Errorf でも複数のエラーを wrap できるようになっている。
  • 複数のエラーを wrap してもそれぞれ unwrap して取り出せるので Is 判定を使うことができる。

errors.Join について

Go1.20 から導入された機能です。

pkg.go.dev

以下のフューチャーさんのブログで丁寧に解説されてますが、error のラップの方法がちょっと便利になる関数になります。

future-architect.github.io

もともと hashcorp が go-multierror というマルチエラーを生成するライブラリを出していましたが、これを公式が実装したような感じかなと理解しました。

具体的には以下のようなコードにおいて、error を1つのエラーにまとめつつ、その先で特定のエラーを unwrap で取り出して検査することが可能になります。

package main

import (
    "context"
    "errors"
    "fmt"
)

func main() {
    err1 := errors.New("err1")
    joinedErr := errors.Join(err1, context.Canceled)
    fmt.Println(errors.Is(joinedErr, context.Canceled))
}

ref: https://go.dev/play/p/XC0MXYbZg0a

これでわかるように Join と言ってるので連結してるのか、配列に追加してるのか、といった印象を若干持ってしまいますが、実態としては wrap をしています。

これは実際の実装を見るとわかり易なと思いました。
現時点における errors.Join の実装は以下なのですが、これを見ると Join といいつつ、参照を入れ子にしていっているのがわかります。

https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/errors/join.go;l=13;bpv=1;bpt=0

ひたすら joinError 型の持つ errors の配列につ生かしていってる感じですね。

例えば今までだと loop の中で連続した処理をする(外部API を叩いたり)場合に、error の slice を作成したりして、loop の中で逐一エラーをハンドリングしたりせず、発生したエラーをその配列に貯めておいて、後でまとめてエラーをハンドリングする、といった処理があったかと思いますが、 errors.Join を使うと追加するだけでなく、追加したエラーの中で Unwrap してみて特定のエラー型のエラーが有った場合のみ例外処理を入れる、みたいなちょいとリッチなハンドリングを入れることも可能です(なお、errors.Join の繰り返しは単純な error slice への append よりも多少コストの掛かる処理になります。後述)

また errors.Join を利用したケースでは以下のようにエラー文言の出力に改行が入ることになるので、wrap する方向を決めておけば、Layered Architecture を採用してるコードベースなどで下層からエラーを伝播させてくるときに、どこでどんなエラーが発生したのかを追跡するのに多少寄与することもあるかもしれません。とはいえ、こういったことは実装方法に依存するので、こうすればいい感じになる、といったプラクティスは自分でも今現在持ち合わせてはいないんですが...。

package main

import (
    "errors"
    "fmt"
)

func main() {
    var errs error
    var errlist []error

    e1 := errors.New("1")
    e2 := errors.New("2")
    e3 := errors.New("3")

    for _, e := range []error{e1, e2, e3} {
        errs = errors.Join(errs, e)
        errlist = append(errlist, e)
    }

    fmt.Printf("%v\n", errs)
    fmt.Printf("%v\n", errlist)

}

ref: https://go.dev/play/p/-35UMpxZ9B3

パフォーマンスについて

実業務で errors.Join を実際に使用するときに追加(wrap) していくときのコストが気になったので合わせて調べてみました。
というのも、上記のユースケースで提示したような loop の処理の中で発生した error をJoin していく場合、loop の回数が多くなるような実装だと、error の追加(wrap) そのものにコストが掛かるのではないか、という仮説があったので実際に計測してみた、という流れになります。

比較したサンプルコードは以下の2つです。

  • loop の中で errors.Join を連続して行うパターン。
  • loop の中で errors の配列に append していくパターン。
package main

import (
    "context"
    "errors"
    "fmt"
    "testing"
)

func Benchmark(b *testing.B) {
    b.ReportAllocs()

    b.Run("slice", func(b *testing.B) {
        b.ResetTimer()
        var errs []error
        for i := 0; i < b.N; i++ {
            err := errors.New(fmt.Sprint(i))
            errs = append(errs, err)
        }
        _ = errors.Join(errs...)
    })

    b.Run("join", func(b *testing.B) {
        b.ResetTimer()
        var errs error
        for i := 0; i < b.N; i++ {
            err := errors.New(fmt.Sprint(i))
            errs = errors.Join(errs, err)
        }
    })
    b.Run("slice unwrap", func(b *testing.B) {
        var errs []error
        for i := 0; i < 1000; i++ {
            err := errors.New(fmt.Sprint(i))
            errs = append(errs, err)
        }
        errs = append(errs, context.Canceled)
        err := errors.Join(errs...)

        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            _ = errors.Is(err, context.Canceled)
        }
    })

    b.Run("join unwrap", func(b *testing.B) {
        var errs error
        for i := 0; i < 1000; i++ {
            err := errors.New(fmt.Sprint(i))
            errs = errors.Join(errs, err)

        }
        errs = errors.Join(errs, context.Canceled)

        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            _ = errors.Is(errs, context.Canceled)
        }
    })
}

このベンチの結果は以下でした。

goos: darwin
goarch: amd64
pkg: sample.com/go-sandbox
cpu: VirtualApple @ 2.50GHz
Benchmark
Benchmark/slice
Benchmark/slice-10               3439363               332.0 ns/op           146 B/op          2 allocs/op
Benchmark/join
Benchmark/join-10                5206392               219.2 ns/op            88 B/op          4 allocs/op
Benchmark/slice_unwrap
Benchmark/slice_unwrap-10          53870             22331 ns/op               0 B/op          0 allocs/op
Benchmark/join_unwrap
Benchmark/join_unwrap-10           21345             58111 ns/op               0 B/op          0 allocs/op
PASS
ok      sample.com/go-sandbox   7.394s

これだけ見ると errors.Join を利用するほうが良さそうに見えるんですが、これ M1 の結果なんですよね....

別で x8664 でやってもらったときは errors.Join より slice への append の方が圧倒的に早かったので、大量の loop を通常の production 環境で利用する場合は slice の方がコストが安い、ということは覚えておいて良いかもしれません。
大体の production で動いてる実行環境というのは x8664 環境であると思いますので....。

Practice

この Go1.20 から追加された errors の新機能を実際にどう使っていくのがいいのか、ということなんですが、以下のエントリが非常に参考になりました。

特にこの In Practice に記載されているユースケースは実際のプロダクト開発をするときにエラーハンドリングとしても参考になることが多いのではないかなと思いました。

lukas.zapletalovi.com

HTTP ステータスのエラーコードごとに表示するエラー文言を変更したいユースケースなんてのは自分もプロダクト開発していてもぶつかることが多いですし、サンプルにあるような実装にできるのであればエラーの表現力にも寄与しそうだなと思いました。
それより何より fmt package の errror 生成で複数エラーを wrap できる、というのはそれ自体でとても便利だなと思いました。

まとめ

Go1.20 については実はそこまで追いかけてはいなかったのですが、調べると結構今までめんどくさかったなと思っていた error ハンドリングに対してまた1つ改善が追加されていました。

errors.Join は使えるユースケースが広そうなので、活用していきたいなと思います。

git restore を使う

Overview

今日教えてもらったのだが、git の差分で余計な差分(時刻の更新とか)が出てしまったときにわざわざ file changes に表示させてレビューのノイズにしたくないときには git restore コマンドを使うと良い。

tracpath.com

使い方

一つ前に戻す

git restore -s HEAD~1 $filePath

特定のコミットに戻す

git restore -s $CommitHash $filePath

感想

自動生成コマンドの副産物としていらない差分を Pull Request 煮含めてしまうことがよくあるのでこの git restore コマンドはそういうときに便利だなと思いました。

Git は毎回発見があって面白い。

Chrome Extension に入門する

Overview

業務で Chrome Extension を使ってちょっとしたツールを実装する機会があったので備忘録としてまとめます。

基本的な作り方は以下のエントリを参考にさせていただきました。

r7kamura.com

Chrome Extension のいいところ

そもそも Chrome Extension を作ってみようと思った理由でもあるのですが、以下のような理由で作れると良いなと思いました。

  • Chrome を使ってさえいればブラウザ上で便利ツールを実装できることです。
  • ディレクトリ構成は JS と manifest ファイルさえあればいいので、余計なセットアップ等々も考慮することなく(ビルドプロセスとか)とりあえず動くものを実装できる。
  • 現職の場合、業務レベルではブラウザは Chrome が推奨なので Chrome 向けの対応さえしておけば簡単なツールは簡単に全社展開できること。(まぁ Chromium 使ってるブラウザであれば同様に対応できるので Firefox か Saferi を使っているユーザーでなければ対応できてしまいます)

かける労力に対して得られるアウトカムが思いの外大きいのは Chrome Extension のいいところかなと思いました。

基本的な実装の進め方

以下の公式のドキュメントを見つつ進めました。

developer.chrome.com

特に chrome のランタイムで必要な API を Call するときには permissions を設定する必要があるので都度権限が必要化を調べる必要があります。

いくつか触ってみた API

chrome.getURL

developer.chrome.com

local のファイルを取得するときもこれを使用します。

参考は以下

stackoverflow.com

URL を生成してそれを fetch で呼び出すので Promise が使えたりと便利です。これをするには manifest ファイルの web_accessible_resources にアクセスしたいファイルパスを予め定義しておく必要があります。

今回仕事で実装したツールについてはある条件においてリンクを別のリンクに張り替える(location.href を書き換える)というものだったので、そのマッピングデータを Extension で実装した local のデータファイルに定義しておいて、その定義ファイルを読み込む、ということをしていました。

サンプルは以下です。

  "web_accessible_resources": [
    {
      "resources": ["$pathToLocalFiles"],
      "matches": ["http://*/*", "https://*/*"]
    }
  ],

matches は Chrome Extension が動作する URL を正規表現で指定します。基本的にどの URL でも読み込みだけさせるには上記のような全公開的な正規表現にすればいいと思いますが、大体はドメインくらいは限定しておいたほうが良いかもしれません。

chrome.declarativeNetRequest

※ permission の設定が必要です。

ページをロードする(window.onload が走る)タイミングで動的になにかの処理を対象のページに施すことが出来ます。

ex. あるURL にアクセスされたら別の URL にリダイレクトしたりとか、あるURLのブロックをしたりとか。

どういった操作ができるのかは以下の公式ドキュメントを眺めるとなんとなくわかります。

developer.chrome.com

はまったところ

Manifest ファイルは V3 を利用する

ネット上で Chrome Extension の実装方法を探すと過去の実装方法をベースにしたものも多く見つかります。
パッケージ化しない(= 公開しない)ものであれば V2 も使って実装できるっぽい(エラーは出るけど)ですが、今回はちゃんとしたものを実装するために V3 をつかった 実装方法

Manifest では 実装した Extension が発火するページを制御できる

正規表現にマッチしたページのみで Chrome Extension を発火させることが可能です。

以下のドキュメントが詳しいです。

developer.chrome.com

Chrome Extension には Chrome 独自の module を使う

※ この module は import 宣言をせずとも実行環境が Chrome 上だと書くだけで動くので eslint 等を入れた場合 no-undef の警告に引っかかってしまいます。

そのため、eslint 等を導入する場合は no-use-before-define を off にしておく必要がありました。

以下がそのために追加した linter の定義ファイルです。

module.exports = {
  env: {
    browser: true,
    commonjs: true,
    es2021: true,
  },
  extends: 'airbnb-base',
  overrides: [
  ],
  parserOptions: {
    ecmaVersion: 'latest',
  },
  rules: {
    'no-restricted-globals': 0,
    'no-use-before-define': 0,
  },
};

Local の Debug がしづらい

これは仕方ないことですが、 local では Chromeエミュレーターを簡単に用意することは出来ないので、簡単なツールであれば実際に開発中のリポジトリChrome拡張機能にそのまま突っ込んで動作確認をしていました。

Chrome の設定から拡張機能を開いて上記のパッケージ化しないでアップロードする対象に manifest をルートとしてリポジトリをそのまま指定すれば Extension をインストールしたことになります。
拡張機能側でエラーが発生した場合もこの拡張機能一覧のページにエラーが発生したことが表示されるので、JS のエラー等々は確認できます。

まとめ

JS さえ使えて、かつパッケージ化しないクローズドな環境(社内限定とか)で使う Extension であれば、かなり簡単に Chrome Extension は実装、導入することが出来ました。
API もふんだんに用意されていて、結構自由度が高いのでこれからもなにか機会があれば Extension を実装して解決するようなアプローチをしていきたいなと思いました。

See Also

developer.chrome.com

qiita.com

Keychron を使い始めた

TL;DR

  1. Keychron Q2 (茶軸を使い始めた)
  2. キーマップのカスタマイズ便利 via Ramap

買った経緯

10年弱くらい HHKB を愛用していましたが、Bluetooth の相性がやっぱり Mac とあっておらず、タイピングにラグがあったりと特に M1 にしてから不安定さが以前より増したような気がしていたので、ちょっと一回別のキーボードを使ってみようかなと思ったのが始まりです。
有線版の過去の HHKB も手元にあるにはあるのですが、Type-S (BT含む)の打鍵感に慣れてしまうと有線版の打鍵感に戻れなくなってしまったので、結局一度 HHKB から離れてみよう、と思うに至りました。

その中でどれを使おうか考えていたのですが、以前からちょっと気になっていた Keychron の Qシリーズの中で Q2 の茶軸を購入しました。


使ってみての感想

使ってみての感想は以下です。

  • 茶軸の打鍵感は HHKB より軽い。
  • 右シフトキーの位置が HHKB と異なっていてまだ慣れない。(後述するキーマップの更新で↑と右シフトキーボタンを交換しようか迷ってやめました。)
  • 有線の方が入力自体は安定する。
  • 有線にしたことで Bluetooth の接続端末を一つ減らしたらマウスの方の接続とレスポンスが安定した。
  • HHKB から移行する場合はデフォルトのキーマップで Ctrl の位置に CapsLock があるのでこれは交換しないとキーの配列が全然慣れない。
  • HHKB は結構特殊なキー配列だったことを初めて知りました。
  • 自分でキー配列をカスタマイズするのが結構楽しくてキーボード沼にハマる人の気持ちが少しわかりました。

キーマップのカスタマイズについて

Keychron は QMK/VIA の規格に対応してるので外部ソフトウェア(公式では VIA が記載されています)を使ってキーボードのキーマップをカスタマイズ可能です。

keychron.jp

何故か自分はこの手順通りに進めても端末(キーボード)を認識してくれずカスタマイズの方法をどうしようかと考えていましたが、以下のエントリに助けられました。

salicylic-acid3.hatenablog.com

QMK を利用するなら現在は Remap と呼ばれるツールを使うのが良いらしいです。これは VIA と異なり、Webアプリでもあるのでこれはこれで、クライアントアプリを追加しなくて良いのは良いなと思いました。 また OpenID でのログイン機能があるので、端末(ブラウザ)を変えてもいくつか引き継げそうな設定があることも魅力でした。
実際にカスタマイズを進める手順ですが、以下のキーマップ設定の JSON ファイル集から Q2 JIS を選択します。(日本語のサイトだと Q2 JIS のリンクは載っていません)

Remap で DL した JSON ファイルをアップロードして以下のようなファイルを追加すると以下のようなマップの画面になります。

www.keychron.com

その中でどのキーに何を割り当てたいかは Drag&Drop で設定可能です。割当を行ったら書き込み(Flush)処理を行うと Key の設定が Flush されます。

かなり簡単にキーマップ設定の追加が行えました。

『Google のソフトウェアエンジニアリング 9, 10章』

久しぶりに読書録です。

Overview

Google のソフトウェアエンジニアリング』を2022年内で通読したので、いくつか気になった章の簡易的なまとめを備忘録として記載します。
ちなみに現職の社内勉強会の一環として輪読会を開いておりその中で読み進めている内容になるので、まとめる順序は章立てに準拠はしておらず、また内容も自分自身の解釈を踏まえた主観を多分に含みますのでその点は保了承ください。

今回は特に印象的だった 9,10 章についてまとめます。

Memo

9章: コードレビュー

  1. コードは債務である と認識しろ。将来の誰かの保守運用コストがかかる。
  2. 債務である前提を元に変更(新機能開発含) が どうして必要なのかをコードを書く前に徹底的に議論 しろ。
  3. 開発者は各々がオーナーになるくらい言語特有のリーダビリティに精通してるべき(これがレビューのリードタイムを短くする)
  4. コードのレビュー時に意味の把握(複雑さの低減)or 機能性(パフォーマンス)を向上させる以外に個人の意見を理由として代案を出すべきでない。
  5. 変更は小さく、明確に説明するべし。
  6. コードレビューは 知識共有の枠組み 。
  7. コードを書く側にもプロ意識を期待する。
  8. 自分という存在と自分が生み出したコードは別物。 自分の作った差分は自分のものではなく「チームのもの」。
  9. 人間がやってる機械的なタスクは自動化せよ。

コードレビューのセクションはメンタリング的な内容が多い。

特に良かった考え方は「コードは負債であること」と「コードの作者(実装者) にもプロフェッショナルの意識を求めること」という部分。
Reviewer, Reviewee に力関係は存在しないし、自分が生み出したコードと自分という存在を分けて考えることは、絶対に身につけておきたいスタンスでもあるので、それがちゃんと記載されている、ということは一般的なベストプラクティスとして Google でも認知されているということで、自分自身のスタンスに自信を持つことができました。

10章: ドキュメンテーション

  1. ドキュメントをコードのように扱う。
  2. 組織とともにスケールし、既存のワークフローと調和するようなプロセスとツールを導入することで質の高いドキュメンテーションを継続できる。
  3. ドキュメントの価値は時間が後になってわかる。(労力がかかる割に即時的な利益を Author にもたらさない)
  4. ドキュメントは書かれることは1度だが、後読まれることは何百回、何千回とある。
  5. ドキュメントは自分のために書くものではない。対象読者のために書く。
  6. ドキュメントにおける 4W (Who = 対象読者、What = ドキュメントの内容、When = 作成時刻、Why = ドキュメントの目的)
  7. ドキュメントにもライフライムがある。
  8. 優れたドキュメントには完全性、正確性、明確性のそれぞれのトレードオフが存在する。

個人的にドキュメント(及びドキュメンテーション)の特性というのは「時間と距離を超えて伝播し、残り続けること」が価値だと考えていたので、その価値の捉え方の方向があっていたことは自身になりました。

今回紹介した2章以外にもこの『Google のソフトウェアエンジニアリング』は スケーリング持続性 をテーマとして様々なソフトウェアエンジニアリングの領域における Google の実験から得たベスト(現時点でのベターくらい?)プラクティスが詰まっている良書でした。
ページ数的にもなかなか気合が必要な一冊ですが、一読して損はない一冊かと思います。

2022 年の振り返り

年の瀬なので今年も振り返りをします。
今年は仕事よりもプライベートでの変化が大きな年でした。

仕事編

1人のエンジニアに戻った

GW前くらいまで PjM 兼務でTechLead をやっていましたが、一旦そのロールを降りて1人のエンジニアに戻りました。
戻った経緯は以下のエントリに記載しています。

ema-hiro.hatenablog.com

忙しいのは相変わらずでしたが、PjM としてやってきたことをそのまま現場の1エンジニアとして活かせた部分もあり、ロールを変えてみて見えるものも変わってきたので、この意思決定は良かったなと思います。

尚、来年はどうなってるのかさっぱりわかりません。

アウトプット

登壇した

小さな勉強会ですが個人では数年ぶりに登壇しました。

speakerdeck.com

ブログ継続した

引き続き継続できました。

忙しくて下書きのまま貯めてるものもあるので年末にかきあげて放出したいなと思っているものもあります。
現職のエンジニアリングブログでもいくつか記事を書きました。

medium.com

medium.com

ブログでアウトプットするのは半分趣味みたいなもので、内容も最近エンジニアリングに限らないことも書いてますが、引き続き継続できていることは良い習慣かなと思います。

価値観の変化

去年の後半くらいから個人の成長といったものを一切目的にしなくなりました。
昨年、本業で1つの大きな成果を残したので、個人としてタイトルを目指すモチベーションが枯渇してしまったことも要因としてありますが、それ以上に結婚したことでもはや自分だけの人生でなくなり、時間の使い方が変わったことが大きいかなと思います。
また同時に、スキルの幅を広げ "続ける" 体力みたいなものが年々枯渇していってる感覚も徐々に出てきており、自分自身の持てるリソースの配分をうまいこと調整しながら、持続的に学び続けることができる方にシフトしている、というのが自分自身の感覚をうまく言語化したものかもしれません。
知識をつけるときも、表面的な技術スタックはもうあまり追っかけておらず、より一層深い部分への理解だったり、技術の歴史だったり、そして技術に限らない汎用的な「仕事力」といったものに対して知的体力のリソースを傾けるようになりました。

まとめると個人としては学習にかけるリソースの内訳を今までと変えて、いわゆるエンジニアリングそのものに関しては良くも悪くも「仕事としてのエンジニアリング」、「飯を食う種としてのエンジニアリング」という割り切りをするようになったという感じです。

一方で、今まで以上に事業やチームとして持続的に成長するにはどうすればいいのか?ということを考える頻度は逆に増えました。
ちょうど年末にかけて『Google のソフトウェアエンジニアリング』を読み切って、この中でソフトウェアエンジニアリングに「時間」の概念を含める話が何度も出てきており、持続的な開発について考えるに至った自分自身の変化や、自分の中でうまく言語化できてなかった諸々がうまいことまとまり、来年はまた違った観点でコトに向かえそうな気がします。

私生活編

家を買った

この住む場所を固定化(= 家の購入)したことが1番大きな変化だったかなと思います。

車を買った

車を買ったので移動が原則車前提になりました。
もちろん出社なり都内で用事があれば電車も使いますが、移動手段として電車しかなかった時代には戻れなくなりました。

結婚式をした

小規模ながら結婚式をしました。
男なので、まぁ結婚式なんかしなくていいんじゃね?と思っていたタイプでしたが、やってみたらやってみたで良かったです。

ema-hiro.hatenablog.com

新婚旅行をした

結婚式の日から10日くらいお休み貰って石垣〜沖縄に新婚旅行に行ってきました。
竹富島の「星のや」と沖縄の「ハレクラニ」に宿泊してましたが、こういったリゾートホテルにまともに宿泊したのは人生で初めてだったので、とても良かったです。
また、うまい具合にインバウンド解禁前に新婚旅行できて、かつ国内旅行だったので思ったより予算をかけずに行けました。

石垣島は毎年行きたいと思うようになりました。

筋トレを継続した

なんだかんだ週1~くらいでジムに通い続ける生活を今年も続けられました。
体重も今年に入ってから順調に増えていて、いい感じにバルクアップできているので、筋肉に関して「だけは」圧倒的成長を実感しています。
来年こそベンチプレス 75kg をクリアしたい。

英語継続した

「スピークバディ」というアプリを使って引き続き英語を週4くらいのペースで継続できました。
そろそろ次のレベルに行きたいのでなにか良い教材、もしくは現職の福利厚生である英会話をちゃんと利用しようかなと画策中。

サッカー観戦した

今年は1月の高校サッカー選手権決勝を皮切りに J リーグを2年ぶりに見に行けました。

来年

まぁ去年に引き続き、特になにか達成したいことなどは決めてないです。
毎年何かしらすごい変化があるので、どうせ今決めても何も達成できなくなりそうなのもあります。
きっと来年も多分なんかあるんだろうなと思っています。

とりあえず本読んでブログ書いて過ごせれば最低限自分のメンタルは安定するので、そこさえ守れればあとは特に求めるものはないです。あ、イカは上達したい。

というわけで来年も引き続き頑張っていこうと思います。

2022 年買ったもの

2022 年買ったもの

とんでもなく金が飛んだ1年でした。
(主に最初の2つのせい)

家を買いました。

まぁこの辺の意思決定はその人の人生における選択の優先順位が現れるところでもありますが、自分の場合は単純に WFH が前提(※1)で働く場所には束縛されないし、住みたい広さの物件に都内で住む経済力はないし、郊外で探すと賃貸より購入したほうが固定費が抑えられる(2022年時点)と考えて買っちゃった感じです。

個人的には、この先数十年住所固定化するので、会員登録や色んな行政系の申請が楽になったり、賃貸だと引けない強強インターネット回線(※2)を引けたり、賃貸だと引っ越しなり、原状復帰義務を考えて改造できなかったことに拘ってカスタマイズできたり(※3) するのがいいなと思っていたりします。

※1. 2022年後半くらいから 週1~2 回は出社はするようになりました。
※2. 住んでる地域にもよりますが、何もしなくても下り 900 弱くらい出るのでオンラインゲームするにも困りません。
※3. スマートホーム化とかドア付け替えたりとか。小さいですが家庭菜園できるスペースもあるので冬が明けたらやってみようかなと思ってます。

ちなみに買ってみて初めて知ったのですが、カーテンレールとかテレビとかは追加で工事する必要があり、家本体を買って後の追加コストがまぁまぁすごかったです。今まで賃貸で当たり前のように最初からついていたので、気づかないポイントでした。

去年すでに購入は決めてましたが今年に10月についに納車されました。
トヨタのヤリスクロス (HV) です。
キャッシュ一括で買いました。貯金が0 になりました。痺れました。

郊外に住むことを決めた時点で自家用車は必須だと思っていましたし、そもそも最近の車がどんなもんかを知らなかったのですが、買ってみると「でかいスマホ」って感じで、便利な機能も多く、新しいおもちゃを手に入れた感覚です。

尚、普段は奥さんの通勤車です。

レンタカーの返却時刻を気にしなくて良くなったり、またものを結構積み込めるので遠出の旅行しやすくなったり、キャンプに行ったりと行動範囲もだいぶ車前提にシフトしました。

iPhone14 Pro

親が使っていた iPhone がいよいよ古くて使い物にならなくなっていたので自分が使っていた SE3 を譲って、自分としては数年ぶりにフラッグシップモデルに買い替えました。

とりあえずカメラがすごい進化を感じましたが、めちゃくちゃ筐体が重たくて、片手で持ってると手が痛くなります。

あとパンチホールはぶっちゃけなくてもいいなと思いましたし、顔認証より指紋認証の方が便利だなと思います。

www.apple.com

AirPods Pro2

一昨年初代 AirPods Pro をタクシーで無くして以降、 WF1000x-m4 で凌いでましたが、今年満を持して全然見た目の変わらない第2世代が出たので買いました。
ノイキャンはすごいですがそれ以外初代と何が違うのか自分の耳ではさっぱりわかりませんでした。
林檎信者恒例のお布施として4万円で耳栓を買った気分です。

www.apple.com

とりあえず 14 Pro と合わせて円安の影響をもろに食らったので、所有欲は満たされましたが、お財布はだいぶダメージを受けました。

WH-1000X M5

WH-1000X M3 を持っていて、今年リニューアルしたバージョンが出たので買いました。
ノイキャン性能は本当に素晴らしくまた軽さもあるので、仕事で使う分には十分なスペックだと思っています。

www.sony.jp

LinkBuds S

実は WF1000x-m4 が自分の耳には合わなかったので、AirPods Pro2 が発売されるまでにこの LinkBuds シリーズのカナル型イヤホンをずっと使ってました。
とにかく軽くてつけ心地も AirPods Pro と遜色ないので、これは地味にいい買い物でした。
最近ソフトウェアがアップデートされてマルチポイントにも対応したので、PC とスマホの付替とかもシームレスでノイキャンに拘りがないなら AirPods Pro じゃなくてこっち買ったほうがコスパはいいのでは?みたいなことを思っています。

www.sony.jp

ダイニングテーブル

戸建ての購入に合わせて1人暮らしのときに使っていた小さいダイニングテーブルから大きなもの(4人用)に買い替えました。
テーブル下部をルンバが通れるものにしたので、掃除のときにも困らず、あとテーブル広いのでだいぶリビングっぽくなっていい感じです。


Dyson Micro

使っていた掃除機が壊れたので、ルンバ到着とほぼ同時期に買い替えました。
コードレス掃除機は軽さが正義だと実感しました。
とはいえ1階はほぼルンバがやってくれてしまうので、これは今2階の掃除担当です。

SwitchBot

カーテンと Hub を購入しました。
特にカーテンは毎日決まった時間に開閉してくれるように設定したのですが、毎朝、毎晩のちょっとした手間が減ってよかったです。

また Hub 経由でテレビやエアコン、部屋の電気も操作できたりして、寝る前にいちいち電気消しに行かずに声で操作して消したり、帰宅前にエアコンを付けておく、みたいなことにも使えて便利です。

Amazon Echo 各種

上記の SwitchBot と被りますが、自宅のVUIデバイスを全て Amazon Echo に統一しました。Wake word が1番短いからと言う理由かつ、自宅で使ってるサービスは割と Amazon に依存してる( EC や Kindle FireTV など)ので Amazon に統一しておいたほうが便利だなと思った次第で Google じゃなくて Amazon に乗り換えました。

しかし、最近レイオフがあったらしくこの先のデバイスの進化に陰りが見え始めているのがちょっと悩みの種です....。

なお、自宅で導入したのは Echo Dot の第4世代と Echo Show 5 です。

Anker Eufy ハンディクリーナー

Amazon サイバーマンデーで買いました。
机の上のホコリ掃除したり、あとコーヒー豆を引いたときの微粉や飛んだ殻の掃除に役立ってます。

FunLogy SOUND3

これもサイバーマンデーで買いました。サウンドバーには元々興味があったのと、テレビの配置的に音が聞こえづらいことがあったので購入してみました。
これより良いものものはたくさんありますし、自分も BOSEサウンドバー等々も検討して実際に見に行ったりもしたのですが、とりあえず入門版としてこちらを試してみたところ、違いがよくわからなかったので価格の面からも十分満足してます。

今年のW杯はこのサウンドバーのおかげで音声も含めてだいぶ楽しめました。

アイリスプラザ 本棚 ディスプレイラック

おそらく今年のベストバイはこれです。
自宅に備え付けのデスクがあって、その下の机下収納をずっと探していたのですが、これは横にしたときのサイズがちょうどよくまた本棚にもなるので書籍や書類の収納先も増えました。

HUAWEI MateView 28.2インチ 4K+ ウルトラHD

今まで使っていた局面ディスプレイを同僚に譲ってこちらの 4K ディスプレイに変更しました。
4K にしてみたのは会社のディスプレイで 4K を使ってみたら明らかに目の疲れ方がフル HD とはことなり、発色が目に影響することを実感したので思い切って買い替えました。
Mateview はアス比も3:2 というちょっと独特な比率をしていて横向きでありながら縦に長いのでコードの読み書きにも向いてるなと思います。

ただ Vesa マウント対応してないのでディスプレイアーム等には非対応、というのは人によっては微妙なポイントかも知れません。

その他

ルンバ S9+

これは買ったわけではなく会社からの結婚祝いで頂いたものですが、掃除の概念が劇的に変わりました。
一階の掃除担当はこの子で、小さなホコリ系は全部任せてます。

ロボット掃除機すごいの一言でした。

www.irobot-jp.com

ReFa FINE BUBBLE S

これも僕が買ったわけではないのですが、シャワーヘッド取り替えるだけで髪を洗う体験が劇的に変わりました。
もともとアレルギー持ちで頭皮がめっちゃ痒くなる症状があったんですが、これがかなり軽減されました。
あとあんまり実感はないですが、水道代も節約できるみたいです。

この体験がどれくらいすごいのか、ということを同僚に聞かれたのでとりあえず Intel Mac -> M1 Max くらいの変化を感じるよ、と伝えておきました(個人の主観です)

mtg-pro.co.jp

まとめ

今年は本当に、本当に、、、ほんとーーーーにたくさんお金を使いました。
来年もたくさん使うぞ!

Vue3 で TextArea を入力に応じて可変させる

Overview

タイトルのとおりです。
ちゃんとアプリとかにありがちな TextArea の入力量(改行)に応じて、TextArea を拡張させる実装のサンプルを記載します。

結構ありがちな実装だと思いますが、Vue3 のサンプルが少なかったのでかんたんなサンプルコードを書いてみました。

Sample

<script setup>
import { ref, watchEffect } from 'vue'

const msgInput = ref('')
const msgTextArea = ref(null);
  
watchEffect(()=>{
  // デフォルトのテキストエリアの大きさを指定する。
  msgTextArea.value?.style.setProperty('height', `30px`)
  
  if(msgInput.value !== ''){
    //
    msgTextArea.value?.style.setProperty('height', `${msgTextArea.value?.scrollHeight}px`)
  }
  
})
</script>

<template>
  <h1>Title</h1>
  <textarea 
            v-model="msgInput"
            placeholder="input"
            ref="msgTextArea"
  />
</template>

ref: https://sfc.vuejs.org/#eNqVUs2K1EAQfpW2ETIDM2nF25iM7EHQm4c99mFjtjLJ0uluujuzuwwBJwFXUC+CBz2K4B/u3edp1uewkkxk1EXYXNJV9dVXX/1s6IHW4boCuqCRTU2hHbHgKr3ksii1Mo5siIFsRk4Tl+YPswxSR2qSGVWSAPMCLrlMlbSOlHb1WOrKkbjLmATBdC9yCGfuwECyC8pKiOl9Lgnhco95MpnGy03nZoz49sK3b33zxbfffPvCby99+9w3333zozObz7796psP6L/6+MlvX/stgt/8fHVxdfneb9/55qV/1nRce+XDdSIqeBBady4gxE6fGKXBuPNJkEOxyl0wI0f37uizIxTfiSOkyCZjZ0M2uRXHBLvrdXZKh/9Nq9zeXJeRGiXEox5UjyrqQUqNRsSGJeF60HBQapE4QIuQKL+7PCycgIjhq/c4ZE+6oQ8Kx289L9UxiJjTsTFO/0Qgawq5EsdgEFVcB8EtDgRjCwOAYeWI/RZGZ3Q4o3mZ6PDEKomH1g8OafuA5XRBdqPkFC+qsznNndN2wZjN0u48T2yozIrhKzSVdEUJIdhy/tSoUwsGiTmd7XEwdK7BzA1IbAHM/zj/gv7Du9tATetf01coHg==

実際の動作は以下の様になります。

ハマったところ

vue3 ref height textarea expand みたいなキーワードでググるといくつかサンプルが出てきますが、Vue3 のものがなく、ref で指定した DOM の要素をどうやって SFC の script のところで取り回せるかわからなかったのですが、結論から先にいうと ref の名前と高さをいじりたい textarea を格納する変数名を同じにする ということでした。
つまりここでいうと ref="msgTextArea" と textarea に ref を貼った場合、script 内で使用する変数も msgTextArea とする必要があります。これが最初わからず時間を溶かしました。

Nuxt3 で Custom Navigation を実装する

Overview

Nuxt3 を使って Custom Navigation を実装します。

大まか内容は公式の Pages の https://nuxt.com/docs/guide/directory-structure/pages/#navigation の部分に記載されてる内容です。

Usecase

以下のような Tab の UI を Nuxt3 を実現する際にこの Custom Navigation を使います。

tailwindui.com

例えばユーザーの詳細ページでいくつか情報のセクションを分けてタブで画面の遷移なく情報の表示を変えたいケースなどを考えます。

Samples

ディレクトリの構成のサンプルは以下です。

pages
   users
       [id]
          [tabName].vue

このときに $tabName で指定した tab にするときに navigateTo() を使ったメソッドを用意することで画面ロードが走らない画面遷移を実現することができます。

const selectTab = (tabName: string) => {
  navigateTo(`/users/${route.params.id}/${tabName}`)
}

上記のサンプルでは例えばユーザーのフォロー一覧を見る場合に /users/1/follows というような URL に遷移するように実装し、follows tab のページを [tabName].vue に実装します。

これでリロード無しで Nuxt3 上でタブ遷移やページ遷移を実装することができます。案外簡単だなと思ったのとこういうユースケースはちゃんとドキュメントで網羅されてるので目を通しておかないといかんなと思いました。