zoomガチャ


いえーい、どもども、ハッピー……何だっけ? 12月なんだから何かしらがハッピーな……えーっと、あれよ、その……アレだ、いえーい! ハッピー! やったー!

この記事は SmartHR AdventCalendar の1日目です。

アジャイルチームとガチャ

SmartHRの各チームでは、今年はだいぶアジャイルチーム、とりわけクロスファンクショナルチーム化を目指す流れがありました。多分クロスファンクショナルチームをググると、各分野の専門家たちが1つのチームで問題解決に当たる的な話が出てくると思いますが、SmartHRではその特色をさらに進めて「専門家のもとで誰でもなんでもできるようになる」ような結構スパルタな感じの開発チームを作っています。例えば、チームのQA専門家のもとでプロダクトエンジニアは全員QAの自動テストにコミットできるようになったり、UXWと呼ばれるプロダクトの文言を万人に使いやすくするためのライティングチームのもとプロダクト内で使う文言に全員で注意を払ったり、プロダクトデザインチームの監修のもと開発時にデザインの草案とアクセシビリティを意識したデザインプロトタイプ駆動の開発全員で行ったり、結構いろんなことをプロダクトエンジニアをはじめとするチームの全員がやっているような感じです。

当然、チームのスプリントを計画するスクラムイベントを中心としてスプリントレビューやリファインメント、プランニングや日々のデイリースクラムまで、ありとあらゆるミーティングで属人化の排除が進むようになっています。これは、特定の誰かがチームから離脱したとしても、それによってチーム全体の活動に悪影響を及ぼさないようにという理由からで、特にスクラムイベントのファシリテーションにおいては全員がすべての役割をこなせるように、トークスクリプトを用意した上でランダムに割り当てられたチームメンバーが行います。

さて、ここで問題になるのが誰がファシリテーターをやるかです。先ほどお話した「チームから離脱」というのは、有給なんかも該当します。たとえば、スクラムイベントでいつもファシリテーションをやってくれる人が、その日に体調悪くなって休んだりしたらじゃあ延期だねとはならないですよね? つまり誰でもファシリテーションができなくてはいけないんです。自分がいるチームでは、ファシリテーターのトークスクリプトと、ランダムにファシリテーターを決めるという運用でこの問題に立ち向かっています。でも、どうやってランダムにファシリテーターを決めるんでしょうか?

答えはガチャです。ガチャ。射幸心を煽る悪い文明のアレです。僕も結構嫌いですよ。でもまあ、リスク(例えば課金とか税金とか)さえなければ、わりとガチャは公平で良い文明です。何の理由もなくサイコロ振ってるのとそう大して変わらないですからね。

単に限定的なメンバーの中で回すガチャは、比較的簡単です。なぜなら、その限定的なメンバー全員をSlackのカスタムレスポンスで登録して、特定のキーワードでランダムに選ぶということが可能だからです。しかし、参加者が不特定なケースでのガチャって、どうすれば良いんでしょうか? 例えば、毎週木曜日に開催されるエンジニア定例だけど、参加自由で誰が来るかわからない、参加の候補者は40人を超えるけど実際に来るのは20人みたいなケースです。事前に登録しておきますか? 無理ですよね。なんならハズレのほうが多いですし。じゃあ当日にzoomのミーティングに参加している人の中からダンダムに選びたいってなったら、どうすれば良いんでしょうか?

zoomとガチャ

これに関しても答えは簡単です。zoomのAPIを使って、今現在ミーティングに参加している人の一覧を取得し、その一覧を何かしらの方法でランダムにシャッフルすれば良いのです。簡単。

https://github.com/kinoppyd/zoom-gacha

Rubyのzoom APIクライアントはなんかあんまりいい感じのがないんですが、別にそんなに複雑な動きをするわけでもないのでシンプルなもので十分です。

非常にペラいコードなので特に解説するところもないんですが、zoomのAPIを使って参加者一覧を取得し、その配列をHeadless Gachaに投げています。

あとはまあ、設置している場所的に社内の人にしか使えないようにGooge OAuth2でメールアドレスを絞っているくらいです。あとは、絵文字とかユーザー名に容赦なく突っ込んでくる人がいるので、アスキー文字以外は消すとか。ほんとそれくらい。

起動時には、環境変数 GOOGLE_CLIENT_ID と GOOGLE_CLIENT_SECRET に Google OAuth のキーとシークレットを、 ZOOM_API_KEY と ZOOM_API_SECRET に Zoom OAuth のキーとシークレットをそれぞれ渡してあげてください。基本的にHerokuにぶん投げれば動くはずです。

zoomガチャのトップページ。不正防止のために全員が回したガチャの結果にアクセスできる。

zoomガチャのトップページ。不正防止のために全員が回したガチャの結果にアクセスできる。

Headless ガチャ????

ヘッドレスなガチャです。流行ってますよね、無頭のたぐい。渡された配列の要素をランダムに並び替えて新しい配列として返すだけのAPIサーバーです。

https://github.com/kinoppyd/headless-gacha

特になにか深いことを考えて作ったわけではなく、酒を飲みながらなんとなくヘッドレスイェーイみたいな感じでついやってしまったんですが、他のAPIをイジるやつと組み合わせてガチャを作るには割と重宝する感じでした。

流れとしては、配列のJSONをパラメータにしてPOSTすると、ガチャった結果をJSONで返すパーマネントリンクにリダイレクトする感じです。パーマリンクを作ることで、不正防止的な感じをまあなんかいい感じにって感じがしています。

こっちもHerokuにぶん投げれば動くはずです。特に認証とか無いので、環境変数無しで動くと思います。

ヘッドレスガチャで返ってきたJSON

ヘッドレスガチャで返ってきたJSON

実際に使う場合

実はzoom-gachaのリポジトリの中に、smarthr.co.jpドメインのみを許可するコードと、ヘッドレスガチャのURLがハードコードされている箇所があります。もし自分の環境で利用する場合は、それらを書き換えてください。そのうち環境変数で渡せるようにしておきます。

属人性の排除

わりと大きく話は逸れましたが、zoomに参加している人のなかからランダムにファシリテーターを選べるという行動が、ミーティングやひいてはチームの属人性の排除に役立ちます。今時分が所属しているチームでは、そういう意味で徹底的に属人性の排除を行うために、スクラムイベントのファシリテーター(レトロ、レビュー、リファ、プランニング、デイリーと全部別の人を選ぶ)や、リリースを実行する人、モブプログラミングのドライバーとナビゲーター、プルリクのレビュアなど、何から何までガチャで決めています。

