TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

580

2022/4/23(土)にオンラインで開催されるGo Conference 2022 Spring Onlineにシルバースポンサーとして協賛し、2名のメンバーが登壇します。 Go Conferenceとは https://gocon.jp/2022spring/ Go Conference 2022 Spring Online Go Conferenceは一般社団法人Gophers Japanが主催し半年に1回行われるプログラミング言語Goに関するカンファレンスです。 前回 に引き続き、オンライン開催です。 今回、弊社は前回に引き続きシルバースポンサーとして協賛します。 ※ The Go gopher was designed by Renee French . Illustrations by tottie . BASEとGo プロダクトの大半のサーバーサイドがPHPで実装されているBASEですが、BASE BANKチームが開発・運用している資金調達サービスである「YELL BANK」、ショップの売上金をVisa加盟店の決済で利用できる「BASEカード」はGoの分散サービスとして実装されています。 thebase.in https://cp.thebase.in/basecard cp.thebase.in 「YELL BANK」のリリースは2018年12月であり、Goを使ったプロダクト運用実績は3年以上になります。 登壇内容について testingパッケージを使ったWebアプリケーションテスト(単体テストからE2Eテストまで) by @budougumi0617 gocon.jp BASE株式会社BASE BANKチームにて、 テックリードをしている清水( @budougumi0617 )です。「testingパッケージを使ったWebアプリケーションテスト(単体テストからE2Eテストまで)」というタイトルでプロポーザルを提出し採択されました。 ソフトウェアとテストは切っても切れない関係です。 いっぽう、Webアプリケーション開発においてはDBなどのミドルウェア・外部API・永続化情報の状態など様々な依存関係が存在します。 本セッションではDBや外部APIに依存するコードの単体テストからテスト中にWebサーバやDBを起動するシミュレーションテストまで、 私がtestingパッケージを使って行なっている様々なレベルのテストについて紹介します。 前職の経験も合わせると業務でGoのWebアプリケーション開発に携わり始めて5年になります。 その中で行なった試行錯誤して書いたテストのアプローチを紹介します。 プロダクトによってテストで担保したい品質、仕様は異なります。私の発表を視聴していただいた方のテストに対する「手札」がひとつでも増えれば幸いです。 Python製の姓名分割ライブラリをGoに移植した話 by @glassmonkey gocon.jp 一般的にわかち書きでは無い日本語で姓名から「姓+名」の分割を行うことは困難です。 しかし、Python製の姓名分割ライブラリ( https://github.com/rskmoi/namedivider-python )を用いるとある程度精度良く分割は可能です。 そこでシングルバイナリで扱えるGoのメリットを活かして、Python製の姓名分割ライブラリをGoに移植した話をします。 その際移植で工夫した点や気をつけた点をお話します。 今回、他の方が書いたコードを他言語に移植することを初めてチャレンジしました。 今回のケースではLL言語故のruntime時の不安定さをGoに移植することで解消することができました。 ただ、一部numpyの実装を独自に行う必要があったなど、メリットばかりでは無いことも学びとしてありました。 別言語の実装をGoやさらに他言語へ移植する際の意思決定の参考になれば嬉しく思います。 オフィス・アワーについて 今回は我々BASEはシルバースポンサーとして協賛させていただくため、イベント当日はRemo上でオフィスアワーを開いて参加者のみなさんをお待ちしております。 オフィスアワー中の弊社ブースでは弊社が定期開催しているGophers Code Reading Partyを開催予定です。 同Partyはコードリーディングに限らず、その場で集まったメンバーが最近気になったGoのトピックを話す集まりで、社外ゲストも含めカンファレンスの廊下やオフィスの休憩スペースのような雰囲気で雑談する集まりです。 普段使っているGoのOSSのコードリーディング ちょっと挙動がわからなかった標準パッケージのコード 直近話題になったGo関連のトピック・ブログ記事について BASEでGoをどうやって使っているか etc... publicリポジトリのissueで毎回メモを取っています。 当日用はこちらのissueを使う予定なので事前にトピックをコメントしていただくのも大歓迎です。 github.com 普段の雰囲気やトピックについては過去の回のメモ(既存issue)を御覧ください。 https://github.com/basebank/gophers-code-reading-party/issues 宣伝 ANDPADさん主催のGoConスポンサー企業合同アフタートークイベントにも参加予定です。 andpad.connpass.com 株式会社Showcase Gigさん、株式会社LayerXさん、株式会社アンドパッドさんと4社合同で行ないます。 弊社からは @budougumi0617 , @glassmonekey が次の内容の発表とパネルディスカッションに参加する予定です。 New Relic Oneを使ったObservabilityの実現方法と活用例 by @budougumi0617 Goで始めるTDD by @glassmonekey 宣伝その2 BASE BANKチームでは Go, Python, PHPを中心に、フロントからインフラまでを一気通貫で開発しています。 また開発だけでなく、機能をグロース・分析・サポートまで担当します。 そんな開発スタイルに興味あるぞって方は永野( @glassmonekey )にDMを送っていただくか、 下記のリンクから気軽にご連絡ください。 open.talentio.com 「転職活動はしていないけど、Goの日々の開発の困りごとってどうやって解決しているの?」のような雑談がされたい方は @budougumi0617 のMeetyでお話しましょう。 meety.net 最後に、Go Conference 2022 Spring Onlineに参加するには次のイベントページより参加登録をお願いします。 gocon.connpass.com それでは4月23日にお会いしましょう!
メンバーが登壇している様子 この度は、4/9(土)~4/11(日)に開催された PHPerKaigi 2022 に4名のメンバーが登壇しました。 今回は、登壇者 4 名からコメントと、他のセッションの感想などをお届けします! PHPerKaigi 2022 とは 2022/04/09(土) ~ 2022/04/11(月) の 3 日間にわたって PHPerKaigi 2022 が開催されました。今年はオンラインとオフラインのハイブリット開催になります。 BASE はこれまでにも開催されている PHPerKaigi への登壇並びにスポンサードをコミュニティ貢献活動として行って参りました。 登壇者のコメント 川島 ( @nazonohito51 ) TechLeadの川島( @nazonohito51 )です。 今回はBASEがサービスとしても組織としても成長していく中で生産性を維持するためのアーキテクチャ戦略についての発表をさせていただきました。社内でこの戦略が打ち出されたのはかれこれ2年ほど前になるのですが、明確な形で社外に公表されたのは今回が初になります。 この戦略はアーキテクチャの本からチーム・組織・文化などの本から「学習する組織」といったテクノロジー系とは言えない本まであちこち読んだ末に考え出されました。実態としてはクリーンアーキテクチャやマイクロサービスなどのアーキテクチャパターンというより、進化的アーキテクチャ+DevOpsといった趣旨の内容であると言ったほうが近いと思います。中長期的な期間で考えればアーキテクチャに固定的な解は存在せず、システムを取り巻く環境の変化の中で常にバランスを取り続ける変化する動体である必要があります。そして変化の方向は、その時の目先の問題だけを反応的に局所最適で解決するのではなく、常に何かしらの目的を達成するような構造へ向かうような指向性が求められます。そして弊社における目的とは資料の前半で触れられていたようなものでした。 各所の反応を見る限り「モジュラモノリス」という単語に惹かれた方が多そうな印象ですが、趣旨としては中長期的にアーキテクチャに対してどんな姿勢で行くかの考えを整理したものがメインコンテンツになります。モジュラモノリスはその中の中心ではありますが一部に過ぎず、「モジュラモノリス」という単語そのものに「組織の生産性」という期待を寄せているならば何か見落としがあると思います。この資料で終始徹底したのは技術的な方法論からは入らない、という点で、事業と技術の整合性をどう取るのかについて一番エネルギーが使われています。 このアーキテクチャ戦略は未だに手探りの部分がほとんどの状況ですが、これから社内で少しずつ進めていく予定です。 Discordチャンネルに送られた質問について Discordに送られてきた質問はおそらく他の多くの方も持たれる疑問だと思われるのでこちらにも記述します。 モジュラモノリスの時点ではDB分割をしていない状態なのか? そうなります。理由はDB分割の境界はドメインが根拠であるべき、と考えているためです。ドメイン基準で分割したいけど境界線が分からない->DB設計の手戻りはコストが高い->アプリケーションの手戻りはDBよりも低コストなのでまずはアプリケーション(=モジュール)境界を安定させてからそれをDB設計に反映する、という戦略を立てているため、モジュラモノリス開始時点ではあえてDB分割していません。 漸進的にマイクロサービスへ向かっていくかどうかでモジュールをまたいだトランザクション境界について考え方が変わらないか? マイクロサービス化するならモジュールをまたいだトランザクションを許してはならないし(マイクロサービス化する時点でトランザクション分かれる)、マイクロサービス化しないならそういうトランザクションを許可する、という考え方にならないか、という質問でした。 結論としてはご指摘のとおりになります。マイクロサービス化する場合、CAP定理にもある通りCAPのいずれかが大きく損なわれます。スライド資料中にも赤文字で記述していますが、モノリスと分散システムにおけるデータ整合性に対する戦略は根本的に異なります。モジュラモノリス時点でやれていたことはマイクロサービス化しても全てが同じようにできるわけでは決してありません。トランザクション境界に対する明確な戦略は打ち出せてはいないのですが、少なくともモジュラモノリス時点で同一トランザクションで処理することもできれば別トランザクションに分けることもできるという選択肢を用意しています。もちろんイベントドリブンな結果整合性の処理を実現することはモジュラモノリスにおいても出来ます。モジュール境界線が明確ならはじめからトランザクションを分けたり、結果整合性の処理にしてしまうことが後のマイクロサービス化するときに有利になります。が、境界線が明確でないなら無意味に更新処理が複雑化したり、更新が反映されていない参照が発生する可能性をもたらしてしまったり、あるいは「モジュール境界線自体が後から見直しやすい」というモジュラモノリスのメリットを一部手放すことになる可能性もあります。トランザクション境界に対しては現状画一的な判断はできず、都度判断することになることになると考えています。 あと実は、発表中には触れませんが、BASEで実際にマイクロサービス化する箇所は極めて限定的になるのではないかと考えている背景もあります。少なくとも全モジュールがマイクロサービスになって動いているような未来はほとんどありえないだろうと考えています。 モジュラモノリスというパターンについて モジュラモノリスというパターン自体は「こう作れ」という明確な指示があるわけではないので、自社が「モジュール」という構造を通して何を実現したいかによってその姿は変わってくると思います。弊社はクリーンアーキテクチャをベースにしましたが、これは一例に過ぎません。「マイクロサービスアーキテクチャによって何を達成したいのか明確に把握していない場合には、マイクロサービスアーキテクチャは悪いアイディアである」という言葉はモジュラモノリスにおいてもそのまま当てはまるかと思います。 スライド資料だけを見て動画を見てない方には誤解を生みかねないのでこちらの記事でも触れさせていただきますが、モジュラモノリスは決して銀の弾丸ではありません。マイクロサービスとは別の形をした諸刃の剣です。振り方を誤ればきちんと怪我をしますのでご注意を。 永野 ( @glassmonekey ) BASE BANKチームでEngineering Program Managerをしている永野( @glassmonekey )です。 今回は個人開発や副業で扱ってるGraphQLに関して、普段業務で扱ってるPHPを通してどうなのかをトークしました。今回事前収録が個人的にも初めてで運営の皆様にはご迷惑をおかけしました。 弊社ではGraphQLに取り組んでいるわけではなかったのですが、改めて導入すべきかどうかを漠然と考えていたので、発表資料を作る過程を通して良い思考実験になりました。皆様も迷ったら登壇駆動はおすすめです。 発表後には、GraphQLの導入に迷ってたがかなり参考になったといった感想をいただく機会もありかなりの励みになりました。特にオフラインだったので直接感想を言い合えるという体験は最高でした。 これも運営の皆様の調整あってこそだったと思うので、改めてありがとうございました。 大津 ( @cocoeyes02 ) Product Dev Division / Service Dev Section に所属している02( @cocoeyes02 )こと大津です。 今回はコミットメッセージ軽量規約「Conventional Commits」の説明と関連ツールを使ってみた様子をトークしました。 PHP カンファレンス沖縄 2021でトークした リーダブルコミットのすゝめ でも少しだけ「Conventional Commits」について触れましたが、今回はガッツリ環境を用意して試すところまでやった他、 「Conventional Commits」の公式ドキュメントサイトにIssueやPRを出すところまでやってみました (とはいえ反応薄くてちょっと悲しい・・・) 今回使用した ramsey/conventional-commits ですが、導入の提案 Issue を laravel/framework や CakePHP など PHP フレームワークのリポジトリで出してみようかなと思っています! 導入そのものよりも、PHP OSS コミュニティ界隈の人がコミットメッセージについてどう思うか議論するというのが目的です。 分かりやすいコミットメッセージのメリットをもっと多くの人が享受できるよう、今回の発表以外でも動いていきたいと思います! 炭田 ( @tanden ) Product Dev Division / Service Dev Section に所属しているtanden( @tanden )です。 今回のLTでは「Webサービスのバウンスメール処理の事始め」ということで、そもそもバウンスメールとは何なのか、AWS SESをつかってバウンスメールをサービスにどのようにフィードバックするのかを簡単に発表させていただきました。 LTの発表は事前収録ではなかったので、ココネリホールの会場での発表でしたが、オンラインでの気軽さや視聴のしやすさはもちろんあるのですが、オフラインでの発表の雰囲気はやはりいいな、素敵だなと改めて思いました(その分緊張もすごいのですが)。 素敵な雰囲気の会場を作ってくださった、運営の皆さまに改めて感謝申し上げます。ありがとうございました。 個人的な心残りは、LTで笑いを全くとらない真面目な発表になってしまったことです。次回はもっとフランクなLTに挑戦してみたいと思います!(笑) 他のセッションについて 若菜 ( @ wakanaction ) Product Dev Division / Service Dev Section に所属しているwkです。 Day1とDay2の少しだけ、オンラインで視聴参加しました。 いくつかかいつまんで感想書きます 👨‍🍳 予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント @t_wada さん ついにt_wadaさんのセッションをリアルタイムで聞くことができ、感激...! PHPerのみならず、そして初学者から上級者まで広い範囲の方に刺さりそうな内容だった 中でも、以下を用いて堅牢な設計を考えていく運びが大変ためになった 型宣言 列挙型 モデリング 普遍性と等価性 完全性 責務の配置 メソッドに渡る値を型宣言によって絞る、さらに扱う内容が限られている値は列挙型で絞る、といった具合 度々、 プログラマが知るべき97のこと から引用されていたが、中でも以下を重点に置いていた。自分の業務でもぜひ参考にしたい考え方だった。 いいインターフェースの条件とは、正しく使用する方が 操作ミスをするより簡単 誤った使い方をすることが困難 「不安や疑念はテストに書いておく」 業務ロジックに限らず、FWの挙動、組み込み処理の動きなどもテストに残しておくことで、PHPの思わぬ落とし穴に気付けたり、一方を直したら一方が壊れた、なんてことに気づきやすくなっていて、精神衛生としてもすごく良かった。 総論:設計やコードレビューの際に常に念頭に置いておきたいような、即業務に活かしていける内容だった。型厳格な方向に進んでくれたPHPの恩恵に感謝しながら頑張ります コミットメッセージ規約「Conventional Commits」を導入してみよう! @02 さん Commitメッセージ、わかりやすく書きたい気持ちはあるものの、実際どうしたら良いかいまいちわかっていなかった 日本語で丁寧に書いてみたり、英語で統一して書いてみたり色々試した Commitメッセージに関する規約があるのは初めて知った フォーマットが決まっており、「Prefix (feat, fix, など) 、タイトル、本文、フッター、破壊的変更」のような内容でcommit メッセージを書く 多少規約が厳しめに感じたが、それくらいの方が規約を用いる意味があるか。または続けやすい形で一部取り入れるのも良いのかも。 また、コミットメッセージをわかりやすく書いていきたいと考えた時、副次的に以下のような考えにも至った。 コミットメッセージをわかりやすくしたい ↓ コミットに含む内容をわかりやすくしないと、わかりやすいメッセージは作れない ↓ 適切な範囲でコミットを切る意識が育てられる! 総論:大規模開発において、Commitメッセージが残す情報は重要であるため、試しにでも実施してみようと思った。 普段使っているSourceTreeでは複数行にわたるCommitメッセージが書きやすいため、試しやすいと思った。 shiiyan( @shiiyannn ) Product Dev Division / Service Dev Section に所属しているshiiyanです。 PHPerKaigi2022のDay1とDay2をオンラインで参加させていただきました。 印象に残ったいくつかの発表に感想を書きます。 day1 - MongoDB に溜まった約1.6億レコード、データ量1TBのあらゆるサイトの記事データを BigQuery で高速検索できるようにした話 植江田さん 大規模データ処理関して、最近業務上でも課題がありました。こちらの発表は課題解決のヒントになれるかと思い、PHPerKaigiの中に特に興味を持ちました。 MongoDBに保存されたデータについて以下のことが紹介されました。 様々なサイトから記事をクロールしクリップする クロールした記事データをMongoDBで保存 1日で10万件のレコードが保存される 1日10万レコードならば、1ヶ月で300万レコードとなり、数年経てば約1.6億ととんでもない規模になっていくことがわかりました。 データ移転中の課題について以下のことが紹介されました。 動的スキーマから静的スキーマへの移行課題 存在しないカラムがあるとエラーとなる カラムの順番が変わるとエラーになる 処理時間とサーバーストレージ容量の課題 移行処理が完了までに20時間以上が必要 移移行処理が完了までに6000以上のcsvファイルが必要 課題に対しての解決法について以下のことが紹介されました。 存在しないカラムにnullを入れる PHPの連想配列でカラムの順番を固定した ストリームコピー( stream_copy_to_stream )を利用すれば2倍高速した 動的スキーマを採用したデータストアでは、カラムが利用中で増えても、カラムの順番が変わってもエラーなく使い続けます。マイグレーションが不要でスキーマの変更やメンテナンスがしやすい一方で、静的スキーマのデータストアに移行する時に、スキーマの整備という手順が発生するという知見を得られました。 また、ファイルを開いて一行ずつコピーする以外に、ストリームコピーというやり方を今回で新しく学びました。高速化というメリットがあるので、今後PHPでファイルに書き出す処理を実装する時に活用できると思いました。 day2 - コミットメッセージ規約「Conventional Commits」を導入してみよう!02さん 今まではコミットメッセージを割と雑で書いていました。そのせいで、PRのコンフリクト対応時やgit rebase時に苦労した経験がありました。 『コミットは他人が見るものだから、他人が書き手の意図を理解できないと❌』というのが発表の内容にありました。まさに、その通りだと思いました。コミットはpushするだけのものではなく、将来の自分や他人がその意図を理解できないと意味がないと理解しました。 Conventional Commitsというコミットメッセージの軽量規約も紹介されました。 featやdocsなどのコミットメッセージのプリフィックス前から知っているものの、コミットメッセージ規約を勉強するのは今回が初めてでした。 発表に利用された サンプルリポジトリ のコミット履歴を眺めると、規約に沿って書いたコミットメッセージがあるとソースコードを見なくても、何をやったのかを想像できることが実感できました。 発表の後、choreというプリフィックスはいつ利用されるかを調べました。 chore (updating grunt tasks etc; no production code change) 私の理解では、featやdocsなど明確な目的があるプリフィックス以外、本番コードに影響しないその他的な変更があった時に使います。 また、破壊的変更(Breaking changes)があるときに必ずフッター部分で明言することも覚えました。 今後はぜひ規約に合ったコミットメッセージを書いて、コミットメッセージが役立つようにしたいと決めました。 最後に 今回計 4 名のメンバーが登壇する機会をいただき、 PHP コミュニティの盛り上がりに貢献することができ大変有意義な時間となりました。 また自身の発表以外にも、多くのスピーカーの発表を通して各々が新たな知見や気づきを持って帰れたと考えております。 業務でお忙しいにも関わらず、スタッフの方々には多くの時間をカンファレンス準備へ割いていただいたかと思います。この場を借りて心より御礼申し上げます。 今回はトーク編の記事となっております。他にもスポンサー編、アンカンファレンス編、スタッフ編の記事を投稿する予定です。 それでは、来年もまた皆様にお会いできることを楽しみにしております!
はじめに こんにちは。バックエンドエンジニアの小笠原です。 今回は、2022年2月18日から2022年3月4日にかけて発生していたこちらの障害に対し私達開発チームが実施した、session.cookieで定義しているCookieのkey名を変更するという影響範囲の大きい対応について、実施に至るまでの経緯や対応過程についてご紹介したいと思います。 ショップオーナー向けに掲載していたお知らせの内容 背景 全ては iOS14.5から端末識別子の取得に同意が必要になったことから始まった ことの発端は、iOS14.5以降からIDFA(端末ごとに持つ固有識別子)の取得に端末所有者の許可が必要になったことでした。 この変更は、端末所有者側から見ると情報の活用範囲を自身で管理できることでよりプライバシーに配慮されるようになった良い変更と言えるでしょう。 一方で、広告出稿側から見た場合は拒否をしたユーザーの広告トラッキングが出来なくなることで広告の効果測定が大幅に制限される、という問題が発生してしまいます。 この問題に対して、Facebookピクセルという広告効果測定ツールを提供しているMeta社(旧Facebook社)は、広告効果測定の仕様を変更して合算イベント測定による集計を行うことでIDFAの取得を拒否したユーザーについても広告の効果測定ができるように対策を行いました。 BASEにおいてもInstagram広告Appがこの影響を受けるので、何らかの対応を行う必要に迫られました。 合算イベント測定に対応する際の詳細については本記事の主題ではないのでここでは省略させていただきますが、結論としてeTLD+1なドメインを認証することで合算イベント測定を使用可能になるということがわかったため、当時開発チームはショップ開設時に選択することができるドメイン群をPublic Suffix List(PSL)に登録するという対応を行っていました。 Public Suffix List(PSL)とは Public Suffix List(PSL)とは、jpやcomなどのTop Level Domain(TLD)と、co.jpやmeguro.tokyo.jpのような実質的にTLDのように振る舞うことが期待されるeffective Top Level Domain (eTLD)を管理しているリストのことで、GitHub上で管理・運営されています。 このリストに対して必要な情報を添えてPull Requestを送ることで、誰でも任意のドメインの追加を申請することが可能です。 https://github.com/publicsuffix/list つまり、PSLに任意のドメインを登録することでそのドメインをeTLDとして扱うようにすることができ、これによってショップのURLがeTLD+1と認識されるため、ショップ単位でドメイン認証を行うことで合算イベント測定を使用可能になる、ということです。 この対応のため、開発チームはショップ開設時に選択することができる以下のドメイン群をPSLに登録する申請を行いました。 base.ec official.ec buyshop.jp fashionstore.jp handcrafted.jp kawaiishop.jp supersale.jp theshop.jp shopselect.net base.shop リポジトリのPull Request履歴を確認すると、登録申請をしたのは2021年9月14日で、マージされたのは2021年12月5日だということがわかります。 https://github.com/publicsuffix/list/pull/1420 PSLに登録されたドメインにはCookieを保存できない ところで、 PSLに登録したドメインにはCookieを保存することができなくなってしまいます。 仮にjpのようなTLDに対してCookieを保存可能にしてしまうと、そのドメインを使用している全てのWebサイトでそのCookieを共有できることになってしまいます。TLDは不特定多数の利用者が様々な目的でサブドメインを取得して運用していることが多く、このような広範囲に対してCookieを参照可能な状態にしてしまうことはセキュリティリスクが高いため推奨されるものではありません。 そのため、TLDにはCookieを保存できないルールになっています。そして、TLDと同様の振る舞いをするeTLDに対しても同じことが言えるため、TLDと同様にeTLDに対してもCookieを保存できません。 つまり、PSLにドメインを登録するということは、そのドメインに対してCookieを保存できなくなる、ということを意味します。 PSLへドメインを登録したことによってどのような影響が出てしまったのか BASEのショップでも例に漏れずCookieを利用しており、例えば「シークレットECショップへのログイン情報」「カートへ商品を追加する際の商品情報」などはCookieの THEBASE というkey名に保存して管理していました。そして、これらのCookieはショップ毎に割り当てられているサブドメインに対してではなく、前項で紹介したPSLに登録したドメインに対してCookieを保存する処理になっていました。 つまり、これらの情報をCookieに保存できなくなったことで「シークレットECにログインできない」「カートへ商品を入れてもカートの中が空のまま」といった不具合が発生していたことが今回の障害の裏側で発生していた事象でした。 なぜPSLにドメインを登録してから数ヶ月経過してから問題が顕在化し始めたのか PSLにドメインを登録したのは2021年12月5日ですが、この障害を開発チームが認識したのは2022年2月19日の段階でした。 なぜおよそ2ヶ月ほど経過するまでこの不具合に気がつくことができなかったのかというと、それはブラウザが最新のPSLを取り込んだタイミングが関係していたようでした。 実は、各ブラウザは常に最新のPSLを参照しているわけではなく、任意のアップデートのタイミングでその時点の最新のPSLのスナップショットをビルドに含めて参照しています。 さらに、以下の表のように必ずしもアップデート時に最新のPSLへと更新しているわけではなく、その更新周期には規則性がないこともわかりました。 ブラウザ名 PSLの更新周期 FireFox Firefox96(2022/01/12リリース)時点ではBASEの登録したドメインは含まれておらず、Firefox97(2022/02/08)には含まれていた Chrome chrome97(2022/01/04リリース)時点で 2021/10/27 のPSLを取り込んで以後、更新されていない 上記の通り、直近のFirefox97のリリースによってこの障害に遭遇する購入者が徐々に増えてきたのではないか、と推測されました。 障害への対応内容 base.shopなどのeTLDに対してCookieが保存できないという問題に対して、今回はショップのURLに該当するサブドメインに対してCookieを保存するように変更するという方法を採用しました。 これは、ショップ毎にサブドメインを割り当てているBASEの仕組みを考えると、基本的にはショップの中でsessionが保持できれば購入者の買い物体験は阻害されないであろう、という判断によるものです。 実現方法として、チームでは以下2点の選択肢が挙がりました。 Cookieのdomain属性でサブドメインを指定する Cookieのdomain属性を指定しない domain属性を指定しなかった場合は一番狭い範囲に対してCookieが保存されるため、挙動としては「サブドメインに対してCookieを発行するように変更する」という点でどちらの対応を実施しても同じ意味となります。 今回はセキュリティの入門書として有名である『体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践』にdomain属性を指定しない状態が最もCookieの送信範囲が狭く安全な状態であるという言及があったことから、後者のdomain属性を指定しないように修正する方針に決まりました。 問題点 さて、前置きが長くなってしまいましたが、ここからが本記事の本題となります。 対応方針が決まったところで検証環境で動作確認をしていると「対応後のソースコードでもシークレットECにログインできない」という障害が稀に発生することがありました。 修正前と修正後の動作確認結果の比較 この現象は eTLD+1をドメイン属性にもつ THEBASE のCookieと、eTLDをドメイン属性にもつ THEBASE のCookieが同時に送信されているケースで発生していることがわかりました。 これは、今まで不具合が発生していたショップ(Cookieが保存できていなかったショップ)では修正後のCookieのみが保存されているために不具合が解消された一方で、今まで正常にログインできていたショップ(Cookieが保存できていたショップ)で新たに不具合が発生するようになった、ということです。 そもそもなぜ同じkeyのCookieが二種類できてしまうのか THEBASE のCookieは有効期限をセッションに設定していたため、この現象に遭遇した場合はブラウザを再起動すれば古いCookieが削除されて問題を解消することができると予想されました。 ところが、実際にブラウザを閉じてショップを開きなおしても、本来であれば消えるはずの前回アクセスした際のCookieが残ったままになってしまっていることが発生していました。 実は、この問題はブラウザが「前回開いたサイトを復元する」機能を実現するために、ブラウザを閉じた後も有効期限がセッションになっているCookieを保持し続ける挙動をすることが原因で発生しているらしいことがわかりました。 さらに、同名のCookieが存在する場合のCookieの取扱もブラウザによって異なっていることが私たちを混乱させました。 ブラウザ Cookieの並び順 同名のCookieが複数ある時シークレットECにログインできるか FireFox 古いCookieが新しいCookieよりも先に並ぶ できない Chrome 古いCookieが新しいCookieよりも先に並ぶ できない Safari 新しいCookieが古いCookieよりも先に並ぶ できる このように、ブラウザによって挙動が異なっており、いつCookieが削除されるのかがブラウザ依存であるという状態であることから、Cookieのdomain指定方法を変更するだけでは障害から復旧できないことがわかってしまいました。 解決案の模索 この問題に対して、私達のチームでは2つの案について検討しました。一つ目の案はこの現象を許容したままで対応をリリースすること、そして二つ目の案はsession.cookieで定義しているCookieのkey名を変更した上で対応をリリースすること、でした。 この2つの案の比較検討と障害の影響範囲の把握のため、開発メンバーで協力してソースコード上でsession.cookieの定義を使用している全ての参照箇所を洗い出しました。 以下の表は、この2つの案に対してそれぞれ比較検討した内容を表にまとめたものです。 案1:Cookieの重複を許容する 案2:Cookieのkey名を変更する 影響範囲 FireFox97とSafari以外のブラウザを使用している購入者 全ての購入者 メリット すぐにリリースできる 完全に不具合が発生しなくなる デメリット Cookieが二重で登録されてしまった購入者には、ブラウザキャッシュを削除してもらう必要がある セッションがリセットされるので、再ログイン等が必要になる 工数 なし リグレッションテストが膨大 影響期間 ブラウザの旧Cookieが消えるまで(つまりいつ収束するか不明) デプロイのタイミングのみ どちらの案でも発生するデメリットとして、デプロイを跨いで購入者が操作した際に以下の影響が出るという問題がありました。 改善リリースデプロイ前に抽選・定期便・コミュニティ限定商品をカートへ追加して未購入状態の場合、改善リリースデプロイ後にはカート内の商品が全て消えてしまう コミュニティ会員ページへログイン済みの状態でも、再度ログインが必要になってしまう シークレットECがかかったショップページへアクセスをしている状態でも、再度PW入力が必要になってしまう デプロイをまたいで購入をしたユーザーの場合、決済が走っているもののCookieを持ち越せないために購入完了画面が表示されないことで、購入に失敗したと誤解をして重複購入してしまう 修正対応リリース前後で問題が発生するケース そして我々はsession.cookieを修正してCookieのKey名を変えた 最終的に、Cookieのkey名を変更する案2の方がより購入者に優しいだろう、ということで決まりました。 一時的な不便を全購入者に要求してしまうことにはなるのですが、購入者に要求する操作としては再ログインやカートへの再度の商品追加など、通常のWebブラウジングの操作の範囲内で対処できるものとなっています。 逆に、案1の場合はブラウザのキャッシュを削除するという通常のWebブラウジングでは行わない操作を購入者に要求してしまう上に、BASE以外のサイトのキャッシュも削除してしまうことになります。もちろん特定のサイトのみのCookieを削除することもブラウザの機能としては可能ですが、その操作はさらに難易度の高いものです。 また、案1については重複したCookieが削除されるタイミングがブラウザ依存であるため、インシデントの収束タイミングを把握できないという問題点がある以上避けるべきだろう、という意見もありました。 そうして、上記のような理由から安全かつ完全な形で障害から復旧させる方法である、Cookieのkey名を変更してからリリースする、という方法を実施する決断を行いました。 リリースに当たっては、万全を期すために調査で判明したsession.cookieの定義を使用している処理を全て網羅するテストケースを作成すると共に、QAチームが使用しているリグレッションテスト項目を共有してもらい、購入者の一般的な操作を全て動作確認することでより安全性を高めました。 これによってさらに障害の復旧までに時間を要することにはなりますが、より安全かつ完全に対応するためには必要な作業だというのが開発チームの共通認識でした。 おわりに 今回、障害の発生を認識してから収束するまでの間に2週間という時間がかかってしまった点と、対応の副作用によって購入者様の皆様にご迷惑をおかけしてしまったことは大変申し訳なかったと感じています。 サービスを提供していく上で、障害を起こさないように気をつけることは重要なことです。そして、万が一障害が発生してしまった際には、如何に素早く影響を最小限に留めて適切な対処で障害を解決することができるか、という点もまた重要なことです。 BASEでは、このようにBASEが提供するサービスを利用してくださる購入者やショップオーナーの皆様のことを第一に考えてサービスを共に発展させていく仲間を募集しております。 カジュアル面談も実施しておりますので、ぜひお気軽にお問い合わせください。 https://open.talentio.com/r/1/c/binc/homes/4380
フロントエンドエンジニアの @rry です。 自分は BASE の Sales Promotion というチームで主に新規機能開発を行っています。このチームでは主にオーナーさんの使う管理画面に新しく機能追加をしています。 そこで、管理画面で使っている API Client と型を、 OpenAPI Generator を使って自動生成するようにしてみたのでそのお話を書きたいと思います。 そもそも OpenAPI とは? https://www.openapis.org/ OpenAPI とは、RESTful Web サービスを記述、生成、使用、および視覚化するための仕様です。 ※ 以前は OpenAPI ではなく仕様自体も Swagger と呼ばれていましたが、現在は仕様自体については OpneAPI と呼ばれており、Swagger というのは OpenAPI を使ったツール群のことをさすようになりました。まぎらわしいので Swagger ではなく主に OpenAPI と呼びます(ツール群のほうも「OpenAPI のツール」と呼んでいきます) BASE では YAML ファイルで記述しています。 OpenAPI とそのツール群を使うことでなにができる? API の仕様(スキーマ)を定義 定義の一元管理ができる API ドキュメントを生成 ドキュメントのメンテナンスが楽 API モックサーバーを立てられる API が出来上がっていなくても先にフロントエンドの開発ができる API Client を自動生成 API Client のコードをフロントエンドで書かずにすむ! API リクエスト・レスポンスの型を自動生成 スキーマから生成した型を使うことでより型安全になる 周辺ツールでいろいろできる バックエンドの実装と定義したスキーマが乖離した場合に自動テストが落ちるようにしたりもできる バックエンドの実装が乖離しないようにできる OpenAPI のようなスキーマを中心にした開発のことを、「スキーマ駆動開発」といいます。 OpenAPI を使ったスキーマ駆動開発をすることでなにがうれしいの?どういう問題を解決するの?というところは、以下のスライドが参考になるのでそちらをどうぞ。 BASE 既存システムへの OpenAPI 導入の背景について BASE では最近カートの大規模リプレイスを行いました。 BASE Tech Talk #1 〜Next.jsを使ったカート大規模リプレイスPJの裏側〜 - connpass 新しいカートのアーキテクチャでは、既に OpenAPI が導入されておりスキーマ駆動開発を行っていました。自分もフロントエンドの開発で OpenAPI から生成した API Client を利用したりしていました。 しかし BASE のオーナーさんの使う管理画面など、カート以外のシステムでは既存の API 定義は OpenAPI ではなく API Blueprint を利用していました。 API Blueprint を使った開発では API の仕様(スキーマ)を定義 API ドキュメントを生成 API モックサーバーを立てられる これらのことはできますが、API Client や型を自動生成することはできず、毎回手動で API Client と型を定義していました。 手動で定義したり API の仕様が変わったときにそれらの追従をすることが大変だと思い、カート開発のときと同様の開発体験を得たかった自分は「今回の PJ から OpenAPI を使ったスキーマ駆動開発をしよう!API Client と型を自動生成していこう!」と呼びかけ、そのための仕組みを導入することにしました。 API Client って何?自動生成ってどういうこと? API にリクエストを送るためのコードを API Client と呼んでいます。 // このような感じのコード export class FooApiClient extends APIClient { async getBar () { return this .request < APIResponseWith < Bar >>( { url: ` ${ BASE_PATH } /foo/bar` , } ) } } BASE では今まで上記のような API Client を手動で書いていたのですが、これからは OpenAPI から自動生成する API Client を利用していくことにしました。 以下は実際にどのようにして API Client と型を自動生成しているのかについて詳しく説明していきます。 ① API Client を自動生成する仕組みの概要 OpenAPI からどのようにして API Client を生成しているかをまとめました。 OpenAPI の個別のファイル群を編集 一つの大きな merged.yaml という OpenAPI ファイルを swagger-merger を使って生成 merged.yaml を元に openapi-generator-cli を使って API Client やスキーマの型を生成 その他便利関数と一緒に GitHub Packages を使って npm パッケージとして配信 openapi-generator-cli の typescript-fetch を使って fetch API の API Client を生成しています。 API Client を生成する流れはこのような感じですが、他にも merged.yaml を元に Docker を利用して色々しています。ReDoc / SwaggerUI を立てて API ドキュメントを読んだり、API Sprout を使って API モックサーバーを立てたりもしています。 どのような開発体験になるか まずバックエンドエンジニアが API を作る前に、OpenAPI の YAML ファイルだけを追加した PR を出して API のスキーマについてフロントエンドエンジニアと共にレビューします。PR がマージされると GitHub Actions が自動で API Client の npm パッケージを配信してくれます。 フロントエンドは配信されたパッケージを利用して、スキーマに沿った API へのリクエスト・レスポンスを実現することができます。 また、便利関数として API モックサーバーへのリクエストもできるようにしています。 これにより API の開発を待たずしてフロントエンド側の実装を進めていくことができます。 このようにして自動生成した API Client は以下のような形で使うことができます。 import { apiConfig , FooApi , FooBarResponse } from 'api-client' const client = new FooApi ( apiConfig ) const result: FooBarResponse = await client.getBar () ② OpenAPI の個別のファイル群について OpenAPI の個別のファイル群は、API Client を生成しやすいようにいくつかの命名規則に沿って作られています。 ディレクトリ構成は以下のとおりです。 ├── README.md ├── docker-compose.yaml ├── src ├── _components.yaml - components 定義 ├── _paths.yaml - paths 定義 ├── components - 共通 components 定義 │ └── error_response.yaml ├── merged.yaml - Docker から参照するためのファイル。特にいじらない ├── openapi.yaml - ベースファイル └── services - サービスディレクトリ └── <service_name> ├── components - service の components 定義 │ ├── xxx_request.yaml │ └── xxx_response.yaml ├── definitions - service の definitions 定義 │ └── user.yaml ├── examples - examples 定義 │ ├── <paths_name> │ │ ├── default.yaml │ │ └── xxx.yaml │ └── 400_example.yaml └── <paths>.yaml src/openapi.yaml の tags に name を定義 src/_paths.yaml に path を定義 src/services/ 配下に yaml ファイルを作成 yaml ファイルの内容が src/merged.yaml に反映される というのがザックリとした編集方法です。 OpenAPI は $ref というキーワードを使って外部ファイルを参照可能なため、 path API エンドポイント リクエスト・レスポンスの schema example これらを services 配下にまとめて、細かくファイル分割をして管理しやすい形にしています。 ③ API Client やスキーマの型を生成するにあたっての命名規則 ファイル名やその他命名など細かい規則をいくつか設けていますが、その中でも API Client の生成に影響するものをまとめました。 フォルダの命名規則 src/services/* 配下 各 API のスキーマを置く場所 API の URL と同じ構成にする この際 path に api が入っている場合は api を抜く 例) /apps/api/foo/bar なら、 src/services/apps/foo/bar.yaml となる 生成される API Client はフラットな階層に一律出力されるため、そもそもの命名としてユニーク性が必要です。そのため URL の構成に従って命名しています。 tags と operationId tags フォルダの path の services から親となるリソースまでの path をつなげる 例) src/services/apps/foo/bar.yaml なら appsFoo になる tags は services の各まとまりごとに同じものを用いる 例) /services/apps/foo/bar.yaml と /services/apps/foo/baz.yaml は同じ tags appsFoo を使う operationId リクエストメソッド + リソース名 例) /services/apps/foo/bar.yaml の GET リクエストだったら getBar となる API Client が生成されるとき、tags が API クラス名で operationId がメソッド名となります。そのため上記の命名規則で生成されるクラスは以下のような形になります。 export class AppsFooApi extends runtime.BaseAPI { async getBar ( requestParameters: GetBarRequest = {} , initOverrides?: RequestInit ) : Promise < AppsFooBarResponse > { // ... } } requestBody と responses のスキーマと examples これらは別ファイルに models として切り出すようにしています。 get : tags : - appsFoo operationId : getBar responses : '200' : description : OK content : application/json : schema : $ref : ./components/bar_response.yaml examples : default : $ref : ./examples/default.yaml '500' : description : Internal Server Error content : application/json : schema : $ref : ../../../components/error_response.yaml examples : barInternalError : $ref : ./examples/bar_internal_error.yaml # ... # ./components/bar_response.yaml title : appsFooBarResponse type : object properties : status : type : number bar : type : string nullable : true # ./examples/default.yaml value : status : 200 bar : null models として切り出して個別に title を定義することで、レスポンスの型の命名が自動的に InlineResponseXXX のようになってしまうのを防ぎます。 また、examples も default のように名前を定義することで、API モックサーバーへリクエストするときに意図したレスポンスを返してもらうことができるようにしています。 import { mockApiConfig , AppsFooApi } from 'api-client' const client = new AppsFooApi ( mockApiConfig ( { status : 500 } )) const result = await client.getBar ( {} ) // 500 エラーが返ってくる // example の value は OpenAPI 定義の examples の key を指定 const client = new AppsFooApi ( mockApiConfig ( { example: 'default' } )) const result = await client.getBar ( {} ) // default で設定した example の値が返ってくる ④ その他 API Client を利用する上で用意した便利関数 非同期関数の catch や try / catch でエラーが起きたときのハンドリングを行う便利関数も用意しています。 apiErrorType を使って起こったエラーを3つのパターンに整形する APIError: API から返ってきたエラーレスポンス Error: それ以外の何かしらのエラー null: 401エラーの場合は一律でエラーハンドリングしており何もしないため null を返す isAPIError を使ってエラーが APIError なのか Error なのかを判別する const client = new AppsFooApi ( apiConfig ) const result = await client.getBar ( {} ) . catch (async ( e ) => { const error = await apiErrorType ( e ) // 401 のときは自動的に FlashMessage が出るようにしているため早期リターン if ( ! error ) return if ( isAPIError ( error )) { // APIError で 404 など返ってきたときのエラーハンドリング } else { // そうでないただのエラーが返ってきたときのエラーハンドリング console .log ( error.message ) } } ) また、エラーハンドリングについての考え方は今まで実装者の判断に委ねていた部分がありましたが、エラーハンドリングのやり方についても別記事でまとめて認識合わせをしたりしました。 Web サービスを開発するときのエラーハンドリングについて ユーザーに表示するエラーメッセージを管理するのはフロントエンド?バックエンド? 注意したいのがここで返ってくる title や detail などは、ユーザーに表示するための文言ではなくあくまで開発者に何のエラーか教えてあげるための文言だということです。 エラーを返しているのは API であって、API を操作するのはフロントエンドのコード(つまりフロントエンド開発者)なので、開発者がわかるエラーメッセージで十分です。 ユーザーに表示するエラー文言については、デザイナーと相談して決めることがほとんどかと思います。ここは細かい調整が行いやすいフロントエンドで管理するのが良いでしょう。ユーザーに表示する領域はフロントエンドの領域です。 ⑤ GitHub Actions を使った GitHub Packages の配信 src/merged.yaml または api-client/package.json に変更があった PR が main ブランチにマージされた場合は GitHub Actions で GitHub Packages を配信するようにしています。 - uses : docker://openapitools/openapi-generator-cli with : args : generate -g typescript-fetch -i src/merged.yaml -o api-client/src/generated --additional-properties=modelPropertyNaming=camelCase,supportsES6= true ,withInterfaces= true ,typescriptThreePlus= true - run : | cd api-client yarn install --frozen-lockfile yarn build - env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} run : | cd api-client npm config set //npm.pkg.github.com/:_authToken=$GITHUB_TOKEN npm publish しかし、main にマージした際に何らかの理由で npm publish が失敗したらどうしましょう?🤔 そんなときのために、CI で publish できるかもチェックしています。 can-npm-publish を利用して npm publish ができない場合は CI が落ちて気づけるように GitHub Actions を設定しています。便利ですね。 - env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} run : | cd api-client npm config set //npm.pkg.github.com/:_authToken=$GITHUB_TOKEN yarn run can-npm-publish --verbose 以上、①〜⑤まで API Client と型の自動生成をする上でのポイントをあげました。 このような形で現在は自分のやっている PJ 以外でも OpenAPI から生成した API Client と型が利用されるようになってきています。 使っていく上でのメリットとこれからの課題 OpenAPI を利用したスキーマ駆動開発をやってみて感じたメリットは以下の通りです。 API の実装を待たずしてフロントエンド開発ができる API モックサーバーを使うことで、外部連携などが必要な複雑な API であったとしてもフロントエンドの動作確認や開発を楽に行うことができる API Client やスキーマの型をいちいち手動で書く必要がなくなる 仕様と実装の乖離がなくなる このように、開発を楽に&速くすることができました! 🙌 とはいえ、全体を通した課題はまだまだ感じます。 フロントエンドの開発にとってメリットを感じられることは大きいのですが、バックエンドの開発にとってはどうでしょうか。 バックエンドの実装と定義したスキーマが乖離した場合に自動テストが落ちるようにしたりなどの設定がまだできていない バックエンドは仕様と実装の乖離が起こっても検知できないので、API に変更がある場合はコミュニケーションでなんとかする必要がある といったように、現状はフロントエンドの開発ではスキーマ駆動開発の良さを享受できるけどバックエンドの開発ではフロントエンドほどの恩恵は受けられていない状況です。 とはいえバックエンドの開発でも、API の実装がフロントエンドの実装のブロッカーにならずにすむというというのはバックエンド開発者の精神衛生上とても良いことだと、うれしいフィードバックを受けたりもしました。 今後バックエンドの状態が変わり次第 OpenAPI のツールを入れるなりして、さらにより良いスキーマ駆動開発を推進していければいいなと思っています。 おわりに OpenAPI を利用したスキーマ駆動開発で得られるメリットはとても大きいものです。 ぜひ API Client と型を自動生成して各 PJ で活用してみてください!
BASEの機械学習チームで論文読み会を実施してみました こんにちは。BASEのDataStrategy(DS)チームでエンジニアをしている竹内です。 DSチームではBASEにおける様々なデータ分析業務をはじめ、機械学習技術を利用した検索、推薦機能のサポート、商品のチェックや不正決済の防止などに取り組んでいます。 先日、チーム内で最新の機械学習技術についての知見を相互に深めるための試みとして、各々興味のある機械学習系の論文を持ち寄って紹介し合う、いわゆる論文読み会というものを実施してみました。 この記事では、その会で私が発表した内容の一部を紹介したいと思います。 ※ 中身は論文読み会用から本記事用に一部修正を加えています。 A ConvNet for the 2020s 紹介する論文について タイトル: A ConvNet for the 2020s 著者: Zhuang Liu, Hanzi Mao, Chao-Yuan Wu, Christoph Feichtenhofer, Trevor Darrell, Saining Xie Facebook AI Research (FAIR), UC Berkeley CVPR 2022 arXivリンク: https://arxiv.org/abs/2201.03545 公式実装: https://github.com/facebookresearch/ConvNeXt ※ 挿入している図(画像)と英文は特に言及がない限り本論文からの引用になります。 TL;DR 直近の画像処理NNのアーキテクチャにおいては、Transformerをベースにしたものがトップクラスの性能を発揮(Swin-T) TransformerのキーとなるモジュールはMulti-Head Self-Attention(MSA)だが、実際にはそれ以外にも従来のConvNetに取り入れられていない様々なテクニックが存在→真にConvNetを上回っているとは言えないのでは そこで従来のResNetに、Transformerに加えられているMSA以外のテクニックを可能な限り盛り込んだ(ConvNeXtと命名) ConvNeXtは従来のTransformerベースのモデルに対して、モデルサイズを抑えながら性能を上回ることができた We gradually “modernize” a standard ResNet toward the design of a vision Transformer, and discover several key components that contribute to the performance difference along the way. 画像分類タスクにおけるConvNeXtとViTの性能比較 画像系NNモデルアーキテクチャの流れ ConvNeXtに到達するまでのターニングポイント的なアーキテクチャをざっくりと振り返ってみる。(リンクはarXiv) AlexNet(2012) ConvNetの始祖的な存在 ImageNetコンテストで圧勝 →以降ConvNetの層を深くするのがトレンドに →層を深くすると以下の二つの問題が浮上 Back Propagation時の勾配消失・勾配爆発の問題 精度の飽和、学習時のエラーの上昇(Degradation)の問題 ResNet(2015) 勾配消失・勾配爆発問題はBatch Normalizationが有効 Degradationに対するアプローチとして、層間のショートカット接続の重要性に注目 ショートカットを含んだブロックを含むアーキテクチャを提唱 ResNetのショートカット構造(He, Kaiming, et al. "Deep residual learning for image recognition."より) ResNeXt(2016) ResNetのブロックを並列に並べて集計する仕組みを提唱 パラメータ数と性能のトレードオフを改善 並列に並べる数Cardinalityをハイパーパラメータとして導入 少ないパラメータ数、小さいモデルサイズ、シンプルな形でResNetの性能を上回る ResNeXtの仕組み(Xie, Saining, et al. "Aggregated residual transformations for deep neural networks."より) EfficientNet(2019) ネットワークの深さ、広さ、解像度(画像サイズ)の3つをパラメータとして最適化 扱いやすく画像系の機械学習コンペ等ではよく見るアーキテクチャの一つ VisionTransformer(ViT)(2020) 自然言語処理系NNにおいてはデファクトスタンダードとなったTransformerを画像処理に応用 画像を16x16のパッチに分け、それぞれのEmbeddingを単語に見立てる 画像分類において少ない計算コストでトップ性能を発揮 画像処理における最近の大きなブレークスルーの一つ ViTで用いられる画像のパッチ化(Dosovitskiy, Alexey, et al. "An image is worth 16x16 words: Transformers for image recognition at scale."より) Swin Transformer(Swin-T)(2021) ViTを物体検知やセマンティックセグメンテーションなど、ピクセル単位の解像度が要求される他のタスクでも効果を発揮できるように改良 パッチ化の処理を階層化+1マスずつズラす処理(Shifted Window)を導入することで画像サイズの2乗であったViTの計算量を線形まで落とした 画像処理系のトップカンファであるICCV'21のBest Paper ConvNeXtの論文では、Swin-Tで採用されているWindowをズラす処理がConvと類似しているため、重要な要素であると考えられることが言及されている For example, the “sliding window” strategy (e.g. attention within local windows) was reintroduced to Transformers, allowing them to behave more similarly to ConvNets. ... Swin Transformer’s success and rapid adoption also revealed one thing: the essence of convolution is not becoming irrelevant; rather, it remains much desired and has never faded. Swin-Tで用いられる階層的なパッチ化(Liu, Ze, et al. "Swin transformer: Hierarchical vision transformer using shifted windows."より) ConvNeXt(2022) 本記事で紹介している論文 ResNetがConvNeXtになるまでに加えられた改良 Chapter2以降ではベースとなるResNetに加えられた手法と、それによる精度の改善幅について順に説明されている。 2.1 学習手法 ネットワークのアーキテクチャを弄る前に学習手法をTransformerに倣って改善していく。 エポック数を90→300に AdamW optimizer(2019) L2正則化とWeight DecayがAdamでは同一視することができないことを示し、AdamのWeight Decayに修正を加えた データ拡張 Mixup(2018) 2種類のデータとラベルをベータ分布からランダム生成された を使って以下のように混ぜ合わせる データ: ラベル: Cutmix(2019) 画像の一部を切り取り、別のラベルの画像を挿入 RandAugment(2020) グリッドサーチで最適な拡張度合いを見つける RandomErasing(2017) 画像内にランダムな矩形を追加する Cutmix(Yun, Sangdoo, et al. "Cutmix: Regularization strategy to train strong classifiers with localizable features."より) Random Erasing(Zhong, Zhun, et al. "Random erasing data augmentation."より) 正則化 Stochastic Depth(2016) ランダムでResブロックをスキップのみにする(後ろの層になるほどその確率が高くなる) Label Smoothing(2016) 正解ラベルと不正解ラベルの値を1, 0ではなく0.9, 0.1などとする これらの追加により性能は76.1%→78.8%に改善 2.2 マクロデザイン ここからは、ResNetのマクロな構造をTransformerに近づけていく。 ステージごとの計算比率の変更 複数のResブロックからなる1かたまりはステージと名付けられており、ConvNeXtには合計で4つのステージが存在する。 それぞれのステージのブロック数はResNet-50では(3, 4, 6, 3)であったが、これをSwin-Tに合わせて(3, 3, 9, 3)に変更した。 78.8%→79.4%に改善 stemで画像をパッチ化するように変更 入力された画像に対して一番最初に処理を行う部分はstemと名付けられており、ViTなどでは画像のパッチ化を行う部分に相当する。 従来のResNetでは、まず入力の画像を適切な特徴量のサイズにするためにカーネルサイズ7×7ストライド2のConv+Max Poolingを使用することで4倍のダウンサンプルを行なっていた。 一方でViTでは画像を16×16のパッチにする処理を行なっているが、これはカーネルサイズ16×16で重複なし(ストライド16)のConvに相当する。 Swin-Tではより小さい4×4のパッチを作成しているため、これに倣ってstemでカーネルサイズ4×4でストライド4のConvを使用する。 79.4%→79.5%に改善 2.3 ResNeXt化 ResNeXtで取り入れられている、1つのConvを複数に分岐させてあとからまとめることでパラメタ数を削減する手法を適用した。 モデルのキャパシティの減少を抑えつつパラメタ数を効率的に削減できるため、モデルサイズを維持したまま性能を大幅に引き上げることができる。 今回はチャンネル数分の分岐を作成するDepthwise Convを使用する。これはTransformerにおけるAttention層のMulti-Head化に相当すると言及されている。 We note that depthwise convolution is similar to the weighted sum operation in self-attention 79.5%→80.5%に改善 2.4 Inverted Bottleneck Transformerでは入力の次元より隠れ層MLPの次元の方が4倍大きくなるInverted Bottleneckというデザインを採用している。 このアイデアはMobileNetV2(2018)ですでに利用されており、その後の改良型ConvNetでもしばしば用いられている。 下図の(a)がResNeXtの1ブロックで用いられる通常のBottleneckで、チャンネルサイズを384→96に落としてからDepthwise Convで96→96に畳み込み、最後にチャンネル数を384に戻している。 (b)がInverted Bottleneckで、チャンネル数を逆に96→384に増やしてから384→96に戻している。 これによってDepthwise Convの計算量は増えるものの、入力部分がダウンサンプルされていることによってResブロックのショートカットの1×1Convの計算量が減るため、全体の計算量は減ることになる。 (a)がResNeXt, (b)がInverted Bottleneck, (c)がDepthwise Convを移動させたもの 80.5%→80.6%に改善(ResNet-200では81.9%→82.6%に改善) 2.5 Large Kernel Sizes 従来のConvNetでは3×3など小さいカーネルサイズ使用するのが主流であったものの、Swin-TのWindowのサイズは小さくとも7×7である点を考慮すると、カーネルサイズは大きい方が有効だと思われる。 これを実現するために以下の二つの手順を踏んでいる。 Depthwise Convの移動 より大きなカーネルサイズを利用するために、Depthwise ConvをResブロックの最初にもってくる。(上の図の(b)→(c)に対応) これはTransformerのMulti-Head AttentionがMLPの前に配置されていることに対応する。 (一時的に)80.6%→79.9%に悪化 カーネルサイズの増加 Depthwise Convを移動させた後、そのカーネルサイズを3から5, 7, 9, 11と増やしていくと計算量は大体保たれたまま性能が改善され、7で大体性能が飽和する。 サイズの大きいResNet-200でも同じ7で飽和することが確認されている。 79.9%→80.6に改善 この時点でViTで採用されているデザインの大部分を実現できていることになる。 2.6ミクロデザイン 大枠のアーキテクチャは完成したため、ここからはレイヤーレベルで改善していく。 ReLUをGELUで置き換える 活性化関数として使用されているReLUをBERTやGPT-2、ViTでも使われている以下の GELU(2016) に置き換える。 GELUと他の活性化関数との比較(Hendrycks, Dan, and Kevin Gimpel. "Gaussian error linear units (gelus)."より) 80.6%→80.6%で性能据え置き 活性化関数を減らす Transformerの1ブロック(入力のKey/Query/ValueをEmbeddingしてMLPに入れる部分)には活性化関数が1回しか使用されていない一方で、ResNetは1ブロックにConv層の数だけ活性化関数が存在する。 これを1×1のConv2つの間のみに絞ることで活性化関数の数を合わせる。 80.6%→81.3%に改善 Normalization層を減らす これもTransformerに合わせて1×1conv層の前にのみBatch Normalization層を置く。 81.3%→81.4%に改善 BatchNormをLayerNormに変更 これもTransformerで使用されている手法ではあるが、単純なResNetのBNをLNに置き換えるだけでは性能が下がることが確認されている。ここまでの改造を全て加えた上でLNに置き換えると性能の改善が見られる。 81.4%→81.5%に改善 ダウンサンプル層を切り離す ResNetでは各ステージの最初のResブロックでストライド2のConvによってダウンサンプリングを行なっているが、Swin-Tではこのような処理は各ステージの間で行われている。 これに倣ってConvNeXtでもステージの間にダウンサンプル層とNorm層を追加することで学習を安定化させた。(Norm層なしだと学習が発散した。) 81.5%→82.0%に改善 ResNetに加えた全ての変更点とそれによる性能および計算量の改善 最終的なモデルのアーキテクチャ 性能 画像分類タスクにおける性能比較 ImageNetにおいてConvNeXtは同程度のモデルサイズのSwinTransformerを上回る性能を発揮している。 物体検出タスクにおける性能比較 COCOデータセットにおける物体検出においてもConvNeXtは同程度のモデルサイズのSwinTransformerを上回る性能を発揮している。 感想など ここ最近の画像処理系の流れを振り返るのにちょうど良い論文で、公式のpytorchによる実装と合わせて内容が非常にわかりやすく良い論文 強いて言えば既存の技術の応用という面が強いため、新しいアイデアや知見、理論的な深掘り(「なぜAttentionよりConvolutionの方が上手くいくのか」など))の面では若干物足りない気もする 昔読んだ深層強化学習系の Rainbow: Combining Improvements in Deep Reinforcement Learning という論文になんとなく立ち位置が似ているなと感じた ベースとなるDeep Q-Networkという手法に7つの改善手法を加えたときのパフォーマンスの改善について研究した論文 自分でベースモデルのアーキテクチャや学習手法に改善を加える際の流れとしても参考になる おわりに 今回のような論文読み会はDSチームとしては初の試みでしたが、新しい知見を取り入れ視野を広げる良い機会に感じたので1Qに1回ぐらいのペースで継続していけたらと思っています。 今後もDSチームでは新しい技術についても積極的に検討し、検証を重ねることで更なるプロダクトの改善、サービスの向上に取り組んでいきます。
初めまして。フロントエンドエンジニアの近藤 @kon_engineer と申します。 本記事では、2022年1月24日(月)にリリースされた、商品在庫絞り込み機能の振り返りと、サービス全体の状況を可視化できるNew Relicというプラットフォームを活用したAPIの観測について紹介します。 今回の事例では、New Relicで観測可能なAPIのレスポンス速度や各クエリパラメータのリクエスト状況などを分析して、効果測定や今後の施策に活かす取り組みを行いました。New Relicの詳しい説明や、BASEがNew Relicを導入した経緯は こちらの記事 をご参照頂けたらと思います。 商品在庫絞り込み機能とは 商品管理画面で商品の在庫数を指定して検索できる機能です。BASEでは商品管理画面の一覧から、在庫切れの商品や在庫が少なくなっている商品を簡単に見つけることができないという課題がありました。特に商品数が50を超える場合は、在庫状況をページングして確認する必要がありました。そこで、在庫数で商品を絞り込めるように改修して、商品管理の利便性の向上を図りました。 Twitter上の告知は以下です。 ╭━━━╮​  NEW✨ ╰━v━╯ BASE( ᐛ )⛺️ 🛒「商品管理」🛒 がアップデート! 💡在庫がない商品がパッと見つけられる💡 管理画面の「商品管理>絞り込み」から 🛒在庫なし 📝在庫が一定数以下 の商品の絞り込みが可能に◎ ぜひご活用ください! pic.twitter.com/7PtLZCf7GW — BASE(ベイス)🔎新機能登場! (@BASEec) 2022年1月25日 New Relicによる監視 New Relicによる監視はBASE全体として推進しているものの、本PJのメンバーは本格的な導入経験はありませんでした。今回はプロジェクトとしてトライしてみようということで、メンバーそれぞれが仕組みを学びながら運用していきました。 開発前 検索利用状況 商品在庫検索機能は、既存の商品検索APIにクエリパラメータを追加することで実装しました。実装前に既存の商品検索APIのレスポンス速度や、使用されている頻度を把握することで既存の問題や、追加で行った方が良い改善がないか、開発前に観測することにしました。 まずは検索API全体の呼び出された回数を測定して、次に各パラメータが指定された回数を測定することで、どのパラメータが使われて、どのパラメータが使われていないか分かるようにしました。 得られた結果として、全体の検索回数に対して、キーワードを指定して検索された回数が約87%あることが測定できたため、キーワードで主に検索されていることが分かりました。また、商品タイプ別の検索は、他の検索条件よりも極端に使用されていないことが呼びされている回数から分かったため、何らかの対応を検討する価値があると把握できました。 さらに、キーワード検索以外は、絞り込みボタンを押して検索モーダルを開かなければ検索できないUIのため、キーワード以外の各検索回数の結果から、検索モーダル経由で一定数の検索リクエストが呼ばれていることが分かりました。今回は検索モーダルに在庫数の検索機能を追加するため、そもそも検索モーダルがほとんど開かれていない、という状況ではないことを把握することは重要でした。 負荷状況 レスポンス速度に改善の余地はないか、また今回の改修によってレスポンスが悪くならないか把握するために、現状の応答速度を事前に把握しました。特に大きな問題は見つからなかったため、この速度を維持することを目標としました。 リリース前後 規模別 事前にショップを規模別に分類して、規模別でどのような効果があったのか観測できるように準備しました。今回の機能は、商品数が増えて在庫管理が大変な大規模ショップにより使ってもらいたいという思いがあったので、規模別に観測することで効果を細かく把握する狙いがありました。 リリース後の検索回数 また、在庫数を検索された回数がどれくらいなのか、リクエストURLを監視してリリース後すぐに分かるようにしました。機能が順調に使われていることが観測できて、PJメンバーで喜ぶ場面もありました。 どのような値で検索されているか把握する 当初は観測していなかったのですが、リリース後に在庫数0で検索されているケースがとても多いことが判明しました。在庫が少ない商品を検索するために使われることは予想していたのですが、それにしても0で検索されることが多い、という印象でした。そのため、どのような数値で検索されているのか、後からNew Relicのダッシュボードに追加して観測しました。結果として8割以上が在庫数0で検索されていたことが分かり、数字から事実として在庫切れ商品を把握したいニーズを把握することができました。これにより、在庫切れ商品を素早く通知するような、在庫切れにアプローチした施策が今後も有効だろうということが分かりました。 負荷状況 事前に十分に検証はしたものの、リリース後もレスポンスが遅くなっていないか、エラーコードが出ていないかなどの検証をしました。特にエラーも発生しておらず、レスポンスも悪化していないことを観測することができました。 終わりに PJが始まった時、今まで効果測定を適切に行えていないというチームメンバーの課題感がありました。そのため、ただリリースして終わるのではなく、機能を使ってもらっているのか、どのような使われ方をしているのかを把握して、反省と次の施策に繋げる取り組みを行いました。 今回の施策は、規模として大きな改修ではありませんでしたが、改修したAPIをNew Relicで観測することで、しっかりと機能が使われていることを把握できて、施策として一定の効果があったことが分かりました。また、当初チームが持っていた仮説として、ショップオーナーの方々は、在庫切れの商品を少ない時間で簡単に把握したい、というものがありました。仮説が間違っていなかったことが改修したAPIの利用状況からも分かり、今後も在庫切れの商品がすぐに分かるような施策が有効であると、自信を深めることができました。 何より、作ったものがしっかりと使われていることを観測できることで、チーム全体の士気も上がったと思います。今後も継続的にこのような取り組みを行うことで、施策の有効性や妥当性を意識して開発していきたいと考えています。
この記事は BASE Advent Calendar 2021 の25日目の記事です。 はじめに メリークリスマス!!! 執行役員 VP of Productの神宮司( @7jin16 )です。 2021年に取り組んだ顧客フィードバックを製品開発に活かすためにおこなったことを書きます。 なぜ始めたか これまでも顧客からのフィードバックを活かしていなかったわけではないのですが、より多く、より広くフィードバックを集める、全社で閲覧・利用可能な形で集積すること製品企画・開発の質が向上すると考えて取り組むことにしました。 なにをしたか "より多く、より広くフィードバックを集め、全社で閲覧・利用可能な形にする" を理想として掲げてそれを達成するための方法を考えました。 1. フィードバック量を増やすために 今までは製品内から直接フィードバックを送信する機能はなかったため、製品のほぼすべてのページのフッター付近にフィードバック送信フォームへのリンクを設置しました。リンクを設置したところリリースから日が浅い頃は1日数百件のフィードバックが送られてきました。 2. 製品開発に役立てるために フィードバックをただ集めるだけでは、製品開発に最大限役立てることはできません。おかげさまで「BASE」でネットショップを運営しているショップは160万ショップを超えていて売上規模や扱っている商材もショップによってさまざまです。同じ機能へのフィードバックでも注文数の多さや扱う商品数によってショップが抱える課題はまったく異なってくるため送信元ショップの注文数や商品数、業種などの情報は必須です。 フォームとしてはひとつですが送信元のショップと注文数、商品数、業種などのショップの情報をフィードバックと紐づけています。 フィードバックを送信すると社内管理画面に送られます。 3. 全社で閲覧・利用可能な形にする 以前から「BASE」にはお問合せや口頭で多くの顧客からフィードバックが寄せられていましたが、SlackやGoogleスプレッドシートに保存されていて集積場所が点在しているため一覧性や検索性が低く利用しやすい状態ではありませんでした。 利用しにくい状態だとフィードバックを集めても社内のメンバーも活用しにくいため集積場所を統一することから始めました。 顧客が「BASE」にフィードバックを伝える方法は カスタマーサポートのお問合せフォーム(Zendesk) 顧客と話している社内メンバーに口頭で伝える 製品に設置されているフォームから送信 の3パターンあり、どこから伝えられても社内管理画面に集積されていくようにしました。 SaaSを選べなかった理由 製品フィードバックを複数チャネルで集めて管理するSaaSは国内外問わず増えていてます。しかし、「BASE」では前述した以下の要件を落とすことができず短期的にはSaaSで達成することができないため自社開発をすることにしました。 同じ機能へのフィードバックでも注文数の多さや扱う商品数によってショップが抱える課題はまったく異なってくるため送信元ショップの注文数や商品数、業種などの情報は必須です。 実際にプロダクト改善に活かせたのか? 6月にフィードバックフォームをリリースしてから6,000件以上のフィードバックをいただいています。フィードバックをいただいたからといってそのまま機能として実装されることはなく、フィードバックを送信してくれた顧客が抱えている本当の課題を探り、課題のコアに辿り着くことが重要だと思っています。 フィードバックフォームをリリースしてから既に50件以上の機能改善につながっており、今後も発展させていきたいです。 オーナー様/お客様の声から生まれた改善レポートまとめ 「BASE」では一緒にプロダクトを改善していくメンバーを募集してます!幅広い職種で募集中ですので少しでもご興味がございましたら 採用ページ からカジュアル面談を申し込んでいただけると嬉しいです!
はじめに CTOの川口 ( id:dmnlk ) です。 これはBASE Advent Calendar25日目の記事です。僕は立候補してないのに勝手に日程が組み込まれてました。 BASE株式会社では積極的にエンジニア採用を行っております。 その中でよく質問を頂くのは「BASEに今から入社した場合にやることはあるのか?」というものです。 確かにサービスは成長し上場もしている企業で自分がやることはあるんだろうか、というのは僕も同様に疑問を持つだろうなとは思います。 ですので今回は話せる範囲ではありますが、今BASEで必要とされていることを書いていこうと思います。 フレームワーク移行 来年以降、BASEシステムとして取り組むものとして非常に大きいウェイトを占めるのは間違いなくこれになるでしょう。 現在BASEの大部分が利用している言語及びフレームワークはCakePHPですが2系を利用しておりEOLを迎えているだけでなく開発生産性などが数世代遅れているということを否定出来ません。(CakePHP2というフレームワーク自体が悪かったという話ではありません) これらを徐々に別のフレームワークや言語のシステムに変更していくというのが主なミッションです。 その中でモノリスからマイクロサービスという形にしていくという選択肢もあるでしょう。個人的にはRDBMSのトランザクションの恩恵を捨てる選択肢を取ることに恐怖はありますが。 BASEシステムのコード量は非常に多くデータのパターンも多岐に渡るため、コードの歴史の解読、調査、修正にテストなど技術理解だけでない幅広いスキルが必要になると思います。 使っていただいているユーザーさんの不便を起こさないよう、安全にシステムをマイグレーションしていくという仕事は胆力のいるミッションだとは思いますが我こそはという方は是非お待ちしています。 open.talentio.com セキュリティ BASEでは多くのユーザー様のデータをお預かりしており、社内やアプリケーションのセキュリティを強化していくことは重要です。 今まで専任のセキュリティエンジニアは採用しておらず、元々セキュリティに一定の造詣があるメンバーによって脆弱性診断の対応等を行っておりました。 しかしこれでは組織として非常に弱く、より踏み込んだセキュリティ対策を行えない状態ではありましたので今回募集をさせて頂いております。 特にアプリケーション側のセキュリティエンジニアに関しては、内部統制や各種認証取得などの整備などとは別のチームとして動いていただきます。 BASEで利用しているAWSのセキュリティ製品の適用によるセキュリティ向上、脆弱性診断の結果トリアージ、不正リクエストの検知対応など様々あります。 脆弱性診断の結果トリアージは、場合によっては修正からデプロイまで行っていただく予定でありPHPである必要はありませんがウェブアプリケーションの開発経験を要求しています。 大量のリクエストや多くの種類のユーザーさんが飛び交うアプリケーション上で起きうるたくさんのセキュリティリスクを低減し、安心して使っていただける環境を作るチームとして立ち上げていきたい方をお待ちしています。 open.talentio.com DX改善 Digital Transformationではなく、Developer Experienceです。 開発者体験が悪い状態での開発は進められますが、複利的に効いてきます。 それを改善するために、デプロイ速度の向上やローカル及び開発環境の整備、CI/CDパイプラインの効率化などやることは非常に多くあります。 先日のイベントで軽く話しましたがこのあたりは常時タスクがあるというわけではなく専属エンジニアが付けづらくCTO職などがやることが多くなってしまうのが現実だったりします。 開発者の現状に寄り添いながらよりよい開発者体験を作っていきたい方を募集しています。 EMの育成や新規メンバーのオンボーディング強化 組織が大きくなると必ず非常に重要になってくるのがマネージャーの存在です。 普段のタスク管理などはそこまで問題となりませんが、メンバーの成長を考えながらチームとしての成果を最大化を考える必要があります。 恥ずかしながらCTOの自分はそのあたりの知見が薄く得意でもないので、EMを育成という観点で何が有効になっていくかがあまりわかっていません、 大規模組織でのマネジメント経験や育成を行っていた方は是非BASEに来てその知見を活かして欲しいと思います。 合わせて、続々と入ってくる新メンバーに大規模になってきたBASEシステムのオンボーディング体験を向上させていくといったことも一緒に考えていただければと思います。 どれだけ優秀なエンジニアでも入社してからの成果を最大化してもらうためにオンボーディングは必須であり、課題は大きくなっているのでぜひとも取り組みたい箇所です。 まだ予定はありませんが新卒採用も見据えてやっていきたいですね。 パフォーマンス改善 多くのショップ様が使ってくれる現状においてパフォーマンス改善は日々のタスクとして取り組まないといけない課題です。 データ量が多くなったショップ様の管理画面パフォーマンス、ショップページのレンダリング高速化など改善すべきページはたくさんあります。 現代的なCDN技術などを利用し高速化を図っていきたいと思っています。 New Relicを積極的に活用しているので監視を超えて可観測性を高めてより高速なネットショップ作成サービスにしていきたいですので、是非パフォーマンスジャンキーの方を募集しています。 最近ではこのようなアウトプットもあります。 devblog.thebase.in おわりに これらの取り組みだけでなく、事業の課題についてのサービス開発、Pay ID開発、さらなるUXの向上、アクセシビリティ、データ基盤整備、機械学習による不正決済対策、レコメンド開発などなどたくさんあります。というかまだまだある!メチャクチャある!助けてください! 是非興味ある方は下記から応募して頂いたり、僕のTwitterにDM、Meetyなどどんな手段でもいいのでご連絡ください。 では、皆様良いお年を!!! open.talentio.com meety.net
この記事はBASE Advent Calendar 24日目の記事です。 BASEテックブログ編集長の松原( @simezi9 )です。 12月もいよいよ大詰め、クリスマス・イブということでそろそろ年内の仕事を納められた方もいるのではないでしょうか。 BASEのアドベントカレンダーも今年で 4回目 となりました。 今年も全部で32本(一日で複数記事の日も作ったため)の記事を12/1~12/25の期間で投稿してきました。 本記事ではブログ編集長としてアドベントカレンダー運営に際して行ったことと、その振り返りをしたいと思います。 これは来年またBASEのアドベントカレンダーが無事に開催されること、 あるいは世の中のだれかがアドベントカレンダーを運営する際になにか1つでも参考になることがあればと思って書き残す備忘録でもあります。 タイムライン 編集部としてのアドベントカレンダーの準備のための流れは以下のようになりました 日時 イベント 11/4 アドベントカレンダーのレギュレーションを社内に公開 & 執筆者の募集を開始 11/18 カレンダーの日程が埋まる 11/25 アドベントカレンダーの各記事に使うアイキャッチ画像のテンプレ用意完了 11/30 告知ページ 公開 12/1 記事公開開始 レギュレーションについて まず11月頭にアドベントカレンダーの趣旨と参加方法を書いたドキュメントを社内ナレッジベースにポストしました。 このタイミングで公開したのは主に 記事の内容と公開手段について 記事作成から実際の公開に至るまでのフロー 参加日を表明できるカレンダー の3つでした。それぞれを掘り下げてみます。 1. 記事の内容について 記事の内容はBASEや開発に関係があることであれば何でも可、としテックブログには技術記事のみ掲載可能とし、記事の内容次第では note や個人ブログを使っても大丈夫ということを明確にしました。 当たり前の内容に思えるかもしれませんが、これについては過去の経緯があります。 過去BASEのアドベントカレンダーではテーマをフリーにしたところ、真面目なものからゆるいものまで幅広いテーマの記事が集まったことがありました。 とてもにぎやかではあったのですが、それらの記事をすべてこのテックブログで公開したことでブログの記事の軸がぶれてしまう結果になってしまったことがあります。 これは既存のブログ読者の方にとってもあまり好ましくない状態であろうと考え、昨年はテックブログでの公開にふさわしい技術記事のみで構成したアドベントカレンダーとしました。 しかしながら、社内の雰囲気をお伝えするという意味で多様な記事がアドベントカレンダーに掲載されることは基本的に良いことであると私自身は考えていました。 過去に発生した問題は結局のところ、公開場所にふさわしくない記事をアドベントカレンダーだから掲載したということで起きているだけなので、 note や個人ブログでアドベントカレンダー公開も可能であるということを最初から明言すればよいと思いそれを盛り込むことで、技術記事でなくてもよいというのを今年改めて決定できた、という流れです。 (余談ですが BASEはnote株式会社と資本業務提携 を結んでいます。) 残念ながら今年はnoteでの記事公開はなかったというオチがついてしまったのですが、とりあえず公開する記事の方向整理ができたという点で来年以降に期待したいと思っています。 2. 記事作成から公開に至るまでのフロー アドベントカレンダー期間は通常の状況を遥かに超える記事のレビュー依頼が来るため兼務で行っているブログ編集部の負荷をへらす必要があります。 そこで記事の公開までに必要な以下の手順を公開しドキュメントとしました。 下書きを書く場所とレビュー依頼の出し方 レビューについては完了までの時間の目安も同時に書く アイキャッチ画像の作り方 これについてはデザイナーの @nomjic が誰でもコピーしてテキストを編集するだけでアイキャッチが作れるステキFigmaを用意してくれました テックブログに投稿する場合の手順 これによって一度アドベントカレンダーが始まってしまえば編集部員の負荷はほぼほぼ記事のレビューだけとなりました。 3. 参加表明のためのカレンダー ブログ編集部としてカレンダーページを公開するタイミングで恐れていたのは以下の2点でした 空白だらけのカレンダー TBDがズラッとならぶカレンダー これらを避けるために11月頭から積極的に動いていました。 まず空白だらけのカレンダーを避けるために執筆者の確保に走りました。 アドベントカレンダーは世にたくさんあり、テーマ次第では空席が増えてしまうこともありますが、企業のアドベントカレンダーでそのような状態で公開されることはあまり印象が良くないと考えていました。 過去数年の経験から記事が不足することはまずないと分かっていたものの、各マネージャー陣にお願いしてメンバーへの働きかけをしてみたりslackの#generalで宣伝をしてみたりと先手を打って動いていきました。 ここで考えておかないといけないことは日程かぶりの問題です。 アドベントカレンダーは一人1日1記事が文化となっていて、カレンダーが埋まったら2個目のカレンダーを作るというスタイルで運営されることが多いです。 実際にBASEでも2019などは1日2記事で2個のカレンダーを走りました。 このスタイルは運営が大変なところが多く、1個目のカレンダーは埋まっているけど2個目のカレンダーがスカスカということも起こりかねず負荷が高いです。 そう考えていくと、そもそもカレンダー個あたり一日1記事という縛りがおかしくて、 一日n記事でいいじゃん という単純なことを思いつきます。 ただ適当に好きな日で、とやると前半が空っぽになって後半に集中しすぎるという可能性もあったので、 「1日あたりまず2人までは先着順で参加表明でよく、全体的に埋まってきたら2人の制約を緩和する」 というスタイルにしました。 これである程度カレンダー全体に記事を散らすことを可能にしつつ、日程がかぶっても問題にならないスケーラビリティを確保しました。 また、参加表明をしてもらったタイミングで記事で扱う予定のトピック、テーマだけは必ず表明してもらい(変更可)、TBD/未定で埋まることを防ぎました。TBDがカレンダーに並ぶとどうしても行き当たりばったりな感じが出てしまいますし、どういう雰囲気のアドベントカレンダーが開催されるかが伝わらないため、興味を持ってもらうこともできないだろうと考えたためです。 振り返り ここまでは実際にアドベントカレンダーを運営する上で行ってきたことを書きました。 ここからはテックブログ編集長の思いを書いてみようと思います。 アドベントカレンダーの意義 技術広報に積極的な企業にとって自社のアドベントカレンダーを開催することはもはや当たり前のようになっています。 ただ忘れてはいけないことがあります。それは 「アドベントカレンダー期間は大量のテック記事が公開されるレッドオーシャンである」 ということです。 もちろん読者側のアンテナが高くなる面もあります。ただ、それを遥かに上回る量で記事の供給量が増える期間です。 読者が技術記事に割く時間の総量はそこまで増えたりしません。その時間を世の様々な記事と取り合うことになります。 純粋に自社の技術を世に広めて自社の魅力をアピールするのであれば、常日頃からの定期的なアウトプットをまず確立すべきで、アドベントカレンダーの激しい競争で燃え尽きてしまうのは勿体ないと思っています。 まずは自社から定期的に記事を出せるような運営が行えるようになってからアドベントカレンダーでお祭りに参加するという流れのほうがよいのではないかという気がしています。 ただ逆に、レッドオーシャンというのを利用して勢いで普段アウトプットを出すことに不安を感じるようなメンバーの背中を押して記事を書いてみてもらう絶好の機会であるとも言えるかもしれません。 対外的なアピールとしてアドベントカレンダーを使うのではなく、メンバーに記事を書く経験値を積む機会の価値を強調することで協力してもらう、という考えで運営するほうが得るものが多いこともあるのかもしれません。 記事のレビューについて 少しアドベントカレンダーの振り返りとは話がずれるのですが、記事のレビューについて考えてることを書きます。 記事のレビューは気を使う作業です。我々自身も別に出版業界で編集者をやっていたわけでもなく文章の素人です。 ただし、企業のテックブログを運営する中で無責任なことはできません。記事のレビューは必要です。そのなかでフィードバックを行うことは少なからず疲れる作業です。 ただ自分は基本的にあまり多くをFBしないようにしています。観点としては ポリコレや倫理に反する内容はないか 業務上公開できない内容はないか 記事として読みやすくなるためにできる構造的な工夫はないか です。最初の2つは言わずもがなで必ずチェックする部分ですしこれは比較的明瞭に判断できる部分です。 それに対して、最後の文章の工夫については感覚的な領域も多く非常に難しくなります。 自分は基本的に「文章の順番を入れ替える」「図を追加する」「得られた成果をわかりやすくする表現を追加する」の3つのFBをします。 いい文章とは引き算によって産まれるとよく言われますが、文章を削ぎ落として引き締めて素晴らしいものに仕上げるというのは文筆の素人には荷が重いことです。人の文章に対して、「何かを足したほうが良い」とは言いやすいですが「この文章はいらない」「この表現は正しくない」というようなことは言いづらいからです。 全然関係のない個人的な余談なのですが、この記事のレビューという行為をするときによく思い出すことがあります。 それはキリコというラッパーの「ありがとう。名無しの2チャンネラー諸君」という曲のことです。 この曲自体はキリコが当時の2ちゃんねるにあった自分のスレッドに対して「全レス」を返す形でラップするという怪作です。(しかも14分弱ある) かなりクセの強い曲で私自身は好きでも他人におすすめするタイプの曲ではないのですが、この曲の最終盤に登場する 自分の世界をいじれるのは自分だけであるべきだ。赤ペンで訂正されて良い気分がしないのはみな同じだろう という一節をよく覚えています。 つまり何が言いたいのかというと、ブログの記事というものも個人の内側から生まれた自己表現であり、それを最大限尊重することが素人編集としてもっとも重視するべきことなのではないか、ということです。 細かい表現への違和感の表明などはやろうと思えばきっといくらでもやれますが、それはおそらくテックブログに求められることでもなければ記事を書いてくれたメンバーの求めている部分でもないのではないか、そう考えて情報の構造に対してなにか加えられることはないか、それだけを考えてレビューをするようにしています。 最後に 最後は少しとりとめがなくなってしまいましたが、最後にこの記事を読んで来年のアドベントカレンダーに役立てていただける方が一人でも居れば幸いだと思っています。 明日の最終日は弊社が誇るCTOの @dmnlk が素晴らしい作品をドロップしてくれる予定です。乞うご期待ください 今年も執筆に協力していただいて素晴らしい記事を提供してくれた弊社の皆様にこの場を借りてお礼を言わせていただければと思います。(この記事で偉そうなことをいいながらレビュー依頼を見落としていたりと大変ご迷惑おかけしました)
BASE Advent Calendar 2021 はじめに コスト考慮型学習とは Cost-Sensitive Learningの手法 コスト行列 閾値の調整による誤分類コストの反映 実際のデータセットを用いた例 まとめ 参考文献 はじめに この記事はBASE Advent Calendar 23日目の記事です。 こんにちは、DataStrategyチームの竹内です。 BASEではより良いサービスを提供するために色々なところで機械学習モデルが活用されています。 BASEに限らず、インターネット上のあらゆるサービスに機械学習の技術が活用されるようになって久しい昨今ですが、こうした実際のサービスやビジネス領域に近いところで活用される機械学習モデルにおいては、計算コストやメンテナンスコスト、解釈性やバイアス、データセットシフトなど色々と考えなければいけない特有の要素が存在します。 今回はその中の1つとも言える誤分類コストの非対称性の問題について考え、それに対するアプローチとしてコスト考慮型学習(Cost-Sensitive Learning)について扱っていきたいと思います。 コスト考慮型学習とは コスト考慮型学習とは、データマイニングにおける誤分類時のコスト(誤分類コストに限らず、計算コストなどの他の要素を考慮する場合もあります。)を考慮した学習手法のことです。 機械学習などによる分類モデルを現実の問題に適用する場合、どのデータをどのクラスに誤分類してしまうかで生じるコストが異なる、いわゆる誤分類コストの非対称性の問題に直面することがあります。 1 よく挙げられる例で言えば、がんのような重大な病気を診断する場合、本当に罹患している人に対して健康であると誤診してしまった際の影響は極めて致命的になり得る一方で、健康な人に対して罹患していると誤診してしまった際は追加の検査費用分のコストで済むことになります。 医療診断における非対称な誤分類コスト 健康な人 罹患している人 陰性と診断 - 治療の遅れ、信頼の失墜(コスト大) 陽性と診断 追加の検査費用(コスト小) 早期治療 医療診断の場合ではどの患者についても概ね同様なコスト行列が適用できますが、中にはサンプルごとに誤分類コストが異なる場合もあります。 例えばカードローン審査のような例では、利用客によって申請金額が異なるため誤分類コストが変わってきます。 カードローン審査におけるサンプルごとに異なる誤分類コスト 返済できる人 返済できない人 審査を通す 金利や手数料分の利益 金額分の損失 審査を通さない 適格な申請数の減少 不適格な申請数の減少 このようなサンプルごとに誤分類コストが異なるタスクにおいてモデルの作成や改良を行う際、サンプル数ベースの正答率や再現率、AUCの改善が必ずしも金額ベースの改善につながらない可能性があることに注意する必要があります。 このように現実の問題を分類タスクとして捉え、予測モデルの作成や改良を行う場合、適切な誤分類コストに基づいた性能評価が求められる場合があります。 適切な誤分類コストの設定には機械学習や統計一般の知識だけではなく、その分野特有の知識、いわゆるドメイン知識を十分に活用することが求められます。 Cost-Sensitive Learningの手法 ここからは上の例のような非対称な誤分類コストに対する具体的なアプローチについて扱っていきます。 誤分類コストを考慮したモデルを構成する手法は大きく分けて3つ存在します。 通常の学習を行なったモデルの出力に対する検出閾値を、サンプルの誤分類コストに応じて変更する 学習データセットのクラス比率、あるいは重みを誤分類コストに応じて変更した上で通常の学習を行う 誤差関数などモデルの学習手法そのものに誤分類コストを組み込む 今回は3つアプローチの中から、1つ目の閾値を変更する手法について説明していきます。 コスト行列 手法の説明に入る前に、コスト行列について整理しておきます。 クラス数Mの多クラス分類において、モデルが と予測したデータの真のクラスが であった時の誤分類コストを と表すことにします。 例えば二値分類の場合、クラス1を陽性、クラス0を陰性とすると、 は「陽性と予測したが本当は陰性だった」ため偽陽性、逆に は「陰性と予測したが本当は陽性だった」ため偽陰性のコストを表していることになります。 この時、 を要素としてもつM×Mの行列をコスト行列と呼びます。 例えば二値分類の場合は以下のような2×2の行列となります。 真のクラスが0 真のクラスが1 予測したクラスが0 予測したクラスが1 や には正しく分類した時の利益(負の値)が入りますが、実際はこの部分を0として誤分類コストの方に機会損失として織り込むことができます。(後ほど説明します。) ものすごく大雑把な例ですが、先程のカードローン審査の例で申請者 の申請金額を 円とし、金利を1%とした上で、rejectした場合の申請数の減少による効果を1人あたり大雑把に20000円と見積もる(返済できる人の申請数が減る場合は損失とし、返済できない人の申請数が減る場合は利益とします。)と、以下のようなコスト行列を考えることができます。 返済できる人 返済できない人 審査を通す 審査を通さない 20000 -20000 閾値の調整による誤分類コストの反映 コスト行列を定義したところで、閾値を調整する手法の具体的な説明に入っていきます。 大まかには「間違えたらまずい(誤分類コストが相対的に大きい)」クラスについては、その予測確率がたとえ50%を下回っていたとしても、予測結果としてそのクラスを出力するという手法になります。 最適な検出閾値はコスト行列から具体的に以下のように計算することができます。 データ がクラス に属する確率が であった時、コストの期待値が小さい方に分類することを考えます。 式で表すと、 (クラス と予測した時のコストの期待値) (クラス と予測した時のコストの期待値 が成り立つ時、つまり が成り立つ時にクラス =陽性と判別すれば良いことになります。 陽性である事後確率 を とおくと、 から が得られます。 ここで「間違えて分類した時のコストは常に正しく分類できた時のコストよりも大きい」つまり すべての となる に対して、 が成り立つことを仮定します。 この時上の式から とすると、 が得られます。 つまり正しい事後確率 が得られた時、検出閾値を として がそれ以上であれば陽性、そうでなければ陰性とすることで誤分類コストの期待値を最小化することができます。 また の式からコスト行列を 真のクラスが0 真のクラスが1 予測したクラスが0 0 予測したクラスが1 0 とおいたものも同じ結果が得られることがわかります。 これは先程説明した通り、正しく分類できた時に得られる利益を誤分類した時の機会損失として扱うことに相当します。 また、真のクラスにかかわらず同じ予測に対して常に発生する同じ大きさのコストは相殺することがわかります。 実際のデータセットを用いた例 実際のデータセットで閾値の調整によるコストの変化をみてみたいと思います。 今回はUCIのMushroom Classificationのデータセットを検証用に使わせていただくことにします。 https://www.kaggle.com/uciml/mushroom-classification このデータセットは、様々なキノコの傘の形や色などの特徴量と共に、そのキノコが食用なのかそうでないかのラベルが与えられたデータセットとなります。 今回はこれを用いて、キノコのいくつかの特徴量からそれが食用なのかそうでないかを予測する単純な二値分類のタスクを考えます。 分類器としては単純な決定木を使うことにします。 食用でないキノコのクラスを1、食用であるキノコのクラスを0とすると、サンプルサイズは以下のようになります。 クラス0: 4208 クラス1: 3916 必要なライブラリのimportや前処理など from sklearn.preprocessing import LabelEncoder from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn import metrics import numpy as np import pandas as pd import seaborn as sns from matplotlib import pyplot as plt def plot_confusion_matrix (y_test, y_pred, y_prob, cost_matrix): cm = metrics.confusion_matrix(y_test, y_pred) tn, fp, fn, tp = cm.flatten() accuracy = (tp + tn) / (tn + fp + fn + tp) precision = tp / (tp + fp) recall = tp / (tp + fn) f_score = 2 * recall * precision/(recall + precision) fpr, tpr, thresholds = metrics.roc_curve(y_test, y_prob) roc_auc = metrics.auc(fpr, tpr) total_cost = np.sum(cost_matrix * cm.T) print (f "accuracy: {accuracy*100:.4f}%" ) print (f "precision: {precision*100:.4f}%" ) print (f "recall: {recall*100:.4f}%" ) print (f "f-score: {f_score:.4f}" ) print (f "AUC: {roc_auc:.4f}" ) print (f "total cost: {total_cost}" ) df_cm = pd.DataFrame(cm.T, range ( 2 ), range ( 2 )) sns.set(font_scale= 1.4 ) sns.heatmap(df_cm, annot= True ,annot_kws={ "size" : 16 }, fmt= "" ) plt.xlabel( "true label" ) plt.ylabel( "prediction" ) plt.show() df = pd.read_csv( "mushrooms.csv" ) le = LabelEncoder() for k, v in df.dtypes.items(): df[k] = le.fit_transform(df[k]) # 全特徴量を使用すると完璧に分類できてしまうぐらいタスクが簡単なので、今回は実験用に使用する特徴量を制限します df = df[df.columns[: 4 ]] df 混同行列と各種指標 今回の場合、食用でないキノコを誤って食用だと判別して食べてしまった時の被害と、食用のキノコを誤って食用でないと判別して食べ損なってしまった時の被害では前者の方がより重大であると考え、以下のようなコスト行列を設定することにします。 真のクラスが0 真のクラスが1 予測したクラスが0 0 10 予測したクラスが1 1 0 このコスト行列のもとで決定木による分類を行い、まずは閾値を0.5に設定して混同行列やrecision、recallなどの各種指標とともにコストの総計を計算してみます。 cost_matrix = np.array([ 0 , 10 , 1 , 0 ]).reshape(( 2 , 2 )) x_train, x_test, y_train, y_test = train_test_split(df.drop(columns=[ "class" ]), df[ "class" ], test_size= 0.2 , random_state= 0 ) dtc = DecisionTreeClassifier(max_depth= 5 , random_state= 0 ) model = dtc.fit(x_train, y_train) y_prob = model.predict_proba(x_test)[:, 1 ] y_pred = (y_prob >= 0.5 ).astype( int ) plot_confusion_matrix(y_test, y_pred, y_prob, cost_matrix) 混同行列と各種指標 コスト行列と混同行列の要素積を取ることで得られたコストの総計(total cost)は2201となりました。 次に閾値を先程の に変えて同じように分類を行ってみます。学習する過程ではコスト行列は使用しないため、再学習せずに同じモデルを使用することができます。 threshold = (cost_matrix[ 1 , 0 ] - cost_matrix[ 0 , 0 ]) / (cost_matrix[ 1 , 0 ] - cost_matrix[ 0 , 0 ] + cost_matrix[ 0 , 1 ] - cost_matrix[ 1 , 1 ]) y_pred = (y_prob >= threshold).astype( int ) plot_confusion_matrix(y_test, y_pred, y_prob, cost_matrix) 混同行列と各種指標 結果としてprecisionが下がる代わりにrecallが上がることでtotal costを2201から690まで下げることができました。 一応閾値に対するtotal costをplotしてみると、 がtotal costを最小化する閾値であることが確認できます。 x = np.linspace( 0 , 0.5 , 5000 ) y = [] for i in x: y_pred = (y_prob >= i).astype( int ) cm = metrics.confusion_matrix(y_test, y_pred) total_cost = np.sum(cost_matrix * cm.T) y.append(total_cost) fig, ax= plt.subplots( 1 , 1 , figsize=( 10 , 8 )) ax.plot(x, y) ax.vlines(threshold, ymin= 0 , ymax= max (y), color= "orange" ) ax.legend([ "total cost" , "p*" ], loc= 'upper center' ) ax.set_xlabel( "threshold" ) ax.set_ylabel( "total cost" ) plt.show() 検出閾値と誤分類コスト まとめ 誤分類コストの非対称性の問題と、それに対するアプローチの1つであるコスト考慮型学習(Cost-Sensitive Learning)について紹介させていただきました。 BASEで活用されている機械学習モデルの一部にも、こうしたコスト考慮型学習のアプローチが用いられています。 実際には正確なコスト行列を設定することが難しい場合もありますが、それでも誤分類コストの非対称性については常に意識する必要があります。 例えばローン審査において機械学習モデルを人手によるチェックのための一時フィルター的な役割で使用する場合には、あらかじめ許容できる件数ベースの偽陽性率の上限を決めた上で、金額ベースの再現率が最も高くなるような閾値を設定する、といった方法が有効かもしれません。 その場合はモデルを開発するエンジニアだけではなく、二次チェックを行うオペレーターともうまくコミュニケーションを取りながら達成すべき目標を明確にしていくステップが必要不可欠となります。 また、実際に運用していく上では誤分類時のコストだけではなく、冒頭で触れた通り計算コストやメンテナンスコスト、解釈性の問題など色々な要素が影響してきます。それらを踏まえた上でドメイン知識を活用し、短期的な利益だけではなく、長期的な利益を見据えて最適なモデルを選択しチューニングしていくことが、ビジネスでの機械学習モデルの活用を求められるデータサイエンティストの役割であると考えます。 参考文献 Elkan, Charles. (2001). The Foundations of Cost-Sensitive Learning. Proceedings of the Seventeenth International Conference on Artificial Intelligence: 4-10 August 2001; Seattle. 1. Ling, Charles & Sheng, Victor. (2010). Cost-Sensitive Learning and the Class Imbalance Problem. Encyclopedia of Machine Learning. 特に現実の応用例を考える際、誤分類コストの非対称性の問題は、必ずと言っていいほど不均衡データの問題と一緒になって現れますが、個人的にはこの二つの問題は分けて考える方が良いかと思っています。というのも不均衡データには不均衡データ特有の、分類器の識別境界にかかるバイアスなど( https://scikit-learn.org/stable/auto_examples/svm/plot_separating_hyperplane_unbalanced.html や https://ieeexplore.ieee.org/document/6137280 などで説明されている)タスクのドメイン(医療診断で使うのか、ローン審査で使うのかなど)とは独立した問題が存在し、誤分類コストが対称であっても生じる可能性がある一方で、誤分類コストの非対称性の問題はタスクのドメインに依存した問題であり、不均衡データでなくても起こり得るからです。ただし、不均衡データの問題を解決するために非対称な誤分類コストを設定したり、誤分類コストの非対称性を考慮するためにunder-samplingやover-samplingなどによって敢えて不均衡な事前分布を設定する(この記事では詳しく紹介できませんでしたが Cost-Sensitive Learningの手法 の2つ目に相当します。)アプローチが取られることはあります。今回の記事ではトピックを絞るため、あまりデータの不均衡性の問題は取り上げていません。(今後機会があれば、2つ目以降の手法と共に記事にしようかと思います。) ↩
この記事は BASE Advent Calendar 2021 23日目の記事です。 こんにちは。 UXライターの藤井です。 ふだんは、BASEプロダクト全般におけるテキストの品質を向上させる、UXライティングを担当しています。「テキストコミュニケーションをデザインする」をキーワードに、テキスト版デザインシステムとして「運用ガイドライン」「用語リスト」を作成、タッチポイントごとに担当を分け、最終レビュー担当者をそれぞれアサインし、運用しています。あらゆるタッチポイントにおいて、日々テキストコミュニケーションの最適化を図っています。 トンマナ≠UXライティング この取り組みのなかで見えてきたのが、こんな2つの課題でした。 トンマナ(トーン&マナー)をひたすら磨く作業が、現状のテキストデザインの大半を占めていること 運用ガイドラインがあっても、品質の担保は、最終的にはどうしても属人的にならざるを得ないこと つまり、たくさんのショップオーナーの皆様が利用するプラットフォームにプロダクトが育っていく課程において、表面的にはトンマナが統一されていることで、一定のテキスト品質は担保されている一方で、UXライティングの本来の役割である「テキストを設計することによって、プロダクトとユーザーのコミュニケーションをデザインすること」、つまり「体験をデザインすること」という本質に、あまり時間を割けていないのでは、ということに気づいたのです。 品質の担保を前提に、本質的な「体験のデザイン」へ この次なるテーマと向き合うにあたり、必要になると考えたのが、「テキスト入力支援ツール」の導入でした。いくら洗練され磨き上げられた「運用ガイドライン」や「用語リスト」があっても、それはあくまで必要なときに紐解く「逆引き辞典」のようなもの。漢字の閉じ開きや記号、正式名称のBASEルールは、そもそも暗記するような類いのものではありません(さすがに何年もトンマナを整えていると、ほとんど身体に染み付いてはいますが)。あらかじめ定義されたルールに則していないものを指摘し、自動的に修正が加えられたら、あらゆるチームから「プロダクトに反映させたい」と提案されるテキストの品質は、すくなくとも「トンマナ」という文脈においては、その時点で担保されます(もちろん、完璧にではないとは思いますが)。これにより、「体験をデザインすること」ーーテキストのターゲット/目的/ゴールを定義し、情報の構成を設計し、マイクロコピーをライティングする、というプロセスに、さらにていねいに取り組むことができるのではないか、と。 入力支援ツールの導入検討、ガイドラインのアップデート そのために取るべきアクションは、2つあると考えています。 テキスト入力支援ツールの選定と導入 正誤の参照先となる「運用ガイドライン」「用語リスト」のDB化 まずは、現状のプロダクトやレビューフローとマッチするツールのメリット/デメリットの調査と検討。また、導入にあたって、エンジニアチームにどのくらいの稼働が発生するのか。 「textlint」や「文賢」をはじめとして、すでにけっこうな数のサービスがリリースされていて、インターフェースやできることがそれなりに違うので、導入に向けてあれこれ調査しつつ、現在先行して作業を進めているのが、もう1つのアクション「運用ガイドライン」「用語リスト」のDB化です。 BASEがUXライティングというアプローチを取りはじめてからおよそ3年、昨年もご紹介しましたが、ユーザー体験全体を通して語られる、ブランドの理念を反映した言葉遣い(ボイス)、ユーザー体験の各部分における言葉遣いの変化(トーン)のたたき台も形作られ、それなりに育ってきてはいるのですが、「運用ガイドライン」は目的や対象範囲などがまだきちんと定義され、明文化されていませんでした。また、「用語リスト」もレビュワー目線で構成されており、お世辞にも「誰が見てもすぐに活用できる」ところまでは整えられていない状態でした。 テキストライティングの方針を定める そこで、まずはテキストガイドラインにおける「テキストライティングの方針」、目的を定めました。 ショップオーナーの皆様だけではなく、これからショップを開設しようと検討している方、BASEのプラットフォームを利用したショップでお買い物される購入者、購入を検討している方をふくめ、BASEのタッチポイントに触れる可能性のあるすべての方を対象に、伴走者としてどんなテキストコミュニケーションであるべきなのか、という観点・視点がベースとなっています。 テキストライティング方針の対象範囲の明文化 また、現段階における「対象とする範囲」「対象としない範囲」も定めています。 2021年の段階において、プロダクトおよびプロダクトに付随するDBC(管理画面のお知らせカード)、メール、自社メディア(BASE活用方法などの記載された記事)などを対象範囲とし、メディアの属性によって受け取り手がキャッチできる情報や、印象に差分が発生しやすいSNS、広告などは、対象からあえて外しています。あくまでも、ユーザーの行動を促す短い文言「マイクロコピー」および「マイクロコピー」を支える屋台骨としての最低限のテキスト、にフォーカスしています。 正誤が検知できる、DB化されたガイドライン そして、来たるべきDB化を念頭に、項目自体をあらためて精査、正誤表にすることにより、正以外を検知できるような作りにしました。 テキストガイドラインとして「漢字・ひらがな」「カタカナ」「記号」「数字・単位・日付」、また用語集として「サービス正式名称」「BASE固有の用語」「用語」、マイクロコピーは用途別に「ボタン・リンク」「エラー・バリデーション」「フラッシュメッセージ・トーストメッセージ」「警告メッセージ」「ダイアログ」と、使いやすく分類し、検索性を高めました。 定形メッセージのテンプレ化による、メッセージ・デリバリーの高速化 さらに、よくある定形のメッセージに関しては、使い勝手を最大化するため、テンプレートも用意しました。 ウェビナー告知やApp新機能紹介など、ある程度フォーマット化できるものをテンプレート化することで、毎回イチからテキストを起こさなくてもよいように、新しい情報をターゲットに最速でアナウンスできるようになりました。 これから そんなわけで、昨年同様、すべては現在における仮説でしかないことを前提に、「運用ガイドライン」「用語リスト」のアップデートとさらなる活用策を模索してきた2021年。すべては、BASEというプラットフォームを通して、ショップオーナー様の皆様やお客様の叶えたい願いを実現するために。2022年のBASEにも、どうぞご期待ください。
この記事は BASE Advent Calendar 2021 の22日目の記事です。 はじめに はじめまして、Owners Success Frontend Shop Frontチームの坂口です。 普段はフロントエンドエンジニアとしてVue.jsを使った開発をメインに行なっているのですが、チームでプロジェクトマネージャーやデザイナーが手軽に動作を確認できるレビュー環境がほしいという話があり、AWS App RunnerとGitHub Actionsを連携して構築をしたのでその話をしたいと思います。 レビュー環境とは レビュー環境というのはGitHubのプルリクエストやブランチごとに動作確認ができる環境で以下のような動きをするものを今回は構築することにしました。 プルリクエストが作成されるとレビュー環境立ち上げ (作成) プルリクエストに紐付いたブランチが更新されるとレビュー環境更新 (更新) プルリクエストがclose、またはマージされるとレビュー環境削除 (削除) イメージとしてはHerokuの Review Apps やNetlifyの Deploy Previews などに近いかもしれません。 使用技術 AWS App RunnerとGitHub Actionsを利用して以下のような構成にしました。 AWS App Runner AWS App Runnerは今年5月に発表されたコンテナベースのフルマネージドサービスで、インフラの知識があまりなくても簡単にアプリケーションをデプロイすることができます。 https://aws.amazon.com/jp/apprunner/ 今回構築するにあたってAWS LambdaやAWS ECSなど選択肢はありましたが、簡単にデプロイできるということで採用しました。 準備 事前にGitHub Actionsで利用するAWSのIAMユーザーを作成して、リポジトリのsecretsにアクセスキーとシークレットアクセスキーを用意しておきます。 必要な権限は こちら の公式記事で書いてあるものになります。 ワークフロー内容 ワークフローは先ほど上げた作成、更新、削除に加えて、一時停止、復帰を追加した以下5パターンを用意しました。 プルリクエストにラベルがつくとレビュー環境立ち上げ (作成) ラベルが付いたプルリクエストへ新たにpushされると更新 (更新) プルリクエストがclose、またはマージされるとレビュー環境削除 (削除) 毎日21時になると稼働中のレビュー環境を一時停止(一時停止) プルリクエストの /resume とコメントすると復帰(復帰) プルリクエストにラベルがつくとレビュー環境立ち上げ name : review-app-deploy on : pull_request : types : - labeled jobs : deploy-review-app : name : Deploy Review App runs-on : ubuntu-latest environment : development if : ${{ github.event.label.name == 'ラベル名' }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Login to Amazon ECR id : login-ecr uses : aws-actions/amazon-ecr-login@v1 - name : Build, tag, and push image to Amazon ECR id : build-image env : ECR_REGISTRY : ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_NAME=`echo review-app_$IMAGE_TAG | cut -c 1-40` docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=service::$SERVICE_NAME" echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - name : Deploy to App Runner id : deploy-service uses : awslabs/amazon-app-runner-deploy@main with : service : ${{ steps.build-image.outputs.service }} image : ${{ steps.build-image.outputs.image }} access-role-arn : ${{ secrets.APPRUNNER_ROLE_ARN }} runtime : NODEJS_16 region : ${{ secrets.AWS_REGION }} cpu : 1 memory : 2 port : 80 wait-for-service-stability : false - name : Comment Review App URL run : | SERVICE_URL=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='${{ steps.build-image.outputs.service }}']" | jq -r '.[]["ServiceUrl"]' ` gh pr comment $PR_NUMBER --body "Review App URL: https://$SERVICE_URL" 大まかな流れは以下になります。 Dockerイメージビルド Amazon ECRへDockerイメージプッシュ AWS App Runnerのサービス作成 今回、プルリクエストのブランチ名をDockerイメージのタグとAWS App Runnerのサービス名に利用しているのですが、AWS App Runnerのサービス名の 制限 に合わせて文字列を置換しています。 またあとから識別しやすいようにサービス名に review-app_ プレフィックスを付与しています。 IMAGE_TAG = ` echo $BRANCH_NAME | sed -e " s/[^a-zA-Z0-9_-]/-/g " ` SERVICE_NAME = ` echo review-app_ $IMAGE_TAG | cut -c 1-40` サービス作成では、AWS公式GitHub Actionsである aws-actions/configure-aws-credentials を利用していて、CPU、メモリは最低限にしています。 wait-for-service-stability を true にするとサービス作成完了まで待ってくれるのですが、完了まで5分ほどかかってしまいGitHub Actionsの枠を使い切ってしまうことを懸念して false にしています。 さらにAWS App RunnerのURLをプルリクエストのコメントに通知するようにしています。(※サービス作成完了後ではないので、利用可能までに5分程度待機が必要です。) SERVICE_URL = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' ${ { steps.build-image.outputs.service } }'] " | jq -r ' .[]["ServiceUrl"] ' ` gh pr comment $PR_NUMBER --body " Review App URL: https:// $SERVICE_URL " 以下のように特定のラベル(今回は deploy review app )を付与するとワークフローが走り、サービスURLをコメントしてくれるようになります。 ラベルが付いたプルリクエストへ新たにpushされると更新 name : review-app-update on : pull_request : types : - synchronize jobs : update-review-app : name : Update Review App runs-on : ubuntu-latest environment : development if : ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名' ) }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Check Service Exist id : check-service-exist run : | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_NAME=`echo "review-app_$IMAGE_TAG" | cut -c 1-40` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='RUNNING']" | jq -r '.[]["ServiceArn"]' ` echo "::set-output name=image-tag::$IMAGE_TAG" echo "::set-output name=service-arn::$SERVICE_ARN" - name : Login to Amazon ECR id : login-ecr if : ${{ steps.check-service-exist.outputs.service-arn }} uses : aws-actions/amazon-ecr-login@v1 - name : Build, tag, and push image to Amazon ECR id : build-image if : ${{ steps.check-service-exist.outputs.service-arn }} env : ECR_REGISTRY : ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} . docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} - name : Update App Runner Service id : update-service if : ${{ steps.check-service-exist.outputs.service-arn }} run : | aws apprunner start-deployment --service-arn ${{ steps.check-service-exist.outputs.service-arn }} 大まかな流れは作成とほぼ同じになりますが、注意点として aws-actions/configure-aws-credentials 内部でサービスが存在する場合は更新コマンドを実行しているのですが、更新コマンドはサービスの設定を更新してくれるのみでソースを更新してはくれないため、そのまま利用せず、 start-deployment を実行する必要があります。 aws apprunner start-deployment --service-arn ${ { steps.check-service-exist.outputs.service-arn } } また細かいですが、 jq では -r オプションをつけて結果の文字列からダブルクウォートを削除して扱いやすくしています。 SERVICE_ARN = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' $SERVICE_NAME '&&Status=='RUNNING'] " | jq -r ' .[]["ServiceArn"] ' ` 毎日21時になると稼働中のレビュー環境を一時停止 name : review-apps-pause on : schedule : - cron : '0 12 * * MON-FRI' jobs : pause-review-app : name : Pause Review App runs-on : ubuntu-latest environment : development steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Pause App Runner Services id : pause-services run : | SERVICE_ARN_LIST=`aws apprunner list-services --query "ServiceSummaryList[?Status=='RUNNING']" | jq -r '.[] | select(.ServiceName | test("^review-app_")) | .ServiceArn' ` if [ -n "$SERVICE_ARN_LIST" ] ; then echo $SERVICE_ARN_LIST | while read SERVICE_ARN ; do aws apprunner pause-service --service-arn $SERVICE_ARN done fi AWS App Runnerはサービスが起動している間課金されてしまうため、深夜の誰も利用しない時間では一時停止するようにしています。タイムゾーンはUTCなので注意してください。 on: schedule: - cron: ' 0 12 * * MON-FRI ' 一時停止するサービスはAWS CLIの --query オプションで稼働中のものを抽出した後、jqでサービス名が review-app_ プレフィックスのものを絞り込んでいます。 SERVICE_ARN_LIST = `aws apprunner list-services --query " ServiceSummaryList[?Status=='RUNNING'] " | jq -r ' .[] | select(.ServiceName | test("^review-app_")) | .ServiceArn ' ` プルリクエストに /resume とコメントすると復帰 name : review-app-resume on : issue_comment : types : [ created, edited ] jobs : resume-review-app : name : Resume Review App runs-on : ubuntu-latest environment : development if : ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/resume' ) }} env : GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Check Service Exist id : check-service-exist run : | BRANCH_NAME=`gh pr view ${{ github.event.issue.number }} --json headRefName --jq '.headRefName' ` SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='PAUSED']" | jq -r '.[]["ServiceArn"]' ` echo "::set-output name=service-arn::$SERVICE_ARN" - name : Resume App Runner Service id : resume-service if : ${{ steps.check-service-exist.outputs.service-arn }} run : | resume-service --service-arn ${{ steps.check-service-exist.outputs.service-arn }} issue_comment をトリガーにしており、作成や更新のように github.head_ref でブランチ名が取得できないため、コメントされたプルリクエストからブランチを特定するための処理を挟んでいます。 BRANCH_NAME = `gh pr view ${ { github.event.issue.number } } --json headRefName --jq ' .headRefName ' ` 以下のようにコメントすることでワークフローが走り、一時停止中のAWS App Runnerサービスが復帰してくれます。 プルリクエストがclose、またはマージされるとレビュー環境削除 name : review-app-delete on : pull_request : types : - closed jobs : delete-review-app : name : Delete Review App runs-on : ubuntu-latest environment : development if : ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名' ) }} env : BRANCH_NAME : ${{ github.head_ref }} steps : - name : Checkout uses : actions/checkout@v2 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ${{ secrets.AWS_REGION }} - name : Delete App Runner Service id : delete-service run : | SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME']" | jq -r '.[]["ServiceArn"]' ` if [ -n "$SERVICE_ARN" ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo "::set-output name=tag::$SERVICE_NAME" fi - name : Delete ECR Image id : delete-ecr-image if : success() env : ECR_REPOSITORY : ${{ secrets.ECR_REPOSITORY }} run : | TAG=`echo "$BRANCH_NAME" | sed -e "s/[^a-zA-Z0-9_-]/-/g" ` aws ecr batch-delete-image --repository-name $ECR_REPOSITORY --image-ids imageTag=$TAG 削除したいサービスの存在確認をして、サービスとDockerイメージを削除します。 SERVICE_NAME = ` echo " review-app_ $BRANCH_NAME " | cut -c 1-40 | sed -e " s/[^a-zA-Z0-9_-]/-/g " ` SERVICE_ARN = `aws apprunner list-services --query " ServiceSummaryList[?ServiceName==' $SERVICE_NAME '] " | jq -r ' .[]["ServiceArn"] ' ` if [ -n " $SERVICE_ARN " ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo " ::set-output name=tag:: $SERVICE_NAME " fi おわりに 今回は公式でGitHub Actionsが用意されているのもあって比較的簡単に連携・レビュー環境の構築ができました。まだサービス作成完了時の通知など改善の余地はたくさんありますが、手軽に構築ができるので、ぜひ一度検討してみてはいかがでしょうか。
この記事は BASE Advent Calendar 2021 の22日目の記事です。 こんにちは!BASEでエンジニアをやっている大津( @cocoeyes02 )です。今回はiikanji-conference-toudanチームの取り組みについてご紹介します! iikanji-conference-toudanチームとは? iikanji-conference-toudanチームとは、BASEから技術イベント・カンファレンスへ登壇する人たちを応援するチームです。登壇を有志による属人的なものにするのではなく、文化として当たり前になっている状態を目指して活動しております。 もともと登壇自体は前から推奨されていましたが、「BASE全体で外部への技術的なブランディングをしっかりやってきたい」という話があり、 iikanji-conference-toudanチームが生まれました! ちなみにiikanji-conference-toudanチーム以外でも、 ブログ や 自社イベント 、 Youtube など精力的に活動していますのでそちらもどうぞ! これまでどんなことしてきたの? チームで応援できそうなことを調査 応援と一口に行っても、様々な応援の形があるかと思います。そこで登壇する人を増やすために我々に何ができるのかをブレストし、まとめました。 ブレストの内容をグルーピングしたり抽象化した様子 特に、登壇のモチベーションのところを深掘りしたいという話になり、実際に登壇に対してどう思っているのかアンケートを取りました。 すると圧倒的に「登壇するネタがない」と思っている人が多かったため、「ブログや社内ドキュメントの内容をベースに登壇を進めたら、登壇してくれるのではないか?」という仮説を立て、登壇を勧めました。 アンケート結果の一部 実際この方法で何人か登壇した人を見たので、登壇を勧めるときの1つの方法として確立しつつあります。 一例として、 @tawamura さんの ElasticsearchとKibela APIを使ってSlackでのCSお問い合わせ対応業務を改善した話 がその1つになります。よければブログや登壇動画も是非ご覧ください! ちなみに、このPHPカンファレンス2021ではBASEから計5人が登壇しました!その時のイベントレポートは以下の記事になります。こちらも是非ご覧ください! devblog.thebase.in 登壇前のトークへのフィードバック より良い登壇ができるように、登壇前にzoom等でトークを聞いてもっと良くなる点などがあればフィードバックします。 例えばトークを聞きながら、以下のようなことについてフィードバックしています。 スライド全体が頭に入ってきやすい構成になっているか スライド等の資料で分かりにくいところは無いか 図や動画を使った方が良いところは無いか 時間オーバーしているのであれば、どこを削るべきか 喋りのスピードが早くて聞いている側が負担にならないか など 技術イベント・カンファレンスでの登壇は、大抵スライド等の資料を使いながらトークする形になります。なので、スライド等の資料のフィードバックが中心になります。 技術イベント・カンファレンスのスポンサー業務 技術イベント・カンファレンスでスポンサーとして名乗り出るのにはさまざまな理由がありますが、登壇している人のモチベーションアップやトークを聞いてもらうきっかけとしてスポンサー業務もやっています! また、カンファレンスによってはスポンサーセッション枠が設けられているスポンサープランもありますので、そちらも積極的に狙っていきます。 例えばPHPカンファレンス2021では、 @tanden さんを中心として「 BASE のお問い合わせ対応の裏側!」という座談会形式のスポンサーツアーの企画・開催をしました。その時の様子は以下の記事に書いてあります! devblog.thebase.in アーカイブ動画をみんなで見る会を開催 近年の技術イベント・カンファレンスでは、各トークのアーカイブ動画がアップロードされていることも珍しくありません。 登壇だけでなく、技術イベントの参加自体もっとわいわいしていきたい。そんな思いから各トークのアーカイブ動画をみんなで見る会の開催もしました! PHPカンファレンス2021のアーカイブ動画を見る会をSlackで募った様子 これからはどんなことをしていきたい? 色んな分野技術イベント・カンファレンスでの登壇を応援していきたい 現在登壇人数で言うと、PHPやGoのカンファレンスでの登壇が多い状況です。ですが、技術イベント・カンファレンスはPHPやGo以外にもたくさんありますので、色んな分野での登壇を応援していきたいと思っています! どの分野の技術イベント・カンファレンスでも日々登壇するチャンスを狙える体制づくりとして、例えば現状技術イベント用のカレンダーを用意して、色々な技術イベント・カンファレンスの日程を追えるようなものを作成しています! 登壇者だけでなく、技術イベントの参加自体ももっとわいわいして行きたい 登壇する人だけにフォーカスするのではなく、技術イベント自体もわいわいして登壇している人のモチベーションアップ&登壇へのハードルを下げるのも狙っていきたいと思います! 技術イベント・カンファレンスを楽しむための施策として、例えば定期的に技術イベントのアーカイブ動画を見る会を開催したりし始めています! 最後に もともとBASEでは、会社全体でOSS(コミュニティ)を応援する文化があり、この記事を書いた直近ではThe PHP Foundationに寄付を行いました。 devblog.thebase.in 我々のチームでは、技術イベント・カンファレンスに登壇して盛り上げていくという形で、OSS(コミュニティ)に貢献できればと思っております。引き続き、色々なアプローチで技術イベント・カンファレンスでの登壇を応援していきます! OSS(コミュニティ)に貢献したい!盛り上げていきたい!と思う方は、ぜひBASEへどうぞ! open.talentio.com 明日は @FUJIIMichiro さんの「UXライティング関連」と、 @shotakeuchi さんの「分類コストを考慮した機械学習モデルの考え方」です!お楽しみに!
この記事は BASE Advent Calendar 2021 の21日目の記事です。 devblog.thebase.in はじめに Payment Devグループの山本( @msysyamamoto )です。 この記事は私たちのグループで行っている読書会の紹介になります。これから読書会を開催しようとしている方や、いまの読書会を改善したいと思っている方の参考になれば幸いです。 こんな感じで行っています 参加者 基本的にはグループのメンバは全員参加になっています(もちろん、業務が忙しい場合などは欠席することは可能です)。現在は14名のメンバが参加しています。 グループの読書会となってはいますが、別のグループからの参加も可能だったりします。 体制 グループのメンバの中の一人が読書会のまとめ役となります。まとめ役は立候補制になっています。もし、立候補する方がいなかった場合は、グループのマネージャからの指名になります。 スケジュール 毎週1回、同じ曜日同じ時間に開催しています。時間は1回あたり1時間となっています。いつも同じ時間に開催することでリズムが生まれ、読書会を続けていきやすくなるのではないかと個人的には思っています。 場所 Zoom で行っています。 読書会は録画をしており、参加できなかった方は後でどのような内容だったかを確認できます。 選書の方法 まとめ役に委ねています。 多くの場合、まとめ役がいくつか本をピックアップし、参加者がそれに投票し読む本を決定します。ピックアップされた本以外で読みたい本があれば、まとめ役でなくとも選択肢に本を追加することができます。 まとめ役に委ねていますので、まとめ役が読書会で読みたい本があれば投票を経ずとも読む本を決定できたりもします。 参考までに今まで読書会で読んできた本を紹介します。2021年12月現在は『DNSがよくわかる教科書』を読み進めているところです。 リーダブルコード オブジェクト指向設計実践ガイド Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 テスト駆動開発 失敗から学ぶRDBの正しい歩き方 Web API: The Good Parts 決済サービスとキャッシュレス社会の本質 DNSがよくわかる教科書 本の難易度やページ数にもよるのですが、今までの実績からして大体1ヶ月から2ヶ月で1冊の本を読み終えています。 会の進め方 事前にやっておくこと 読む本が決定した時点で、読む範囲の決定と、その範囲は誰が発表者になるかを決定しておきます。発表者は1回の会で5人となっています。発表者はランダムで決定されるのではなく、参加者全員が同じくらいの回数発表者になるように決めています。 発表者は会が始まる前まで該当範囲を読み、気づき・疑問を弊社でドキュメントサービスとして利用しているKibelaに書いておきます。要約は書きません。 発表者でない方も事前に該当範囲を読んでおきます。 その会の司会者を決めます。司会者はボットによってランダムに決定されます(まとめ役 ≠ 司会者 となっています。ランダムなのでまとめ役が司会者になる場合もありますが)。 読書会本番 発表者がKibelaに記述した気づき・疑問を説明し、それに対してディスカッションして行きます。発表者は5人ですが、この進め方で毎回ちょうどよい時間に終わるか、少しオーバーするくらいの時間に収まっています。 以上が私たちのグループで開催している読書会の現在時点でのやり方になります。私が読書会に参加するようになったのは2021年7月なのですがその時とのやり方と現在のやり方を比べると、改善されている部分があります。 例えば発表者の決定方法ですが、以前のやり方はランダムで決定していたのですが、連続で発表者になったりする人が出たりするのでよくないよね、ということで今の方法に改善されました。 今のやり方がベストだとは思い込まずに、改善できそうなところはどんどん改善していき、よりよい読書会にできるといいと思っています。 よかったこと この記事の締めくくりとして、私個人の感想にはなるのですが、読書会でよかったと思うことを紹介します。 自分では読まなさそうな本を読むことができる 具体的に言いますと『決済サービスとキャッシュレス社会の本質』がそれにあたります。正直なところ、読書会で読む本のとしてピックアップされるまでその本を知りませんでしたが、決済に関わる仕事をしている身として、興味深く読み進めることができました。 自分とアンテナの張り方が違う人が集まることで、自分の知らなかった本を読むこと・知ることができるので、知識の幅を広げるのにいいと思いました。 コミュニケーションの場になる 私たちのグループは全員が同じプロジェクトを進めているわけではありません。同じプロジェクトに参加しているメンバとはよくコミュニケーションを取るのですが、別のプロジェクトに参加しているメンバとはそうではありません。 また、新型コロナウィルスの影響によりリモートで仕事をする機会が増え、グループのメンバと顔を合わせる機会が減ってきています。 それゆえ、同じグループの中でもお互いのことをよく知らないということがあるのですが、読書会がお互いのことを知るよい場所になっていると思います。
この記事は BASEアドベントカレンダー 21日目の記事です。 まえがき BASE BANK株式会社でエンジニア兼Engineering Program Managerをやっている 松雪( @applepine1125 ) と 永野( @glassmonekey ) です。 BASE BANKでは組織の拡大に伴って表出した課題を解決するために、プロダクトのデリバリー、クオリティに責任を持つEngineering Program Manager(以下EPM)という役割を導入しています。 今回はまだ馴染みのないであろうEPMについてと導入の背景、具体的な働きざまについてご紹介します。 Engineering Program Manager(EPM) とはなにか そもそもEPMという役割自体が日本で馴染みのないものだと思います。 Apple や Amazon 、 Meta(旧FaceBook) 社などで正式なポジションとして存在しており、ざっくり各社のjob descriptionから要素を抽出してみると、主に以 下のような役割を担っていることがわかります。 開発のリードや開発プロセスの改善 設定したリリーススケジュールへの責任とプロダクトのクオリティの担保 プロジェクト、プロダクトの技術的な阻害要因の排除 ステークホルダーと適切なコミュニケーションを取り、自身での意思決定や意思決定のエスカレーションの実施 Product Manager(以下PdM)や外部のステークホルダーと開発チームの間に入り、適切に開発のサイクルを回していくために必要な役割です。 組織によってはこれらの役割をProject Manager(以下PjM)、Engineering Manager(以下EM)、Tech Lead(以下TL)といった役割が担っていたり、もしくは名も無き仕事として誰かが対応しているのではないでしょうか。 PjM、TL、EMとEPMの違い PjM、TL、EMといった、一見EPMと近い役割がすでに存在する中でEPMという役割が新たに生まれたのはなぜでしょうか、PjM、TL、EMとEPMとでは何が違うのでしょうか。 それぞれ重なる部分もありますが、役割としては微妙に異なります。それぞれ見比べてみましょう。 PjMとEPM PjMはPJの完遂に責任を持つ役割です。 PJの要件定義や進捗管理、メンバー集めや、場合によっては予算を獲得してくるなど、PJ完遂のために様々な業務を行います。 EPMとの一番の違いは、技術的なバックグラウンドがなくてもPjMの役割を担える点です。 PjM自身が技術的なバックグラウンドを持っていたり、メンバー内にそういった判断材料を揃えることができるエンジニアがいると開発に関する意思決定がしやすく比較的円滑にPJが進行します。 しかしそういったコミュニケーションや意思決定ができないとPjMはただエンジニアが提示する開発スケジュールを飲み込むしか無く、根本原因にテコ入れできずにスケジュール遅延が発生するいわゆるデスマーチへと突入していってしまいます。 こうならないようにPjMの意思決定を補佐し、協力してプロダクトのリリースを行うEPMのような存在が重要になってきます。 組織によってはPjMを開発面から補佐する役割はTLが担うというケースも多いかもしれません。 TLとEPM 組織によってTLに期待する役割は様々だと思いますが、どの組織でも割と共通しているのはコード品質や設計、アーキテクトの面でチームや組織に影響を及ぼす存在であることかと思います。 EPMでもそういった側面を求められる場面はありますが、そこを主戦場とするメンバーがすでにいるのであればそのメンバーに役割を委譲しつつ、連携しながらプロダクトのデリバリーへの責務を全うするのがEPMの大きな特徴です。 EMとEPM EMは組織によってTL以上に多様な役割を持っています。 参考に、広木氏が提唱する マネジメントの4象限と強め/弱めのEMの定義 と照らし合わせてみると、 主にPeople, Team Managementを行う弱めのEMとEPMは補完関係になるのではないでしょうか。 採用や評価、日々の1on1なども含め、仕事を通した各メンバーの成長やチーム作りと向きあうEMと、メンバーそれぞれのスキルを最大限に生かしてプロダクトのデリバリーとクオリティを担保するEPMとでは、その役割の中で向き合う要素は似ているものの最終的な責任が異なります。 この両方を一人で担うのはスイッチングコストがとても高く難しい仕事となってしまいます。 下図のように、強めのEMがいれば弱めのEM、EPM含め様々なマネージャ業を一手に引き受けてもらうことはできますが、そういったスーパーマンはそうそういないため、弱めのEMとEPMそれぞれで責務を分割してフォーカスするのは有効な打ち手なのではないでしょうか。 なぜEPMが必要なのか ではそのようなEPMはどのようなケースで必要になるのでしょうか? 一つ考えられるケースとしては「目的別組織」のマネジメントの場合ではないでしょうか。 目的別組織とEPM 組織形態としてよく比較されるのが"機能別組織(職能別組織)"と"目的別組織(職能横断型組織)"です。 機能別組織の場合、PJ毎に各チームからメンバーが集まり、PJが終わると解散、各々別のPJへ・・・というのがよく見る流れではないでしょうか。 こういった組織運営を行っている場合、わざわざEPMなどを配置せずとも、PjMやEM、TLがEPMの役割も担うことでとりあえず目の前のPJを完遂させる事はできると思います。 しかし目的別組織になると、自分が所属するチームが受け持つ機能、プロダクト、領域と継続的に向き合えるようになります。すると開発プロセスやステークホルダーとの関わり方の改善も半永久で継続的に行う必要があったり、チームの経験を形式知として蓄積しやすくなるため、それらの活動を主な責務とするEPMを配置することは合理的ではないでしょうか。 特に特定の領域と継続的に向き合い価値提供していくためには、その領域の深いドメイン知識も必要になってきます。仮に開発プロセスやコミュニケーション力が高かったとしても、深いドメイン知識がないと適切なエスカレーションや開発スコープの変更判断などができないためです。 BASE BANKがEPMを据えた背景 ここからは具体例の一つとしてBASE BANKがEPMを据えた理由とBASE BANKでのEPMの働きざまについてもう少し深掘ってみます。 BASE BANKはショップオーナーが売上を立てたあとの活動に関わる機能やプロダクトを受け持っているチームです。 またBASE BANK社を設立した2018年はまだBASE社は目的別組織ではなかったため、BASE BANKはある意味BASE内での実験的な目的別組織(チーム)としてチームを成長させてきました。 そういった事も踏まえ、メンバーが増え受け持つプロダクトも増える中で、上記のようなEPMとしての責務を持つ役割が必要となったのは必然とも言えました。 BASE BANK黎明期 BASE BANKの立ち上げ期はざっくりエンジニアとPMが存在しており、明確に役割を細分化しなくても開発を回していける規模でした。 メンバーが増え、よりチームらしく エンジニアメンバーがjoinし、人数が少しづつ増え、受け持つ機能やプロダクトも増えててくると、ある程度意識して開発プロセスや採用、評価を回していく必要が出てきました。 そこでTeam Managementを行うEM、TLといった役割が生まれ、PMと協力してTeam Management、People Managementを行うようになりました。 このときはまだ、各機能の開発の運営はTLやPMが並行して受け持つ形になっていました。 ピザ2枚 を分け合う以上の規模に 更にメンバーの採用が進み、関わるプロダクトもYELL BANK、BASEカード、振込申請などさらに増え、各プロダクトのグロースにも本格的に着手しなければならなくなりました。 すると、それまで並行して複数プロダクトの開発プロセスを主導していたEMやPdMが本来の責務である組織全体の事業推進やTeam、People Managementに注力せざるを得なくなり、各プロダクトの開発プロセスの主導やPdM、各ステークホルダーとの橋渡し役として開発を主導するEPMという役割が生まれました。 ちなみに、BASE BANKのエンジニアは各々の強みを活かしつつ開発の全てのライフサイクルに関わる フルサイクルエンジニア としての能動的な働きを求められます。 そのため、EPMは主導といってもマイクロマネジメントをするわけではなく、開発チームとしての目的やビジョンを掲げつつ各メンバーと協力して日々の運営や改善をしたり、メンバーがより能動的に動けるようなお膳立てをします。 それぞれのプロダクトごとのEPMとしての役割 では、EPMのプロダクトごとの具体例を紹介します。 現時点では、主にBASE BANKとしては BASEカード と YELL BANK がメインのプロダクトです。 決済のドメイン知識を持つ松雪( @applepine1125 )がBASEカード、 元々YELL BANKの開発をしていた永野( @glassmonekey )がそれぞれEPMとして開発を主導しており、それぞれのEPMの役割について紹介します。 BASEカードにおけるEPM BASEカード のEPMとして、現在以下のような仕事を行っています。 - スプリントの運営 - QAの仕込み - 社内外のステークホルダーとのコミュニケーション - 関係各所へ決済の知識やBASEカードの仕様をインプット - 意思決定のための技術面または仕様面(特にカード決済の知識)の判断材料の提供、場合によっては自身で意思決定 https://cp.thebase.in/basecard cp.thebase.in BASEカードはショップの出金に直接関わる機能のため、内部統制などを責務とする Corporate Solutions Engineer(CSE) やリスクマネジメントチームとの関わりが多く、日頃から密にコミュニケーションを取っています。 また、機能実現のために外部企業が提供しているAPIを利用しており、仕様面の調整や、精算上の証跡を用意するためにBASEの経理との連携も密に行う必要があります。 BASEカードの場合、リアルカードの発行など直近の大まかな開発マイルストーンが割と明確であることから、 実装したい各機能のスケジュール上での優先度をPdMと議論、調整し、開発をすすめる上で発生する技術的な阻害要因を排除すること がEPMとして重要になります。 YELL BANKにおけるEPM 簡単にYELL BANKというサービスを紹介しておくと、BASEを活用されてるショップオーナーの皆様へ簡単に資金調達を提供するサービスです。 thebase.in 最近はスタートアップ企業の隆盛もあり、VCなどから資金調達をすることも馴染み深くなりましたが、BASEのようなロングテールの規模感だとまだまだ未知の市場です。 そのため、YELL BANKの開発では 少しずつ小さな仮説検証すること を重きにおいて開発サイクルを回しています。 特に機能をデリバリーするときはできる限りユーザーストーリー作成し、機能と価値をセットで考えられるように気をつけています。特に以下を意識してメンバーと一緒に作ります。 不確実性はどこになるあるのか? 仮説検証したい内容はどこか? 機能を横断的に作る メンバーと一緒に作ることで、メンバー全員がゴールを意識すること、EPM以外もオーナーシップを持って開発をすることに繋がると考えています。 またバックエンド・フロントエンドで区切るのではなく、横断的に作ることで手戻りはできる限り少なくします。 ユーザーストーリーの分割に関しては アジャイル開発におけるユーザーストーリー分割実践 〜画面リニューアルの裏側〜 をご覧ください。 devblog.thebase.in また、仮説検証を実践するにあたって作っては放置ではいけません。そこでPdM/PMMがPDCAを回しやすくするためのデータ基盤の整備もしています。 過去の実践した事例に関しては Google Apps Script× BigQuery × Googleスプレッドシート × データポータルで簡易CRMを作ってみた や Lookerでショップのサービス活用カルテを作成した話 をご覧ください。 devblog.thebase.in devblog.thebase.in BASE BANKとしてのEPMのこれから 一言にEPMと言っても、プロダクトの特性によって役割や重点の置き方が変わってきます。 今の役割の定義は来年には変わってることも全然あるでしょう。正直模索中です。 現在のEPMはプロダクトのデリバリーやクオリティにフォーカスした役割です。 ただ、プロダクトの成功を目指すためにはそれを支えるメンバーの中長期的な成長も重要な要素です。 そのため今後の可能性としてはメンバーに協力してもらうだけでなく、それを各メンバーの評価として還元するためにPeople Managementも担うようになる可能性も大いにあるでしょう。 おわりに これから更にプロダクトのデリバリー速度やクオリティを高めていくには、現メンバーの成長だけでなく、より多くのメンバーの力が必要です。 BASE BANK自体や各プロダクトへの興味でもEPMへの興味だけでもなんでもいいのでぜひ気軽にお話しましょう! open.talentio.com
この記事は BASE Advent Calendar 2021 の20日目の記事です。 はじめに こんにちは。Owners Experience Backend Group の杉浦です。主にサーバーサイドのアプリケーションの実装をしています。 エンジニアにとって、外部企業から提供されるAPIやCDN(Content Delivery Network)といった『外部サービス』をどう扱うかは悩ましい問題です。 特にシステムの設計段階において「外部サービスをどうやって内部システムに組み込むのか?」という方針は、その後のアプリケーションの生産性に大きく影響します。 仮に、密結合に設計してしまうと「外部サービス」という不確実性に影響されやすくなるため、好ましい状況とはいえません。どのように疎結合を実現するのか?という設計が、外部サービスから不確実性をハンドリングする生命線になります。 そこで、この記事では、2021年11月にリリースを行なった「Akamai Image Video Manager の導入プロジェクト」を例に、外部サービスの不確実性を最小化する「設計戦略」の実践方法を提示します。 本稿は3章立てになっています。 第1章:外部サービスの不確実性という問題 第2章:設計戦略のプランニング 第3章:設計戦略を実行するチームマネジメント 第1章では、外部サービスの不確実性について考察しています。内部サービスがミクロな不確実性への対処が主軸になるのに対して、外部サービスの不確実性がよりマクロな範囲、特に外部企業の経営方針の影響を受けやすいという点に言及しました。 第2章では、不確実性を最小化するための設計戦略を提示します。疎結合な設計はもちろん、外部企業の経営状況を読み解くために財務状況(PL/BS)を踏まえる必要性を提示しました。 第3章では、Akamai Image Video Manager の導入プロジェクトを例に、設計戦略を遂行するためのチームマネジメントについて言及しています。外部サービスの導入・移行プロジェクトには技術的な知識が不可欠なことから、エンジニアである筆者がチームを取りまとめた実践例をとなります。 第1章:外部サービスの不確実性という問題 良い設計をするための1つの重要な論点として「不確実性の最小化」があります。 ここで重要なのが、内部のシステムに対する「不確実性」と、外部のシステムに対する「不確実性」は、それぞれ「似て非なる存在」ということです。 内部システムにおける不確実性は、社内のエンジニアでなんとか対応できる範囲が多いと言えます。あくまで、社内における不確実性に対峙するため、その対処も社内で完結しやすいためです。 一方、外部システムは「存在そのものが不確実性に満ちている」といえます。期待したレスポンスが返却されるか?というミクロな不確実性に加えて、それとは別次元のマクロな不確実性を伴います。 例えば、外部システムが「3年後にもサービスを継続している」という保証はどこにもありません。加えて「値上げをしない」という確証も(契約に入れない限りは)どこにもないのです。 つまり、外部システムの不確実性は、干渉できない範囲に及ぶため、これらの不確実性を考慮した設計を考えることは容易ではないといえます。 第2章:設計戦略のプランニング なぜ戦略なのか? 外部サービスの不確実性に対処するためには、単なるアプリケーションの設計はもちろんですが、その大前提となる「設計戦略」が鍵を握ります。 あえて「戦略」という仰々しい単語を使う理由は、その影響力の時間軸が長いからです。戦略を間違えると、リリース直後は問題が発生しなかったとしても、数年後に苦労します。出発点である戦略を間違えた場合、改修コストへの投資が必要になりますが、こうなると当然、ROIも低下するため、経営的にも好ましくありません。 つまり、方向性を間違えることは将来にわたってビジネスに好ましくない影響を与えることを意味するため、設計の上位概念である「設計戦略」が必要になるということです。 取り替え可能性を考慮して設計する 「外部サービスの取り替え可能性」を考慮した設計こそが、不確実性の最小化にあたって有効な方針となります。 取り替えが可能であれば、別の代替サービスへの移行や、代替機能の開発(内製化)が容易になります。そして、仮に、外部サービスで「値上げ」や「サービス停止」といった不確実な事態が発生した場合に、影響を最小限に抑える最後の砦になります。 ただし、この設計方針は、競合製品が存在する「コモディティーな市場」でしか取り得ない選択肢になります。 逆に、代替が存在しない「独占的なサービス」を利用する場合は、価格決定権は外部サービスの側にあります。さらに、代替サービスの登場は考えにくいため、取り替え可能な状態にする必然性に乏しいといえます。 この意味で、外部サービスの利用を前提とした設計戦略を考える上では、外部サービスのビジネス上の競争環境や、経営状況の見極めが必要になります。 このうち、外部サービスの経営状況は、財務の分析によって明らかにできます。米国企業であれば10K(Security Report)、日本企業であれば有価証券報告書が、そのサービスの置かれた競争環境を示す材料になります。 今回のプロジェクトではAkamai(証券コード:NASDAQ: AKAM)が提供するサービス「Akamai Image Video Manager」の利用を前提としたため、Akamaiが公表した2021年の10Kについて、ざっくりと目を通しました。 https://www.ir.akamai.com/sec-filings 責務を1つのリポジトリに集約する 今回の「Akamai Image Video Manager の導入プロジェクト」では、画像配信基盤を「取り替え可能な状態」にするという設計戦略を採用しました。 具体的な実現方法は、外部サービスを利用した画像配信に関する責務を、1箇所のリポジトリに集約するということです。 従来のBASEにおいては、複数のリポジトリに画像配信の外部サービスのコードが点在しており、責務分離の観点であまり好ましい状態ではありませんでした。 そこで、画像配信に関する実装上の責務を1つのリポジトリに集約することによって、疎結合の実現を目論みました。 第3章:設計戦略を実行するチームマネジメント エンジニア駆動のチームマネジメント 筆者の普段のBASEでの仕事は、コードを読み書きすることなので、プロジェクト関連のマネジメントには関与しません。 ですが、今回の「Akamai Image Video Manager の導入プロジェクト」においては、プロジェクトチームを取りまとめました。 その理由は、このプロジェクトを回すためには、エンジニアリングの専門的な知識が必要だったからです。 一般的なBASEの社内プロジェクトであれば、プロダクトマネージャの元に、複数のエンジニアが参画して、1つの機能開発に携わることが多いといえます。 ですが、このプロジェクトにおいては、画像ファイルの基本的な知識に加えて、キャッシュやCDN、BASEのリポジトリ構成を理解する必要がありました。そこで、普段はコードを書いている筆者が、チームを取りまとめることになったのです。 なお、このプロジェクトでは、筆者は実装を行なっていません(ただしプルリクのレビューは行なっています)。 これは社内事情が影響しており、当初は取りまとめと実装を両立して行う予定だったものの、急遽実装が必要になった別のプロジェクトに追加アサインされたため、時間の都合上、Akamaiの移行プロジェクトは「取りまとめ役」に徹することになったからです。※ ※急ぎの実装とは「Facebookドメイン認証」というプロジェクトで、これに関してもテックブログを書いています。 →「ROI(投資利益率)を意識したエンジニアリング」 答えのない意思決定に対峙する チームの取りまとめにあたって、実装の担当箇所の振り分けと、局所的な意思決定の2つを行いました。 1つ目の実装担当箇所の振り分けとは、(1)改修範囲の調査と見積もりを行い、(2)チームの各メンバーに実装箇所を割りあて、(3)リリースに至る手順を交通整理する、ということです。 2つ目の局所的な意思決定とは、リリースにあたって実装以外で決めなければならないことについて、社内の担当者と調整したうえで決定することです。今回は「ショップに対するコミュニケーション」と「APIの互換性」という2点で、意思決定を行う必要がありました。 「ショップに対するコミュニケーション」とは、BASEの顧客であるショップへの通知のことを意味します。画像配信基盤の移行にあたって、画像表示の仕様が微妙に変化することが避けられなかったため、利用者の方にその内容を事前にお伝えすることが必要でした。 この際に、どのような方法(メールなのか?管理画面なのか?)でお伝えするのかを判断する必要があり、プロダクトマネージャの方の大きな助けを借りつつ方針を決めています。 「APIの互換性」とは、BASEが外部に公開しているAPI(BASE-API)への影響を加味することです。 Akamaiへの移行によって、BASEが提供するAPIの構造には変化はないものの、画像関連の「パス」が変更されるため「潜在的に影響あるかもしれない」という懸念がありました。 従来の画像パスとの互換性を残すか?それとも残さないか?というのは非常に難しい問題で、BASE社内の各方面の責任者の方と相談しつつ、方針の決定に至りました。 これらの経過を経て、2021年11月ごろまでに「Akamai Image Video Manager の導入プロジェクト」は無事に終了しました。 協力していただいた社内の皆様、チームの方々に大感謝しております。 終わりに 筆者の個人的な見解として、インターネット業界では、2020年代を通じて外部システムが提供する機能のボリュームは増え続け、業界全体でAPIなどを通じた外部サービスとの連携が1つの主戦場になると見ています。 近い将来、エンジニアは「外部サービスの不確実性をいかに最小化するか」という問題に、より一層、真剣に対峙する必要が出てくるでしょう。 その際に、このテックブログの内容が少しでも参考になれば幸いです。 最後にいつもの宣伝です。BASEでは各職種で採用を強化しておりますので、お気軽にお声がけください! https://open.talentio.com/r/1/c/binc/homes/4380
この記事は BASE Advent Calendar 2021 の19日目の記事です。 BASE BANK株式会社でエンジニアをしている若野( @sam8helloworld )です。 私が普段見ているサービスではBASEの他のアプリケーションや外部サービスのページから遷移してくることがよくあります。 さらにその時々でユーザに提供する情報や振る舞いを変えたくなることもあります。 今回はそういったケースの中でも 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない という仕様を実現するために私が行った調査検証が記事の内容となります。 仕様を簡単に図で表すと以下のようになります。 アプリケーションの構成の整理 今回の実装を行うに当たって前提となるアプリケーションは以下の3つの登場人物で成り立っています。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 特定のディレクトリ配下(e.g. /shop_admin/xxx)のURLにアクセスしてきた時に、SPAの起点になるindex.htmlをレスポンスとして返すことが責務です。 BFFサーバ SPAから呼ばれて表示要件に応じたjsonをレスポンスとして返すことが責務です。サービスのビジネスロジックはここに集約されている場合が多いです。 SPA BFFサーバにサービスのビジネスロジックを委譲しており、画面の表示やユーザのインタラクションを扱うことが主な責務です。 どこにロジックを持つか問題 さて登場人物がわかったところで、今度はその中でも上記の 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない というロジックを持つべきなのは誰なのか?ということが問題になります。 今回のケースでは以下の要件を満たすことができるかが鍵になります。 ページ遷移してきた時に処理を実行できる 直前のページのURLを取得できる 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない では登場人物を1つずつ検証していきます。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 結論から言うと全ての要件を満たすことはできませんでした。(厳密に言えばやろうとすればできるけど遠回り感すごい。) [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない まず1つ目の要件の「ページ遷移してきた時に処理を実行できる」に関しては、外部サービスからページ遷移してくる都合上必ずバックエンドサーバのControllerでSPAの起点になるindex.htmlを返す処理を行うので以下のようにページ遷移時にAPIを叩くことも可能です。 public class XXController { // 省略 public function index() { // ページ遷移してきた時の処理を行う // $api->call(); $this->render('SPAのindex.html'); } } 次に2つ目の要件の「直前のページのURLを取得できる」に関しては、以下のようにリファラを参照することでブラウザセッション内の直前のページのURLを取得できます。 public class XXController { // 省略 public function index() { $referer = $_SERVER['HTTP_REFERER']; if ($referer === '特定のURL') { // ページ遷移してきた時の処理を行う } $this->render('SPAのindex.html'); } } ※ 注意点: <meta name="referrer" content="no-referrer"> タグがあるなど、リファラを送信しない設定になっているページのURLは取得できません。 最後に3つ目の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、以下の3パターンを場合のサーバサイドの状態を考える必要があります。 外部ページから戻ってきた時 外部ページから進んできた時 ページを更新した時 外部ページから戻ってきた時 例えば以下のように a.example.com -> b.example.com -> c.example.com とリンクを押して遷移して、戻るボタンで b.example.com に戻ってきた場合を考えます。 ブラウザの進むボタン(history forward)や戻るボタン(history back)はリファラの状態も元に戻してしまいます。なので、 c.example.com から遷移してきたにも関わらずバックエンドでリファラを参照するとあたかも a.example.com から遷移してきたように見えてしまうのです。 developer.mozilla.org html.spec.whatwg.org 外部ページから進んできた時 「外部ページから戻ってきた時」とは違って確かに直前のページの意味合いはあっていますが、今度はバックエンドでリンクを踏んだページ遷移と区別がつかないので要件を満たせません。 ページを更新した時 ページを更新した場合も「外部ページから進んで来た時」と同様にリファラはそのままですが、バックエンドではリンクを経由したページ遷移と区別がつかないので要件を満たせません。 ※ セッションやCookieを使えばバックエンドで状態を持てるのでページの更新や2回目以降の遷移かどうかが判断できますが、後述の方法の方がシンプルなので採用していません。 BFFサーバ そもそものSPAから呼ばれてjsonを返すことが責務なので、BFFサーバに画面遷移の状態を判別させるのはお門違いです。 [ ] ページ遷移してきた時に処理を実行できる [ ] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない SPA SPAではなんとか3つの要件全てを満たすことができました。 [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [x] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない 1つ目の要件の「ページ遷移してきた時に処理を実行できる」と3つ目の要件の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、ブラウザのNavigation Timing APIを使うことで要件を満たせます。 Navigation Timing APIではバックエンドで判別できなかった戻る・進む・更新・ページ遷移をそれぞれ状態として取得できるので、ページ遷移の時だけ処理を行うということが可能になります。 developer.mozilla.org ただ注意が必要なことは、バックエンドと違ってSPAではページ遷移という概念が普通のリンクを踏んだページ遷移とvue-routerなどのjsで制御されたルーティングによるページ遷移の2つあるということです。 2つ目の要件の「 直前のページのURLを取得できる」に関しては、 document.referrer がバックエンドのリファラと同じ役割を果たすので要件を満たせます。 mounted() { const referrer = document.referrer if (referrer === '特定のURL') { // ページ遷移してきた時の処理を行う } } Navigation Timing API さて、Navigation Timing APIを使えば要件を満たせると言いましたが、Navigation Timing APIとはどういうものなのでしょうか? Navigation Timing APIの MDN Web Docs を参照すると主に以下の使い方をするAPIであると書かれています。 Collecting timing information(タイミング情報の収集) Determining navigation type(ナビゲーションタイプの決定) さらにDetermining navigation typeでは以下のことが判別できると書いてあります。 Was this a load or a reload? (ロードかリロードか) Was this a navigation or a move forward or backward through history? (ページ遷移かhistory backかhistory forwardか) How many (if any) redirects were required in order to complete the navigation? (遷移が完了するまでに何回りダイレクトしたか) これは上述したクリアすべき要件にピッタリハマりそうです。 Navigation Timing APIを使ってナビゲーションタイプを判別する方法 ナビゲーションタイプを判別する方法は2つあります。 1つは window.performance.navigation.type を参照する方法です。 if ( window . performance . navigation . type === window . performance . navigation . TYPE_NAVIGATE ) { console . log ( 'ページ遷移' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RELOAD ) { console . log ( 'ページ更新' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_BACK_FORWARD ) { console . log ( '戻る・進む' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RESERVED ) { console . log ( 'その他' ) } developer.mozilla.org 2つ目の方法はPerformanceNavigationTimingインタフェースを使用する方法です。 const entries = window . performance . getEntriesByType ( 'navigation' ) for ( const entry of entries ) { if ( entry . type === 'navigate' ) { console . log ( 'ページ遷移' ) } if ( entry . type === 'reload' ) { console . log ( 'ページ更新' ) } if ( entry . type === 'back_forward' ) { console . log ( '戻る・進む' ) } if ( entry . type === 'prerender' ) { console . log ( 'その他' ) } } developer.mozilla.org 2つの判別方法の使い分け 2021年12月現在、主要ブラウザで window.performance.navigation.type を利用することはできますが、参照することは非推奨となっています。 代替の方法として2つ目のPerformanceNavigationTimingインタフェースを利用することが推奨されている状況です。 ただし、PerformanceNavigationTimingはiosのsafariが今のところサポートしていない状況です。 const types = window . PerformanceObserver . supportedEntryTypes if ( types . includes ( 'navigation' )) { // PerformanceNavigationTimingインタフェースに対応 } 上記の supportedEntryTypes でPerformanceNavigationTimingのサポート状況は確認できます。 navigation という文字列を含む配列を返す場合は対応済みなのでPerformanceNavigationTimingインタフェースを使った実装を。 navigation という文字列を含まない配列を返す場合は window.performance.navigation.type を使った実装を行うのが良さそうです。 developer.mozilla.org Navigation Timing APIを使った判別はvue-routerを利用した遷移にも使える? Navigation Timing APIにおける「ページ遷移」「戻る・進む」はあくまでも document オブジェクトが初期化・再構築されるような処理に対して遷移に対して判定されるようです。 www.w3.org html.spec.whatwg.org VueをはじめとしたSPAのルーティングでは document オブジェクトを初期化するのではなく、一部のDOMを更新する仕組みになっているので「ページ遷移」「戻る・進む」は判定できませんでした😢 ただ、今回処理を行いたいのはVueのルーティングではなく外部ページからリダイレクトされてきた時なので要件は満たせるというわけです。 document.referrerと組み合わせて「特定ページからの初めての遷移を判定する」をコード isFromCertainPage() { // safari on ios か確認 if (! window .PerformanceObserver.supportedEntryTypes. includes ( 'navigation' )) { // ページ遷移か確認 if ( window . performance .navigation. type !== window . performance .navigation.TYPE_NAVIGATE) { return false } } // ページ遷移か確認 const entries = window . performance .getEntriesByType( 'navigation' ) for ( const entry of entries) { if (entry. type !== 'navigate' ) { return false } } const expectedReferer = '特定のURL' let referrer = document . referrer . replace ( /\?.*$/ , '' ) // safariのバージョンによってreferrerのトレイリングスラッシュの有無が分かれるので、一律トレイリングスラッシュをつける // https://trac.webkit.org/changeset/280342/webkit/ referrer = referrer. slice (- 1 ) !== '/' ? referrer + '/' : referrer if (!referrer. startsWith (expectedReferer)) { return false } // SPA内での回遊判定 if ( this .$store. state .alreadyVisited) { return false } return true } ※ SPAのルーティングの遷移に関しては現状判定するAPIがないので、状態を持つようにしています。 終わりに Navigation Timing API自体は複雑ではないので、すぐに実装に入れました。ただ、SPAではなぜNavigation Timing APIでページ遷移が判定できないのか?ということを深掘りしていくとブラウザAPIの仕組みが垣間見えて結構面白かったです。 あとは今回みたいにブラウザのAPIを利用したコードのテスト書こうとしたときに、jsdomだとモックだらけになっちゃうし実ブラウザを使ったIntegrationテストにするにもコストかかるしで塩梅が難しいと感じました。ここら辺のテスト詳しい方がいたらぜひ @sam8helloworld まで🙇‍♂️ また、もしBASEBANKに興味のある方は @sam8helloworld や下記のリンクまで open.talentio.com 明日はOwners Experience Backend Groupの杉浦さんです。
この記事は BASE Advent Calendar 2021 の19日目の記事です。 こんにちは、BASE株式会社でバックエンドエンジニアをしている小川です。 私たちのチームでは10月からレスポンス改善PJとして、BASEのレスポンスが遅い処理を改善する施策を行なってきました。 その中で下記の例のようなクエリが重い処理として挙がり、そのクエリではGROUP BYが使われていたのですがDISTINCTを使った方が早くなるのではという意見がありました。 SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; そこで、DISTINCTとGROUP BYでどちらが早いのかをネット上で調べたところ、DISTINCTの方が早い説やGROUP BYの方が早い説、どちらも変わらない説などいろんな説があり、実際のところどうなんだろうと思ったことがきっかけで検証してみることにしました。 DISTINCTとGROUP BYどちらを使った方が良いか検証した結果をご紹介したいと思います。 DISTINCTとGROUP BYの違いについて DISTINCTとGROUP BYは重複行をまとめるという目的で使われると思いますが、それぞれどんな違いがあるのでしょうか。 DISTINCTについて 射影を行う段階で重複を排除 GROUP BYについて 指定された列名でレコードのグループ化を行う。 グループ化したレコードは、集約関数を用いることで集計することができる。 つまり、 DISTINCTは、重複を排除した結果を出力する場合に使用 GROUP BYは、重複を排除した結果に対して集計処理を行いたい場合に使用 という違いがあります。 今回例に挙げたクエリはGROUP BYを使用していましたがグループ化した後、集計処理をせずに重複行を削除するためだけに使われています。 こういった重複行を削除するためだけなら、用途にあったDISTINCTを使うべきだと思います。 次に肝心なDISTINCTとGROUP BYどちらが早いのか見ていきます。 DISTINCTとGROUP BYどちらの方が早い 下記のようなDISTINCTとGROUP BYを使用したクエリを使って、クエリキャッシュを無効にしてそれぞれの実行計画と実行速度を検証してみました。 SELECT DISTINCT c1 FROM t1 WHERE c1 > const; SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; ※c1にはインデックスが貼ってある状態 検証環境 MySQL5.6 検証結果としましては、 実行計画は全く同じで実行速度もほぼ同じ結果となりました。 MYSQL-DISTINCTの最適化 In most cases, a DISTINCT clause can be considered as a special case of GROUP BY. For example, the following two queries are equivalent: Due to this equivalence, the optimizations applicable to GROUP BY queries can be also applied to queries with a DISTINCT clause. Thus, for more details on the optimization possibilities for DISTINCT queries, see Section 8.2.1.14, “GROUP BY Optimization”. こちらのURLのMYSQLのドキュメントにも記載されている通り、オプティマイザによって最適化された結果、内部では同じ処理になり性能は同じということが分かります。 実行速度も検索結果も同じということであれば、DISTINCTとGROUP BYは使用用途によって使い分けてください。 おわりに DISTINCTとGROUP BYについての違いや、速度に関して紹介させていただきましたが参考になったら幸いです。 今回の調査ではレスポンスの改善にはなりませんでしたが、ほんの些細なことでも調べて、検証することによって新たな発見があると思います。 実際に私も検証するまでは、GROUP BYよりも重複行を削除するだけの処理のDISTINCTの方が早いと思っていました。 ぜひ気になったことがあれば検証してみてください! 明日のアドベントカレンダーはyusugiuraさんとShoTakeuchiさんです!お楽しみに!
バックエンドエンジニアの @cureseven です。レスポンス改善プロジェクトという名前で、BASEのレスポンス速度を早くするために10月より動いてきました。 経緯に触れた後、レスポンス改善をどう進めてきたかとおこなった施策を紹介します。 レスポンス改善プロジェクトの経緯 始めにプロジェクトが立ちあがった経緯を少しお話します。 BASEはリリースして9年ほど立ちました。その間、さまざまな機能が追加されたり、ありがたいことに多くのユーザーさんに使っていただいています。その結果、DBに保存しているデータ量が増えました。データ量が多いことによりSelectの実行時間が重くなるなどが起こっていました。 ユーザの方から複数件画面の重さについてのお問い合わせをいただいており、今回プロジェクトとして改善することになりました。 重いリクエストの特定 それではここから、どう進めてきたかとおこなった施策を紹介します。 BASEではNew Relic Oneを導入しており、リクエストごとのレスポンス速度の統計を見たり、Transactionの詳細を見ることができます。私たちはNew Relic APMで重いリクエストを突き止め、施策を実施しました。 New Relic APM Transactionsのページで、 Most time consuming(実行回数 × 平均実行時間) Slowest average responce time(平均実行時間) を見比べながら、平均実行時間が遅すぎるものやたくさんの人が利用していて不便に感じそうなものを中心に見ていくことにしました。 Most time consuming順にエンドポイントをsortしている 今回は購入者が触れる画面ではなく、オーナーさんの作業がサクサクできることにフォーカスして、オーナーさんの使う管理画面の重いものから見ていきました。 最も重い画面についてはある程度見通しがついていましたが、Most time consumingについては予測を立てることが難しいです。New Relic APMを見ることでどんなAPIが重いのかを把握することができました。根拠となる数字を持って優先度を決め施策を実施していくことで、効率よくオーナーさんの問題にアプローチできました。 また、フロントエンドで時間がかかっているかどうかの検証にはBrowserの機能を利用できます。これから使っていくつもりです。 dashboardによる重いリクエストの可視化 オーナーさんからのお問い合わせを受けるより先に社内で重いリクエストがあったことを知るために、Transactionのdashboardを作成し監視することにしました。 dashboardでは注文数の多いshopをいくつかピックして、オーナーさんの使う管理画面に限定したTransactionの95パーセンタイルの重さが目立つリクエストを表示しました。 alert飛ばす設定 dashboardを作成しましたが、張り付いて監視しないでいいように、alertを飛ばす設定をしました。 今回は購入者が触れる画面, オーナーさんの使う管理画面両方において、それぞれにTransaction timeの閾値をセットし、リクエストごとに95パーセンタイルが閾値を超えたときにslack通知するようにしました。 設定に役立ったリンクです。 slackのhookリンクの生成 alertの設定 alert設定のパラメータの説明 通知頻度の設定 レスポンス改善施策の実施 New Relic APMのTransaction traceという機能で、Transactionの詳細を見ることができます。どのメソッドのどのクエリが重いかまで断定できます。 私たちは重いリクエストのTransaction traceを見ることでボトルネックを把握し、リファクタ方針を考えました。リファクタ施策の具体例をいくつか紹介します。 indexを貼った 重いクエリを発見したとき、explainして実行計画がどのようなものかを確認しました。indexが適切に使われていないクエリを発見したので、indexを貼る対応を実施しました。 不要な処理を消した 機能のリニューアルや機能を廃止する中でいらなくなった処理が残ってしまうことがあります。今回、そんな処理がTransaction timeを圧迫してしまっていました。不要な処理を削除することでレスポンスが改善しました。 クエリ改善 今回よく目にしたのが、レコードの存在の有無だけ分かればいい時に、集計したり大量にデータを取ってきたりしているというものでした。レコードの存在有無確認は最初の1件だけとれば良いので、集計処理などをやめることで、クエリ実行時間を短縮できました。 また、画面描画毎に一つずつのデータを取ってきて集計するのではなく、バッチで集計したデータを格納しているテーブルを利用するようにする修正を実施しました。こちらの修正では、95パーセンタイルで15秒ほどかかっていたリクエストが、1秒以下で返ってくるようになりました。 また、注文数をカウントする処理では、画面上は99以上になると「99+」という表示になるところがあります。大量に注文があるshopのクエリがとても重くなってしまっていたため、サブクエリを使って100件以上は数えないようにしました。 バッチ実行の負荷軽減 New Relic APMのTransaction traceは、1分ごとに、基準値より重かった最大Transaction timeの詳細を残しています。 取れたTransaction traceを眺めていると、どうやら15分ごとに基準値に引っかかっていることがわかりました。詳細を見ると、15分おきに出ているTransaction traceではdb接続に決まって6秒ほどかかっていることがわかりました。 原因は15分間隔で実行されるバッチにて大量レコードのtruncateおよびinsertのSQLを実行していることでした。一旦メモリにもった上でループ処理することで急激な負荷を抑えることで、15分おきに出ていた6秒のdb接続時間は解消されました。 dashboardを見ているだけではこのような傾向を読み解くことはできませんでした。Transactionの99パーセンタイル、95パーセンタイル、50パーセンタイルを見比べたり、Transaction traceの出力時間を見たりすることで見えてくるものもありました。 プロジェクトを通して 3ヶ月間対応を行なってきた結果、画面ロードが遅いことに関してお問い合わせいただいていたshopさまから改善されたねの声をいただきました。しかしながらまだまだ遅い画面はあるため、来年以降も改善にむけて動いていこうと思っています。 最後に、レスポンス改善プロジェクトを通して感じたことを記して終わろうと思います。 機能リニューアルなどのタイミングで適切に不要な処理を消すのはもちろんですが、今回のプロジェクトのようにたまにリファクタをするための期間を設けてそのような処理がないか見直すのは大事だなと思いました。大掃除の季節ですし、ソースコードの見直しを実施するいいタイミングなのかもしれません。可読性も上がります。 機能開発のタイミングで貼ったindexが、データが増えることにより効かなくなるときがあります。今回の施策で劇的に効果が上がったのはindexの付与でした。こちらもリリース後年月が経ってから改めて見直すのが良いなと思いました。 あまりにもレスポンスが遅いと素晴らしい機能も使い物にならないので、仕様を少し変更して大幅に改善するものであれば、変更することも検討してみるのがいいなと思いました。 これからも使っていけるサービスであるために、さまざまな実装方法を検討する必要があると改めて感じました。
この記事は BASEアドベントカレンダー2021 18日目の記事です。 UIデザイナーの Yoshioka です。コードベースのデザインツールとして個人的に気になっている UXPin Merge を試してみました。 UXPin Mergeとは UXPinはベクターベースのデザインツールとは違い、HTML/CSSのコードで定義されていることをベースにしているデザインツールです。 UXPin Merge はReactやStorybookのコンポーネントを取り込み、実装されているものと同じコンポーネントを元にデザインができる機能です。 試してみる 今回は簡単にローカルでつないでみました。 ReactComponentsやStorybookを読み込むことができます。 UXPin MergeのReactComponents、Storybookでできることが違います。(詳しくは後述します) また、storybookの場合にはコンポーネントの配置に若干のラグが発生します。 立ち上げる 該当のReactComponentsリポジトリから、ローカルでExperimentalModeを立ち上げます。 これにより、URLが生成されるのでコード変更してどうUXPinで反映されるのか確認することができます。 配置していく コンポーネントがこのようにリスト表示されるので、これをドラッグ&ドロップして配置していきます。 簡単なデザインですが配置してみました。 できること 各コンポーネントをクリックするとコンポーネント定義されているpropsが表示されます。 今回はReactComponents連携したので、コンポーネントをネストすることができます。 (storybook連携ではできない様です) ネストすることのメリットは、下記の様にレイアウトコンポーネントを作成し、その中にコンポーネントを入れ込むことができます。 ネスト内のコンポーネントを削除することもできます。 今回はStackというレイアウトコンポーネントの中に、SelectとButtonを内包しています。 StackのpropsでDirectionを持っているので、レイアウトを切り替えることができます。 レイアウトコンポーネントのMarginなど覚えておくのも大変なので便利です。 できないこと Figmaなどのデザインツールと違ってコンポーネントのDetachはできません。 もちろんテキストや図形など配置できますが、コンポーネント内に配置するなどはできません。 このあたりはプロダクトやデザイナーの好みによりそうですが、自由にデザインデータを作れないことにあまりネガティブな印象はありませんでした。 また、Figmaの様にテキストをアートボード上で編集することはできず、props上で入力する必要があります。 プロトタイプについて まだ上記の様にコンポーネント配置しただけですが、この状態でどのようなプロトタイプが作成されるのかみてみます。 Interactionにはなにも設定していませんが、このようにプロトタイプが作成されます。 Figmaでも似た表現を行うことはできますが、実装が元になっていることでコンポーネントライブラリと差分なくプロトタイプ確認できるのはメリットかなと思います。 デザインツールで起きがちな問題 ここでたまに聞くデザインツールで起きがちな問題を洗ってみます。 このコンポーネントない、などのデザインデータと実装のギャップなどどう解決してよいかわからない沼が出てきます。 コードベースだと解決できるかもしれない 簡単に触った程度ですが、ここまででコードベースのデザインデータにした時のメリットは デザイナー・エンジニアがコンポーネントに対し同じ基準でデザインできることにあります。 これまでの企画・デザイン・コーディングといったフローから、デザイナーとエンジニアが共同でプロトタイプを作ることができることで、既に共通の認識があるのでコミュニケーションコストを抑え開発スピードをあげることができそうです。 現状ではデザイナーがコードへの理解を示すこと、開発環境作りやGitの連携にかなりエンジニアに工数を割いてもらう必要もありますが、検討してみる価値はあります。 来年以降もコードベースのデザインツールを注目していきたいとおもいます!