私が歌川です

@utgwkk が書いている

人間ドック

今年で30歳になるので、毎年の健康診断が人間ドックにランクアップした。

  • 視力
    • 左右ともに1.0
    • 目を凝らせば見えそうだけど無理するまでもないな、と思ったらスルーするようにしているので、数字が落ち着いている?
    • まだ裸眼でいける
  • 身体測定・腹囲
    • 去年よりよくなっていそう
    • なんでだろうね
      • 歩く機会が多かったとか?
      • これもある種のKaigiEffectってことになりませんか
  • 眼圧検査
    • 右目だけ2回検査することに
    • 右目見えにくいとかありますか? って聞かれたけどなにも心あたりがない
  • 肺機能検査
    • 植田まさしの漫画でよく見るやつだ!! と思った
      • バリウムとかもそうでは?
  • バリウム
    • さながら薄いヨーグルトのような味
    • げっぷしたすぎて死ぬ!! というほどではなかった
    • 自分で身体を回転させるのだけど、右回転がどっちなのか最後まで分からないまま終わった
  • 血液検査
    • せっかくだから、と検査オプションを追加したら採血量が増えた
      • 血液を取られることに抵抗ないので問題なし
  • 下剤
    • 検査項目を全て終えたあとに早速2錠飲んだ
    • 帰宅中の電車待ちでおなかがボコボコ鳴っていた?
      • バリウムと一緒に飲んだ発泡剤の影響かもしれない
    • 帰宅して昼食を食べたあとさっそくトイレに行ったぐらい
    • どういう体験になるか未知数で不安だったけど、おなかの調子が悪いときだいたいこんな感じかも
      • 普段からよくトイレに行っているため、下剤の効能なのかどうかの区別がつかない

GraphQLスキーマの設計にあたって読んでおくと捗る本・ドキュメントなど

新たにGraphQLスキーマを設計する機会があり、モブプロ的にGraphQLスキーマに対していろいろ口出ししていたのだけど、そもそも前提知識をどうやって揃えたらいいのか、という話があると思う。いい機会なので、自分が読んだ本やドキュメントなどを紹介しておく。あと経験上持っている雑多なノウハウもメモしておく。

本・ドキュメント

初めてのGraphQL

www.oreilly.co.jp

そもそもGraphQLに対して入門したい、となったらまずこの本から始めるのがいいかなー。GraphQLという概念の説明から入り、サーバー・クライアント実装についてもまんべんなく触れている。2019年の本で、クライアント・サーバーの実装部分については現代だと書き方が変わっているところもあると思う。

Shopify GraphQL Design Tutorial

github.com

Shopifyがもともと社内向けに用意していたGraphQLスキーマの設計ガイド。まずはこの文書をGraphQLスキーマ設計の指針として定めておいて、必要に応じてアレンジしていくのがいいと思う。日本語版もある。

github.com

GraphQLスキーマ設計ガイド

booth.pm

GraphQLスキーマの設計指針やGraphQL APIの実装に関する話題から、実際にGraphQLスキーマを設計してうまくいった・いかなかった事例もまとまっている。GitHubの公開リポジトリで無料で全文が読める。

github.com

Web API: The Good Parts

Web API: The Good Parts

Web API: The Good Parts

Amazon

www.oreilly.co.jp

GraphQL以前に、そもそもWeb APIの設計について知っておきたい場合はこれも読んでおくとよさそう。過去に読んだときの記事があったのでそっちも見てください。

blog.utgw.net

GraphQLを使った共同開発の心構え 〜 フロントエンドの視点から / Hatena Engineer Seminar #18

speakerdeck.com

手前味噌だけど、GraphQL APIの利用者として実際にGraphQLスキーマを育てていくための心構えとかはよくまとまっているんじゃないか。要するに、APIの利用者がやりたいことを実現できるか、を気にしたいという話になるだろう。

GraphQLスキーマの設計で考えたこと

speakerdeck.com

これは同僚 (当時) の発表資料。実際のサービス・機能開発におけるGraphQLスキーマ設計のケーススタディとして読めていいと思う。限定公開・状態をどうやってGraphQLの型に落とし込むのか、の事例になっている。