おかげでチームにおいて、誰々がいなかったら困るということはかなりなくなりました。POはいなかったら流石に困りますが、プロダクトエンジニアに関しては誰が同じバスに乗って事故っても問題ない状態です。

レッツガチャ

人生は属人的。だからこそそれ以外は属人性を排除していきましょう。みなさんも12月はレッツガチャ!


カテゴリー: 未分類 | コメント / トラックバック: 0個

大急ぎでRails+Reactのアプリケーションを作るときにやったこと後編


ある日突然、大慌てでWebアプリを作らなくてはいけなくなった

このエントリは、第二のドワンゴ Advent Calendar 2020の23日目です。

このエントリは、大急ぎでRails+Reactのアプリケーションを作るときにやったこと前編の続きです

このエントリは、自分は仕事でRails+ReactのAPI+SPAプロジェクトをいくつか経験してきたが、0からその環境を作ったことがないということに気づいてしまった私の記録です。多くの躓きを経て、非常に非常にかんたんな機能しか持たないアプリをつくるのにのべ20時間ほどの時間を要しまし、自分はReact+Railsエンジニアになったつもりでいたという反省文、その話の後編です。これから書くことは、0からRails+React環境を用意したことのない人に向けて書く、まさに書くは一時の恥、書かぬは一生の恥のエントリです。

前回までにやったこと

前編では、サンプルアプリ「ねこねこかわいい」を作るために以下の工程を説明しました。

  • rails new
  • DBの用意
  • Webpackerの設定
  • RailsとReactのインテグレーション
  • UI FrameworkとしてSmartHR UIの導入

後編のこのエントリでは

  • axiosのクライアントを用いてReactからRailsに通信
  • Google OAuthでユーザー登録
  • S3に画像の投稿
  • Herokuへのデプロイ

までを解説します。

axiosクライアントの作成

ReactからRailsのAPIに通信するためには、axiosを使う。なんでaxioかというと、手でFetchAPIを書くよりはだいぶ書き心地が良いことと、将来的にOpenAPIなどでスキーマ定義を作成して通信クライアントを自動生成するときにaxiosを使ったクライアントを選択できるからと考えたから。自動生成されたaxiosのクライアントに切り替えるときに、大きなショックが無いようにということですね。それ以外は特に積極的な理由はないです。

axiosをインストール。ついでに、Cookieもいい感じに処理してほしいので、axios-cookiejar-supportとtouch-cookieも入れる。ただ、入れてから気づいてまだ検証してないけど、Cookie系はもしかしたら必要ない可能性もある(とはいえ、最終的にSPAに持っていくときにはいると思うので入れておいて悪いことはないんじゃないかな、くらいに思っている)。

[yarn add axios @types/axios · kinoppyd/nekonekokawaii@8a9e5fd]

[yarn add axios-cookiejar-support tough-cookie @types/tough-cookie · kinoppyd/nekonekokawaii@6c0a6cd]

入れたら、どこのコンポーネントからでも使えるClientクラスをつくておく。また、Railsとの通信ではCSRFで保護してほしいので、CSRFトークンも送れるようにしておく。ここでのCSRFトークンは、Rails側のViewレンダリングでheaderに埋め込まれたものを取得して使う。つまり、いまはRailsのViewにReactコンポーネントを埋め込んでいるので対応可能だが、完全SPAなどにシフトしていくためには、また違う方法をとる必要がでてくるということになる。が、とりあえず今はこれで十分。

これを実際に利用するときには、各API用のクラスを作成して分割しておくと、後々ごちゃごちゃしなくて良いと思う。ひとまず簡単に、/postsにPOSTするだけのクライアントを作る。

[Add api client · kinoppyd/nekonekokawaii@56b9d11]

もちろん、Rails側にこのリクエストに対応するエンドポイントもはやしておく必要がある。

[Add Create post action · kinoppyd/nekonekokawaii@cb536da]

最後に、フロント側に非常に雑ではあるがAPI Clientを使ってPOSTを実行するコードを追加して、動作を確認する。非常に雑な感じではあるが、フォームの内容はStateで覚えておき、ボタンのクリックハンドラでAPIを呼び出すときに投げる。成功したらページリロード。

[Add new post form for index · kinoppyd/nekonekokawaii@eca716b]

ここまでで、axiosを使ってRails側のAPIを呼び出して、Reactから新しいPOSTを作れるまでの一連の流れが完成した。

GoogleのOAuthでユーザー登録

ここはあまり真面目に読む必要はない。Google API ConsoleからOAuth2の認証情報を追加して、OmniAuthを使ってGoogleアカウントからEmailなどの情報の認可を受け、それをもとにユーザー登録などを行うだけだ。正直、ググったほうがここより詳しい解説が出てくると思う。

ここでは、単にエッセンシャルなユーザーログインだけを書いていく。

まずは、email, display_name, avator の3つのフィールだけを持ったUserモデルを作成する。

[rails g model user email:string display_name:string avatar:string · kinoppyd/nekonekokawaii@8ba68dc]

Userモデルは、OAuthのコールバックのハッシュをもとにユーザーを作成するつもりで、次のようなユーザー作成ヘルパを書いておく。

[Create User from oauth hash · kinoppyd/nekonekokawaii@891b186]

次に、omniauthとomniauth-google-oauth2を導入する

[bundle add omniauth omniauth-google-oauth2 · kinoppyd/nekonekokawaii@22afc7c]

Omniauthの設定は、config/initializers/omniauth.rb 内に記述する。

[Add omniauth initializer · kinoppyd/nekonekokawaii@70880d6]

そして、OAuthからのコールバックを受けてUserの作成を行う、セッションコントローラの作成と、ルーティングの追加を行う。

[Add sessions controller and routes · kinoppyd/nekonekokawaii@a1ef697]

最後に、React側にログインボタンを追加して、 /auth/google_oauth2/ にリンクを貼れば、ユーザーの作成とログインまでの流れが完成する。

簡単に言ったが、これはController側でユーザーのログインをチェックしたり、状態によってUIを出し分けたりする処理が入るので、そう簡単にはできないし分量が多い。下記のコミットを参照してほしい。

[User login interface · kinoppyd/nekonekokawaii@410e365]

