Blog

日常や技術的な学びを書き溜めた一覧です

🛤️
Idea

Kaigi on Rails 2023で登壇しました!

昨日、一昨日(2023//27 28)で開催された にSpeakerとして登壇してきました!今年は初のオフライン開催。私自身Rubyコミュニティのオフラインイベントに参加するのが初めてだったコトと、カンファレンスでのオフライン登壇自体も初めてだったので(今年はスクラムフェス仙台でも登壇したけど、あれはサテライト配信だったので目の前に観客がいなかった)いろんな刺激を受けることができました。この記事では参加者・登壇者それぞれの視点で今回のKaigi on Railsで感じたことを振り返っていきたいと思います。 これが「オフライン」...!初オフライン Kaigi on Rails めちゃめちゃ楽しかったです!発表があって、ブースがあって...という構成は変わらないものの、やっぱり受ける刺激の量は全く異なり、来てよかったなあと感じました。これが「オフライン」...! オフラインしぐさ<blockquote class="twittertweet" datadnt="true" align="center"><p lang="ja" dir="ltr">「懇親会の開始前に前説でパックマンルールを説明しよう」「2人で会話している人を見かけたら話しかける狙い目。2人だと話題を繰り出し続けるのも大変だし、会話抜けるタイミングも計れないしで困っているはず」みたいなオフライン懇親会仕草を徐々に取り戻していきたい。失われた技術。</p>&mdash; Takafumi ONAKA (@onk) <a href="https://twitter.com/onk/status/833302275769776?ref_src=twsrc%5Etfw">July 24, 2023</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf8"></script>前述の通りオフラインイベント慣れしていなかったので、懇親会やブースではonkさんのツイートに記載のあったロストテクノロジー[^をふと思い出し。自分は積極的に話しかけるのが苦手なので、会場うろちょろして2人でお話している方々を見つけては「話している人が2人......来るぞikuma!」[^2]と心の中で叫んでいました。!一応自分のアイコンを含めたポストカードを前日にデザインして持って行きましたが、アイコンによってオンラインの姿と同期ができたのは一定効果があったようです。XのQRコードもつけたのですが、URLが間違っていたらしくこちらは全然意味をなさず...。来月もオフライン登壇の機会があるので、おとなしく を作成しようと思います。オンラインで知っている方/知らなかった人とお話しさせていただくことができて、嬉しかったです!お話しいただいたみなさん、ありがとうございました! 冷めても美味しいお弁当よりも、今ここでしか味わえない味を登壇者的には「もっと現地を重視した構成にすればよかったなあ」と反省もありました。エンジニアになってからの登壇はすべてオンライン配信だったため、いずれも意識的にオンライン向けに特化させた形になっていました。リアルタイムでの反応が弱めなら、終わった後の体験の方に力を入れよう、具体的には後から見た時にも資料からおおよそのコンテキストが汲み取れる状態にする、といった具合です。お弁当用にちょっとおかずの味を濃いめにするみたいな気持ちです。当たり前ですがオフラインの場合はすぐそこに聞いている人がいます。次回以降の登壇では以下のようなポイントを意識したいなあと、他の方の発表を見ている中で思いました。 全編とまでは言わないが、心に残すためのインタラクションを 丁寧に構成された緻密なロジックよりも、その瞬間で理解できる単純なサンプルを 完全な解説よりも、考える余地というギフトをただ後から見返すための資料はそれはそれでメリットがあるので、そこはバランスですかね〜(2種類作るか)。 なんだか強くなった気がする!今年は最初に自分の番を終えることができたので[^3]、常にHall A/Bいずれかのセッションを聞かせていただいておりました。Kaigi on Railsのコンセプトに「明日からの仕事に使える」がありますが、前回の参加時(エンジニア経験4ヶ月)に比べ、「仕事」も増えたことから「わかりみ」と「やらねば」のオンパレードでした。最近Railsも書いていなかったのですが、発表を聞いていく中で自分の中で眠っているRails筋が活動を再開する音が聞こえたような気もします(それは本当か?)。ところで結構AI生成のイラストを使っているスライドが多く見受けられた印象が合って、逆張り系人材としては来年までに油絵や水彩画を学習して、オリジナリティを確保しようかなと思っています、たぶん。 自分の発表のふりかえり!昨年に続き、独自系・網羅系でいくと後者の分類のトークでした。。 発表のモチベーションそもそも今年Kaigi on RailsにCFPを出すのか、そもそも行くのか迷っていました。昨年の6月にエンジニアになってから仕事では 昨年のKaigi on Railsまで:Rails 今年のKaigi on Railsまで:React(+ときどきGo) 今年のKaigi on Railsから:Railsと、近くフロントに傾倒していたので、今年はパスかな〜と思っていたのですが、社内でRailsを書いていたhoguccさんとsanfrecce_osakaさんがCFPを出す雰囲気を感じたので、その勢いに便乗して書くことに。結果CFPは通って登壇できたし、偶然またRails書くことになったしで、やってよかったです。 登壇テーマ上述の通り、Railsをテーマにして話すのはちょっと無理があったので、今回も昨年同様「曖昧に使っている技術を最小実装で自分のものにしよう」という方向性で検討。資料中にもあったようないくつかのファイルアップロードに関する経験から、今回はHTTPを手で書いてファイルアップロードの基本を理解する、というテーマにしました。「この技術は課題に対してオーバーキルではないか?」という疑念がいつもあり、そういう視点でちょいちょい仕様とかを調べたりしたものがCFPのタネになっているのですが、いかんせんまとめていないので、CFPを書く際も、実際に資料を作る際にも時間がかかる...。生活をリリースして登壇をアドバンス召喚するのはなかなか大変なので、よくアウトプットをされている方たちを真似て[^4]これ登壇できるかもと思ったら、ストックするようにしたいなとあらためて思いました。 登壇資料「フロントでは頑張ってたんだよ〜」という免罪符が欲しかったのか、発表資料はHTML + CSS + JSで書いていて、(ほぼ出番のなかった)資料埋め込みデモはReact + Ruby Wasmでできていたりします。ここは長いので気が向いたら別記事で書こうと思います。「ファイルアップロード」が一般的で広いテーマゆえ、今回は昨年よりも内容の取捨選択が難しかったです。HTTPの仕様改訂やら、もう少し実世界と繋げた説明やら、JSにおけるBlobとか、もともと入っていたが消えた内容も...。加えて練習時間があまり取れなかったこともあり、半分ぶっつけ本番・超早口で30分に無理やりねじ込む内容になってしまいました...。これは次回以降の反省点です。 Grow.rbに今年も感謝チーフオーガナイザーの大倉さんが開催されているGrow.rbでは、Kaigi on RailsのCFP提出が解禁されると登壇特別回と称したCFP相談会[^5]が開かれています。昨年に引き続き今年もこちらで壁打ちをさせていただいたおかげで、CFPをブラッシュアップできました。大倉さん、アドバイスいただいた皆さん、ありがとうございました! おわりにあまりコミュニケーションが得意な人間ではないので、「勉強会やイベントには登壇者として参加することで、謎の免罪符を得る」戦略をFJORD BOOT CAMP時代からやっているのですが[^6]、この戦略に頼りすぎて、免罪符なしで存在を保つことができるのか不安です。...ってことは存在を保つために、来年もKaigi on RailsのCFP出すしかないな!!ブログを書いたし、アンケートも全部答えたのでこれにて私のKaigi on Rails 2023は終了です!運営の皆様、お疲れ様でした! 現存しない技術というよりかは取り戻すべき技術 だったので、最後までそわそわそう。またそう。[^5]: 私の中での通称は「大倉塾」[^6]: 謎の免罪符とかは言及されていないが、初心者でも登壇してみるのは話しやすくなるしおすすめ、ということは言われていたと思う、たぶん。

Tech

.env.developmentからdirenvで読み込む用の.envrcを作成するワンライナー

ベースとなる環境変数ファイルから、direnv用のファイルを一発で作りたい環境変数のベースとなるファイルを`.env.development`やらで作成しておき、実際の値は.gitignoreに記載された.envなどのファイルを各開発者のローカルで編集する、といった運用はよくある。プロジェクトだけで使う環境変数をグローバルにexportするのもアレなので、こういった時にはが使える。しかし .env.developmentは`HOGE=foo`形式で記述されている .envrc(direnvの読み込みファイル)は`export HOGE=foo`で記述するという時に、単純に`cp .env.development .envrc`とするだけでは動かない。とはいえ全部に手でexportをつけるのもだるいのでワンライナーだとどうやるのか少しばかり試行錯誤してみた。 結論```bashawk '{if (NF > 0 && $0 !~ /^/) print "export " $0; else print $0}' .env.development > .envrc``` 解説解説というほどの大きなスクリプトでもないが、小さな心配りを説明する。 `NF > 0`: Number of fieldsが0より大きい、つまり非空行のみを対象にする。環境変数のグループを明示的にするために空行があったりする場合にexportだけが残らないようにする心配り。 `$0 !~ /^/`: ``から始まらない、つまりコメント行でない行を対象にする。```bashHOGE=fooFUGA=bar animalsTAMA=catPOCHI=dog```例えば上記の.env.developmentから次の.envrcが生成される。```bashexport HOGE=fooexport FUGA=bar animalsexport TAMA=catexport POCHI=dog```

🐳
Tech

dockerのimageとvolumeを選択して消せるか、賑やかしができるdockern(どっかーん)コマンド

作ったものdenoを使っているので、READMEの通りinstallして`dockern`で実行できます。 経緯色々とDockerコンテナをガチャガチャしていて、何度もimageやvolumeをまとめて削除したいケースが発生しました。docker compose系のコマンドはエイリアスも貼ってあるし、覚えているのですが、dockerコマンドは全然覚えていないので、ここら辺をラップしてやってくれるコマンドを作ることにしました。(なおアイデアとライブラリだけ選んでほぼChatGPTで作った)。仕事中に「dockerをドッカーンと」とか聞いたような記憶があったので、名前はdockernです。 ぐるぐるどっかーんスプー世代の方はわかると思います。```bash$ dockern guru2ぐるぐるぐるぐるどっかーん🎺```まあメインコマンドは`dockern`だけで実行できるので、明日以降これを実行することはないでしょう。 おわり今消したいimageやvolumeがないので、あまりしっかり動作確認はしていません。業務中に動かなくなったら都度直していこうと思います。

🦆
Idea

O'REILLYのLearning PlatformにLLMを使った質問機能が追加されていた(Beta)

O'REILLYの書籍読み放題のサブスクリプションLearning Platformを開いたら、サイドバーに「Ask a question」というメニューが追加されていました。!近頃ではこの形式のUIを見ると「AIか〜」と当たり前のように納得するようになってきましたね。最初は現在開いている書籍に関して問い合わせできるのかと思ったのですが、そうではなくLearing Platformに登録されている書籍から回答を生成しているようです。!日本語書籍も登録はされていて回答の参考にされていましたが、日本語で質問しても回答自体は英語で返ってくる模様。のトピックに対して複数の書籍をあたりたいケース、例えばプロダクト開発や開発プロセスに関する書籍を探す際には、この機能をきっかけに書籍を見つけるのもありかな〜と思いました。

🧩
Tech

WEBRickのqueryをパターンマッチで取り出す

WEBRickではリクエストのクエリを`req.query`で取得できます。この戻り値はHashなので、例えば`hoge`を取得するのであれば`req.query['hoge']`のようにすれば値を取得できます。できますが、せっかくなのでパターンマッチで取得します。 `req.query.transform_keys(&:to_sym)`: req.queryのHashのキーはStringなので、パターンマッチで取得するためにSymbolに変換します。2. `req.query.transform_keys(&:to_sym) in :hoge`: パターンマッチでhogeを取り出します。ローカル変数`hoge`にreq.query["hoge"]の値が格納されます。気がついたらパターンマッチのカッコは省略可能[^になっていたので、変数2個取り出すくらいならガンガンに省略していきたいですね。[^: Ruby3.らだったみたい。知らなかった。

Tech

slidevでスライドの進捗に応じたプログレスバーを表示する

Slidevでスライドを作成する際に、現在のページを表示しつつ、全体に対する進捗をプログレスバーとして表示したいと思い作ってみました。Slidevではglobalbottom.vueというファイルを作成すると、自動ですべてのスライドにそのコンポーネントがレンダリングされます。以下の内容をglobalbottom.vueとして配置することで、プログレスバーを表示できます。```vue<script setup lang="ts">import { computed } from 'vue';const r = 40const strokeWidth = const circumference = 2 Math.PI rconst progress = computed(() => { return ($slidev.nav.currentPage / $slidev.nav.total) circumference;});const offset = computed(() => { return circumference progress.value;});</script><template> <footer class="absbr m3"> <svg w8 viewBox="0 0 0 0"> <circle class="basecircle" cx="50" cy="50" :r="r" fill="none" :strokewidth="strokeWidth" /> <circle class="progresscircle" ref="progressCircle" cx="50" cy="50" :r="r" transform="rotate(90 50 50)" fill="none" :strokewidth="strokeWidth" :strokedasharray="circumference" :strokedashoffset="offset"/> <text class="progresstext" fontsize="9" x="50" y="52" dominantbaseline="middle" textanchor="middle">{{ $slidev.nav.currentPage }}</text> </svg> </footer></template><style>// 色は適当に調節してください:root { bgstrokecolor: e2e8f0; progressstrokecolor: b9; textfillcolor: 0206;}.basecircle { stroke: var(bgstrokecolor);}.progresscircle { stroke: var(progressstrokecolor);}.progresstext { fill: var(textfillcolor);}</style>```!

📶
Tech

このブログのRSSフィードを配信できるようにした

はじめにRSSフィードを配信できるようにしました。 からご購読いただけます。 RemixでのRSSフィードの作り方PRはこちらです。もうPR以上に説明することもないのですが、やっていることとしては以下の2つになります。 `routes/feed[.]xml.tsx`を作成して、`/feed.xml`のコンテンツを作成する2. RSSフィードを配信するloaderを実装する おわりに解説することがなさすぎて、秒でおわりにまでたどり着いてしまいました。最後にあらためましてチャンネル登録・高評価お願いいたします!

🦀
Idea

Chat GPTを使ったオリジナルチュートリアルでRustを学ぶ

はじめに今年に入ってから何度かRustを勉強したいと思って初学者用コンテンツか何かを開いてみるも、だいたい最初の数章でやめてしまっていました。Rustに限らずなんですが、最初の方の内容は別言語とおよそ似たような内容で退屈で、かといって飛ばしすぎると中腹の内容は全然わからん...となりがちです。私は偏屈な人間なので「ただのTODOアプリには興味ありません(以下略」という思想のもと、何かしら自分にとって興味あるものを作ることで学習が捗るタイプなのですが、なかなかいい感じの難易度で、その言語の特性を抑えられて、なおかつ自分の興味あるものを作れるコンテンツは転がっていません。使い慣れた言語の別FWとかであれば自分で考え出せるのですが...。で、こういった分野はLLMにやらせるのが昨今はよかろうということで、Chat GPTでオリジナルチュートリアルを作ってみたところ、モチベーション高く進められたのでメモを残しておきます。 成果物 コード実際に書いてみたコードはこちらです。超簡易Markdownパーサーで、``と``しか変換できませんが、とりあえずHTMLに変換してくれます。雑な単体テストもあります。よりみち) リポジトリ名について。もともとRustでMarkdownに書かれたコードブロックを実行できるようにするツールを作ろうと思っていましたが、やめてMarkdownパーサー用のリポジトリに上書きしたという経緯があり、そのツール名が「RuMDn」だったのでリポジトリ名がチュートリアルっぽくありません。Rust でできたMarkdownのなんか、でRuMDnです。読み方は「ラマダン」です。 習得した(はずの)技術 所有権 参照と借用 構造体 列挙型 マッチ テストの書き方歯切れが悪い感じなのは、Chat GPTの返答内容のすべてについて、最新のドキュメントを参照できているわけではないからです。 チュートリアル チュートリアルの作成見ればわかる、細かいところは後で自分ではまって学べばよいと思い切り、簡単な部分はすっぱ抜いた上で、Markdownパーサーを作りながらRustっぽいところをいい感じに学べるコースをお願いしてみます。!良さげな回答が返ってきたので、> いい感じですね。分割して順番に取り組めるとよいです。Rustの概念自体は全く知らないので、中学生が分かる程度の内容で、いくつかのセクションに分けて教えてください。まずはセクションを示してくださいと返答してセクションを提示させます。実際に帰ってきたセクションがこちらです。!なんかRustでよく聞く単語入っている...。良さそうなのでゲームスタートです。> ではセクションらお願いします。途中質問等もあると思うので、私が「次へ」と言ったら次のChapter(セクション)に進んでください。 Chat GPTによるゆるい説明が続くミリしらの状態で挑んでいるため自分がジャッジすべきでないことは重々承知ですが、なんとなくゆるい説明が続きます。!中学生にもわかるように指定したからでしょうか、やたらおもちゃを使って比喩表現が出てきます。中学生はトミカやるのかな...?!ゆるいとはいいつつも、「次へ」と言わない限りは無限に質問を挟めるので、内容で疑問に思った内容は雑に質問していきます。冒頭で言語の簡単な部分はすっ飛ばしてよいと指示しましたが、やはり細かい部分で想像できない部分が出てくるので、そういった部分も適宜質問しています。 簡易Markdownパーサーの構築とテスト追加最初にChat GPTから提供されたコードは以下のとおりです。```rustfn parse_line(line: &str) > String { if line.starts_with("") { let content = &line[.].trim(); return format!("<h{}</h", content); } let emphasized = line.replace("", "<em>"); format!("<p>{}</p>", emphasized)}fn parse_markdown(markdown: &str) > String { markdown .lines() .map(|line| parse_line(line)) .collect::<Vec<String>>() .join("")}fn main() { let input = " This is a header\nThis is emphasized text"; let output = parse_markdown(input); println!("{}", output);}```いい感じに見えるものの、今後自分で機能拡張することと、単純に提供されたロジックがあっていない気がしたのでテストの書き方を聞きます。!で、教えてもらったテスト方法で単体テストを書いてみたところ見事にバグがあった(空行未対応、に複数の強調がある場合に対応できない)ので、これを修正して今回のチュートリアルは終了です。 おわりにくらいで終わるチュートリアルをお願いしましたが、テスト書いたりちょこちょこ調べたり含めて大体くらいで本当に終わりました。すごい。全然プロダクションレベルではないという自覚はありますが、それでも学習を進めるための砂場とざっくりとした知識が手に入ったのは大きいなと思います。新しい言語を始める際にはくらいでこの形式でまたやってもいいかもしれません。あと単純にMarkdownパーサーがテストしやすい、自分で考えなくてもパターンが広がりやすくて、自分の中の新しいもの学ぶ際のテンプレに加えてもいいなと思いました(ブログ作るか、HTTPサーバー書くか、Markdownパーサー書くか)。今年中にMarkdownパーサーが拡張されて、このブログでもwasm経由で使えるようになるといいなあ。

📋
Idea

ちょっとした個人プロジェクトの管理にVoltaを使ってみる

はじめに夏休みなので時間がとれたタイミングでいくつか個人の学習用のプロジェクトを立ち上げています。このサイトもその一つです(Remix + CloudflarePagesの学習用)。時間がとれるうちにコアな部分は実装しますが、日常ではなかなかまとまった時間が確保できないため、各リポジトリのGitHub Issueで管理することにしました。このときリポジトリ横断でのイシュー管理ができると満遍なく開発が捗りそうだ、ということでというGitHub オーケストレーションツール(?)を導入してみました。 VoltaとはVoltaはNuxt Labsが立ち上げた、GitHubのIssueとか通知をいい感じにまとめるツールです。Rust製のNodeバージョンマネージャとは全く別物です。 リポジトリの追加とファーストビューリポジトリにGitHub Appとしてインストールする形式で使用します。!サンプルとして個人サイトのリポジトリに接続してみました。GitHub Issuesと自動で連携され表示されています。使ったことある方はわかると思いますがToDoアプリのLinearに見た目が酷似しています😅。ボードビューの他にリストビューとタイムライン(GitHubのマイルストーンに紐づく)があります。 ステータス管理GitHub上のステータスとは別にVolta内部のステータスとして Triage Backlog Todo In Progress In Review Done Releasedがあります。いや個人プロジェクトなんでこんないらないなと思いましたが、それぞれのステータス列横にある「…」、もしくはBoard全体の設定からHideできるようです。 イシュー作成全体としてコマンドパレットが効いているのでそちら経由 or ショートカット:`c` or UI経由でGitHubにイシューを作成することができます。!GitHubのIssue作成が地味に重くて複数Issueをサクサク立てたい時には便利そうです。Issueへの返信もVolta上で行えます。 ステータスの自動管理「PRがIssueに紐づくとInProgressになる」みたいな自動化ルールが入っているみたいです。 通知管理!リポジトリごとに通知設定を行うことができ、設定した内容でInboxに通知が溜まります。個人リポジトリならこれはあまり関係ないですね...。 Chrome拡張該当のGitHubリポジトリやIssueに「Open on Volta」のボタンを追加するChrome拡張が提供されています。 おわりにNuxt Labsが作っているとのことですが、Vue本家周辺はこういうツール作るのうまいですよね。有料プランもあるらしいですが、さすがにそこまではせず無料プランでとりあえず使ってみようと思います。

🌬️
Tech

Tailwind CSSのクラスをいい感じにマージするshadcn/uiの`cn`ユーティリティ

はじめにをインストールすると、utilsとして`cn`関数がついてきます。これが自分でTailwind CSSを使ったコンポーネントを作る際にも便利なのですが、何をやってくれているのか理解していなかったので調べてみました。 要約`cn`は`twMerge`で`clsx`をラップした、「外部から指定されたTailwind CSSのクラス名をマージしつつ、オブジェクトの形式で条件付きのクラスを定義」をすることができるユーティリティ関数である。 cnの中身記事執筆(2023/08/)時点での`cn`関数の実装は次のとおりです。```tsimport { type ClassValue, clsx } from "clsx"import { twMerge } from "tailwindmerge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs))}````twMerge`で`clsx`をラップしていることがわかります。他に実装はないので、これら2つの効能を見ていきます。 twMergeの効能`twMerge`はライブラリtailwindmergeから提供される関数のです。Tailwind CSSでは同じ効果をもたらす別のプロパティがclassに指定された場合、CSSのルールに則っていずれかのプロパティしか適用されません。例えば、`px2 py4`を持つコンポーネントに`p5`を適用しても、`p5`は適用されません。そういうことを頻繁にやるべきかどうかの議論はさておき、これは既存のコンポーネントのスタイルを外側から上書きする際に不便な挙動です。`twMerge`を使用することで、衝突するクラス名だけを外から渡したものに上書きすることができます。```tsxconst MyButton = ({ classNames, ...props }) => { return ( // hover:opacity70はそのままに、Propsとして提供したclassNamesが適用されるようにclassNameがマージされる <button className={twMerge('px2 py4 hover:opacity70', classNames)} /> )}```詳細なマージの挙動についてはを参照してください。 clsxの効能clsxは文字列や配列、オブジェクト含めてclassNameをいい感じに連結することのできるユーティリティライブラリです。公式のUsageをそのまま貼り付けます。```tsimport clsx from 'clsx';// orimport { clsx } from 'clsx';// Strings (variadic)clsx('foo', true && 'bar', 'baz');//=> 'foo bar baz'// Objectsclsx({ foo:true, bar:false, baz:isTrue() });//=> 'foo baz'// Objects (variadic)clsx({ foo:true }, { bar:false }, null, { 'foobar':'hello' });//=> 'foo foobar'// Arraysclsx(['foo', 0, false, 'bar']);//=> 'foo bar'// Arrays (variadic)clsx(['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there']]]);//=> 'foo bar baz hello there'// Kitchen sink (with nesting)clsx('foo', [&& 'bar', { baz:false, bat:null }, ['hello', ['world']]], 'cya');//=> 'foo bar hello world cya'```さまざまなパターンでの連結と、Falsyな値をパージする機能があります。後者は特にオブジェクト記法でクラスと条件をマッピングしておくことで、条件付きスタイリングの際に重宝します。 twMergeとclsxを組み合わせて使う理由「Tailwind CSSのクラス名のマージと、オブジェクトでのクラス名の指定を行いたいから」 です。tailwindmergeでもクラス名の文字列連結を行うための`twJoin`という関数が提供されていますが、こちらはオブジェクトでの記法がサポートされていません。実装されていない理由はに記載があります。曰く、オブジェクトの記法はキーにクラス名が、バリューに条件がくることで、そのクラスがいつ適用されるかの認知負荷が高いと判断した、とのことです。リーダブルコードにもこんな感じの条件分岐の話がありましたね。Discussionの続きになりますが、スタイルを変更するにはクラスをまずはみるのだからオブジェクトにも対応してほしいとの返信があり、議論の結果としてshadcn/uiでも使用されているcn関数のような記述が返されています。```tsimport { twMerge as twMergeOriginal } from 'tailwindmerge'import clsx from 'clsx'export function twMerge(...args) { return twMergeOriginal(clsx(args))}```これによりこの関数になんでもかんでもクラス名を指定すれば、自分が最後に指定した内容が意図通りにスタイリングとして反映される便利関数の完成です(雑)。 型定義再掲になりますが、`cn`では次のようにして、`clsx`と同等の引数を受け取れるようにしています。```tsimport { type ClassValue, clsx } from "clsx"import { twMerge } from "tailwindmerge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs))}``` おわりに今回はshadcn/uiで提供されているユーティリティ関数`cn`を見てみました。`cn`という名前がclassName由来なのか、shadcn由来なのか気になりますね...。shadcn/uiにはもう、ウィンドウ幅に応じてTailwind CSSにおけるブレイクポイントを画面上に表示してくれるDevtools的なものがあるので、また別の機会にそちらの実装をまとめてみようと思います。

🧵
Life

刺繍をはじめてみました

刺繍をはじめてみたTwitterXでペンギンの刺繍を見かけたことをきっかけに刺繍を始めてみました。ドット絵を過去にやったこともあったのですが、そのときも事物をデフォルメした形で表現するのがいいなあと思って始めたので、こういう系統が自分の趣味かもしれません。 やってみたものこんな感じの刺繍キットを買ってみました。!お花を刺繍していって、最後にトートバッグを作るようです。!刺繍をやる部分はこのような枠で布を貼るのですが、この道具のビジュアルが最高です。手仕事感が前面に出ていて、クリエイター魂が燃え上がります。!目は青い花を縫い終わりました。 習得した技術 ロングアンドショートステッチ フレンチノットステッチ サテンステッチロングアンドショートステッチは細かさよりも、均等に幅を刻んでいくことが大事だと学びました。 感想楽しい!!時間を忘れて縫っていました。針を手に持っているのでスマホを途中で取るという行為もできず、デジタルデトックスが必要な私には非常に良かったと思います。新規開発であっても`dockercompose`や`npm create`をすれば一発で環境が立ち上がる、ミスをすればgitで戻せばよい開発と異なり、環境構築も全くわからんし、ミスったら物理的に戻せない点も非常に刺激的でよかったです。今回の進捗は花なのですが、これがあと個 + エコバッグを組み上げる作業があるので、アニポケでサトシがジムバッジ集めてリーグ挑戦するくらいかかりそうな予感はしていますが、ぼちぼちやっていこうと思います。

🎓
Idea

技術書を写経する時は、予習の写経・復習の写経の2段階に分けると定着がいい

はじめに:ハンズオン系の技術書を読むときの難点ハンズオン系の技術書、得意ですか?私は結構苦手です。読書自体はそこまで苦手ではなくて、紙面だけで完結するような書籍であれば割とスラスラと読めるのですが、ハンズオンが含まれると途端に進みが悪くなります。読み始めて最初の数章はいいんですが、だんだん写経するのが面倒になってくる一方で、とはいえハンズオンが売りの書籍でハンズオンをやらないと知識定着しないし...という感情のせめぎ合いに、しまいには読み進めること自体を放棄してしまうこともしばしば。今回はこの課題感を解消するためにやってみた「予習の写経」「復習の写経」が結構良かったので紹介します。 技術マスター 予習の写経 / 復習の写経アイデアのきっかけは、以前時雨堂さん主催の勉強会の形式にあった「勉強会の中で同じコンテンツを2回繰り返す手法」です。これに倣い、写経を同じセクションに対して2回行うのが、今回紹介する方法になります。 予習編Git管理下のリポジトリを用意し、そこに読んでいる書籍のハンズオンを実装していきます。もし書籍側から提供されているコードがあればそれを利用するのもいいでしょう。この段階では書籍の内容を読むことを中心にして、ざっくりと概要を掴みます。書籍にもよりますが、大体の章の単位で区切りをつけると良いと思います。あまり復習までの間隔が短すぎると内容を覚えてしまっているので退屈感が増してしまい、もともと解決したかった課題にそのままぶつかることにになるからです。 スイッチ予習が終わったら写経したハンズオンをコミットした上で、実装の詳細を全て消します。例えば次のような形です。```ts// Beforeconst greet = (name: string) => { return `Hello ${name}`;};// Afterconst greet = (name: string) => { // 実装の詳細を消す};```消す範囲は自由に決めて良いと思いますが、全部消すと内容が思い出せなさすぎるので、コードを見ればなにをすればいいのかがわかるくらいには残すのがポイントです。 復習編テキストを見ないで吹っ飛ばした実装の詳細を自分で実装します。ここではのセクションやファイルが自分で実装できるたびにGitの差分を確認し、復元できているかをチェックします。動作としてはハンズオンでやったとおり動いていても、微妙に実装内容が違ったりすることもあり、思わぬ自分の手癖の発見に繋がったりします。 メリット・デメリット一度予習した内容が頭にありつつも、完全に定着していない知識をあぶり出し、定着させられるのがこの方法のメリットです。実装が思いつかない・思い出せない部分は自ずと詳しく読むことになるので、最初の予習時に心理的にサラッと読めるのも良いと感じています。一方で一度だけ読む方法に比べれば手間がかかるのは事実です。知識が定着できなかった場合はトータルで見ても時間ロスかもしれません。またこの手法は今のところ何かしらのファイルに実装を重ねていくハンズオンでしか実施していませんが、コンソールで試していくタイプのハンズオンには向かないかもしれません。zxがマークダウンを解釈できますが、これの各言語版があると全ては解決かもしれません。 おまけ:大AI時代ならではの困りポイント初めてこの方法を思いついて実施してみた時に、穴埋めになったコードをGitHub Copilotが全力で補完してきました😅。実施時はオフにしておくのがよさそうです。

🆕
Idea

個人サイトをリニューアルしました!(N度目)

N度目のリニューアルもう何度目かわかりませんが、個人サイトをリニューアルしました。元のサイトはです(vercelにFreeでアップしているので、生き残ってはいる)。本当は以前のサイトをもう少し継続して使用する予定だったんですが、色々とローカルでいじっていたらAstroがぶっ壊れてしまい、直すのも面倒なので作り直したという経緯です。 使用技術 Remix Tailwind CSS / shadcn/ui Newt Cloudflare Pages今回はこんな構成で作ってみました。Next.jsのAppRouterを使っても良かったのですが、Cloudflareを使ってみたかったので相性の良いRemixを採用した次第です。 Remixかなりゴリゴリ書いてしまったので、正直Remixの開発体験がいいのか悪いのかはジャッジできません。リファクタリングしないといけない箇所がたくさんあるので、修正するなかでまた感想を書こうと思います。Cloudflareとの相性という点ではセットアップ時にCloudflare Pagesへのデプロイを指定できるので、ここはめちゃくちゃ良かったですね。あとからちゃんと確認しましたが、npm scriptsでnpm run devをwranglerにリフレクトするような仕草もデフォルトで入っていたので、デプロイはかなりスムーズでした。 NewtAstroでローカルがぶっ壊れたのはエラーメッセージ的におそらくMDXのパースと踏んでいたので、同じ轍を踏まぬように今回はCMSを使うことに。microCMSを使っても良かったのですが、あえてNewtを使ってみました。なんとなくです。CMSのエディタが個人的には好印象で、表示部分はNotionのようにいくつかのビューを選択でき、登録・編集部分では「絵文字」や「カラー」などの珍しいフォーム部品もありました。このブログでも絵文字をCMS側で登録して、それをブログ一覧に表示しています。マイナス点としては2つあり、はモデルの定義の場所がわかりにくい & 設定変更後保存しなくてもアラートが出ないのでよく保存し忘れること。もうはSDKの使い方が微妙に分かりにくいことです。後者はある程度使っていけばわかるようになりますが、初見でGitHubのREADMEを見ただけでは、複雑なクエリの発行が難しかったです。 Cloudflare Pagesずっと使おうと思って早数ヶ月、やっとことさ使うことができました。Pagesの方はNetlifyやVercelを使ったときの体験と大体同じなので割愛です。今回ブログの動的OGP生成のためにCloudflare Pages functionsを使おうとしたのですが、結果としてBの制限を超えられず使用を断念しました。vercel/ogのcloudflarepages版が出ていたのでそれを使った実装を考えていたのですが、おそらくvercel/og自体のバンドルサイズやらで容量制限に引っかかっていそうです。考えている過程はを参照いただければと思います。まあ固定でもいいっちゃいいんですが、時間がある時にまた対応しようと思います。 残タスクとりあえず公開できるくらいにしましたが、まだまだ実装残があります。 記事移行 技術スタックのページをつくる アンカーリンクがページ移動するだけで、リンクが切り替わらない ブログ、登壇の動的OGP対応 リンクカード対応 ページネーションまずは過去記事を移行しようと思っています。 おわりにもともと記事しかなかった個人サイトもリニューアルするたびに機能が増えていっています。前回の更新では登壇情報が増えましたが、今回は以下の機能が増えました。 ブログのカテゴリわけとタブ表示 個人的なお知らせ機能 年ごとの登壇数集計突然の移管は不本意でしたが、乗り換えることによって、自分の実装力の向上を図ることができ、結果として良かったかなと思っています。とはいえ次は以上もつといいなあ...。

🌀
Tech

[Node/Deno]ちょっとしたCLIで使えるスピナー

はじめにちょっとした CLI を作っている時に、ある程度長い処理(最近だと OpenAI の API 呼び出しとか)を実行している際に、ローディング時間が気になります。そんなときにスピナーを表示して、実行中であることを視覚的にわかるようにしたいのですが、これをやるためだけにライブラリを追加するのもやや微妙です。そのため、Node.js/Deno でちょっとしたスピナーを作ってみました。 実装いずれの場合も基本形は同じで、それぞれのランタイム固有の API に置き換えているくらいです。 Node.js```typescriptconst FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];export class Spinner { constructor( private intervalId: NodeJS.Timerundefined = undefined, private currentCharIndex = 0 ) {} start(message?: string) { this.intervalId = setInterval(() => { const spinner = FRAMES[this.currentCharIndex++]; const spinnerMessage = message ? ` ${message}` : ""; process.stderr.write(`\r${spinner}${spinnerMessage}`); this.currentCharIndex %= FRAMES.length; }, 0); } stop(message?: string) { clearInterval(this.intervalId); process.stderr.write(`\r${message ?? ""}`); }}``` Deno 実装```typescriptconst FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];export class Spinner { constructor( private intervalId: numberundefined = undefined, private currentCharIndex = 0 ) {} start(message?: string) { this.intervalId = setInterval(() => { const spinner = FRAMES[this.currentCharIndex++]; const spinnerMessage = message ? ` ${message}` : ""; Deno.stdout.write( new TextEncoder().encode(`\r${spinner}${spinnerMessage}`) ); this.currentCharIndex %= FRAMES.length; }, 0); } stop(message?: string) { clearInterval(this.intervalId); Deno.stdout.write(new TextEncoder().encode(`\r${message ?? ""}`)); }}``` 使い方```typescript// 使い方(Node.js)import timers from "node:timers/promises";(async () => { const spinner = new Spinner(); spinner.start("ロード中..."); await timers.setTimeout(3000); spinner.stop();})();// Denoimport { sleep } from "https://deno.land/x/sleep/mod.ts";const spinner = new Spinner();spinner.start("ロード中...");await sleep(3); // sleepはミリ秒ではなく、秒を渡すインターフェースspinner.stop();```spinner 自体には実行時間等の役割は持たせず、あくまで表示のみを行うようにしています。そのため、何かしら長い処理の前後に `spinner.start()` と `spinner.stop()` を挟む形で使うことになります。このインターフェースは大きめの CLI になるとやや微妙かな?と思ったりしましたが、今回はささっと使える Spinner を用意するのが目的だったのでこれでよしとしています。 解説 NodeJS.Timerwindow.setInterval の場合や Deno の場合も、setInterval の戻り値は number なのですが、Node.js ではイベントループを管理するためのオブジェクトとして NodeJS.Timer という型が用意されています。Node.js においては、setInterval の戻り値がであり、このクラスの型が NodeJS.Timer です。実際の型定義は以下の通りです。```tsdeclare module "timers" { // ... let setInterval: typeof global.setInterval; // ... global { namespace NodeJS { interface Timer extends RefCounted { hasRef(): boolean; refresh(): this; : number; } // ... } }}``````tsinterface RefCounted { ref(): this; unref(): this;}```ドキュメントを読む限りでは、以下のような感じでした。 デフォルトでは Timer はイベントループが有効な間は生存する このデフォルトの挙動をカスタマイズできるように、ref() と unref() が用意されている Timerrefresh() は Timer オブジェクトをリフレッシュする(タイマーがリセットされる) TimerSymbol.toPrimitive() は Timer オブジェクトを数値で返す。これによって、ブラウザでの setInterval との実装の互換性を実現できる。今回のように、ブラウザの setInterval と同じような感覚で使用している場合にはあまり効果はないですが、イベントループを意識した処理を書く際にはこれらのインターフェースが役に立つのかも? キャリッジリターン`\r`のことですが、単純に`console.log()`してしまうと、改行が発生してスピナーのフレームが変わるたびに 行ずつ表示されてしまいます。そのため、それぞれのランタイムの出力を利用して出力し、さらにキャリッジリターンを使って、カーソルを行の先頭に戻すようにしています。これでその位置に留まり続けるスピナーの完成です。 カスタマイズ ローディングに表示されるアイコンを変えるFRAMES に好きなアイコンを定義すれば、その通りに表示されます。今回表示している点字は を参考にさせてもらいましたが、これ以外にも適当な絵文字(🍣 とか 🍺 とか)を使っても良いかもしれません。 標準出力にまぎれないようにするこういったことをしたいユースケースとしては、「ターミナル上ではローディングを見せたいけど、処理結果を pipe 等で後続処理に渡したいので、標準出力には出力したくない」というケースかと思います。これは現時点での暫定解なのですが、標準出力ではなく標準エラー出力に出力すればとりあえず解決します。解決は解決なんですが、本来エラーのために用意されている箇所を、こういったハックのために使うのはいかがなものかという気もしますね...。 おわりに今回 intervalId を持っていて欲しかったので、イメージしやすくクラスとして作りましたが、関数型でやるとどうなるのかは気になるところ。気が向いたらやってみようと思います。...こういっていて気が向いたことはないので、多分やりません。どなたか実装されたら教えてください。

🛤️
Idea

Kaigi on Rails 2022で登壇しました!

月 2日、22 日に開催された Kaigi on Rails 2022 に Speaker として登壇しました!初のカンファレンス登壇で順番最後、かつギリギリまで準備が終わらず、最後の最後までずっとソワソワして参加者としてはガッツリは参加できなかった(アーカイブ見るぞ)のですが、とてもいい経験ができました!運営の皆さん本当にありがとうございました!登壇資料はこちらです!せっかくなので自分の発表についてつらつらと書いていきたいと思います。 登壇へのモチベーションスライドにもあるとおり、FJORD BOOT CAMP での学習期間を経て、今年の 6 月からエンジニアとして働き始めています。で、やっぱりエンジニアになったからにはカンファレンスとか登壇してみたいわけですね。学び初めの時は「地域 rb に顔出すのは上級者」くらいの気持ちだった人がそう思っているのは、完全に FJORD BOOT CAMP にいた影響だと思っています。なんか知っている人結構いるし、卒業生の中から RubyKaigi に登壇している人もいるし、じぶんもやってみたい!と自然に思えるようになっていました。 登壇テーマの選定今回はブラウザ自動化技術を登壇テーマに選んだんですが、理由としては 2 つありました。 ブラウザ自動化を便利に使っていたが、その中身はよく知らないままだなと思っていたので、その中身を知りたかった2. もともとブラウザ自体に興味があったつ目は発表にあった通りで、2 つ目については Web アプリを作る以上ブラウザに載せることがほとんどなので、そこをちゃんと知りたいなあという思いです。昨今では CSS フレームワークとかもよくできているので、見栄え的にはあまり考えなくてもまあまあ綺麗なものができるのですが、でももうちょっとブラウザ自体を知らないとブラウザの真価を発揮したりとかセキュリティ意識したりとかいろんな環境を意識して作るとかできないよなあと。動きを把握する時にはやっぱり実際に動きが始まるところから見るのがわかりやすいと思うのですが、ブラウザ自動化は自分が知っている領域でもあり、ブラウザに対するエントリポイント的な位置付けなので、今回はここにフォーカスしようという気持ちもありました。近いうちに簡易なレンダリングエンジン実装とかやってみたいですね(Olelink とか OleKit とかですかねえ) 準備 実際に WebDriver や CDP を読んでいく発表ではわりとさっくり「意外とシンプルに接続できる」という流れで紹介していましたが、当然のごとくミリしら状態で仕様を見始めたので、なかなかうまくいかず苦労しました。資料の中で「Capabilities」や「WebSocket」が用語で ページとってありますが、これは実際に試している中で「なんだこれ」とか「いまいちわからん」と詰まった箇所です(ActionCable ちゃんと使ったことないのがバレた)。例えば WebSocket の接続をする際にも WebSocket 自体の理解が曖昧で、最初は色々と回りくどい方法で URL を取得していたのですが、Ruby の CDP クライアント ferrum のソースを読んでいる中で「あ、サブプロセスで起動した Chrome の標準出力を正規表現で抽出して、WebSocket の URL 取得しているじゃん、これでええやん」と理解し、発表のような説明となりました。先人偉大なり。大変といえば大変だったんですが、その分色々と自分が知らない知識も増えて面白かったです。例えば上記箇所だと ObjectSpace.define_finalizer を知ることができました(一人 igaiga ふむふむタイム)。 chromedp/pdlgen の実装を読むのが大変だったなにが大変だったって、Go の文法を一切知らない状態から始めたのもあるんですが、chromedp で CDP をパースしているライブラリの最新ブランチが master じゃなくて old だったことなんですよね 😅自分のローカルに Clone して chromedp/pdlgen を実行した時に panic になって落ちてしまいました。Go の知識が乏しく「自分の環境が悪いってことかな」と思って何日か文法調べつつ読んでいき、最終的に一部分岐で落ちていることがわかったため、Issue で報告しようとしたタイミングで同じ事象の Issue があることに気が付き...> I guess its required to use the old branch?> Yes, the old branch is the latest branch.old ブランチにしたら動きました 😅 実装:Olaywright の実装で頭ごっちゃにCFP 提出時点で Olenium は動くものができていたので、上記の調べ物をしたのち、Olaywright の実装に取り掛かりました。が、この辺りで「フロントエンドエンジニア」となったため、仕事前には Ruby 書いて業務中は TypeScript だけ書いてて、仕事終わったら Ruby 書いて...となり、地味に大変でした。マジで何回 const で変数定義して、const がなくて怒られたと思っているんだ?(===もめっちゃやったなあ)まあそれくらいならまだいいんですが、オブジェクトに対する考え方が完全に JavaScript に引っ張られていたのが一番大変でした。今回の発表では JSON をパースして、Ruby のメソッドとして呼び出せるようにしていましたが、そこです。縛りなしで、個人的にもっとも実行しやすいインターフェースを考えると、たとえば Target.createTarget という CDP のメソッドは`target.create_target(url: 'https://google.com')`という形で呼べるのが Ruby っぽくかつシンプルで分かりやすいと考えました。で、これを静的に定義しておこうと思ったところで頭がこんがらがります。 Chrome へのメソッド実行は、実際のところメソッド名をただメッセージに渡すだけ。つまりメソッドの中身がないので、わざわざあらかじめ定義しておく意味があまりない。 JSON で落ちてきた CDP には、そのメソッドに関する説明が多分に入っており、それをすべて削除するような構造になってしまう。 `target.create_target()`がメソッドだとすると、CDP でやってくる「Target.createTarget の説明」や「Target.createTarget は experimental かどうか」といった情報を`target.create_target.description`、`target.create_target.experimental?`のように持たせるのが難しい(私がわからなかっただけなので、できたらどこかで教えてください〜〜!)さらにめんどうなことに Go の方読んでたら構造体が出てきてクラスが歪み始める 一日の大半は TypeScript を書いているので、またまたオブジェクトに対する認知が歪み始めるとなり、かなりの日数手が止まったあげく最終的には以下のような方針・実装になりました。 やっぱり`target.create_target.description`とか`target.create_target.experimental?`とか呼び出せると嬉しい(なお boolean を返すメソッドには Ruby ライクに「?」つけたかったのですが、忘れていたので実際のコードではやっていないです。) WebSocket へメッセージを送信する部分以外は振る舞いではなく、単なる情報(説明とか実験的機能だとか任意のパラメータだとか)の集まりなので、Struct でいいんでは? JS の Function オブジェクト的な感じで格納できればなおのことよかったんだけど、それは Ruby ではできない(?)ので、Struct のプロパティの中で他の情報と並列に`exec`というプロパティを保持し、ここに Proc オブジェクトを持たせる形で定義する Sturct のメソッドとして定義すりゃよかったんですが、Olaywright クラスで持っている WebSocket の接続情報のコンテキストを渡すのがさっとできなかったので、呼び出しが Proc の呼び出し(`exec[]`)になったわけです。 愚直によぶと`browser.domains.page.navigate.exec[url: 'https://google.com']`のような長さになる。これは嫌だ。 というわけで最終的に必要な Domain の情報を一行パターンマッチでキャプチャすりゃあええやろ(`browser.domains in { page: }`)かなり頭の中で考えるのに時間を使ってしまって、ちょっと前髪整えますねくらいのリファクタリングはしたんですが、そこまでで力尽きてインスタンス生成に 3 秒かかるプログラムになりました 💦 どう考えてもループしすぎですね、どうもありがとうございました。 実装:Gem 使うかどうか`Stringunderscore`を refinement で拡張しているところとか別に ActiveSupport 入れてもよかったんですが、ライブラリ使わないでも Ruby 本体だけで欲しいもの書けるんだ!っていう思いも発表にこめたかったので自前で定義しています(発表では見せませんが)。じゃあなんで WebSocket はライブラリを使っているかというと、この Gem の管理を現在やっているのが運営のうなすけさんだったので、何か選考に有利に働いてくれないかなあと思い、CFP の時点でこの Gem を使うということを明記したという経緯でした笑名前の通りめちゃくちゃシンプルでコードも読みやすいので、想定聴者がジュニア〜中級くらいのこの発表の筋にもあっている気がして、WebSocket の中でもこの Gem にしたというのもあります。websocketclientsimple のコードを読む過程で EventEmitter について知り、一人ターミナルチャットアプリやってました。勉強にはなりましたが寂しかったです。 なんで irb で動かすことにしたの?苦肉の策で上記の通り多段 Struct をやった結果、Olenium 同様にスクリプトを書いても全然補完が走らず、これはどうしたものかと思ったところ、「irb 上でインスタンス化した後なら補完走るじゃん!!」と思い立ちこうなりました。いやあ irb 便利。ちなみに補完が走る際に、Struct ではセッターの方のメソッドも補完が走ります。これはスクリプトを動かす際には使わないのでどうにか消したかったんですが(freeze はしているけど)やり方わからず断念しました(Struct のサブクラスに対して、呼べないですよね?できるのであれば教えてほしいです〜〜)。 Olaywright.usageこうやって作っていく上でもう Playwright の面影なぞ全くないに等しかったので、もう好き勝手やろうと思って追加しました。```rubydef self.usage puts <<~USAGE ○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○● ____ __ _ __ __ / __ \/ /___ ___ ___ _______(_)___ _/ /_ / /_ / / / / / __ `/ / / //| / / ___/ / __ `/ __ \/ __/ / /_/ / / /_/ / /_/ /| |/ |/ / / / / /_/ / / / / /_ \____/_/\__,_/\__, / |__/|__/_/ /_/\__, /_/ /_/\__/ /____/ /____/ ○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○●○● ┌─────────────────────────┐ │ What is Olaywright? ├────────────────────────────┐ └┬────────────────────────┘ │ │ "Ole no" Playwright. │ └─────────────────────────────────────────────────────┘ ┌─────────────────────────┐ │ Basic Usage ├────────────────────────────┐ └┬────────────────────────┘ │ │ Setup │ │ browser = Olaywright.new │ │ browser.domains in { page:, target: } │ │ │ │ Exec Command ex) Target.createTarget │ │ Notice: Use snake_case. You're in Ruby world. │ │ target.create_target.exec[url: 'https://google.com']│ └─────────────────────────────────────────────────────┘ USAGEend```これは Monodraw というツールで描いてみました。GitHub の人が ER 図をアスキーアートで書いてコミットメッセージに貼ることがあるという記事を見かけて、なるほどお!と思ったのをきっかけにインストールし全然違うきっかけで使ってしまいました。非常に便利なので今後も活躍しそうです。 資料作成今回の資料、再掲部分も結構ありますが、合計で 78 枚となっています。自己最多量です。 カラーパレットとコンポーネント作成は Figma で行なっていたのですが、調査とか実装が煮詰まった時にカラーパレットとコンポーネントを作成していたのが、直前になって非常に役に立ちました。!今回は背景色に真っ白を選択しました。今年の Kaigi on Rails のテーマカラーが大人ダークな色だったので、Youtube に載せた時に画面共有の枠が目立つように白にしたという感じです。 フォントフォントに関しては今回は 3 種類をミックスしていて Noto Sans JP New Rodin Pro Montserratを使用しています。これまではコーポレートロゴ ver2 とかラグランパンチとか奇抜なフォントを使っていたのですが、今回は時間も長いのとカンファレンスなので、読みやすいフォントにしました。 コード部分コードを掲載する部分も結構あったのですが、これには Figma の プラグインを使用しています。生成されたものをちょっとだけ加工して、角丸を作ったりしています。 表紙についてあと、最後にほんとどうでもいいところなのですが、表紙に載っている丸は実は点字になっています。! 発表練習資料ができたのが、本番二日前の遅い時間だったので、通しでの練習を始めたのは発表の前日。そのため、日目は要所要所でしか発表が聞けず残念でした 🥲今回すごい当たり前のことに気がついたんですが、30 分の登壇って通しで練習すると 30 分かかるんですよ...。なので 2 回練習すると 時間とぶ。なので、セクションごとに目標時間決めて各セクション単位で練習し、早口で喋ればぎりぎり時間内におわることは計測できたので、2 日目は普通に楽しみました。本番の時間オーバーだけは避けなければならなかったので、とにかく「とまるんじゃねえぞ」で早口でしゃべっていたらなんと巻きで終わりました。あらら笑 感想今回 CFP 提出にあたり、業務経験値的には話せるものはあまりないのでソフトトークに踏み切るという選択肢もありましたが、職業エンジニアになった以上、今回は絶対技術系の話をメインにしたいという思いが強くありました。「スパイス的にソフトトークを混ぜる感じにするといいかも」と CFP 考案中にレビューをいただいたので技術 9:ソフト くらいの構成にしましたが、ちゃんと技術メインで話せたこと、話を構成していくその過程で業務とは直接関係ない知識をじっくり読み解いていき、それを自分の言葉に落とし込んでいく過程を締切つきでやれたのは非常によかったと思っています。また CFP を出すこと自体も勉強になりましたし、カンファレンスで話すというのがどういう準備が必要なのかというのも今回知ることができました。一方で Olaywright のコードを書く部分で技術力的な脆さを感じたり、資料デザインも使える引き出しの数に限界を感じている部分もあったので、そこは今後の課題としてやっていこうと思います。今は仕事で Ruby を書いていないですが、今後も Ruby 関連のイベントには参加していきたいと思いますし、あとは今回カンファレンス登壇をさせていただいた経験を糧にフロントエンド関連のカンファレンスも登壇挑戦していきたいです! 謝辞今回 CFP を出すのも初めてだったので、チーフオーガナイザーの大倉さんには Grow.rb を通じて 2 回相談にのっていただきました。回目の時にはまだテーマも決まっていない状態で、2 回目の時には技術トークとソフトトークをどれくらいの割合にするかの方向性についてなど相談させていただきました。当初は自分の発表で 30 分も話すのは...と思っていたのですが、「話したいことがあるなら 30 分でもいいと思う」とアドバイスをいただけたおかげで今回の発表につなげることができました。この 回目の Grow.rb ではうなすけさん、imaumi さんに実際に RubyKaigi に提出した CFP を見せていただき、どういうふうに CFP を書いていくべきかについて非常に参考にさせていただきました。おかげさまでしっかりと CFP を書ききることができました。会社の開発チームの方も CFP 提出前にレビューいただきありがとうございました!運営の皆さんも準備から当日までありがとうございました 🙇‍♂️ 来年はハイブリッド開催とのことで非常に楽しみです!ではでは!