apollographql/skills

github.com

AI時代なのでAgent Skillsがあるのでは? と思って軽くググってみたところ見つかった。skills/graphql-schema/SKILL.md 単体よりは、skills/graphql-schema/references/ 以下のドキュメントを読みつつスキーマ設計の参考にする、ぐらいが人間が読むにはちょうどよさそう。

Relayに学ぶGraphQLのスキーマ設計

cockscomb.hatenablog.com

GraphQL Server Specificationに準拠したGraphQLスキーマにすることで、クライアント側から見たときにどのような嬉しさがあるのか、よくまとまっている。

雑多なノウハウ

union・interfaceの使い分け・使いどころ

自然言語でざっくり言い表すと、以下のようなことを意識するのがいいと思う。

  • 異なる概念の集合に対して名前をつけて扱えるようにするのがunion
  • 似たような概念に対して抽象化するのがinterface

あとは先述した本などでも触れられているけど、GitHubのGraphQL APIにおいてどのような概念がunion・interfaceとして定義されているのかを見てみるのも参考になるかもしれない (膨大すぎて途方に暮れるのかもしれない)。

型を分けておくとinline fragmentによって取得するフィールドを切り替えられる、ということは頭に入れておけるとよさそう。unionやinterfaceを使わずに全く同じ型を使い回すのか、union・interfaceという形で抽象化するのか、のヒントになるだろう。

GraphQL Server Specificationと id: ID! フィールド

GraphQL Server Specificationの世界観では、オブジェクトは id フィールドによって正規化されるので、異なる概念が異なる id の値を持つように注意を払う必要がある。同じオブジェクトの状態が変わったときに id の値も変わるようなことがあると混乱の元になるだろう。


他にもあると思うけど、思い出したら追記します。

RubyKaigi 2026に参加した

今年もRubyKaigiに参加しました。早めに出してしまわないといつ記事を出せるか分からなくなるので、さっさと出します。

トーク