何をやったかを簡単にまとめておくと、OAuthの認可情報を取れていればUserモデルをセッションという名前でReact側に渡して、それによって処理を切り分けるようにしている。Sessionがなければログインボタンを出し、あればユーザーアイコンとPostようのフォームを表示している。とにかくしちめんどうなことが書かれているので、コミットの方を参考にしてほしい。

S3に画像の投稿

ねこが可愛いことを伝えるために、画像も投稿したい。当然そう思うので画像も投稿できるようにするには、ActiveStorageを使う。

まず、S3のバケット作成とIAM設定を行う。これは手順が本質的じゃない話なので、いくつか参考になるブログを見てほしい。

[ActiveStorageでファイルの保存先にAWS S3を利用するための準備 – Qiita]

この準備ができたら、まずActiveStorageを使えるようにする。今回はあえてrails newするときに省いたので、次のコマンドで使えるようにしていく。

[rails active_storage:install · kinoppyd/nekonekokawaii@46ae493]

[bundle add aws-sdk-s3 · kinoppyd/nekonekokawaii@9ac7424]

これにによって、ActiveStorageの使うDBのテーブルが作成され、S3と通信するようのGemも入る。

次に、config/storage.ymlを編集してS3を使うようにする。

面倒なので本番環境でも開発環境でもS3を触るようにする。本来ならばバケットを分けるべきだが、これはチュートリアルなので気にしない。

さらに、PostモデルにもActiveStorageのBlobを扱えるようにリレーションを書いておく。

[Add ActiveStorage configures and relations · kinoppyd/nekonekokawaii@8554224]

これで、PostにActiveStorageで保存した画像を紐付ける準備ができた。

最後に、Reactから画像を受け取りそれを保存するControllerと、更にReact側からどうやって画像を送るのかのコードを追加していく。ActiveStorageは、Railsのエコシステムとがっちり組み合わさって動くため、本来であればViewHelperを使ってファイルをアップロードする専用のフォームを作るのだが、フロントは全部Reactで書きたい。であれば、どのようにしてReactから送られてくるリクエストをもとに、ActiveStorageでBlobを作成すればよいのか? 最も手っ取り早い方法は、input type=”file” のフォームを用意し、Base64をJSONに入れて送り、それをController内でデコードしてStringIOに詰め直すことだ。

このように、リクエストパラメータにpictureが詰まっていれば、その中のdataというBase64エンコードされた文字列を、Base64.decode64でもとに戻してStringIOに詰める。それだけだ。フロントから送られてくるときは、Base64の文字列の先頭にファイル属性などの文字列が付いているので、カンマでスプリットしてデータ部分のみを取り出す(これは別にフロントでやっても良い処理だが、Rubyのほうが楽だった)。

フロント側は、このように変更を加えている。

FileReaderを使って、inputから渡されたファイルをBase64化した上でStateに保持し、リクエスト時にクライアントに渡している。クライアントはコードを次のように変えた。

また、Postモデルが画像を扱えるようになったことで、Rails側のViewではこうやって画像情報を渡している。

React側では、受け取ったURLを表示する。

全体的にやや雑な実装だが、これはプロトタイプなので気にしない。本気になったときにブラッシュアップしてほしい。

ここまでの流れで、Reactから渡した画像はRails経由でS3にアップロードされ、それを参照することも可能になった。

[Implements API and Front · kinoppyd/nekonekokawaii@2ae89d0]

Herokuにデプロイ

ここまでくれば、あとはアプリケーションをデプロイするだけだ。といっても、Herokuを使えば何も問題なくすべてが終わる。ちょっともう時間がないので、こればっかりは自分で調べてほしい。GCPのキーとAWSのキーを設定するのを忘れないように。

完成したアプリ

https://nekonekokawaii.herokuapp.com/posts

おわりに

React+Railsのアプリケーションを0から作って形にする一連の流れを解説した。なるべく、いくらでも拡張が効くようにしっかりとしたベースを作るつもりでコードを書けたと思う。これさえわかれば、あとはいつも仕事でやっているように、自分の好きなようにアプリを拡張できるようになると思う。

これでようやく、RailsとReactは書けるけど自分では何も生み出せないという疑念から自分を開放できた気がする。気持ちが凄く楽になった。


カテゴリー: 未分類 | コメント: 1 件

大急ぎでRails+Reactのアプリケーションを作るときにやったこと前編


このエントリは、SmartHR Advent Calender 2020 の1日目です

ある日突然、大慌てでWebアプリを作らなくてはいけなくなった

先日、勤め先のSmartHR社である奇妙な福利厚生制度が爆誕し、盛り上がった。この福利厚生制度に関してはちょっとした理由で詳しく書けないので、ぜひSmartHR社にカジュアル面談という形で話を聞きに来て確かめてほしい。

で、その福利厚生制度は、あるルールで複数の従業員の組み合わせが条件を満たしたときに効力を発するものだった。その組み合わせの条件はそんなに大変ではないが、自分から能動的に制度を利用しようと思う人でなければなかなか面倒というか、とにかく誰もかもが「制度を使うためにマッチングしてくれるアプリほしいな」と思っていた。私もそう思った。だから、普段仕事で使っているRailsとReactでサクーっと一晩で社内マッチングアプリを作り、社内でのスーパーハカーの名声をほしいままにしようとした私は、早速 rails new した。

そして気づいた。確かに自分は仕事でRails+ReactのAPI+SPAプロジェクトをいくつか経験してきたが、0からその環境を作ったことがないということに。薄々気づいていたが、もしかして自分は用意された環境でしか仕事が出来ないのではないか。いやそんなことはない。そういう気持ちを打破するためにも、なかば強迫観念に突き動かされるようにマッチングアプリの作成に取り掛かった。結果として、多くの躓きを経て、非常に非常にかんたんな機能しか持たないアプリは4日後に完成し、なんとのべ20時間ほどの時間を要した。

最終的にアプリは完成したが、この簡単なアプリを作るために20時間を要したという事実は、私の心臓を強く締め付けた。普段社内で「いやー僕RubyとかRailsとかReactとかできまっす」みたいな顔してる自分が急に恥ずかしくなってきた。頬に含羞の色が浮かび、空恥ずかしさに心がざわめき、穴があれば入りたい、生き恥をさらすとはまさにこのことである。しかしだからこそ、これから書くことは同じく0からRails+React環境を用意したことのない人に向けて書く、まさに書くは一時の恥、書かぬは一生の恥のエントリである。

ねこねこかわいい

突然だが、最近猫を飼い始めた。いろいろあって猫を飼うのが夢だったが、それが叶った形だ。猫は可愛い。本当に可愛い。だから、うちの猫を自慢したくなる。ということで今回は、社内の福利厚生マッチングアプリで得た知見をもとにうちの猫を自慢するためのアプリケーションをRails+React構成で作ってみようと思う。なお、最速で完成させつつアプリを大きく成長させるための土台もしっかり作ることを目指すので、まずはSPAとかのしゃらくさい方法はとらず、Webpackerを使いRailsからReactのコンポーネントをレンダリングするし、RailsはAPIモードとかにはしない。しかし、すぐにでもWebpackerを抜け出し、Webpackでフロントを完全に管理し、RailsはAPIとして機能できるようにする準備もやる。だから、きっちりした構成を最短で作るための、自分がやった最速の構成ということになる。そのため、それなりの分量があるので、前編と後編で分けることにした。

このアプリ「ねこねこかわいい」が目指すのは

  • RailsでバックエンドのAPI作成とWebpackerで作成されたアセットの配信
  • React+TypeScriptでフロントの作成
  • axiosを使用してReactからRailsに通信
  • Google OAuthでユーザー認識
  • S3に画像の投稿
  • Herokuへのデプロイ

の5つの項目となる。S3はActiveStorageを使うのでGCSでもいいし、Herokuも大して変わらんのでBeanstalkやAppEnginでもいい。BeanstalkはEC2のことを考えるのがめんどくさく、AppEngineは無料でFlex環境が使えないからHerokuを使うだけだが、とりあえずこの構成で始める。

また、このエントリの流れをコミットしたリポジトリも用意した。

https://github.com/kinoppyd/nekonekokawaii

各見出しの中では、実際に手順をコミットしたパーマネントリンクも提示する。

rails new

兎にも角にも、新しいアプリを作るにはここからはじまる。rails newは、新しいバージョンのRailsがリリースされたときに、記念行事のようにポチッとやるだけで、実はそんなに何度もちゃんとやったことがないので、ヘルプをしっかり読んだ。その結果、このコマンドになった。

順にオプションの話をしていく

  • DBはPostgreSQLを使うので -d postgresql
  • Git管理をするが、.keepファイルは別にいらないので –skip-keeps
  • ActionMailerもActionMailboxもActionTextもActionCableも使わないので、 –skip-action-mailer –skip-action-mailbox –skip-action-text –skip-action-cable
    • もし必要になったら後から足せる
  • フロントをReact+Webpackerでほぼ全て配信し、スタイリングもStyledComponentsを使うので、Sproketsは不要 –skip-sprockets
  • 将来的にフロントをRailsから離すことを考えると、TurboLinksも不要 –skip-turbolinks
  • Webpackerを使ってReactを書きたいので、 –webpacker=react

edgeはつけようかどうか悩んだが、いまはWebpackのメジャーバージョン移行期であり、かなり混乱がある(webpack-dev-serverがservになったりとかで動かなかった)ので、つけなかった。

rails new nekonekokawaii -d postgresql –skip-keeps –skip-action-mai… · kinoppyd/nekonekokawaii@fbe4f3c

DBの用意

世の中には、開発はSQLite3で行って本番はPostgreSQLやMySQLでやる、なんて甘い世界は存在しない。現場Railsがそう言ってるし、SmartHRが用意したRailsのブートキャンプでもその方針でやってきたし、何よりそんな甘っちょろい考えではメキシコで生きてはいけない。

ローカル環境での開発用DBは、もはやDockerComposeでサクッと用意する以外の方法が思いつかない。

DockerComposeは、各自でインストールしておいてほしい。今はLinuxだけでなくMacにもWindowsにも問題なく入る。

Install Docker Compose | Docker Documentation

DBは本番運用に耐えるものならば何でもいいが、HerokuへのデプロイをゴールとしたのでPostgreSQLを用意した。プロジェクトディレクトのルートにdocker-compose.ymlファイルを用意し、中身を書く。

config/database.ymlには、DockerComposeで立ち上げたDBにつなぐための設定を追加する。

これらの設定を用意した後、docker-composeコマンドでDBを立ち上げる。

これで開発用のDBが用意された。

Add docker-compose file and configure database · kinoppyd/nekonekokawaii@78102e1

Webpackerの設定

Webpackerを利用し、React+TypeScript環境を、フロントのコードをRailsの配下から極力分離した形で書いていくことを目標にする。最初のnewコマンドでインストールされるWebpackerは、最新の5系ではなく何故か4系が入る。おそらくedgeオプションを入れなかったからだと思うが、よくわからない。

一応Webpakerについて少し触れておくと、WebpackをRailsから便利に使うためのラッパーだ。WebpackDevServerとかも使えるように用意されている。ユーザーは直接Webpackを扱えずWebpackerを経由するしかないので、Webpackに慣れ親しんだ人やWebpackerを酷使したい人は結構辛いらしい。とはいえ、最初から入っているアドバンテージは大きいので、後々卒業してWebpackに切り替えていくことを視野に入れながら、最初はWebpackerで十分だろうと思う。

まずはTypeScriptを入れる。そして、型チェックをBabelでの変換時ではなく、Webpackのコンパイル時に行うように設定を追加する。

bundle exec rails webpacker:install:typescript · kinoppyd/nekonekokawaii@4d01262

yarn add –dev fork-ts-checker-webpack-plugin · kinoppyd/nekonekokawaii@a866f11

2つのコマンドを実行したら、config/webpack/development.ymlを編集する。

Add pre-compile type check settings · kinoppyd/nekonekokawaii@b7e48f8

この手順はWebpackerのTypeScript Integrationのヘルプに書かれている。

webpacker/typescript.md at master · rails/webpacker

また、デフォルトで作られているapp/javascript配下のReactコンポーネントはJSXなので、TSXにリネームする。とはいえ自動生成されたHelloReactコンポーネントは使うことは無いので、これは別にやってもやらなくてもいい。

Rename jsx to tsx · kinoppyd/nekonekokawaii@ccef21c

今度はTypeScriptの設定を行う。せっかく型の恩恵を受けるのだから、ある程度厳しくしておいたほうがいい。

asをたくさん書くのが面倒なのでSyntheticDefaultImportsを有効にする。そしていくつかのstrict系オプションを有効にする。noUnusedLocalsとかnoUnusedParametersとかも有効にしたほうが良いのだろうけど、開発中に機能をつけたり消したりしてると意外と腹が立つのでとりあえず外しておいた。TSあんまり詳しくないけど、productionのビルドのときだけ有効にする方法とかないのかな……とか思ったりする。