3年ぶりにRuby Committers and the Worldを見れたので、言うことはありません。今年は音が出るセッションがいくつもあってよかったと思います。組み込みのSetクラスをCで再実装する話も計算機科学って感じでおもしろかったです (ちなみに、これが #rubykaraoke の伏線になります)。CygwinのRubyパッケージをメンテナンスする話では、およそ15年以上ぶりぐらいにCygwinのことを思い出すことになって感動的でした。当時わけもわからず使っていたパッケージマネージャの仕組みが知れてよかったです。

day1までのトークのメモはいくつかCosenseにまとめてあるけど、途中からXでの実況に切り替えたので、あんまりまとまっていないです。

scrapbox.io

NOC

例年通り、今年もNOCとしてケーブルを敷きまくる活動をしました。今年は建物が2つに分かれており、果たして前日のうちに作業が終わるのか……とビクビクしていたのですが、蓋を開けてみると人数が多かったのもあって無事に終わりました。

今年は8の字巻き最強決定戦が開催されました。なんか異常な盛り上がりを見せていてよかったと思います。こんなイベントが開催されることになるとは予想もしていませんでした。

会期後の片付けもほどほどの時間に切り上げられてよかったです。

#rubykaraoke

25時ぐらいから閉店までアニソン部屋にいました。

あとで (day3のRubyIlluminationsで声をかけられて) 気づいたけど、同じ部屋にJeremy Evans氏がいたようです。すごいですね。

観光とか

4月中旬の函館は、五稜郭公園の桜が咲き誇っており絶景でした。まさか1年のうちに満開の桜を2回見れるとは。

函館山から見える夜景も素晴らしかったです。天気がとても良く、タイミングが良かったのだと思います。

レーシングは12時間遅れで合流しました。

Duolingo

ステージ10まではクリアできたけど残り2つが全く見当がつかないまま会期が終わった……。

総括

普段はRubyを書いていないけど、Rubyを作っている人たち・使っている人たちのこの熱狂を味わうために毎年予定を合わせているのだろうな、としみじみ思います。

来年の宮崎も行きます。

docker runでコンテナにホスト側のAWS CLIの認証情報を伝えたい

はじめに

docker run コマンドで実行するDockerコンテナ内でAWSのAPI呼び出しを行うとき、何もしないと認証情報がなくて失敗すると思う。なんとかしてホスト側の認証情報を伝えたい。なお、令和なのでアクセスキーはホストのどこにも (~/.aws/credentials ファイルにも) 永続化されておらず、一時的な認証情報 (SSOやAssumeRole) しかないものとする。

やりかた

結論から言うと、aws configure export-credentials コマンドを使って認証情報をアクセスキー・シークレットキーの形式で取得できるので、これが使える。ホスト側でアクセスキーを使っていない限りは永続性のない (有効期限のある) 認証情報として取り回せる。

aws configure export-credentials のようにフォーマットを指定してあげることで、以下のような形式で環境変数の定義が取得できる。実用上は AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY があればよくて、残り2つの環境変数は要らない気がするけど、今のところ実害を確認できていないので放置してる。

export AWS_ACCESS_KEY_ID=xxxxxxx
export AWS_SECRET_ACCESS_KEY=yyyyyyy
export AWS_SESSION_TOKEN=zzzzzzz
export AWS_CREDENTIAL_EXPIRATION=YYYY-MM-DDThh:mm:ssZ

--format env-no-export 引数を指定すると、環境変数を export しない文字列が出てくる。

AWS_ACCESS_KEY_ID=xxxxxxx
AWS_SECRET_ACCESS_KEY=yyyyyyy
AWS_SESSION_TOKEN=zzzzzzz
AWS_CREDENTIAL_EXPIRATION=YYYY-MM-DDThh:mm:ssZ

あとはこの環境変数をなんとかして docker run コマンドで使える形に整形する。docker run コマンドでコンテナに環境変数を渡すには -e オプションを使う必要があるので、ひと工夫したらよい。以下はamazon/aws-cliイメージを使う例。サブシェル内で認証情報を環境変数としてexportしつつコマンドを実行している。リージョンだけ個別に渡してあげる必要があるので注意。

% ( eval $(aws configure export-credentials --format env); docker run --rm -e AWS_REGION=ap-northeast-1 -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN amazon/aws-cli sts get-caller-identity )

あるいは、bashやzshを使っているのであれば --env-file オプションと匿名パイプを組み合わせるともっとシンプルに書ける。<(cmd) のように書くことでコマンドの実行結果をファイルディスクリプタ経由で取得できる。少なくともMac zsh + Rancher Desktopではこれで動作した。

% docker run --rm -e AWS_REGION=ap-northeast-1 --env-file=<(aws configure export-credentials --format env-no-export) amazon/aws-cli sts get-caller-identity

追記

もともとは aws --profile configure export-credentials コマンドの実行結果をワンライナーで加工して -e AWS_ACCESS_KEY_ID=xxxxx -e AWS_SECRET_ACCESS_KEY=yyyyyyy のようなコマンドライン引数を組み立てる方法を紹介していましたが、これだとコマンドライン引数から認証情報の中身が見えてしまう問題があるため、修正しました。

まとめ

aws configure export-credentials コマンドを活用することで、ホストのAWS CLIの認証情報を環境変数経由でコンテナに伝えることができて便利。

参考

ミニPC日記

Windows10のサポートが終了し、ESU (拡張セキュリティアップデート) プログラムに登録していったんしのいでいたが、世界情勢やらAIの台頭やらでいつ新しいWindowsマシンを買うのがよいか分からない状態になっていた。10年前のデスクトップマシンはWindows11にアップデートできない。性能的には困っていないのだけれど、困ったね。

ちょっと考えて、このミニPCを買った。考えてみるとWindowsマシンでヘビーなゲームをするわけじゃないし、インターネットやYouTubeが見れたらじゅうぶんだろう、というところに落ち着いたのであった。仕事でも趣味でも開発にはMacを使うようになっている。

たまにファンが回る音が気になるけど、今のところ快調に動いている。SSDが512GBなので換装してもいいかもしれないけど、困ったら考えることにする。

古いマシンのSSDやGPUはどうしようかな……。

React Testing Libraryでuse関数を使うコンポーネントをテストする (workaround編)

はじめに

PromiseをawaitせずにReactコンポーネントのpropsとして引き回すように修正し、テストコードで単に値を Promise.resolve() でラップするようにしたらテストがじゃんじゃん落ちて困った。ちょっと試行錯誤して直せたので事例共有とします。たぶんworkaroundなのでタイトルもそのようなテンションにしています。

利用しているライブラリのバージョンは以下の通り。

  • react, react-dom 19.2.4
  • @testing-library/react 16.3.2
  • jsdom 29.0.2

やりかた

use 関数*1を使ってPromiseの解決を待つ (suspendする) コンポーネントがあったとして:

import { use } from "react";

type SuspendComponentProps = {
  data: Promise<string>;
};

export function SuspendComponent({ data }: SuspendComponentProps) {
  const str = use(data);
  return <h1>Hello, {str}!</h1>;
}

コンポーネントに渡したPromiseが解決された後の状態をテストするとき、単に render 関数を呼ぶのではなく、render 関数の呼び出しを await act(async () => ...) で囲んでやる必要がある。つまり、以下のテストコードは1つ目のテストケース (rendered with await act(async () => ...)) しか通過しない。2つ目以降のテストケースでは screen.findByText("Hello, foo!") が1秒でタイムアウトする。

import { act, render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SuspendComponent } from "./SuspendComponent";
import { Suspense, ReactNode } from "react";

function wrapper({ children }: { children: ReactNode }) {
  return <Suspense fallback={<div>loading...</div>}>{children}</Suspense>;
}

describe("SuspendComponent", () => {
  it("rendered with `await act(async () => ...)`", async () => {
    await act(async () => {
      render(<SuspendComponent data={Promise.resolve("foo")} />, { wrapper });
    });
    expect(await screen.findByText("Hello, foo!")).toBeInTheDocument();
  });

  it.fails("rendered with `act(() => ...)`", async () => {
    act(() => {
      render(<SuspendComponent data={Promise.resolve("foo")} />, { wrapper });
    });
    expect(await screen.findByText("Hello, foo!")).toBeInTheDocument();
  });

  it.fails("rendered without `act(async () => ...)`", async () => {
    render(<SuspendComponent data={Promise.resolve("foo")} />, { wrapper });
    expect(await screen.findByText("Hello, foo!")).toBeInTheDocument();
  });
});

以下のリポジトリで実際に動作確認できる。

github.com

なぜこうなるのか

どうやら以下のissueの事象が起こっていそう。すごくざっくり言うと、React 18→19で挙動が変わったことの影響を受けているらしい。

github.com

12:21 追記: actrender 関数を非同期にする、というPRが用意されているっぽい。非互換変更なのでいろいろありそう。

github.com

React Testing Libraryでuse関数を使ってuse関数を使うコンポーネントをテストする (workaround編) - 私が歌川です

へーと思って調べてみたら <a href="https://github.com/testing-library/react-testing-library/pull/1214" target="_blank" rel="noopener nofollow">https://github.com/testing-library/react-testing-library/pull/1214</a> で render 関数を非同期にして内部で await act(...) 相当のことをするようになるっぽい。これマージされると良いですね。

2026/04/09 10:31
b.hatena.ne.jp

おわりに

React Testing Libraryでuse関数を使ってsuspendするコンポーネントをテストする場合、もうしばらくは render 関数を await act(async () => ...) で囲む必要がありそう。少なくとも、自分が遭遇したパターンでは、この記事に書いたような修正が有効そう。

*1:useで始まる関数だけどフックではないので、コンポーネントやカスタムフック中の条件分岐やループの中で呼び出してもよい

utgw.netをNext.js App RouterからHonoに移行した

はじめに

utgw.netをNext.js App RouterからHonoに移行しました。Claude Codeが一晩でやってくれました。

utgw.net は id:utgwkk のホームページです。前回までのあらすじは以下の記事を読んでください。

blog.utgw.net

blog.utgw.net

なぜ移行したのか

個人サイトなので好きにしたらいい、というのは前提であるとして……。

Next.js App Router (以下、単にApp Routerと書く) のキャッシュ戦略と、自分のホームページでやりたいキャッシュ戦略が噛み合っていないのをいい加減解決しよう、というのが移行のモチベーションです。

utgw.net には、SpeakerDeckのRSSフィードから最新のスライド一覧を取得して表示する面があります。都度SpeakerDeckにHTTPリクエストを送信すると、レイテンシが増大したり、SpeakerDeck側に負荷がかかったりします。スライド一覧はある程度キャッシュしつつ、スライドを公開したら一覧にも反映されてほしくなります。

App Routerには時間ベースのキャッシュ破棄の仕組みがあるため、一見するとこれが使えそうに見えます*1。つまり、page.tsx ファイルに以下のように書いたら10分キャッシュしてくれそうです。

export const revalidate = 1200;

ところが、これだけだとビルド時にデータキャッシュが焼き込まれ、再度ビルド・デプロイしないとSpeakerDeckのスライド一覧が更新されなくなってしまいます。Pages Router時代は getServerSideProps 関数でデータフェッチしつつ Cache-Control レスポンスヘッダをつけたらよかったのですが、同じことをApp Routerで実現する方法が分かりませんでした。完全にキャッシュを無効化することでスライド一覧が更新されるようになりますが、これは都度SpeakerDeckにリクエストを投げていることになり、やりたいことに反します。Next.js middlewareで無理やり Cache-Control レスポンスヘッダをつけようとしたのですが、デプロイしてみるとNext.jsのどこかの層でヘッダごと叩き落とされていたように記憶しています。

長々と書いたけど、要するに「スライド一覧をキャッシュしつつ適度に更新されるようにしたい」というのが最大のモチベーションでした。ここまで書いて思ったけど、今ならAIに聞いたら解決できたのかも。

構成

移行前の構成は以下の通りです。

  • ペライチのWebページとリダイレクト機能がある
  • Next.js App Router
  • AWS Lambda上で動いている
  • 前段にCloudFrontがある

移行後に期待することは「壊れていない」ぐらいしかなくて気楽です。

移行の流れ

テストを書く

移行に先んじて、リダイレクト機能が動いていることや、画像がリンク切れになっていないことなどを確かめるE2Eテストを書きました。テストがあることで、どういう状態が期待されているのか明確になります。

github.com

Hono (hono/jsx) に移行する

期待する状態が明確になったので、あとは移行するだけです。

まずはおもむろに CLAUDE.md ファイルを用意します。AIコーディングエージェントに移行してもらおう、という算段です。

github.com

あとはClaude Codeを開くだけです。「Next.jsからHono (hono/jsx) に移行してください」というプロンプトでPlanしてもらったあと作業に取りかかってもらったらだいたい完了したと思います。

github.com

デプロイ時に必要なスクリプトのコピーを忘れていてLambda関数がコケるようになったのですが、そこを直したら無事に動くようになりました。

github.com

ちなみに、HonoはネイティブでLambda上で動作する*2のでLambda Web Adapterは外せます。

github.com

ESLint flat configに移行する

ESLintを更新しきれていなかったのですが、重い腰を上げて (プロンプトを投げて) flat configに移行しました。

github.com

oxlint, oxfmtに移行する

欲が出たので、そのままoxlint, oxfmtに移行してもらいました。lintは速ければ速いほどいいですからね。

github.com

github.com

おわりに

App RouterからHonoに移行することで、狙い通り Cache-Control レスポンスヘッダでCDNキャッシュの制御をしやすくなりました。よかったですね。

自分の代わりに動いてくれる頭と手が増えるだけで物事が一気に進みはじめる、という事例でした。今年度からClaudeのProプランに課金しました。AI時代になっても個人サイトを技術の砂場にする流れは変わらず、むしろ加速していくでしょう。

あわせてよみたい

HTTPレベルのキャッシュについてはMDNの記事がよくまとまっているので、まずはそちらを読むとよいでしょう。

キャッシュやCDNについては以下の本が詳しいです*3

時間のないサイト運営者リング