Modify tsconfig · kinoppyd/nekonekokawaii@0101fe6

最後に、今後Webpackerから離れていくことも考えて、Webpackerで管理するファイルをRailsのapp配下から移動させる。名前は好きにしていいが、プロジェクトのルート配下にclientやfrontendという名前のディレクトリを用意し、その中にsrcディレクトリを置いてコードを追加していくのが一般的な気がする。

まず、Webpackerの設定を変える。

そしてファイルを移動する。

これで、client/srcのディレクトリが、Webpackerの扱うフロントのコードのルートとなる。後々、Webpackに切り替えるときも、app/javascriptよりもここに配置してあるのは自然だと思うので、早めにやっておくに越したことはない。

Change front-end source code directory · kinoppyd/nekonekokawaii@315446e

RailsとReactのインテグレーション

WebpackerによってRails内でReactを扱えるようになったので、Reactのコンポーネントに値をバインドするための方法を簡単に実現したい。

Webpakerを使って作成されたReactのページに、Rails側からPropsを渡してレンダリングをするための方法はいくつか方法があるが、公式の比較表を見ると長期的にReact側をRailsと切り離していくためにはreact_on_railsを選択するのが良さそうに見えた。しかしその一方で、react_on_railsは複数プロセス立ち上げ前提のためにforemanを提示(べつに使わなくてもいいけど)されたり、SSRをHMRでやろうとするとProプランが必要だったりで、最速でいい感じにするには気にすることが多い気がする。

そのため、ここではほぼゼロコンフィグで動かせるreact-railsを使う。ゼロコンフィグというだけの理由で選んだ。react-railsでも最低限SSRできるし、アプリケーションを成長させていく過程で取り外しが楽だというのも見越している。

reactjs/react-rails: Integrate React.js with Rails views and controllers, the asset pipeline, or webpacker.

やることは簡単で、Gemを追加してgenerateするだけ。

generate react:install コマンドは忘れやすいので注意。

Add react-rails · kinoppyd/nekonekokawaii@fe7da2c

bundle exec rails generate react:install · kinoppyd/nekonekokawaii@a7d295d

ここまでの動作確認

ずっと準備をしてきたので、そろそろコードを書いてReactとRailsの動作を確認しないと不安になる。ということで、railsでおなじみのgenerateを使ってPostリソースを追加して表示を確認してみることにする。

bundle exec rails g resource post title:string body:text · kinoppyd/nekonekokawaii@71f8a3f

確認用にSeedデータも用意する。

DBをマイグレーションして、DockerComposeで用意したDBと疎通していることを確かめつつSeedデータを入れる。

データを表示するためのコンポーネントを作っていく。ディレクトリ構成は次のようにする。

Postの内容をレンダリングするPostと、一覧表示のIndex、詳細表示のShowを用意した。client配下のディレクトリ構成は、なるべくAtomic Desingの推奨するディレクトリ構成を模倣し、コードの分割に耐えるようにしていく。

表示用のコンポーネントはこれで完成。 ./bin/webpack コマンドを実行し、コンパイルできることを確認する。

もちろん、これらのファイルを書きながらシェルでwebpack-dev-serverを動かして、常にファイルの変更を検知しながらコンパイルエラーを確認しても良い。

フロントが終わったら、バックエンドのコードを書く。まずは、Controllerの簡単なリソース取得の箇所。

このコードは特に何の変哲もなく、Controller内でindexはPostを全件取得し、showでは特定のIDのものを取得しているだけだ。

問題はViewのファイルで、react-railsによって生えたreact_componentヘルパを使ってControllerで取得したデータをReactのコンポーネントに渡していく。いくのだが、当然渡す先はWebpackがコンパイルした単一JSファイルなので、型情報などは存在しない。そのため、全てがレンダリング時に動的に解決されるため、コンポーネントのIFとViewが渡す変数が本当に正しいバインドなのか、そもそもコンポーネントの指定が正しいのかなど、ありとあらゆる問題がエラー無しで通ってしまう。なので、react-railsの最も難しい点は、Reactの世界とRailsの世界の間を橋渡しするものがなにも無いという点にある。一度慣れてしまえば何となく分かるが、一度躓くとここが一番むずかしい。なにせ何のエラーも表示されないので、試行錯誤するしか無い。このブログに書かれている内容は自分が試行錯誤した結果にうまく行った方法なので、是非真似をして勘を掴んでほしい。

Viewのファイルは次のように用意する。

それぞれのControllerが標準で使うViewのERBファイルに、react_componentヘルパを書いて、コンポーネントのPropsに該当するハッシュを渡すだけだ。ページのレンダリングは全てReactに任せるので、これだけで良い。

これらのファイルを保存し、rails serverを立ち上げて /posts にアクセスすると、Reactで作成された一覧のページが表示される。

スクリーンショット 2020-11-30 4.51.13

特に何のスタイリングもしてないので、こんな感じの表示になると思う。

Add fetch Post resource · kinoppyd/nekonekokawaii@11833b9

ここまでで、ReactがRailsのViewとして配信されデータが正しく受け渡しされていることがわかった。

UI Framework

UI Frameworkには、SmartHR UIを使う。これはSmartHR社が公開しているアプリケーション用のコンポーネント集で、React+StyledComponentsで作られている。普通にアプリを作るのであれば、Material UIとかを使うべきなのだろうが、もともとは社内用のアプリを作っていた過程の記録なので、宣伝も兼ねてSmartHR UIを使う。また、スタイリングにはStyled Componentsを使う。CSS in JSには色々思うことが多い人も少なくないだろうが、コンポーネントの中にスタイルを閉じ込められて簡単に管理できるし何よりRails側のCSS配信とかを考える必要がなくなるので、Styled Componentsを利用する。

インストール

yarn add smarthr-ui · kinoppyd/nekonekokawaii@e31da09

yarn add styled-components @types/styled-components · kinoppyd/nekonekokawaii@ae8c8f7

次に、client/src/components/atomsディレクトリを作成して、基本となるパーツをimport、プロジェクト用にスタイリングしていく。ディレクトリ構成は次のようになる。Atomic Designにならって、最小の部品はcomponents/atomsディレクトリに置いていく。さっきと違ってindexファイルを置いてimportしやすくしているが、これはやってもやらなくてもいいし、むしろreact-railsの前では無力になるので結局export default書く必要があったりとややこしい。

ひとまず、BaseとHeadingをSmartHR UIからimportして少しStyledComponentsで加工。HeaderはSmatHR UIのパーツをそのまま使うのは難しいので、適当にCSSを書きます。

こんな感じで、スタイリングした部品を用意していく。同じように、既存のテンプレートとコンポーネントも書き換えていく。

これでなんとなくスタイルがついたはずなので、再び /posts を表示してみるとこうなる。

スクリーンショット 2020-12-01 2.18.49

なんとなーくそれっぽくなってきた。

後編に続く

普通に長いので、残りは後編に続きます。

前編では、railsアプリの初期化とWebpackerの設定、DBの準備と基本的な表示用コンポーネントの動作を確認しました。一旦ここまででも、最速かつ長期的な変更に強そうなRails+React+TypeScript構成のアプリケーションの作成をお見せできたかと思います。

後編では、axiosを使ったCRUD、Google OAuthでの認証、S3への画像アップロード、Herokuへのデプロイをやってみましょう。

それでは、よいAdvent Calendarを。


カテゴリー: 未分類 | コメント: 1 件

APIがカオスってたプロダクトでOpenAPI対応やってみた


このエントリは、 SmartHR Advent Calendar 2019 1日目の記事です

こんにちは、SmartHRでエンジニアやっているppydです。いま会社では、SmartHRに蓄積されたデータを可視化して分析する簡易BIツールを開発するチームに居て、フロントエンドとバックエンドとインフラのエンジニアをやっています。それくらい人手が足りてません、みんなSmartHRにきてください。助けて。

カオスったAPI

いまのプロダクトには途中参加なので、先人たちの名誉のために言っておきますが、そもそものAPIの設計がカオスっていたというわけではありません。グラフを描写するフロントライブラリのAPIの都合や、検索フィルタの条件が複雑すぎるために、エンティティの一部がカオスっていたというのが正しい表現です。とはいえ、それのおかげでAPIのレスポンスの全容がつかみにくく、またきちんとした型定義が無かったために、せっかくフロントはTypeScriptを使っているにも関わらず型の恩恵を受けられない箇所があったことは残念でした。他にも、現在のバックエンドはRailsを使っているのですが、ActiveRecordを使った実直な実装が果たしてこの先BIツールの要求に対して耐えられるのであろうかということも検討する段階にあり、もしバックのアーキテクチャを変えたときにフロントと不整合を起こさないためにも、APIの厳密な仕様を確定させることが必要だと感じていました。

そのため、フロント側のAPIクライアントを型付きで自動生成したい、そしてバックエンドの変更でフロントを破壊したくない、という2つのモチベーションで、プロダクトにOpenAPIを入れることにしました。

OpenAPI

すでに有名なので軽く触れるくらいにしておきますが、OpenAPIはRESTのWebAPIにおける仕様の記述方法です。もともとSwaggerという名でしたが、後にOpenAPIという名前に変更になり、現在はバージョン3が公開されています。RESTのAPIのかなり厳密な仕様を、YAMLもしくはJSONで記述します。

RubyとOpenAPI

Ruby製のプロダクトで(別にRubyに限った話ではなく、ほとんどの言語でこうだと思いますが)OpenAPIに対するアプローチは2つの方法があります。

1つは、何かしらの方法でOpenAPIの仕様に則った定義ファイルを書き、その定義をcommitteeなどのGemで常にチェックする方法です。これはOpenAPIの定義そのものに則りAPIサーバーを作成する、いわばTDD的な手法です。この方法の優れた点は、正しいテストがあればOpenAPIの定義を守ることを実装に強制することができることです。もちろん、APIを作成してからOpenAPIの定義を書くことも多々あると思いますし、これからお話する内容もそうなので、完全にTDD的とは言えません。が、方法としてはまず定義があり、実装を合わせていく、ということに変わりはありません。

もう1つは、swagger-grapeなどを使い、実際のAPIの実装仕様そのものからOpenAPIの定義を作り出す方法です。この方法の優れた点は、OpenAPIの定義が絶対にAPIサーバーの実装と乖離しないことです。そのため、APIサーバーから自動で生成されるOpenAPIの定義から、更に自動で生成される各言語用のAPIクライアントは、常に間違いなく最新のAPIサーバーの仕様を満たしていると保証されている点です。

この2つの方法には、それぞれ利点と欠点があり、そしてほぼお互いに真逆の特性を持っていると言えます。前者のメリットは、APIの定義は決してズレないということであり、後者のデメリットはAPIサーバーの実装によって定義はすぐに変わるということです。そして後者のメリットは、極力少ないコードで型情報付きのAPIのクライアントを自動生成できることで、前者のデメリットはAPIの定義の作成や変更に大きなコストがかかることです。

この相反するメリットとデメリットに対して、今年はいろいろな人と意見交換をしましたが、概ね次のような話に集約しました。

  • OpenAPIの定義を自分で作成したほうが良い時
    • APIの定義がほぼ定まっていて、大きな変更があまり無さそうな場合
    • APIの定義を変えないまま、バックエンドの大きな変更が考えられる場合
    • APIの定義にフロントの仕様が関わる場合
  • OpenAPIの定義をAPIサーバーのコードから自動生成したほうが良い時
    • APIの定義の変更が頻繁に考えられる、つまり開発スピードが求められるケース
    • 常に最新のOpenAPI定義が必要な場合
    • バックエンドの大きな変更があまり無さそうな場合

もちろん、最初はコードから自動生成して、ある程度固まったらOpenAPIの定義ファイルだけ分離して管理するという方法も考えられます。しかし、自動生成される定義ファイルはJSONの場合が多く、かつ人間が編集するのはやや大変なファイルが生成されるケースがほとんどなので、それはそれで大変だと思います。grape-swagger-entityなどを使って人間にもまだわかる自動生成ファイルを作ることもできるでしょうが、どっちにしろ大変であることに変わりはないと思います。また、プロジェクトの途中からこの方法を選択することも現実的ではありません。

すでにあるRailsのプロダクトにOpenAPIを入れる

いくつかの複合的な話になりますが、すでにある複雑なAPIの仕様を実装から完全に理解するのは難しいです。特に自分のプロダクトの場合は、フロントのライブラリの都合による箇所もあったので、尚更把握が大変でした。そのためまずやったのは、OpenAPIのAST(のようなもの)をJSONから生成するライブラリを書いて、すべてのリクエスト/レスポンスに適用するRack Middlewareを作成し、ある程度自動で定義ファイルを作成することでした。実はこのライブラリを書いた時点で、OpenAPIの仕様を一部勘違いしているところがあり、実際にはOpenAPIの自動生成としてはあまりうまく動かなかったのですが、副産物としてすべてのAPIのリクエスト/レスポンスに対する型情報を得ることが出来ました。このライブラリの詳細に関しては、SmartHR Advent Calendar がまだ全日埋まっていないので、この先も埋まらなかった場合はその枠でOSS化して詳細に書きます。

すべてのAPIの型情報が手に入ったら、次はその情報を元に定義を書いていきます。とはいえ、OpenAPIの定義くらい巨大になるとYAMLもJSONももはや人間が書くべきものではないので、ビジュアルエディタを使う必要があります。今回は、Stoplight Studioを使いました。OpenAPIのビジュアルエディタって、どれもなんとも言えない書き味のものばかりなんですが、Stoplicht Studioはその中でもまあそんなに違和感がないと言えるのではないかな、と思います。とはいえ、これもそんなに使いやすいとはいえないので、どちらかといえば一番マシという消極的な選択です。特に自動でモックサーバーを立てる機能があって、何も考えずにローカルのRailsと同じポートを定義に指定したりするとバッティングして死にます。どうにかならんのかこれ。

ビジュアルエディタを使えば、最初に集めた型定義ファイルをもとにサクサクとOpenAPIを書いていくことが出来ます。しかしここで気づいたのですが、先におすすめしないと言った「自動生成した定義ファイルから手動で定義ファイルを編集する」というパターンを自分で踏んでいることに気づきました。自分でやったからはっきり言えますが、これは大変な作業です。みんななるべく早めにOpenAPI定義しましょう。

また、OpenAPIを記述するのであれば、私はYAMLをおすすめします。ビジュアライザに取り込んだり、ライブラリで読んだりするための取り回しは1ファイルのJSONのほうが良いのですが、100行を超えるJSONは人間の扱える代物では無いので、素直にファイル分割したYAMLで書くべきだと思います。そしてYAMLでファイル分割した記述した定義を、openapi-generatorなどのツールを使いCIで1ファイルのJSONに変換してパブリッシュするのが最も良いと思います。しかし、自分の環境ではなぜかStoplicht Studioで書いたYAMLの定義ファイルは、swagger-codegenを使ってYAMLからJSONに変換すると、変換は成功するのに$refを一部うまく解釈してくれず、OpenAPI Parserに入れると音もなくcommitteeが死ぬ問題がありました。OpenAPI Parserはエラーを返さない上に、committeeはnilのメソッドを読んで死ぬ情報しかくれなかった(つまり、OpenAPI Parserの成果物が間違っているのに、それに気づかずcommitteeが死ぬ)という問題に直面したため、もうどうにもならず最後はJSONを直接編集してcommitteeにデバッグコードを入れながらJSONを修正していました。これはかなり時間がかかる辛い作業でした。本当に数千行のJSONは人間が扱うものではありません。そうなる前にYAMLをもっとちゃんとチェックしましょう。(この問題に関しては、後にopenapi-generatorに突っ込むとYAMLがぬるぽで死ぬという問題に直面し、どうやらSpotlight Studioが書き出したYAMLに問題があるのではないかという推測をしましたが、未だにちゃんとした原因がわかっておらず、解決できていません。けれど今ならOpenAPI完全に理解しているので、YAMLをただしく書き直せると思います!)

OpenAPI定義ファイルを作り終わったら、そのファイルをCommitteeに読ませて、実際のリクエスト/レスポンスと乖離していないかをチェックします。プロダクトにはユニットテストは大量にあったのですが、E2Eはまだ整備されていなかったので、開発環境でのみ例外を吐きステージングと本番ではエラー通知用のサービスに通知を飛ばすようにしました。その結果、作成したOpenAPIの定義はほとんど問題なく既存のプロダクトに適合することがわかり、今後はこの定義をマスター情報として守っていこうという状態です。本当によかった。

おめでとうございます! ついにカオスなAPIのプロダクトに、OpenAPIが入りました! 私はこれを実施するために想像の3倍の工数がかかりました!

まとめます。

  • やるべきこと
    • OpenAPIやYAMLでファイル分割して書き、CIで1ファイルのJSONに変換してパブリッシュすること
  • やってはいけないこと
    • OpenAPIへの理解が低い状態で便利なツールを書いてはいけない、事前に小さなプロダクトで実験しよう
    • 自動生成されたファイルを人間が読めるようにするのは、つらい、早いうちに決断を
    • ファイル分割したYAMLを書きましょう、巨大なJSONは人間がどうこうできるものではない
  • OpenAPI関連の工数は実際の見積もりの3倍かかる

私が言うんだから間違いないです。

OpenAPIについてよくわかっていなかったこと

もうひとつ、自分がOepnAPIについてよくわかってなかったなということを書いておこうと思います。OpenAPI最高だよみたいなのはよく見るんですが、俺はOpenAPIなんもわかってなかったみたいなのはあまり見ないので、俺は全然わかってないということを伝えたいと思います。

その前にまず最初に伝えたいのは、BOOTHでこの同人誌を買えということです。OpenAPI 3を完全に理解できる本 – ota42y。この本は、おそらく日本語で手に入る最も詳細なOpenAPIの仕様を解説した本です。英語でOpenAPIのSpecを読んでも良いんですが、正直色んな意味で疲れると思うので、この本を買うのが良いです。本当に。

買いましたか? 良いです。それでは、いくつかわかっていなかったことを列挙したいと思います。

allOfって何に使うの

マジでわかっていなかった。allOfは、2つのエンティティを合成したものを表現するときに使うんですね。

具体的に言うと、新規リソースを作るときのPOSTのボティって、大抵の場合はその要素を取得するときのGETのサブセットになりますよね? 例えば、ユーザーリソースがid, name, email持つ場合、GETではその3つを持ってくるけど、POSTするときに必要なのってnameとemailだけですよよね? その2つの要素の違いって、idがあるかどうかだけなんですが、エンティティ的には別になってしまいます。しかしそれは、二重管理では……? となります。allOfは、そんなときにこういう表現を可能にします。

allOfは、配列に含まれるすべてのスキーマの条件を満たしている際に正となります。つまりこの例では、Userエンティティはidという要素をもつことと、UserElementの要素を持つことを同時に要求しています。これは、Userを構成する本質的なElementと、メタ情報であるIdを別のエンティティとして表現できるということです。実際に取得されるUserと、作成時に必要とされるUserElementを分離しています。なんとなく冗長な感じがするかもしれませんが、二重管理よりは遥かにマシですし、ユーザーを表す本質的な情報は何なのかというのを明示的に示せます。

nullable

OpenAPI3からtypeのnullが消えてた、知らんかった

required

これは根本的に勘違いしてたんだけど、propertiesって定義してても実際にキーが無かったら通過するのね……propertiesと同じ階層でrequireに必要なキーを配列で渡さないと、キー消えてても何も言ってくれない

any

仕様上 {} で良いと思ってた。そうじゃない、こんな感じに書かないと死ぬ

あまりに妙なので、これに関してはまだ理解してない可能性がある

最後に

OpenAPIのいい感じの書き方、実はOpenAPI Parserのテストを読むのがよく分かります。各個のスペックを見てもいいし、そもそもリソースとして用意されてるYAMLを見ても良い。みんな読もう。

そんなわけで、SmartHR Advent Calendar 2019 よろしくおねがいします。


カテゴリー: 未分類 | コメント / トラックバック: 0個

技術書典7で、「ActiveRecord完全に理解した」という本を出します。


免責事項

本書のタイトルにある「完全に理解した」とは、ActiveRecordを完全に理解することではなく、あくまで社会通念上相当のActiveRecord「完全に理解した」であり、本書はActiveRecordを完全に理解することを何ら保証するものではありません。

 

技術書典7

ドワンゴを退職してSmartHRで働き始めそろそろ一ヶ月のkinoppydです。技術書典に出ます。場所は「◎貴サークル「トレイリア学園」は、 か01C に配置されました。」です。ブックマークはここからどうぞ。今回は、ActiveRecordのソースコードリーディングの本を出そうと思います。500円です。前回はメタプログラミングRubyの解説本でしたが、それを書いている過程で「そろそろActiveRecordのソースとか、俺でも読めたりするんじゃないかな……」という気持ちになったので書いてみました。

cover

そんな思いつきから、ひたすらActiveRecordのソースを読んでみた結果を、簡単にまとめて本にしてみました。

内容としては、ActiveRecordで最もよく使われている(であろう)機能のestablish_connection, find, where あたりのメソッドがどうやって動いているのかを、実際にソースコードを追いながら見ていく本です。実は7月の頭あたりからこの本の構想を考え、がんばってActiveRecordのソースコードめっちゃくちゃ読んだんですが、いかんせん相手は巨大すぎて、全てを読むことはできませんでした。そのため、一応自信を持って解説できるであろう範囲に狭めて深堀りしていく内容の本となっています。

そのため、AssociationやMigrationやSTIの内容などは全く出てきません。というより、それらのソースコードをまだ読めていないので、多分次の技術書典では「ActiveRecordなにもわからない」という本を出すと思います。

この本を読むためには

この本を読むためには、メタプログラミングRubyを通して読んだことがある程度の知識が必要である前提になっています。

そのため少し敷居が高いかもしれませんが、この本はRubyを書く上で必読の本のひとつなので、いい機会なので読んでみましょう。なんなら前回の技術書典で頒布した「Real World Metaprogramming Ruby」も一緒に頒布するので、ご一緒にどうぞ。

幾つかのトピック

今回の本は、電子書籍版のみの頒布です。物理版を作るにはちょっと締切の関係で間に合いませんでした。

また、この本が対象にしているActiveRecordのコードは、5.2.3です。原稿を書き始めてしばらくしたら、6出ちゃったんですよね。どうしようかと思ったけど、書き直してる時間なさそうなので5系にしました。

本を書いた感想

ActiveRecordのソースって、想像していたより全然きれいだな、というのが素直な感想です。その前に読んでいたのがSinatraだったから、余計にそう感じるのかもしれません。さすがにもう十数年メンテされている第一線のコードは、様々な人の目に触れているだけあって圧巻でした。そしてそれ以上にすごいなと思ったのが、RDocの充実っぷりです。ActiveRecordとActievRecord::BaseのRDoc読むだけで、もう大体ActiveRecord完全に理解できるんじゃないかという気持ちすらありました。すごい。

執筆環境

前回とあまり変わらず、まず草稿をesa.io上でMarkdownで書き、その後清書と校正をRe:VIEW形式でAtomとAtom用のRe:VIEWプラグインを使って書きました。ファイルの管理はGitHubでやっており、CIは特にありません(書いてるの一人なんで、手元で都度回せば困らない)。本当にほとんど何も変わっていないので、前回の記事を参照してください。

唯一前回と変わった点は、Re:VIEW Starterを使った点です(参考:技術系同人誌を書く人の味方「Re:VIEW Starter」の紹介 – Qiita)。そもそもRe:VIEWをそのまま使った出力にあまり満足していなかったというのはありますが、色々カスタムするにはTeXの知識なさすぎて詰み太郎だったため、色々いい感じにカスタムしてくれそうなこれを選びました。

大きな誤算が2つほどあり、1つ目は良い意味の誤算で、Re:VIEW Starterを使うことで前回の執筆時に悩んでいた「長いソースコードをどうやって行番号付きで折り返すか」問題が勝手に解決してくれたことです。素晴らしい。

2つ目は悪い意味の誤算で、Atomが何故か勝手に改行コードをCRLFにしてくれちゃったせいで、それに気づかずしばらくビルドが通らなくて締め切り直前に泣いていました。

近況報告

私は元気ですが、毎日ちゃんと期待されたパフォーマンス出せてるか不安になります。これは転職直後にはよくあることだと思うので、そのうち慣れます。

夜型の生活時間はやや改善されつつありますが、最近は原稿やるために朝四時に寝て朝八時に起きて会社行くとかいうよくわからないことやっています。まあでも、飲酒量がものすごく減りました。技術書典が終わったら、完全に人間の生活リズムを取り戻せる予感があります。

また、今回は時期の関係で寄稿できませんでしたが、SmartHRのエンジニアたちも技術書典で本を出すようです。

こちらもぜひよろしく。

追記

技術書典の日程が、ぱんっあふぉー23と完全に被ってて若干やる気がありません。ぱんっあ行きたかった。


カテゴリー: 未分類 | コメント: 1 件