TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

587

はじめまして!フロントエンドエンジニアのがっちゃん( @gatchan0807 )です。 9月7日にBASE主催で「 BASE Engineer 座談会 〜BASEの若手エンジニアがそれぞれの今と未来を語る!〜 」というイベントを実施したのですが、その中のLTパートで発表した「 Changelogを読んで自分のエンジニアキャリアを作る 」でお話しきれなかった部分があったので、そこも含めてお話した内容を記事にまとめなおしました! 当日ご参加いただいてLTを聞かれた方も、そうでない方も、ぜひご覧ください! 「Changelog」と「エンジニアキャリア」がどう繋がるのか 早速この記事でお伝えしたいことから書いてしまうのですが、 Changelogは最新機能だけでなく、未来のビジョンも含めたプロダクトの情報の宝庫で、それらをうまく使えばエンジニアキャリアづくりに活かせる! ということを記事を読んでいただいた皆さまには持って帰っていただければなと思っております。 ここからは、私がどういう経緯でChangelogを読むようになったか、Changelogを読むことでどんなメリットがあるのかについて、具体的に紹介していきます。 なぜChangelogを読み始めたのか ことの始まりはdependabotくんの対応から BASEにはショップオーナー向け管理画面で利用する社内用コンポーネントライブラリ「BBQ」があります。 今年2月頭頃、そのリポジトリに dependabot という、依存しているライブラリのバージョンアップ情報を調べ、アップデートするPull Requestを自動的に作成してくれるシステムを導入したところから話は始まります。 私は今年1月に入社したのですが、オンボーディング研修やたくさんのオリエンテーション、BASE内のコードリーディングを行っていた1ヶ月がちょうど過ぎた頃に、上記のような話題が上がり、実際に導入されることになりました。 ちょうどBBQのコードを読むきっかけにもなりそうと思って、空いた時間で参加させてもらうことになりました。 実際の dependabot の対応自体は難しいものではなく、自動的にOpenされるPull Requestを確認し、必要に応じて動作確認を行うだけです。 dependabot が設定した頻度で package.json / yarn.lock (Node.js関係のライブラリの依存関係情報が載っているファイル)をチェックする アップデートが公開されているライブラリがあった場合に、そのライブラリのバージョンを変更した( $ npm upgrade が実行された)Pull Requestが自動的にOpenされる 「マージ待ってるよ~」状態になるので、それをチェックして問題なければマージする 実際にOpenされるPull Requestの例 上記対応の3つ目の「マージして問題ないかのチェック」の中で、BBQのコードリーディングついでに 該当ライブラリの利用箇所の確認 と 該当ライブラリのアップデート内容が書かれたChangelog確認 を行っていたのですが、BBQのコードに詳しくなれるのはもちろんのこと、副次的に BBQ以外にも知見を得れていること に気が付きました。 dependabot で見つかるもの以外にも世には色んなChangelogがある 少し話は変わって、それまではプロダクトのアップデート情報やChangelogを見ても、流し見するだけで真剣には読んでいなかったのですが、 dependabot の対応でライブラリのChangelogを読むようになってから自然と世の中のプロダクトのアップデート情報が気になるようになりました。 いくつか読んだ中で印象的だったものとしては下記2つで、これらは社内ドキュメントツールにざっくり翻訳してまとめた記事を上げてみたりもしました。 https://hasura.io/blog/announcing-hasura-graphql-engine-2-0/ DBにかぶせたらGraphQLのエンドポイント生やせる君のHasuraに大型アップデートが入るアナウンス記事 複数DB接続できたり、使えるデータソースがPostgreSQL以外も対応するようになった https://developer.chrome.com/blog/new-in-devtools-90/ Chrome 90でのバージョンアップ内容がまとまった記事 flexboxがDevTools内でデバッグしやすくなったり、CWVが見れる機能がついた やってみてわかったChangelogを読むメリット LTの中では下記3つでまとめていましたが、時間の都合上1つ目の「変更点から、プロダクト・ライブラリがどう変化しようとしているのか気づくきっかけができる」についてしか触れることができませんでした。 しかし、個人的には2つ目、3つ目のメリットも非常に大きいと感じているため、こちらの記事でご紹介できればなと思います。 Changelogをフックにプロダクトを深ぼる リリースされているフレームワークやライブラリ、アプリケーションなどはすでに一定プロダクトとして成立する形で出来上がってる構造になるので、どこから見ていいのかわからなくなることも多いです。 しかしながら、Changelogには多くの場合、アップデートされた内容とともにアップデートされた理由と関連するユースケースの情報(こういう使い方を想定して機能を追加しましたみたいなこと)が書かれているので、アップデート対象そのものの知識をつけるのに効率が良いというメリットがあります。 これが こうなる これは2つ目の「アップデートからその対象の全体的な構造・関連性について知るきっかけができる」というメリットに大きく関わる部分で、Changelogからアプリケーションやライブラリの構造を深ぼっていく糸口になりますし、どういった機能が関連しあっているのかに気づくきっかけにもなり得ます。 また同時に、Changelogに書かれている想定ユースケースから「そんな機能があったのか!」と気づく3つ目のメリットにも繋がります。 実際に、ChromeのChangelogを読んだ際にDevToolsの知らなかった機能をいくつも発見して驚いた記憶があります。(意外と深い階層に便利機能があって気づいてないことが多々ある) 技術力向上にはアウトプットが重要と言うけども また、Changelogを読むだけでなく、(Changelogが英語記事の場合に)ざっくり翻訳して記事にまとめるという活動は自然とアクティブラーニングになって良い勉強になることを実感したので、合わせて紹介しておきます。 私は、学生時代に「知識の定着のためにはインプットだけではなく、アウトプットまで行うことが大事」と聞き、技術力の向上のために下記のような形で学習を行ってきていました。 とにかく勉強会に参加して知らない単語を知る(とりあえず情報に体当たりして、出てくるキーワードのインデックスだけ貼る) ↓ そこで知った単語をあとでググって調べてみたり、ちょこっとデモ環境を作って肌感覚を得る ↓ ☆(機会があれば)実践投入して更に自分の血肉にしたり、一定ラインを超えてきた辺りで手を動かして得たしくじりや知識を登壇・発表側に回る このやり方は☆の発表まで持っていくところが肝要で、 登壇・発表側に回ることによって 、自分の知識が整理されるし、理解があやふやなところを再度調べ直して知識の深さが高まったりするので 得られるものが沢山ありました 。 しかしながら、 常に身の回りに登壇・発表する場があるわけでもない のと、 発表する場の期待値のコントロールであったり表現方法を考えたりしないといけなかったりで意外と1回のカロリーが高い という問題をずっと感じていました。 アウトプットの方法としては、登壇・発表まで行かずとも経験した内容をブログ記事にする学習方法なども選択肢にありますが、上述したとおり Changelog記事自体が構造的にとっつきやすいようになっているため、初級者を脱してさらに技術力に磨きをかけたい!という人にとって効率よく知識を吸収できる という点で効率的だと感じたので、個人的におすすめできるなと思いました! 最後に… ここまでお読みいただきありがとうございました! 私がChangelogを読むようになった経緯と、実際にやってみて感じたメリットをつらつらと書かせていただきました。 読んでいただいた中で、もし少しでもここは良さそうだなと思うポイントがありましたら、ぜひ一度実践してみていただけると嬉しいです! これからも価値あるプロダクトを作り続けられるようにするためにも、自分の技術力アップのためにも、色々なChangelogを読んで行ければなと思います! それに、みんなでChangelogを読んでみよう会みたいなのを開催できたらなぁ…とか妄想したりもしています…! 最後に、イベントでお話したときの資料( https://speakerdeck.com/gatchan0807/changelogwodu-ntezi-fen-falseensiniakiyariawozuo-ru )と当日のアーカイブ動画( https://www.youtube.com/watch?v=mslmwMd2_rM )のURLを記載しておきます! イベントでは座談会という形で社内の雰囲気なども感じていただけるかと思いますので、よければご覧ください! 参照 世界のJavaScriptを読もう @ 2014 What's New In DevTools (Chrome 90) Announcing Hasura GraphQL Engine 2.0!
BASE BANKでエンジニアをしている @budougumi0617 です。 先日行われたNew Relic User Group Vol.0でGoでNew Relic APMを活用するためのOSSを紹介するLT発表をさせていただきました。 New Relic User Group Vol.0 New Relic User Group(NRUG)はNew Relicを活用するユーザーの集いです。NRUGは ヌルグ と読むとのことです。 コロナ禍後初開催となるVol.0は2021年9月15日にオンライン形式で次のコンテンツが行われました。 New Relic One 最新機能紹介 Nerd Life Talk (LT) ネットワーキング 関連ハッシュタグは #NRUG です。 https://twitter.com/hashtag/NRUG BASE BANKを含めBASEグループでも2020年9月よりNew Relic Oneを利用した開発・運用体制を敷いており、今回LT発表の機会をいただくことができました。 発表資料 私の当日の発表資料は次のスライドになります。 私の発表では、BASE BANKのGoアプリケーションでNew Relic Oneを活用するために私が開発し、OSSとして公開している3つのツールについて紹介しました。 当日紹介したOSS スライド中で紹介したOSSは次の3点です。 https://github.com/budougumi0617/nrseg https://github.com/marketplace/actions/action-newrelic-segment-lint https://github.com/budougumi0617/nrzap github.com/budougumi0617/nrseg nrseg コマンドはGoアプリケーションの関数・メソッドにNew Relic APMでSpanを生成するためのNew Relicライブラリの呼び出しを自動挿入するツールです。 GoアプリケーションへNew Relic APMを導入する際、Goでは実行時間を計測したい関数・メソッド全てで次のようなライブラリの呼び出しをする必要があります。 defer txn.StartSegment( "mySegmentName" ).End() nrseg コマンドは指定ディレクトリ配下の全てのGoのコードに含まれる関数やメソッド 1 にスパンを生成するために必要なコードを自動挿入します。 import ( "context" "fmt" "net/http" + + "github.com/newrelic/go-agent/v3/newrelic" ) func (f *FooBar) SampleMethod(ctx context.Context) { + defer newrelic.FromContext(ctx).StartSegment("foo_bar_sample_method").End() fmt.Println("end function") } func SampleFunc(ctx context.Context) { + defer newrelic.FromContext(ctx).StartSegment("sample_func").End() fmt.Println("end function") } func SampleHandler(w http.ResponseWriter, req *http.Request) { + defer newrelic.FromContext(req.Context()).StartSegment("sample_handler").End() fmt.Fprintf(w, "Hello, %q", req.URL.Path) } // nrseg:ignore ignoreコメントをしておけば無視します。 func IgnoreHandler(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Hello, %q", req.URL.Path) } 既存アプリケーションの関数やメソッド全てに上記のような呼び出しを手動で挿入するのは手間なのでこちらを利用しております。 内部実装的にはASTから関数名を取得したり関数の位置を読み取って先頭行にコードを挿入したりと楽しい静的解析になっております。 action-newrelic-segment-lint actions action-newrelic-segment-lint はNew RelicのSpan対応漏れがある関数・メソッドがPull Request(PR)に含まれていたときに警告コメントをするGitHub Actionsです。 既存コードに対しては nrseg コマンドでNew Relicの対応ができます。 しかし、我々のGoアプリケーションは機能追加が活発に行われており、毎日関数やメソッドが追加されています。 新しく追加された関数やメソッドがNew Relicの対応をしていなかった時警告コメントをしてくれます。 PR上でNew Relic対応漏れの関数にコメント github.com/budougumi0617/nrzap nrzap はNew Relicの Trace ID や Span ID を uber-go/zap のログに埋め込むためのヘルパーです。 New Relicには分散トレースとログを紐付けるLogs in contextという機能があります。 しかし、New Relicの公式Goライブラリは logrus というロガーパッケージしか現在はLogs in contextに対応していません。 弊社のGoアプリケーションで利用しているロガーパッケージは uber-go/zap です。 Logs in contextに必要なNew Relicの Trace ID や Span ID を uber-go/zap のログに埋め込むためのヘルパーが nrzap です。 GetNrMetadataFields 関数を使うと context.Context オブジェクトにNew Relic関連情報が含まれていれば uber-go/zap のログに適切なJSONキーでNew Relicの Trace ID などの情報を含めることができます。 func ExampleHandler(w http.ResponseWriter, r *http.Request) { logger, _ := zap.NewProduction() defer logger.Sync() // nrgorillaなどでcontextから*newrelic.Transactionが取れる前提 + nrfs := nrzap.GetNrMetadataFields(r.Context()) logger.Info("failed to fetch URL", nrfs...) } 会に参加して 会ではNew Relicのコンサルタントの皆様よりPixieなどのNew Relic Oneの最新機能を紹介していただきました。 また、他社の皆様には開発組織へのNew Relic導入活動のコツを教えていただいたり、本番ダッシュボードを実際に拝見させていただけました。 懇親会では紹介したOSSを実際に利用してくださっている方と話しかけていただき、とてもモチベーションが高まりました。 どのOSSもまだまだバギーなところがあるので時間を見つけて改善を重ねておこうと思います。 終わりに NRUGの次回開催は12月を予定されているようです。今回とても有意義な時間だったのでぜひまた参加する予定です。 New Relicの最新機能、現場の生の声を聞ける貴重な会なので現在活用中の方、導入を検討されている方とも12月にお会いできればと思います。 正確には引数に context.Context オブジェクトまたは *http.Request オブジェクトを持つ関数/メソッドです。 ↩
BASE株式会社 Owners Experience Frontend チームのパンダ( @Panda_Program )です。 BASE では社内用コンポーネントライブラリ「BBQ」を使ってフロントエンドの開発をしています。 BBQ は Vue2 + Storybook v5 で作成されていましたが、 TypeScript Compiler API と社内のフロントエンドエンジニアの有志たちのおかげで Storybook のバージョンを最新の v6.3 にする対応が完了しました。 以前執筆した 「Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える」 という記事で、Storybook v5 から v6 の書き方である Component Story Format(CSF) への変更手順を確認しました。 この記事では、TypeScript Compiler API を使って前回の記事で紹介した書き換えをスクリプトで自動実行した話を紹介します。 前置き 率直な意見を書くと、TypeScript Compiler API(以下、Compiler API)はとっつきにくいです。なぜなら、初めて出会う単語や概念がたくさんあるからです。さらに、それをコードで操作するので慣れるまでは書きにくいし読みにくいです。 記事の途中で出てくる単語の意味が分からなくても、途中でコードの input と output の例を多めに掲載するので文章は読み飛ばしてもらっても大丈夫です。 TypeScript Compiler APIとは何か TypeScript Compiler API(以下、Compiler API) とは、TypeScript の Compiler を操作するための API です。 つまり、TS/JS のコードを解析したり、TS を JS に変換したり、コードの文法エラーを検出するといったコンパイラの機能を利用するための API です。 Compiler API の機能は多岐に渡りますが、今回利用したのは大きく分けて AST(Abstract Syntax Tree。抽象構文木) の走査(AST という木構造のデータを深さ優先探索で順番に見て回ること)、ファクトリ関数によるコード生成、visitor によるコードの部分的書き換えの3点です。 Compiler API で実現できること 百聞は一見に如かずです。CSF への書き換えに用いたスクリプト(以下、「書き換えスクリプト」または「置換スクリプト」)を使って、 「Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える」 で紹介した storiesOf で記述しているコンポーネントを置換してみましょう。 こちらが変換元のコンポーネントです。 // bbq/stories/v5/elements/button-group.stories.js import { action } from '@storybook/addon-actions' import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import README from '../../elements/buttonGroup/README.md' import { devices } from '../../values/Devices' // Storybook コンポーネント名 const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories // addon-knob .addDecorator(withKnobs) // addon-info .addDecorator(withInfo) .add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // Vue Template template: ` <div : class = "'theme-'+device" > <p> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </p> <p> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </p> </div> `, // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] } , } , // methods methods: { change: action( 'change' ), } , } } , // Parameters { notes: README, } ) 置換スクリプトを実行することで、このコンポーネントが以下のように書き変わります。 // bbq/stories/v6/elements/button-group.stories.js import { action } from '@storybook/addon-actions' import { number, select, withKnobs } from '@storybook/addon-knobs' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import { devices } from '../../values/Devices' import README from './README.md' export default { title: "V6/Elements/ButtonGroup" , component: ButtonGroup, parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , argTypes: { device: { options: devices, control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } } } ; const Template = (args, { argTypes } ) => ( { components: { ButtonGroup } , props: Object .keys(argTypes), template: ` <div : class = "'theme-'+device" > <p> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </p> <p> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </p> </div> `, methods: { change: action( "change" ), } } ); export const Default = Template.bind( {} ) Default.args = { device: select(`device`, devices, "pc" ), selected: 0, width: select( "width" , [ "" , "full" ] ), selected: 0, tag: "ul" } ; プロパティの重複や改行の不足もありますが、あえてありのまま記述しています。 Compiler API を使ったスクリプトを実行することでコードを劇的に書き換えられます。 これでCompiler API の威力を実感していただけたかと思います。 Storybook の書き換えに Compiler API を使った動機 さて、書き換えスクリプトの詳細に入る前に Compiler API で書き換え対応した理由を説明します。 想定読者はエンジニアなので早めに技術の話に入りたいのですが、Compiler API での書き換え実行に至った背景は普遍的なはずなので共有する価値があると考えています。 以下、Storybook の書き換えに Compiler API を使った理由を箇条書きで記述します。大きな目的は「Storybook の v5 から v6 へのバージョンアップを楽に実行すること」ですが、Compiler API を採用したのは以下のような背景があります。 書き換え作業にまとまった時間と人が確保できないこと( 人海戦術ができない ) BBQ というコンポーネントライブラリの保守・メンテは数人の有志者(以下、メンテナー)がスキマ時間に行なっている 体制として、例えば20%ルールなどの負債解消の時間を設けていない Storybook のバージョンアップは喫緊の課題でもないのでプロジェクト化されない ファイル数が多いこと BBQ の Storybook のコンポーネントは60ファイル BBQ の他にも Storybook を利用しているレポジトリがあるため、置換スクリプトを書くと再利用できる 簡潔に書くと、手作業による書き換え作業コストはファイル数に応じて比例しますが、スクリプトを適用できるのであればコストのほとんどはスクリプト作成だけで済むため、トータルコストが抑えられると判断して Compiler API での書き換えを採用しました。 またメンテナーに書き換えタスクを割り振って手作業で対応する場合は、新しい書き方を決定したり、書き換え方がわからないケースの調査結果の共有やコードレビューといったコミュニケーションコストが発生します。さらにそれらは往々にして同期的なものです。 このようなコミュニケーションコストを最小限にするために、書き換え方や気をつけるポイントを自分でドキュメント化して他の方に共有しました。これによりコミュニケーションを非同期化することができ、各自のタスクに集中できます。 書き換えの作業コストのイメージを可視化すると以下のようになります。 一人がスクリプトを書くほうが全体的にコストが少ない 書き換えスクリプトを使うと、最も簡単なケースでは、あるファイルを置換した後 Storybook で動作確認を完了するまで1分もかかりませんでした。 置換スクリプトで除外したケース ただし、スクリプトで細かいケースまで全部対応するのは実装コストが高くなるだけです。先程紹介した重複プロパティを削除する、などちょっとしたところであればあえて自動化しないという局所的な判断もしています。 また、1つのファイル内で forEach を記述して複数の storiesOf を実行しているようなパターンもスクリプトでの対象外としています。 [ 'blue' , 'red' , 'green' ] .forEach((color) => { const component = storiesOf(...) } ) このケースは、コードを書くより人の手で書き換えた方が早いと判断してメンテナーに割り振って対応してもらいました。 結果、60ファイル中置換で対応できたのが40ファイル、人の手で書き換えたのが15件ほどでした (15件のうち半分は TSX で書かれており、また残り5件は元から CSF でした)。 書き換えのスケジュール スケジュールは以下のように進みました。 1,2週間目: スキマ時間でCompiler API、Storybook v6 対応調査、書き換えスクリプト作成と調整 3週間目: スクリプトを実行して一括置換し、自分を含むメンテナに微調整タスクを割り振り 4週間目: PRレビュー依頼、コメント対応 5週目: 動作確認、Storybook v6 化リリース 最初の2週間は自分一人で動き、3週間目からはメンテナーの有志たちの力を借りました。量としては一人でも対応できるものだったのですが、v6 化にあたって知識の俗人化を避けるためにレビューも含めみんなで対応しました。 以上、置換スクリプトで「何ができるか」「なぜ Compiler API を使ったのか」「どのような背景があったのか」はお伝えできたかと思います。 Compiler API を使ったファイル書き換えの流れ 早速置換スクリプトの中身に入っていきたいところですが、 まずは Compiler API を使った処理の大まかな流れをお伝えします。 Compiler API でファイルを書き換える処理自体は簡単です。大まかな流れは以下のようなものです。 ファイルをメモリに保持する AST を解析する 特定の箇所を書き換える 書き換えた値をメモリからファイルに出力する これをコードで表現するためにミニプログラムを作成します。 ここでは説明のために「読み込んだファイルの import 文 だけ抜き出す」というシンプルな処理だけをしています。 import * as ts from "typescript" import * as fs from 'fs' // 1. ファイルを入力してメモリに保持する const code = fs.readFileSync ( './input.js' , 'utf8' ) const outputFilename = './output.js' const sourceFile = ts.createSourceFile ( outputFilename , code , ts.ScriptTarget.Latest ) const imports: string [] = [] // 2. AST を解析する function printRecursive ( node: ts. Node , sourceFile: ts.SourceFile ) { const text = node.getText ( sourceFile ) // node の解析結果を出力する(後述) const syntaxKind = ts.SyntaxKind [ node.kind ] const textWithSyntaxKind = ` ${ syntaxKind } : ${ text } ` console .log ( textWithSyntaxKind ) // 3. 特定の箇所を書き換える if ( ts.isImportDeclaration ( node )) { imports.push ( text ) } node.forEachChild ( child => { printRecursive ( child , sourceFile ) } ) } printRecursive ( sourceFile , sourceFile ) // 4. 書き換えた値をメモリからファイルに出力する fs.writeFileSync ( outputFilename , imports.join ( '\n' )) このコード例では文字列を抜き出しているため、Compiler API をご存知の方は少し変だと感じるかもしれません。なぜなら、eslint の plugin などでは「ASTを解析し、特定のパターンのASTを書き換えた後、printer.printNode で文字列に変換する」処理が一般的だからです。 ただ、storiesOf の書き方と CSF では構造が大いに異なるため、置換スクリプトではコードの場所に応じて使い分けています。 例えば、import 文では上記のように配列と文字列で扱い、 export default { title: 'SomeComponent' } のようなオブジェクトの箇所では AST を操作する形にしています。 Compiler API を使ったことがない方が大半だと思うので、今回は2つのうち簡単な方をコード例に挙げました。 ミニプログラムを実行する さて、このミニプログラムを実行してみましょう。入力と実行結果は以下のようになります。 入力 // input.js import * as path from "path" import * as fs from 'fs' const exists = fs.existsSync(path.resolve() + '/input.js' ) console.log(exists); 実行結果 // output.js import * as path from "path" import * as fs from 'fs' また、プログラムの途中に仕込んだ console.log(textWithSyntaxKind) の出力は以下のようになります。 このままでは読みにくいと思うので、今回抜き出しの対象とした import 文があるところにコメントを追加します。 # より右は出力結果はではなく追加したコメントです。 SourceFile: import * as path from "path" # ソースファイル全体 import * as fs from 'fs' const exists = fs.existsSync(path.resolve() + '/input.js') console.log(exists); ImportDeclaration: import * as path from "path" # import 文 ImportClause: * as path NamespaceImport: * as path Identifier: path StringLiteral: "path" ImportDeclaration: import * as fs from 'fs' # import 文 ImportClause: * as fs NamespaceImport: * as fs Identifier: fs StringLiteral: 'fs' FirstStatement: const exists = fs.existsSync(path.resolve() + '/input.js') VariableDeclarationList: const exists = fs.existsSync(path.resolve() + '/input.js') VariableDeclaration: exists = fs.existsSync(path.resolve() + '/input.js') Identifier: exists CallExpression: fs.existsSync(path.resolve() + '/input.js') PropertyAccessExpression: fs.existsSync Identifier: fs Identifier: existsSync BinaryExpression: path.resolve() + '/input.js' CallExpression: path.resolve() PropertyAccessExpression: path.resolve Identifier: path Identifier: resolve PlusToken: + StringLiteral: '/input.js' ExpressionStatement: console.log(exists); CallExpression: console.log(exists) PropertyAccessExpression: console.log Identifier: console Identifier: log Identifier: exists EndOfFileToken: 深さ優先探索で AST の node を一つずつ辿っている様子が分かります。 例えば、 console.log(exists) という箇所は以下のように解析されています。 CallExpression: console.log(exists) PropertyAccessExpression: console.log Identifier: console Identifier: log Identifier: exists 深さ優先探索でツリーの探索順の説明 また、この出力結果を見ると、なぜ ts.isImportDeclaration(node) というメソッドで import 文を判定できるかわかると思います。 それは、 import * as path from "path" が ImportDeclaration であると判定されるからですね。 これで大まかな動きはイメージしてもらえるかなと思います。 Compiler API を使った置換スクリプトの中身を紹介します では、実際に置換スクリプトの中身を紹介しようと思います。 ただし、書き捨てのコードであるため書きやすさ重視でコード量が多く、極力整理はしたもののあまり洗練されていないコードになっています。 そこで、置換スクリプトと Compiler API のエッセンスが伝わるような箇所を2つ抜き出して紹介しようと思います。 import 文の書き換え 1つ目は、元ファイルから import 関連の文字列を抜き出して少し書き換える処理です。 まずは変換前と変換後を紹介します。 // 変換前 import { action } from '@storybook/addon-actions' import { number , select , text , withKnobs } from '@storybook/addon-knobs' import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import README from '../../elements/buttonGroup/README.md' import { devices } from '../../values/Devices' // 変換後 import { action } from '@storybook/addon-actions' import { number , select , withKnobs } from '@storybook/addon-knobs' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import { devices } from '../../values/Devices' import README from './README.md' 変換ポイントは以下の通りです。 @storybook/addon-knobs から import している text を削除 storiesOf 、 withInfo の import を削除 README の import パスを書き換える これを実現するためのコードは以下の通りです。説明のために処理を先程のミニプログラムに組み込みます。 import * as ts from "typescript" import * as fs from 'fs' const code = fs.readFileSync ( './input.js' , 'utf8' ) const outputFilename = './output.js' const sourceFile = ts.createSourceFile ( outputFilename , code , ts.ScriptTarget.Latest ) const imports: string [] = [] let hasReadme = false function printRecursive ( node: ts. Node , sourceFile: ts.SourceFile ) { const text = node.getText ( sourceFile ) if ( ts.isImportDeclaration ( node )) { imports.push ( text ) if ( text.includes ( 'README' )) { hasReadme = true } } node.forEachChild ( child => { printRecursive ( child , sourceFile ) } ) } printRecursive ( sourceFile , sourceFile ) // import 文を書き換える function getFilteredImport ( imports: string [] , hasReadme: boolean ) : string { // filter で不要な import を削除し、 // concat で必要な import を追加する let filtered: string [] = imports .filter ( item => ! item.includes ( 'storiesOf' )) .filter ( item => ! item.includes ( 'storybook-addon-vue-info' )) // README が存在したらパスを書き換える if ( hasReadme ) { filtered = filtered .filter ( item => ! item.includes ( 'README' )) .concat ( `import README from './README.md'` ) } return filtered.join ( "\n" ) .replace ( 'withKnobs,' , '' ) .replace ( "import { withKnobs } from '@storybook/addon-knobs'\n" , '' ) // knob は `preview.js`で読み込むので不要 .replace ( 'text,' , '' ) // knob の text を削除 } const filteredImport = getFilteredImport ( imports , hasReadme ) // ファイルとして出力する const newFile = ` ${ filteredImport } ` fs.writeFileSync ( outputFilename , newFile ) 変数 imports は string の配列なので、 concat と filter を使って追加と削除をしています。 この処理は、元ファイルの import 文をほぼ流用できるのでこのような愚直な実装にしています。次は、元ファイルの情報を使い回せない場合の処理方法を紹介します。 export default の追加 次に、AST を生成する方法を紹介します。CSF ではコンポーネントのメタ情報をオブジェクトとして default export します。これは storiesOf の書き方とは全く似ていません。 // storiesOf const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) // v6 の書き方 export default { title: "V6/Elements/ButtonGroup" , component: ButtonGroup , } 何か流用できるものはないかと探しながら前後を見比べると、ストーリー名だけは利用できることがわかります。 変更前のファイルからストーリー名を抜き出してV6という接頭辞をつけてストーリー名を差別化し、コンポーネント名はストーリー名から抜き出せれば OK です。 これをコードで実現していきます。前出のコードに追記していきます。 import * as ts from "typescript" import * as fs from 'fs' const code = fs.readFileSync ( './input.js' , 'utf8' ) const outputFilename = './output.js' const sourceFile = ts.createSourceFile ( outputFilename , code , ts.ScriptTarget.Latest ) const imports: string [] = [] let hasReadme = false // 以下を追加 let prevText = "" let title = '' let component = '' // SB コンポーネント名 const isPrevStoriesOf = ( text: string ) => text === 'Identifier: storiesOf' const includesStorybook = ( text: string ) => text.includes ( 'storybook' ) function printRecursive ( node: ts. Node , sourceFile: ts.SourceFile ) { const text = node.getText ( sourceFile ) const syntaxKind = ts.SyntaxKind [ node.kind ] const textWithSyntaxKind = ` ${ syntaxKind } : ${ text } ` // import 文 if ( ts.isImportDeclaration ( node )) { ... } // Storybook コンポーネント名を抜き出す(後述) if ( isPrevStoriesOf ( prevText ) && ! includesStorybook ( text )) { // クォーテーションを削除 title = text.substring ( 1 , text.length - 1 ) } // 一つ前の node を文字列でメモする prevText = textWithSyntaxKind node.forEachChild ( child => { printRecursive ( child , sourceFile ) } ) } printRecursive ( sourceFile , sourceFile ) // import 文を書き換える function getFilteredImport ( imports: string [] , hasReadme: boolean ) : string { ... } const filteredImport = getFilteredImport ( imports , hasReadme ) // コンポーネント名を抜き出す component = title.split ( '/' ) .pop () || '' if ( ! component ) { throw new Error ( 'Component 名を取得できません。' ) } // 以下の2つの関数を組み合わせて export default の AST を作成する function getExportAssignmentProperties ( title: string , component: string ) { const properties: ts.ObjectLiteralElementLike [] = [ ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "title" ), ts.factory.createStringLiteral ( title ) ) ] properties.push ( ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "component" ), ts.factory.createIdentifier ( component ) ) ) return properties } function createExportAssignmentAst ( node: ts.ObjectLiteralElementLike [] ) { return ts.factory.createExportAssignment ( undefined , undefined , undefined , ts.factory.createObjectLiteralExpression ( node , true ) ) } // 元の Storybook コンポーネント名と区別するために V6 でディレクトリを分ける const storybookComponentTitle = `V6/ ${ title } ` const properties = getExportAssignmentProperties ( storybookComponentTitle , component ) const exportAssignmentAst = createExportAssignmentAst ( properties ) // printer const printer = ts.createPrinter () const printNode = ( node: ts. Node ) => printer.printNode ( ts.EmitHint.Unspecified , node , ts.createSourceFile ( '' , '' , ts.ScriptTarget.Latest ), ) const exportAssignment = printNode ( exportAssignmentAst ) // ファイルとして出力する const newFile = ` ${ filteredImport } ${ exportAssignment.normalize( 'NFC' ) } ` fs.writeFileSync ( outputFilename , newFile ) import 文の処理より複雑なので、ポイントを解説していきます。 直前に出てきた要素を prevText にメモしておく コンポーネント名だけを抜き出すために、 printRecursive という再帰関数の中で prevText という変数に直前の node の名前を保存するようにしました。 以下のコードを解析するとその下のような結果になるため、直前の node の情報が必要なのです(関係のある箇所だけ抜き出しています)。 const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) CallExpression: storiesOf('Elements/ButtonGroup', module) Identifier: storiesOf StringLiteral: 'Elements/ButtonGroup' もしストレートに実装するなら「 storiesOf の第一引数を抜き出す」という方法で良いのですが、少し試してもうまく取得できなかったので直前の要素を参照する形にしています。 今回抜き出したいのはコンポーネント名なので StringLiteral: 'Elements/ButtonGroup' だけ使えれば良いです。 ただし、StringLiteral はただの文字列であるため import 文の from '@storybook/vue' のように他の場所でも出てくるので、 isStringLiteral だけでは判定条件が不足してしまいます。 このため、 storiesOf 関数の第一引数は必ずストーリー名の string であり、AST の node を辿る順番は深さ優先探索であることを利用しようと考え、試行錯誤した結果以下のような記述方法になりました。 const isPrevStoriesOf = ( text: string ) => text === 'Identifier: storiesOf' const includesStorybook = ( text: string ) => text.includes ( 'storybook' ) if ( isPrevStoriesOf ( prevText ) && ! includesStorybook ( text )) { // クォーテーションを削除 title = text.substring ( 1 , text.length - 1 ) } isPrevStoriesOf は関数名の通り、直前が storiesOf であるか判定します。 includesStorybook はその node が以下のような import 文ではないことを判定しています。 import { storiesOf } from '@storybook/vue' 以上が、 storiesOf('Elements/ButtonGroup', module) からコンポーネント名を抜き出す方法です。 factory.createExportAssignment でオブジェクトを default export する AST を作成する 次は default export するオブジェクトの作成です。 文字列操作でオブジェクトを作成するのはとても複雑です。このため、Compiler API の factory 関数を使いますが、factory 関数はたくさんあるため使い方を一つずつ知るのは大変です。 ここで裏技を使います。作成したいオブジェクトは以下のようなものだとわかっていましたね。 export default { title: "V6/Elements/ButtonGroup" , component: ButtonGroup , } このコードから factory 関数を生成する手段があるのです。 TypeScript AST Viewer を使えば関数の自動生成を実現できます。 サイトにアクセスし、左上の入力欄に作成したいコードを書き込めば左下に factory 関数が生成されます。 TypeScript AST Viewer の画面 今回は以下のようなコードが出力されています。 [ factory.createExportAssignment ( undefined , undefined , undefined , factory.createObjectLiteralExpression ( [ factory.createPropertyAssignment ( factory.createIdentifier ( "title" ), factory.createStringLiteral ( "V6/Elements/ButtonGroup" ) ), factory.createPropertyAssignment ( factory.createIdentifier ( "component" ), factory.createIdentifier ( "ButtonGroup" ) ) ] , true ) ) ] ; このコードを一度見てしまえば、変更すべき箇所は明白ですね。 プロパティはそのままでいいので、ストーリー名 V6/Elements/ButtonGroup とコンポーネント名 ButtonGroup を変数化すればいいと分かります。 先述のコードで読みにくい関数が出てきたと思いますが、これはストーリー名とコンポーネント名を変数にする関数を作り、factory 関数をラップしているだけです。 function getExportAssignmentProperties ( title: string , component: string ) { const properties: ts.ObjectLiteralElementLike [] = [ ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "title" ), ts.factory.createStringLiteral ( title ) ) ] properties.push ( ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "component" ), ts.factory.createIdentifier ( component ) ) ) return properties } function createExportAssignmentAst ( node: ts.ObjectLiteralElementLike [] ) { return ts.factory.createExportAssignment ( undefined , undefined , undefined , ts.factory.createObjectLiteralExpression ( node , true ) ) } createExportAssignmentAst の返り値である AST を printer で string に変換すれば、オブジェクトの default export が出力できます。 // printer。AST を string に変換する const printer = ts.createPrinter () const printNode = ( node: ts. Node ) => printer.printNode ( ts.EmitHint.Unspecified , node , ts.createSourceFile ( '' , '' , ts.ScriptTarget.Latest ), ) const exportAssignment = printNode ( exportAssignmentAst ) 処理をまとめて書くと、以下のような単純なものだと理解できます。 const exportDefault = printNode ( createExportAssignmentAst ( getExportAssignmentProperties ( 'V6/Elements/ButtonGroup' , 'ButtonGroup' ) ) ) 実際は argTypes や parameters をオブジェクトに追加するのでもう少し複雑になっていますが、基本的な考え方は以上の通りです。 なお、以下のようにオブジェクトをネストさせる書き方は今回は解説しません。詳しく知りたい方は 実際のコードをご覧ください。 export default { // ... argTypes: { width: { options: [ "" , "full" ] , control: { type : "select" } } } } ; 記述量は多いものの結局はプロパティと値の組み合わせなので、組み合わせ方さえわかってしまえば作成自体はそれほど難しくはないです。 終わりに ここまで読んでくださってありがとうございました。コードを追うだけでも大変だったと思います。 実際に使った変換スクリプトは GitHub で公開しています。 コードコメントもそのまま残しているので生々しいと思います。 このスクリプトの目的は「BASE 社内の Storybook + Vue のコンポーネントを CSF に書き換える」ものなので、あまり一般化していません。 そのため、そのままスクリプトを流用しようとすると齟齬が出るかと思いますが、処理の流れを追ったり実装で詰まったところの対処法の参考にはしていただけるかなと思います。 TypeScript Compiler API の記事はほとんどなく、あったとしても記事内容が古かったり書き換えの全体像が見えにくかったり、再帰関数の中で部分的に書き換えるようなものが多かったです。 このため、公開しているコードは手探りの中で実装したコードであり、より良い書き方があるだろうということは書き添えておきます。ただ、拡張できるように綺麗に完璧に書くことは当初の目的から除外していたこと、全部 CSF に書き換わったので目的を達成して役目を終えたので、このようなコードもありかなと思います。 この書き換えスクリプトは私が入社して1~2ヶ月目の時で、アサインされたタスクをこなしつつ空いた時間で作成したものでした。試しに小さいスクリプトを書いて、これを応用すれば労力をかけずに Storybook のコンポーネントを置換できるとわかったタイミングでマネージャーに相談して、そのまま Go サインを出してもらったため完遂できたことだと思います。 転職してまだ日が浅かった上に、自分は慣れていない少しレベルの高い手段で、解決できるかわからない課題にチャレンジさせて貰えた環境とマネージャーの決断に感謝しています。 BBQの週次定例でメンテナーのみんなに置換スクリプトを披露し、「便利ですね」といってもらったことで方向が間違っていないことを確認でき、しっかりやり切ろうというモチベーションに繋がりました。 TypeScript Compiler API はうまく使うと人海戦術を避けられる強力な武器になるため、興味のある方は課題解決の手段としてぜひ試してみてください。 参考 TypeScript AST Viewer AST Viewer です。コードを貼り付ければ AST やそのコードを生成するためのコードが表示されます Compiler API (TypeScript) TypeScript Compiler API の使い方がコードとともにわかりやすく紹介されているページです。 TypeScriptのcompiler APIをいじる TypeScript CompilerAPI - 創出の落書帳 - TypeScript Compiler API を解説した書籍 TypeScript Compiler APIを使って型を中心に実装を自動生成する
Customer Product Dev Groupの北川です。 直近では主にショッピングアプリ「BASE」のiOSアプリの開発をしています。 私たちモバイルアプリエンジニアの所属するNative Application Teamでは、 『チームで育てるAndroidアプリ設計』 の社内読書会を行いました。 『チームで育てるAndroidアプリ設計』について peaks.cc この書籍は2021年3月30日にPEAKSより出版されたもので、公式のHPによると対象読者として以下を挙げています。 アプリ開発をこれから始める方 チーム開発をよりよくしたい方、技術的なチームビルディングに興味がある方 アーキテクチャの選択、設計の維持に難しさを感じている方 新規開発/継続開発などチーム開発に関わる方 引用: https://peaks.cc/books/architecture_with_team ショッピングアプリ「BASE」はiOSアプリは2013年、Androidアプリは2015年にリリース以降、機能を追加してきました。アプリ開発を取り巻く技術トレンドも変化していく中で、設計パターンも数世代が混在しているという現状でした。 チームメンバーも増えていく中で、こうした秩序の綻びはアプリ開発のスピードの障害になると感じており、アーキテクチャの選択や改善をチームでどう行取り組んでいくか、という課題解決のヒントとして本書をチームで読むことにしました。 読書会の進め方 チームでTandemというコミュニケーションツールを導入しており 、こちらを使ってリモートで開催しました。各章ずつ読み進め、参加者は事前に読んでおいて、当日は読んでいて気になった部分について議論する形式で進めました。 読書会の記録は1つのGoogleドキュメントで管理し、話したいトピックを事前にコメントしたり、議論で出た観点、実際に取り入れられそうなアクションを記録したりできるようにしました。 読書会によって得られたもの 本書には既存のプロジェクトであっても取り組みやすい実践的なプラクティスが多く、読書会での議論をきっかけに実際に取り入れた改善がいくつかありました(前半が新規プロジェクト向け、後半が大規模プロジェクト向けという構成になっています)。 採用しているアーキテクチャを明文化する 序盤の3章を読み、最初に取り組んだのはアーキテクチャの明文化です。 どのような書き方や設計が推奨されるのかを改めて整理しました。指針が明文化されていることで、新しく参加するメンバー、レビューをする既存のメンバー双方の負担を下げることができます。 例えば、プロジェクトで採用しているライブラリについての説明として、RxJavaはKotlin Coroutineに移行中のため新たに利用するのは非推奨であることなどを記載しています。そのような異なる書き方が混在している状態と対処については、第3章「アーキテクチャの浸透と改善」で詳しく解説されています。 GitHub Issueでアーキテクチャについて検討する また、取り入れたい設計パターンや、技術課題についてGitHub Issueで議論するようにしました。 これまではSlackなどで議論が完結することが多々ありましたが、技術的な意思決定の経緯をGitHub Issueの形で残しておくと、その時にいないメンバー含めて後から意思決定を追いやすくなり、チームの齟齬を減らすことができます。 読書会を進めている期間中にもKotlin Coroutineへの移行、Jetpack Composeの導入、マルチモジュールの導入などいくつかの技術課題について検討を進めていたのですが、GitHub Issueに議論を集約することができました。 終わりに このように平易なものではありますが、読書会をきっかけにチームの開発フローの改善することができました。 『チームで育てるAndroidアプリ設計』は、チームでアプリ開発に取り組む際に最初に検討すべきことや設計を取り入れる具体的な進め方などがまとまっています。 実際にプロジェクトに取り入れた改善はもちろん、普段のミーティングの場では見えづらい、チームの課題や改善点について議論できたことが社内読書会を開催して最もよかったことの一つだと感じました。 今後もプロダクトを継続して成長させられる、よりよいチーム開発を目指してさまざまな取り組みにチャレンジしたいと思います。 また、BASEではiOSアプリエンジニアを絶賛募集中です。 少しでも興味を持っていただけましたら、お気軽にご連絡いただけると嬉しいです。 iOS: 採用情報 / カジュアル面談
8/21(土)に開催された ISUCON 11 予選に BASE から4チームが参加しました。 参加者の感想をお届けします! チーム「牡蠣に当たる時の効果音→カキーン」 BASEバックエンドチームの @cureseven です。このチームはエンジニアコミュニティのメンバーから募ってできたチームであり、BASEからは私のみ参加しています。 ISUCON11では31位 92336点でした。ISUCONに向けて 16回の練習 を重ねてきましたが、予選敗退という結果に終わり非常に悔しいです。 やったことの詳細は 個人ブログ をご覧ください。 BASEはPHPのプロダクトですが、 弊チームはGoで出場し、ISUCON練習のためにGoを書いてきました。BASE BANKではGoを利用しており、2週間に1度 Go Code Reading Party を開催しているので、参加してISUCON練習で困ったことを聞いたりしました。 去年も同じメンバーで出て本戦出場したのですが、今年は当時よりもずいぶん早く、去年よりもいろんな施策を打てるようになりました。ISUCON練習を通して成長を感じられたので有意義な大会でした。 またISUCONを通してDB周りの知識が身についたので、業務でも実行計画はどうか?より軽い実装はどのようなものか?有効なindexが貼れているか?などを意識して仕様検討・実装できるようになりました。 来年こそ本戦出場したいです。また、3年連続アプリケーション担当として参加しているので、今度はインフラ担当もやってみたいかもと思っています。 ISUCON12の準備は来月あたりから始めます。 個人ブログ @cureseven: 「ISUCON11に参戦しギリ敗退しました!!!!!!!!!悔しい!!!!!!!!!!!!!!!!!!!!!」 チーム「example.com」 川口 CTOです。 去年から前職の先輩である横山さんと、前職の超優秀だけど現在無職のエンジニアと出場しています。 案の定他メンバーのほうがスコアが良くて悲しい限りです、CTOたる威厳がない。 雑にdeployの仕組みを作ったりtrendのキャッシュとかを組み込んだりとかをしていました。 limit 1とか入れてたけど別修正とかも合わせて入れていて整合性チェックでfailして諦めたりしたので、もっと細かく手数を作れるようにならないとなと反省しきりです。 次こそは本戦、という気持ちで1年いますが来年もまた無準備になりそうな気がしています。 運営の皆様お疲れさまです。 横山 BASE所属の横山です。去年に引き続きCTOとニートエンジニアと3人で参戦しました。 初速だけはよく開始1時間ぐらいまでは10位内に張り付いていたのですがそこからの伸びがなく最終スコアは43,684点で90位でした。 自分は主にMySQLまわりを触り、1台をレプリカにして参照クエリをレプリカに投げる、みたいなことをやっていました。 レプリカ化とかはじめてやってみましたができてよかったです。 スコア的な伸びはなかったんですが・・・。 朝から仕様を頭に叩き込むのは辛かったですが、終わってみればあっという間でした。 また参戦します。 チーム「Speed of Sound」 松雪(@applepine1125) BASE BANK 所属の budougumi0617 と applepine1125 で「Speed of Sound」というチームを組みエントリーしました。 2人とも今回が初参加だったので、競技終了時に0ptにならない&100位以内という目標を掲げ、当日は無事? 最終スコア47,995点で79位をマークしました。 スロークエリを発見してindexを貼ったりbulk insertを活用できれば初参加でも100位以内に食い込めるんだなと思いつつ、それ以上を目指そうと思ったときにはもっといろいろな武器を持っていないと上位入賞は遠いな・・・と悔しい限りでした。 来年も是非参加したいと思います。運営の方々、準備から開催までありがとうございました!本戦出場される方も頑張ってください! 清水(@budougumi0617) 今回初の個人スポンサー、初の参加をしました。 以前から「いつか出るぞ出るぞ…」と思っていたので予選参加600チームに潜り込めてよかったです。 事前準備として環境構築スクリプトなどを作成して臨みました。最初の各種計測の設定は準備のおかけでスムーズにできてよかったです。 しかし、その後の実際のパフォーマンス改善はなかなか難しかったです。 「計測結果が悪いけど直し方がわからない」「たぶん良くなるからいじってみる」というような振る舞いが多くなってしまい、計測データに基づいた原因分析、修正対応ができませんでした。 分析力・対応力をもっと身につけて次回は本戦に出場したいと思います。 環境構築はCloudFormationで一瞬終わりましたし、ベンチマークは気軽に何回も実行できて快適な競技環境でした。ありがとうございました。 詳細な当日の様子は、それぞれの個人ブログの記事に記載しています。ぜひそちらもご覧ください! 個人ブログ @applepine1125: 「ISUCON11予選に参加した(47995点で予選落ち)」 @budougumi0617: 「ISUCON11予選に参加した(最終スコア47,995点 79位) #isucon」 チーム「賢者のつどい」 BASE のフロントエンドチームの @Panda_Program です。 大学時代の友人である 0daryo が声をかけてくれたので「賢者のつどい」という2人チームでエントリーしました。 私は BASE でフロントエンドエンジニアをしているため、普段の業務ではサーバーのコードを書いたりインフラを触ることがありません。 前職では業務で PHP も書いていたものの、これまで特にパフォーマンス向上の業務はしたことがなかったのでいい機会だと思ったのがエントリーの理由です。 ISUCON は初参加で右も左もわからなかったところ、幸い社内の Slack に ISUCON に挑戦する人たちのチャンネルがあったので参加してみました。 エントリーの期限や参加後に対応が必要なことのアナウンスやリマインド、また他の方もそれぞれ予選に向けて事前準備をしているという発言を見ることで ISUCON 参加の実感が湧いてきたり、自分も準備をしなければというやる気をもらいました。 予選当日の様子は 個人ブログ に記載しているのでそちらをご覧いただければと思います。 最終スコアは16680(598組中249位)と BASE の他のチームの戦績に比べて結果は芳しくなかったです。 ただ、事前準備では普段業務で触らないところが勉強できたこと、パフォーマンス向上の鉄則である「推測より計測」を実際に体験できたため参加した甲斐がありました。 昼休憩で PC から離れた時に肩があまりにも凝っていたことから、集中と緊張のあまり体が強張っていたのだなと後から気づき、なんだか大会らしいなと感じたのはいい思い出です。 ISUCON の結果は事前準備でほとんど決まるのだなと思いました。来年はしっかり準備して予選に臨む所存です。 個人ブログ @Panda_Program: 「ISUCON11に参加しました」 最後に ISUCON 運営の方々、とても有意義な大会を開いていただきありがとうございました。 また来年も参加したいと思います!
BASE開発担当役員の藤川です。 BASEのYoutube動画で、EC系SIerからBASEに転職してきてくれて4年半を経過したタイミングでの宮村さんにBASEでの働き方についてインタビューをしました。今回は前編と後編の2本立てになります。 前編動画:スタートアップの成長過程の風景について www.youtube.com 2017年に転職してきて、今では技術責任者と呼ばれる開発プロジェクトの設計や技術責任を牽引する役割を担ってもらっていて、SIerからスタートアップに転職したリアルな話を聞いてみました。 動画の中で思い出のプロジェクトとして携帯キャリア決済の実装についてお話いただきました。当時のBASEはクレジットカード決済、後払い、銀行振込等は実装されていて、そこに携帯キャリア決済が実装されました。 キャリア決済を実装したことがある人ならわかると思いますが、相応に決済フローが複雑で情報もわかりにくい上に3キャリア毎に挙動の癖があります。実際リリースした後に、想定しないタイムアウトエラーが発生したり、フロー制御にも気をつけなくてはいけなかったりしました。しかし、先方の実装が見えるわけでもないし、相手は超大企業ということもあり、開発ドキュメントの理解力が問われます。 また、SIerや受託出身の方は開発ドキュメントの作成や管理に慣れているので、当時、まだまだスタートアップ初期の内輪な状態から人数も増え、いわゆる組織として変化させていく中で、ドキュメント整備を通じて開発ナレッジの整理を自然としてくれていたというのが印象的でした。 よくスタートアップとSIerを比較した時にドキュメントが整備されていなかったり、いろんなことが曖昧であることが転職するメリット・デメリットとして挙げられることがあります。 とりわけ初期のスタートアップでは、明日をも知らぬフェーズで仕事をしていく必要があるわけで、実装面、ドキュメント面共々、5年後10年後を見据えた活動をしているよりは、今を生き抜くことが優先されます。 しかし、ある程度サービスが成功し安定成長を開始してからは、人材採用は未来を作るための最重要課題となり、常に実装力、ドメイン知識に関する初心者の方が毎月入社してくる流れにシフトします。それ故に、先輩の方々からドメイン知識や実装に関する情報が適切に共有されていく流れは重要です。 今回はそういうところでの心構えなども聞いてみました。 大手企業に新卒で入った人たちは、各社のドメイン知識を吸収し成長をお膳立てされるプロセスが当たり前に存在しています。入社後数年は昇給が横並びの大企業も少なくないですが、その間は教育期間として考えられていることが理由だと思います。 しかし実際に企業が急成長するタイミングを目の当たりにすると、そういうことが当たり前に存在するのではなく意図して作っていくものだということを再認識されられます。それがスタートアップが大きくなっていく時に必要とされるスキルです。 後編動画:リモートだからこそ重視されるアジャイル設計 後半の動画では、BASEの現状の開発状況についての話を聞きました。 www.youtube.com その中でも昨今のリモートワーク下での新入社員の適切なオンボーディングは非常に重要な課題です。まだ心理的安全性も適切に構築できてない状態で、オンラインコミュニケーションを強制されるコロナ禍において、入社いただく方に如何に馴染んでもらって活躍いただくかを常に注視しています。その中で設計や開発プロセスに業界標準のノウハウを取り入れているという話をしました。 開発はGitHubフローを活用していますが、設計面でもドメイン駆動設計、アジャイル開発などと言ったノウハウを我々も導入しています。組織としては、まだまだ試行錯誤中ですがプロジェクト毎にチャレンジをしている状況です。 このような勉強会も随時行っています。 devblog.thebase.in スモールチームだったショップが成長し続けることにしっかりついていく 群雄割拠のEC支援事業の市場でBASE社が存在し続ける理由としては、個人やスモールチームにフォーカスした機能を提供することでビジネスが成立するんだよという、それまで言語化されていなかった独特な領域に市場を見出したことだと思います。そういった初心者にも使いやすいサービスはこれまでいくらでもあったにも関わらず、そこに特化した意思決定を続けてきたことがBASEが成長した鍵だと考えます。 一方で、BASEで成長したショップさんがBASEの機能では物足りなくなって別のサービスに移動されてしまうということは避けなくてはなりません。EC市場の中で、「お店を作ってみたい」という入り口を押さえているからこそ、成長したショップさんがずっとBASEを使い続けていただくというのは最重要課題として、常に存在し続けています。 BASEのプロダクトデザインとしては初心者の方がすぐにお店が作れますというシンプル性を担保しつつ、成長したショップさんが使い続けることができる機能やスケーラビリティを提供し続けるという部分で、ある種の二律背反的な要素も存在します。宮村さんがインタビューで話しているように、実は以前の方が限られたリソースの中で割り切ったデザインをしていたのに対して、最近は、大きくなったショップさんの組織構造等も踏まえて、しっかり全体を考えていくことが求められており、それらを担保し続けられる人材が不可欠です。 もちろん、それは当社の開発経験の中で培っていけばよいので、これまでのBASEがどのようにサービスを成長させてきたかをしっかりトランスファーしながらになりますが、そのようなドメイン知識を通じてBASEの作り手として活躍する人をもっと増やしていかないといけません。 エンジニアの働き方はBASEの成長を生み出すことと連動する 今回、改めて宮村さんと話したことを反芻してみると、中途採用である個々人がこのチームに投入されるノウハウというのが、BASEというサービスの成長の中でうまく溶け込んでいるということを再認識させられました。 我々はまだまだ発展途上のチームであり、一人ひとりの経験が新しい視界を生み出すことに繋がります。開発のプロセスにおけるアジャイルの導入も決してCTOのビジョンで先導したのではなく、それの先導者がいて、フォロワーとしてついていく人たち、それぞれの前向きな姿勢があってこそできていることです。CTOがやったのは、そういう人達の考え方を聞き、開発の進め方としてオーソライズしたということです。 一人ひとりが有名な人とか、そういう人たちを優先する採用はしていなくて、このチームを全体で良くしていくんだという期待が持てる人しか採用していないので、是非、一緒に成長していきたいという人がいたら、是非、お話させてほしいです。
はじめに こんにちは! BASE 株式会社 Customer Product Dev で Android エンジニアをしている小林です。 ショッピングアプリ「BASE」のAndroid版アプリの開発を担当しています。 最近、フォロー中タブ追加というアプリのトップ画面を大きく変えるリリースを行いました。 その際、RecyclerViewの実装でPagingライブラリの3.0.0を導入してページング処理を実装してみたのでその話をしていきたいと思います。 Pagingライブラリとは? データの追加読み込みやメモリに保持するデータ量の管理をやりやすくしてくれるライブラリです。 2021年9月時点ではPagingライブラリの最新バージョンは3.0.1になっています。 https://developer.android.com/topic/libraries/architecture/paging/v3-overview 2021年5月にメジャーバージョンが上がり、v2の時とは色々と実装方法が変わっています。 Paging 3ライブラリを使うと、以下の機能がサポートされます RecyclerView Adapterと組み合わせることで、自動で次のデータを取得してくれるページング機能 Kotlin コルーチンやFlowを使ってのデータの取得や反映 リフレッシュやローディング中、エラー時のUIの切り替え 今のアプリ内のPaging処理の現状と問題点 今のアプリはScrollListenerを使って独自にページングを実装しています。 スクロール時に何件まで商品を表示したかをチェックして、ある程度読み込んだら次のページを読み込み始める形です。 この実装はJavaで書かれていてKotlin コルーチンとの相性が悪く、今のアプリの構成に合わない実装になっていました。 Pagingを使おうとした理由 上記の問題点を解消するために、現状のページング処理のKotlin化&アーキテクチャ適応も検討しました。 しかし、以下のことを考慮した結果、今回実装する新規画面にPaging 3ライブラリを導入することにしました。 機能的にPaging 3ライブラリが優れていること 特に、次のデータを自動で取得してくれるページング機能はコード量を大きく減らせると期待できる 実装 公式ドキュメントを参考に、ViewModelにFlowを定義して、それをFragmentで購読する形で実装していきました。 https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data?hl=ja#pagingdata-stream ViewModel MVVMにおけるViewModelです。AACのViewModelを継承しています。 Pagerクラスを生成して、remoteMediatorを設定します。 class ItemsViewModel( //... ) :ViewModel() { // 商品情報を取得するRepository @Inject lateinit var itemsRepository: ItemsRepository // お気に入り情報を取得するRepository @Inject lateinit var favoriteItemsAllRepository: FavoriteItemsRepository // APIで取得したリストの情報をキャッシュするDatabase @Inject lateinit var itemDatabase: ItemDatabase // 1画面に表示される商品数 // pagingの先読み商品数の設定のために定義している private val viewIngItemNum = 15 @OptIn (ExperimentalPagingApi :: class ) val flow = Pager( config = PagingConfig(pageSize = viewIngItemNum, initialLoadSize = viewIngItemNum), remoteMediator = ItemRemoteMediator( itemDatabase, itemsRepository, favoriteItemsRepository ) ) { ItemDatabase.ItemDao().pagingSource() }.flow.map { pagingData -> pagingData.map { it.convert() } }.cachedIn(viewModelScope) //... } RemoteMediator Repository経由でAPIから商品情報を取得しています。 後述するお気に入り情報の更新のために、ローカルキャッシュとしてRoomのテーブルクラスに格納しています。 色々書いていますが、やっていることは以下のとおりです APIから読み込むページ数を決める リフレッシュならRoomのテーブルを削除する APIから取得する 取得した情報をRoomのテーブルに格納する Roomのテーブルに情報が更新されたことをViewModelのFlowが検知して、Adapterに反映されます。 @OptIn (ExperimentalPagingApi :: class ) class ItemRemoteMediator( private val database: ItemDatabase, private val itemsRepository: ItemsRepository, private val favoriteItemsRepository: FavoriteItemsRepository ) : RemoteMediator< Int , ItemEntity>() { override suspend fun load(loadType: LoadType, state: PagingState< Int ,ItemEntity>): MediatorResult { val page: Int = // 略 リフレッシュ時には1,追加読み込み時はnextKeys等の読み込むページを指定している try { // itemRepository.itemsはRxJavaのSingleで返ってくるので、await()を使ってKotlin コルーチンに変換しています。 val result = itemsRepository.items(page).subscribeOn(Schedulers.io()).await().items // APIからの値をローカルDBに格納できるクラスへ変換する val entities = result.convertToEntity() entities.forEach { // 取得した商品がお気に入りしているかどうかを取得して設定する it.isFav = favoriteItemsRepository.isFavorite(it.itemId) } val endOfPaginationReached = result.isEmpty() database.withTransaction { if (loadType === LoadType.REFRESH) { // PullToRefresh等で、最初から読み込む場合はRoomのテーブルの情報を消す database.remoteKeysDao().clearRemoteKeys() database.itemDao().clearAll() } val prevKey = if (page == 1 ) null else page - 1 val nextKey = if (endOfPaginationReached) null else page + 1 val keys = entities.map { entity -> ItemRemoteKeys(repoId = entity.itemId, prevKey = prevKey, nextKey = nextKey) } // Roomのテーブルに取得した商品情報とページ数を格納する database.remoteKeysDao().insertAll(keys) database.itemDao().insertAll(entities) } return MediatorResult.Success(endOfPaginationReached) } catch (e: RetrofitException) { return MediatorResult. Error (e) } } } Fragment ViewModelで設定したflowで、変更された値をAdapterへ通知します。 class ItemsFragment : Fragment(){ lifecycleScope.launch { binding.viewModel?.flow?.collectLatest { pagingData -> //... // PagingDataAdapterのメソッド adapter.submitData(pagingData) } } } Adapter PagingDataAdapterを継承していること以外は通常のRecyclerView.Adapterと実装方法は変わらないです。 ViewModel class ItemsAdapter( //... diffCallback: DiffUtil.ItemCallback<ItemViewData> ) : PagingDataAdapter<ItemViewData, ItemsAdapter.ViewHolder>(diffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int ): ViewHolder { return ViewHolder(ViewItemBinding.inflate(LayoutInflater.from(parent.context)), listener) } override fun onBindViewHolder(holder: ViewHolder, position: Int ) { val item = getItem(position) holder.setData(item, position) } class ViewHolder( //... ) : RecyclerView.ViewHolder(binding.root) { fun setData(data: ItemViewData?, index: Int ) { //... } } } // Paging 3ライブラリが、Adapter内の商品のチェックを行うための処理 object ItemComposer : DiffUtil.ItemCallback<ItemViewData>() { override fun areItemsTheSame(oldItem: ItemViewData, newItem: ItemViewData): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: ItemViewData, newItem: ItemViewData): Boolean { return oldItem == newItem } } 実装してみての詰まったところ アプリでは、ユーザが商品をお気に入りする機能があります。 他の画面でお気に入りが変更された場合、フォロー中画面の商品リストに反映する必要があります。 しかし、Paging 3ライブラリでFlowを使って商品リストの反映を行っていたため、すでに取得/反映している商品の情報をAdapterやViewModelでは更新することができませんでした。 これはPaging 3ライブラリの仕様となっています。 一度リストの情報をPagingDataとして取得した後は、個々の情報の中身を取得したり変更することができません。 現在更新を可能にするissueが上がっている状態です。 https://issuetracker.google.com/issues/160232968 解決策 Roomを使ったローカルキャッシュを実装して解決をしました。 Paging 3ライブラリは中間にRoomのテーブルを挟んで実装した場合、Roomのテーブルを唯一の情報源として動作します https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ja#basic-usage RemoteMediator 実装は、ページング データをネットワークからデータベースに読み込む際には有用ですが、データを直接 UI に読み込むためには使用できません。代わりに、アプリはデータベースを信頼できる唯一の情報源として使用します。つまり、アプリはデータベース内にキャッシュされたデータのみを表示します。PagingSource 実装(Room によって生成されたものなど)は、データベースから UI へのキャッシュ データの読み込みを処理します。 これにより、お気に入りが変更された場合に、API通信を行うこと無く情報の反映ができるようになりました。 実装では、RxBusなどで他画面からのお気に入りが変更されたことの通知を受け、changeFavを実行してテーブルの更新を行っています。 class ItemsViewModel{ // APIで取得したリストの情報をキャッシュするDatabase @Inject lateinit var itemDatabase: ItemDatabase val flow = // 略 // map = お気に入り状態を変更した商品ID=Stringと、変更後状態=Booleanが格納されている private fun changeFav(map: Map < String , Boolean >) { viewModelScope.launch { itemDatabase.withTransaction { val table = itemDatabase.itemDao() // 商品IDでキャッシュの情報を取得する table.selectByItemId(map.keys.toList()) .forEach { val beforeFav = it.isFav it.isFav = map[it.itemId] ?: false if (it.isFav != beforeFav) { // リスト表示用のキャッシュに、お気に入り変更を更新している table.update(it) } } } } } まとめ Paging 3ライブラリでのリスト表示処理の実装と、他画面からの通知を受け取ったお気に入り変更処理の実装について書いてみました。 Paging 3ライブラリはリスト表示についてどんなシチュエーションでもカバーできるようになっていて、一見難しいです。 でも、使いこなせるようになれば自分で実装するよりも手間もコード量も少なく済むのでとても便利なライブラリだと思います。 おわりに 最後になりますが、BASEではAndroid/iOSのエンジニアを募集しています。 カジュアル面談もありますので、興味のある方は気軽にご連絡ください! Android: 採用情報 / カジュアル面談 iOS: 採用情報 / カジュアル面談
こんにちは!New Owners Dev GroupにてEngineering Managerをしている植田です。 皆様の会社・組織ではSlackを導入・活用されていますか? 私は2021年4月にBASEへ入社したのですが、色々と驚いた点のうちの一つに 「Slackを全社をあげて活用しまくってる!」 というものがあります。 BASEではただのチャットツールのみならず、あらゆる場面でSlackを活用し業務を効率化していることに大きな驚きを覚えました。エンジニアは当然のことながら、非エンジニアもSlackをフル活用しているので今回はそれを大公開してしまおうと思います! ※実際社内で利用しているSlackのキャプチャを掲載しているため、画像にはマスクを施しています BASEでのSlack活用例 今回、記事を書くにあたり社内の関係者に声をかけ、どんな場面でSlackを活用しているかヒアリングをかけたところ、ありとあらゆる場面でSlackを活用していることがわかり、図にまとめてみました。 ご覧の通り、各組織でSlackを活用していることがおわかりいただけると思います。では、それぞれの代表的な活用事例を細かくみていきましょう。 ①コラボレーションツールとの連携 BASEでは、社内Wikiツール、タスク管理ツール、カレンダーツール等、いわゆるコラボレーションツールとSlackとの連携がなされています。特に、WikiツールであるKibelaとの連携は強力で、新しい記事が作成されたり、記事にコメントが追加されると通知されるチャンネルがあるため、記事の見逃しがなくコラボレーションを後押ししています。 Kibela通知の例 また、Googleカレンダーに登録している「機能リリースカレンダー」とも連携しているため、リリース情報を見逃すこともありません。 リリース通知の例 その他の活用事例 「Asana」との連携で、自身に割り当てられたタスクやステータスの変わったタスクが通知されます(Asana=タスク管理ツール)。 「Redmine」との連携で、SQLレビューチケットの通知がされます。※BASEではSQLのレビューをRedmineのチケットで管理しています。 ②エンジニアリングの中でのSlack活用例 現状Slackなくして、エンジニアリングは回らないといっても過言ではないほどエンジニアリングとSlackは密接に紐付いています。 GitHubと連携することで、プルリク通知やIssue通知をSlackで受け取っています。 プルリク通知の例 また、ChatOpsを利用したデプロイといった連携もしておりとても便利です。 デプロイ連携の例 Looker を利用したデータモニタリング(データ通知)も仕組み化されています。SlackとLookerを連携し、デイリーで主要KPI実績をSlackチャンネルに流しています。こうすることで全社員が数値、KPIに対する意識を高めています。KPI情報ということでキャプチャを載せられないのが残念です。 その他の活用事例 「Mackerel」や「Sentry」との連携で、メトリクス情報を通知 「Geekbot」を利用し日報の登録 CRONの正常稼働を通知 ③総務・人事・労務におけるSlack活用例 BASEでは総務・人事・労務といったバックオフィスチームもSlackを活用していますよ! 例えば、健康診断の日程調整といったフローも、Slackのワークフローで完結されています。入社して地味にすごいなと思った一つです。 健康診断ワークフローの例 また、労務関連のお問い合わせはやはりワークフロー化されています 労務ワークフローの例 こちら の記事で紹介されている通り、BASEでは自社サービス利用サポートの福利厚生があります。「BASE」で購入した商品を申請し、共有用の回答欄に商品URLを入力すると、該当のチャンネルでどんな商品を購入したかが通知されるようになっています。他の社員が購入した商品やショップが分かり、素敵なショップを知るきっかけになります。 利用補助の投稿例 その他のSlack活用事例(一部) 慶弔・特別休暇の申請をワークフローで受け付け、労務メンバーのみのチャンネルに通知 人事発令情報を必要関係者へ通知 Twitterなどで都やメディアからのコロナ関連のニュースがあったら該当のチャンネルに通知 Twitterなどで気象・地震情報が発信された場合に該当のチャンネルに通知、震度5以上の地震発生で必要関係者へ通知 コロナ関連や、天災情報をSlackに通知するというところに総務、労務らしさが出ていますよね。 ④採用におけるSlack活用例 採用チームでは各種採用サービスを利用していますので、Slackと連携し採用サービス側での通知をSlackへ連携し、スピーディな採用活動を実現しています。 採用サービス通知の例 その他の活用事例 ワークフローにて、お知り合い紹介フォームを構築(リファラル採用フォーム) Slackのワークフローにて、採用会食申請フォームを構築 BASEテックブログ のTweetをSlackチャンネルへ通知 ⑤法務におけるSlack活用例 続いては法務でのSlack活用です。 いわゆる法務チェックはワークフロー化されています。 法務チェック申請フォーム 法務では クラウドサイン を利用しており、締結が完了するとSlackへ通知されるようになっています クラウドサイン通知連携 こちらは後述する「リアク字チャンネラーアプリ」を使った活用例です。 Slackチャンネル上のメッセージに対して「法務check」リアクションをすることで、任意のチャンネルへメッセージを連携することができます。 法務チェックのリアク字チャンネラーアプリ使用例 ⑥CSにおけるSlack活用例 Zendeskと連携し、購入者さんからのご意見をSlackへ通知し、プロダクトの改善に繋げています。 Zendeskとの連携例 また、ショップオーナーさんからのご意見もSlackへ通知し、こちらもプロダクトの改善に繋げています。 オーナーさんからのフィードバックの例 BASEでは こちらの記事 の通りCS対応を当番制で運用しています。CSチームからエンジニア向けに調査依頼が投稿されると、自動的に過去の似たようなお問い合わせを拾ってきて、調査のヒント度案内してくれるbotも構築されています。 CS_q_bot ⑦情シスにおけるSlack活用例 社員からのヘルプデスクをワークフローを使って受け付けています。 ヘルプデスクの例 その他の活用事例 情報セキュリティ事象・事項の報告をワークフローにて受付 翌営業日に物理出社するかのアンケート(リモートワーク期間中の運用) ⑧その他の活用例、Slackの便利機能 Slackには無数のアプリや便利機能がありますが、ここではBASEが利用している便利機能の一部をご紹介します。 Unipos連携・・・BASEではピアボーナスである Unipos を導入しているのでSlack上で感謝の気持ちを送り合うことができます。 Colla連携・・・ Collaアプリ を利用すると、毎日botがメンバーにインタビューをし、指定したチャンネルでメンバー紹介をしてくれます。お互いのことを知ることができコミュニケーションの円滑化に役立ちます。 新規チャンネルを通知・・・Zapierというアプリを利用すると、スペース内で新しいチャンネルが作成されたら通知することができます。新しいチャンネルの存在を見過ごすことも減りますね。 新規絵文字を通知・・・同じくZapierを利用すると、新しく絵文字が追加されたら通知することができます。 話題のツイートをSlackへ通知・・・例えば新しい機能をリリースした際に、Twitterで反応があったらSlackへ通知するようにしています。こちらは3月に Amazon Pay 対応をした時の反応ツイートの通知の例です。 リアクションで任意のチャンネルにメッセージをコピー・・・「 リアク字チャンネラーアプリ 」を使うと、パブリックチャンネルに投稿されたメッセージにリアクションすることで、別のチャンネルにメッセージをコピーすることができます。例えば「これはあのチャンネルの話題だ」と思った時にリアクションすることで、話題の集約といったことができます。 おわりに いかがでしたでしょうか。おそらくご存知のものもあれば、「こんな使い方があったのか!」という発見もあったのではないでしょうか。手作業でやっているルーチン業務などもSlackの機能をうまく使うことで自動化できたり業務改善ができるかもしれません。ぜひチャレンジしてみてください! お知らせ BASE 株式会社では、「BASE」の開発を一緒に盛り上げてくれる仲間を募集しています。 一緒にかっこいい開発チームを作りましょう! ぜひよろしくお願いします! open.talentio.com
こんにちは!! BASE株式会社 SRE Group Managerの富塚( @tomy103rider )です。 以前、以下のSREの求人票についての記事を公開してから多くの方とカジュアル面談でお話をさせていただく機会が増え、カジュアル面談の中でも「求人票ブログの記事見ました!」という声をいただき嬉しい限りです。ありがとうございます。 devblog.thebase.in そんなカジュアル面談の中でよくいただく質問があり、今回はその中から最近のSREチームについて、 チームで大事にしていることは? チームの働き方は? チームの業務は? この3つについて紹介したいと思います。 チームで大事にしていることは? 現在SREチームでは、 「信頼性=ユーザの期待値を超え続けること」としてこれを維持し続ける BASEの機能等々の価値を高めるための時間を多く作っていけるようにする が大事であると考えています。 この2つの言葉は、以前チームのミーティングの際に、 「そもそもBASEのSREチームとして今の時点ではどういうチームを目指しているの?」 「BASEのSREは何を大事にしているのか?」 という話題になり、「自分たちが考えるBASEのSRE像とは?」をチーム内で持ち寄って話し合った結果を言語化したものです。ちなみにBASEのSREチームから見たユーザとしては、前者は「オーナーズ(ショップオーナー)とカスタマー(購入者)」の方々、後者は社内でサービスに関わる(SRE含む)人たちを指していて、BASEに関わる全てのユーザ対しSREとしての活動をしていくことが大事であることを表しています。 チームの働き方は? 昨今のコロナ禍ということもありBASEのSREチームにおいても現在は基本的にリモートで業務をしています。そんな中で、日常的なコミュニケーションとしては基本的にはSlackでテキストコミュニケーション、ミーティングはZoomで行っていて、最近はSlackハドルミーティングも活用してより気軽に音声だけでのコミュニケーションを取るようにもしています。 またSREチームの1週間のイベントとしては大きく2つで「朝会」と「SyncSRE」があります。 朝会 - 月〜金 12:00〜12:30 - リリースや大きな作業などのカレンダー確認など - 各自の進捗 - 共有/相談タイム 朝会の様子 朝会はタスクを進めている中での困りごと/相談の時間を特に大事にしていて、より話を掘り下げる必要があるときは30分を超えることもあります。また当然ながら何かあればZoomやハドルミーティングで朝会以外でも都度相談しあっています。 SyncSRE - 隔週金曜 17:00〜 - フリーテーマでチームみんなで会話をする時間 SyncSREは朝会とは異なり、特に議題は決めず主に最近起きたことや感じたことなどをざっくばらんに話してチーム内の認識や意識を合わせる時間にしています。これを始めたのは、リモートワークが始まる前は日常的に自然発生していた会話がリモートワークとなり減ってしまっていることに危機感を感じ、このような時間を意図的に作りました。チーム内での意識/認識をSyncする良い時間になっていると思っていて、実際に1つの事例として最初に書いた「チームで大事にしていることは?」で紹介した2つの言葉を話し合うキッカケになったのはこのSyncSREでの話題からでした。 チームの業務は? ここについては色々ありますが、今回は最近注力している業務や各開発プロジェクトとの関わり方について簡単に紹介したいと思います。 トイル削減/サービスの運用効率化 現在BASEのサービス拡大に伴ってインフラリソースの作成/変更/運用の機会が増え続けており、日常の運用負荷であったり環境の再現性の担保という点において課題があると認識しており、今後の様々な改善を加速していくためにもここを最優先で改善する必要があると考えています。そのため、今更ではありますがInfrastructure as Codeの実践というところでインフラ環境のコード化+断続的な構成レビューのフロー整備を試行錯誤しながら進めているところです。このあたりはある程度形ができた段階でチームのメンバーがブログを書いてくれることを期待しています。 他チームや開発プロジェクトとの関わり 他チームや各開発プロジェクトとの関わりという点では、例えば必要となるAWS上のインフラに関わる業務があります。ただ、単に機能要件を満たすだけの作業をするだけではなく、各開発プロジェクトにおいて機能要件以外に「非機能要件」という観点でもっと事前に考慮しておくべきことはないか?など、リリース後に小さく積み重なっていくと重い課題になりがちな懸念点をSREからも伝えていけるようにしています。 まとめ いかがでしたでしょうか。少しでもBASEのSREチームの雰囲気が伝わればと思います。 さて、引き続きSREチームでは成長するサービスを一緒に支えていっていただける仲間を募集しています! BASEのサービスとSREチームに少し興味が出てきたのでもっと話を聞いてみたい! 似たような試行錯誤をしているのでカジュアルに話してみたい! サービス成長とチームの成長に対して一緒にチャレンジしてみたい! など、少しでも興味を持っていただけましたら、悩み辛みの情報交換でも構いませんし、ここに書ききれなかった話など、ざっくばらんにカジュアル面談でお話ができればと思いますので、お気軽に以下までご連絡いただけると嬉しいです! → カジュアル面談はこちらまで! ← ※参考 open.talentio.com 余談 まとめを書いたあとに余談ですが、最近以下の記事を読みました。 「これでよいのか: SREチームの成熟度評価について考える」 cloud.google.com ここに書かれている「このグループはこの職務をSREと呼ぶために必要な原理と実践の適用において十分なレベルに達しているでしょうか?」という問いに対し、記事内の4つの原理と自分たちがSREとして進めていっている活動を照らし合わせると、方向性としては間違っていないとは思いつつ、実際SREとしての振る舞い/行動ができているかというとまだまだであろうという感想を持ち、この記事については近々SyncSREで改めて話題にしてみようと思います。
はじめまして、フロントエンドエンジニアの @rry です。 BASE では社内勉強会として「 Frontend Weekly LT 」を毎週開催しています。 今回は Vite 特集で LT をしたので、Frontend Weekly LT の紹介も兼ねて内容を発信していきたいと思います。 Frontend Weekly LT とは BASE はフロントエンドエンジニアが、業務委託の方も合わせて約20名ほどいます(7/16現在)。 普段は別チームに分かれており、プロジェクトもそれぞれ別で開発を進めているのですが、この勉強会ではそんなフロントエンドエンジニアが一堂に集まってわいわい LT するイベントです。 LT イベントは基本的に毎週2名ずつ登壇して、 新しく BASE でリリースされた機能の紹介 いま気になっている&プロダクトで使用している技術について 自己紹介 LT などなど、毎回さまざまな内容で LT しています。 7/16 は Vite 特集の LT 会 Vue.js v-tokyo オンライン Meetup#13 - connpass 最近の v-meetup でも Vite 特集があったりと、最近 Vite がとてもアツいです。 BASE ではまだプロダクションでの利用はありませんが、Vite v2 が Vue や React などライブラリに限らず利用できるというのもあり、期待を寄せているビルドツールです。 今回の LT 登壇者は @rry と @gatchan0807 がそれぞれ発表する流れでした。 次世代のフロントエンドビルドツール Vite 登壇資料はこちらです。 最初は Vite x React で何か作ってみようと思っていたのですが、Vite について調べていくうちにそもそものビルドツールの特徴についてのほうが気になってしまい、結果としてこのような内容の LT になりました。 登壇してみて Vite のドキュメントを改めて全部読んだり、Vite に関係する記事をいくつか読んだりできたので良かったです。 今回参考にした記事: https://speakerdeck.com/kazupon/native-esm-powered-web-dev-build-tool https://speakerdeck.com/kazupon/vue-with-vite https://ics.media/entry/210708/ https://tech.recruit-mp.co.jp/front-end/post-21250/ Frontend Weekly LT はみんなの思い出作り 最近のテックブログで @fshin2000 さんがこんなことを書いていました。 そもそも、オンラインコミュニケーションにおいては何が足りないのだろうか?と思っていたのですが、一言でいうと、 良い思い出を作るイベントが圧倒的に少ない ではないか?という事を考えています。 リモートワークの弊害は職場でのよい思い出が作りにくいこと - BASEプロダクトチームブログ この Frontend Weekly LT は今年の 1/15 から始まった取り組みです。 コロナ禍でコミュニケーションがオンラインになる中、こういったみんなで集まってわいわいできるようなイベントを毎週開催するのは、良い思い出作りになっているのではないかなと考えています。 これからも継続して Frontend Weekly LT を盛り上げていきたいなと思います! 💪 おわりに いつものやつですが、BASE では一緒にフロントエンドを盛り上げていく仲間を募集しています。 Vue や React など新しい技術で EC サービスを支えていきたい!という方のご応募お待ちしております ☺ 1.BASE_フロントエンドエンジニア - BASE株式会社
Owners Experience Backend Group で Engineering Manager をしています、炭田( @tac_tanden )です。 BASE では決済などの複雑な事業ドメインに立ち向かうために、ドメイン駆動設計(DDD)を使った開発を進めているチームがあります。 そんな中で先日、DDD の有識者として松岡幸一郎( @little_hand_s )さんをお招きし、DDD モデリングデモ会社内で開催しました。 その模様をレポートします。 開催の経緯 BASE では日頃から DDD に関する質問ができるように、松岡さんを Slack チャンネルにお招きし、開発や DDD の勉強会で出てきた困りポイントや疑問を質問させていただいています。 そんな中で「DDD の勉強をしているけれど、モデリングを実際にどうするのか見てみたい」、「自分でモデリングにトライしてみたけれど、このやり方でよいのかいまいちピンとこない」などの声が上がっていました。 なので、実際にモデリングをどうするのかのイメージを掴み、今後 DDD での開発に取り組む際のモデリング作業に活かせるよう、この度 DDD のコーチとして日頃お世話になっている松岡さんをお招きして BASE の決済領域のモデリングデモ会を開催することになりました。 参加者 BASE の決済領域のモデリングということで、エンジニア以外のメンバーにも有意義なのではと思い、Slack の #general にて開催の告知をしました。その甲斐あって、当日はエンジニアメンバー以外にも、プロダクトマネジメントチームやデザイナーチームのメンバーの方たちにも参加していただくことができました! Zoom での開催だったので、気軽に多くの方が参加できたのもあり、夜の遅い時間だったのですが当日は 40 名近くの方に参加いただきました。企画者として嬉しかったですね。ありがとうございました。 当日 デモ会当日は、BASE からは長年決済領域の開発に携わってきた Payment Group のエンジニアメンバーがドメインエキスパートとして参加しました。 ドメインエキスパートの知識をもとに、松岡さんが画面上にモデリングをしていくのですが、複雑だと考えていた決済領域の関係が次々に明らかになっていく過程は壮観でした。 当日のデモによって出来上がったドメインモデル図をブログ上でお見せできなのは残念ですが、各概念の関係性が分かりやすく表現されていて、これがあれば実装に入れそうだなと自然に思えるような完成度で驚きました。 また、モデリングの過程も、具体的にどういう手順でモデリングをしているくのかステップバイステップで説明/デモをしていただいたので、終わったあとには「自分でもできそう」と思えるようになりました。 2 時間という短い時間でしたが、松岡さんやドメインエキスパートの方の協力もあり、とても有意義な時間になりました。ありがとうございました。 参加者の感想 エンジニアメンバーのお二人の感想をお届けします。 ドメインエキスパートとして参加 今回ドメインエキスパートとして参加した Product Dev Division 所属の島田です。 脳内にあった抽象的な全体像をドメインモデリングを行ったことでより具体的にできました。 そのおかげで概念を言語化したり図にすることができ自分以外の人でも共通認識できるようになりました。 共通認識ができるようになるとドメインに対して色々な意見が出るようになりドメインの精度が上がったように思いました。 聴講者として参加 同じくProduct Dev Divisionに所属している 大津 です。 今回のドメインモデリングを見て、「ドメインモデリング自体がドメインを理解する行為である」ということが腹落ちできました! 当日の Slack の一連の流れを見ても今回のテーマである BASE の決済に対して理解を深められた参加者が多く、現状の認識のすり合わせとしてドメインモデリングが有効ではないかと感じました! 参加者で記念撮影 最後に 改めて、モデリングデモ会を通じてモデリングとはこんな風にやるのかと具体的なイメージを持つことができました。講師としてモデリングのデモを行っていただいた松岡さん、本当にありがとうございました! BASE では複雑な事業領域に正面からタックルしてユーザーのためにサービスを一緒に開発してくれる仲間を募集しています! まずはカジュアル面談でお話できることを楽しみにしております!よろしくお願いします! open.talentio.com open.talentio.com
こんにちは。BASEの藤川です。 緊急事態宣言も続く状況下で、当社もリモートワーク(Work From Home)中心の仕事の進め方をしています。ネット系企業は、幸いにしてVPN、Slack、GitHubやドキュメント管理ツール、その他仕事に必要なSaaSやZOOMがオンライン化しているため仕事の作業そのものは、それほど違和感なく自宅からでもできているのではないかと思います。 でも、仕事というのは作業だけで済むものではありません。業績を上げるための作業を生み出す活動を始めとする考えるタイミングであったり、不確実なものを埋めていくためにお互い議論するタイミングなど、曖昧なプロセスの先に、決定をして作業の的を絞り込んでいくプロセスが不可欠で、ここで複数人のチームワークが不可欠です。 今、一緒に仕事をしている仲間においては、コロナ以前から社内で人間関係を構築済みの人と、コロナ禍においてリモートだけで人間関係を構築している人と二分されています。当社の採用活動でも緊急事態宣言が出てからは、オンラインコミュニケーションのみで内定を出しておりますた、他部署の方などで最短で退職されてしまうと、せっかくそこまで大きくない会社にも関わらず一度も会うことなく去ってしまう人もでてくる可能性があるという状況です。 そのため一日でも長くいただくために、入社される方へのオンボーディングおよび心理的安全性、チームの信頼関係の構築に対するケアは、マネジメントにおいても最重要項目として常に議論をしています。 そもそも、オンラインコミュニケーションにおいては何が足りないのだろうか?と思っていたのですが、一言でいうと、 良い思い出を作るイベントが圧倒的に少ない ではないか?という事を考えています。 オンラインコミュニケーションで起きていたことが職場でも起きる時代 僕は高校一年生の頃からパソコン通信をやっており、オンラインのコミュニケーション経験は長いです。オンラインではパソコン通信でも、その後のインターネットの世界においても、新しいネットワークが大きくなる時に、必ず見かけてきた風景として、 オフ会によって人間関係の距離が変わる オフ会を開催して、誰かと誰かが仲良くなって重鎮化していき、ある種の既得権みたいなのが生まれ、また新しい人が入ってきて新しい関係性が生まれる、常に人間関係のシャッフルが生まれ、コミュニティの歴史が長くなってくると、必ず起きる問題が、 新参者が入れない問題 というのが起きます。 よく組織設計の話で、ダンパー数というものが語られることがあります。 ・5~9人=「社会集団(クリーク)」…最も親しい友人やパートナーの数 ・12~15人=「シンパシー・グループ」…ほぼどのような状況下でも心から信頼できる人の数 ・30~50人=「一団(バンド)」…危険な国を安全に往来できる小さな団体 ・150人=「フレンドシップ・グループ」…共同体の中で一緒に暮らすのに最適な人数 ・500人=「部族・種族(トライブ)」…出会うと会釈する程度の顔見知りの人数 ・1500人=「共同体(コミュニティ)」…人間の長期記憶の情報数の限界、頭の中で名前と顔が一致する人数 出典:チームを成功へ導く魔法の数字たち 「7」「30~50」「150」 https://www.sankeibiz.jp/smp/workstyle/news/190401/wsa1904010700003-s4.htm ダンパー数は組織の人数規模を考慮する時に参考にする数字なのですが、肌感覚としても一致はしていて、小規模のコミュニティにおいては、お互いの距離感は均等に短く、何もしなくても親密になりやすいです。これが人数が30〜50人であるとか、100人を超えてきて、なおかつ参加したタイミングがバラバラともなれば、相互の距離感が不均衡になりがちです、そして、不均衡が生まれれば生まれるほど、既に存在する人間関係や情報の非対称性の中で孤独感を感じる人が出てきます。 特に何十人の規模のオフ会ともなると、人脈を持つものと持たざるものの差は大きく、また、それを切り開くだけのコミュ力や、そのコミュニティで名を売るんだぞ!という根性を持ち合わせていないと、家に帰った後にどっと疲れて、このコミュニティは内輪ばっかりで、疎外感に満ちあふれていたなどの言説が駆け巡ってトラブルになることを、この目で散々見てきました。 こう言ったことが、SlackとZOOM主体のリモートワークの働き方においても適用されるのではないかと考えています。 以前入社した100人ぐらいの会社が、実は1000人以上のグループの一社だったということを入社日に参加したグループ全体の定例会議で知ることになり、なんか大企業に来ちゃったなぁということに疎外感を得たことがあります。自分のアクセス可能なネットワークと、手の届かない全体感の規模感のギャップというのはストレスを感じるという経験をしたことがあります。 そのような会議の後に、心の支えになるのは隣の席の同僚だったり、同期入社した人だったり、即座に小さなコミュニティを構築し、雑談をすることが心のクッションになったりします。 しかし、コロナ禍においてフルリモートのような状態で働くと、自分の部屋でパソコンを閉じても、隣の人と雑談をして共通見解などの意見を交わして、息を抜く所がありません。まして真面目に自粛していて、外にも全く出歩かない独身の方だと、プライベートのネットワークが補完することもできず、どうにも仕事モードを発散できないような人もいるのではないでしょうか。 職場で「何も考えずとも」得られていたもの そういう時に起こりうる疎外感や寂しさと言った無用な感情を埋めてくれる存在は、同じ部署のメンバーやプロジェクトのメンバーなのですが、まだ人間関係ができていないと難しいですし、仕事の中で培われるというよりは、一緒にランチに行く時に路上やエレベーターで会話するシーンや、タバコ部屋の議論とか、仕事の隙間に培われていいた何かというのは確実にあったような気がします。 このようなタイミングで培われるものを「遊び」と定義してみます。 仕事は何かの目的を持って、無駄なく最適化された活動をすることが生産性が高いと表現されます。それが有能な会社員の行動であれば、無駄を許容することはその逆になるので、一人でやるのは難しいです。 Slackで雑談するにしても、相手にしてもらえる人がいればこそ。まして、一緒にタバコを吸いに行くとか、コーヒーを飲みながら雑談するとか、仕事終わりに飲みに行くなど、最適化された活動以外のものは、概ね気の合う数人レベルのマイクロコミュニティとして行われるものではないでしょうか。 ZOOMの活用で、会議室の移動は一瞬でできるようになり、仕事そのものは大変効率化されました。時間ギリギリまで会議していても、10秒後には次の会議室に入ることができ、我々は仕事マシンとして仕事をし続けられるようになりました。その一方で、移動のタイミングで生まれていた、アジェンダのない会話 = 「遊び」は消え去りました。そこで副次的に得られていた相手との心理的安全性や、仕事をする意義みたいなものを学ぶために、実は大切なものではなかったのか?というのが仮設になります。 更に、この会社で働いてよかったこと、自分の成長につながったなぁという手応えは、なんだかんだと新機能のリリースイベントであったり、会議室での議論でのお互いの顔の表情であったり、ホワイトボードに書いたものという手触り感、プロジェクトの打ち上げでの会話、メンバーの笑顔など視覚情報によって脳内に記憶が定着され、それ故に実感を得られていたのはないか?と思うことがあります。 この思い出こそが、10年後20年後に振り返った時に脳の中に残っていることが人生にとって大切なのではないか? 残念ながら、これはZOOM上のバーチャル背景の集合体では得られにくいのではないでしょうか?この1〜2年で働いていたことは人生の記憶やストックとして積み上がるのだろうか。 このような実感を得られるイベントを、オンライン主体の変わりなき日常風景の中で、どうすればみんなが実感できるのだろうなぁというのを常に考えています。何か工夫をされている会社さんやマネージャの方がいたら是非教えてほしいです。 オンラインで「仕事の思い出」は生み出せるのか コロナ以前であれば、合宿、社内イベント、飲み会などは、それらを補完し、思い出を業績に変えていく装置だったように思えます。打算的に言うと、あえて遊びを作ることで、仕事の生産性を生み出す装置であったという見方ができます。 組織を語る表現において、歯車というのはネガティブな表現とされますが、歯車と歯車の間には遊びは必要だし、それらが適切に回って全体が駆動されなければ、そもそも良いプロダクトは作れないのです。 こう言った遊びの構築を、それをオンラインでは補完できるのだろうか? 間違っても、その処方箋はZOOM飲みではないなとは個人的な感覚としてはあるのですが、無駄や行間をあえて作って、そこから何かを生み出していくというのは、全員に高度なネットリテラシーが問われるようにも思えます。 もしかしたらヒントは、ツイキャスや17LIVEのようなライブ配信の関係性にあるのかもと思わなくもないのですが、とりあえず今回は、Youtubeの動画を作ってみて、こういう考えを深めるきっかけを作ってみました。 こちらの動画は、採用候補者の方にBASEを検討する際に見ていただきたい動画として、BASE社におけるPHPの現在・過去・未来というタイトルで作っています。今回は、当社の社員として活躍いただいているyakkunへのインタビューという形で収録したのですが、更に、最後のサイドトークに今回のリモートワークについての話にも言及しています。 www.youtube.com 今後も当社のメンバーとこのような会話を公開用のコンテンツとして作っていくことで、1on1とはまた違った思い出が作れないかと思って作っています。全社員できるかな?! 今回の内容は「最近は他の言語がメインにしてきた人がBASE社でPHPを使う気持ち」という長く続くことに成功したサービスに携わる人達であれば共通の経験がある話をしています。 ラジオ感覚で聞ける話ですので、もしよかったら是非見てみてください。
こんにちは。Product Dev Divisionに所属している 大津 です。 PHPカンファレンス2021 のトーク募集が始まりましたね。 僭越ながら、私は過去にPHPカンファレンス 2019 と 2020 に2回登壇したことがあり、今年もトーク応募をしてカンファレンスを盛り上げていこうと思っています! そこで今後登壇する人のお力に少しでもなればと思い、トークするまでの流れを私なりにまとめてみました。 ぜひ参考にしてみてください! 話したい内容を軽く決める まずは話したい内容をなんとなく決めます。 これは普段から思っていることでも良いですし、社内向けドキュメントとして書いた内容でも良いです。 はたまた、誰も話してなさそうなテーマだから自分が話してみよう!みたいな動機でも良いです。 大事なのは自分がトーク準備の過程を楽しめそうなテーマであるか?ということです。 ターゲットを決める どんな人に向けてトークをするのか具体化しましょう。 例えばWebエンジニアがターゲットだとしても、初心者〜上級者、フロントエンド〜バックエンドなど色んなセグメントがあります。 私はトークを聴く人のペルソナのようなものを、なんとなく作っています。 どのくらいの知識を持っているか どんな課題感を持っているか 自分が話したいことを知ることでどんな状態になるか 例えば私が過去に話した PHPerのためのテストコード入門 では、以下のようなペルソナを考えていました。 参考にしてみてください。 ex.「PHPerのためのテストコード入門」ターゲット1 職業プログラマなりたての新人エンジニア テストコードを言われるがままに描き始めているが、テストコードを書くモチベーションが湧かない 発表を聞いてテストコードを書く理由を知ることで、意味があることをしているんだとモチベーションを高めたい ex.「PHPerのためのテストコード入門」ターゲット2 そこそこコードを書いているエンジニア とりあえずテストコードを書け!と言っているが、何故テストコードを書かなければいけないんですか?と言われると言葉に詰まる 発表を聞いてテストコードを書く意義を伝えられるようになりたい ターゲットに合わせた登壇内容を決める ターゲットが決まったら、ターゲットの達成したい目的を果たせるような登壇内容にしてみましょう。 例えばターゲットが職業プログラマなりたての新人エンジニアであれば、一部のマニアしか分からない内容を書く必要ありません。 逆にベテラン向けの話であれば、初歩的なことはある程度省いても良いでしょう。 この段階で、話すこと話さないことが決まるので、トークの最初に話すこと話さないことをスライドに載せても良いかもしれません。 プロポーザルを出す トーク内容が決まったらプロポーザルを出しましょう。 プロポーザルとは、自分が話すトークの概要です。下記のようなトークの募集が始まった際に、プロポーザルを出します。 PHPerKaigi 2021 PHPカンファレンス 2021 例として僕が今までに出したプロポーザルのリンクをいくつか貼ります。 拙筆ではございますが、どれも採択されたプロポーザルです。参考にしてみてください。 テストピラミッドを意識したテストコード実装戦略 PHPerのためのテストコード入門 リーダブルコミットのすゝめ よくある採択されないプロポーザルについては、こちらのスライドが良くまとまっているので読むと良いでしょう。 プロポーザル・アンチパターン - Speaker Deck プロポーザルの構成についてはこちらのスライドが参考になります。 登壇の可能性をあげる!カンファレンスプロポーザルの書き方のススメ - builderscon::blog また、過去に採択されたトークを見て分析するのも良いでしょう。 トークの採択をするのはカンファレンススタッフですが、スタッフも人間である以上カンファレンスが盛り上がるかということやそれぞれの好みを軸に選ぶはずです。 例えば、10月に開催されるPHPカンファレンス 2021では、採択に関するルールで 「採択者はPHPプログラマー歴が長い人から2年程度の短い人まで、過去登壇してくれた方を中心にお願いしております。」 と書いてあります。 余裕があればこの辺りも見てみましょう。 トーク内容と構成を練る 無事採択されましたら、いよいよトークの内容を考えていきます。 私からトーク内容を練る上で、重要な点をいくつかまとめます。 十分に時間を割こう 目安ですが、私はスライド作成や発表練習などトークの準備時間を最低でもトークする時間 × 120分用意しています。 例えば、25分の発表であれば、25分 × 120分で50時間準備時間に費やしています。 ※LTはこの限りではありません 気をつけて欲しいのは、ここにはデモなどの環境を用意する時間は含まれていません。 作成する内容に合わせて、追加で時間を設けると良いでしょう。 想定外を考慮する 資料を作っていると、以下のようなことに陥ることがあります。 プロポーザルに書いた(想定していた)内容と違う点を見つけてしまった 調べれば調べるほどよく分からなくなり、何もわからんという状態になる 今伝えたいことが、今回のトークの時間だけでは足りないことに気づいた この辺りはプロポーザルを書いた時点ではわからない不確定要素です。 できるだけ不確定要素は取り除いた方が良いものの、想定外はあるという前提で資料作成のスケジューリングをした方があとあと自分が楽になるかと思います。 1回トークのレビューをしてもらう 資料がある程度完成したら、一度他の人の前でトークしてFBをもらいましょう。 誤字脱字などがないか スライドに書いてある内容が本当に正しいか 聞いている人が理解しやすいような構成、流れになっているか トークの時間に間に合う分量か 喋りのスピードは適切か 一番良いのは、トークのターゲットにあたる人と今回のトークテーマについて詳しい人へレビューをお願いすることです。 企業に所属している人は、スライドのレビューもしてもらう スライドは、トーク中だけでなくトーク後のことも考えて作成しなければなりません。 基本的にトークで使ったスライドは、発表後も多くの人の目に触れます。 カンファレンスがトークの録画をしていて、後日カンファレンスに参加していない人や別のトークを聴講していて見れなかった人が動画を見る 公開したスライドのURLが拡散されて、カンファレンスに参加していない人がスライドを見る 内容が企業にとって不都合がないか、広報へ確認の依頼を出しましょう。 また企業によっては、指定したスライドテーマを使うよう指示されることもあるかと思います。 その際には、作成したスライドが企業のブランドポリシーに反していないかのチェックをお願いしましょう。 カンファレンス当日にトークをする いよいよカンファレンス当日です。 トークをするにあたって、考えておきたいのは質疑応答です。 事前に「こんな質問が出そうだから、その時はこう回答しよう」とかある程度考えておくと良いでしょう。 また、答えられない質問やいわゆるマサカリが来たときは、素直にわからないということを伝えましょう。 この辺りの話は、以下のスライドへ譲ります。 LT・登壇でマサカリに備えるたった一つの心得 トーク終了後 登壇お疲れ様でした!トーク後にぜひやってほしいことが2点あります! スライド共有サービスにアップロードしよう トークで使ったスライドを、slideshareやspeakerdeckなどでアップロードしましょう。 スライドのURLをツイート等すると、トークを聞いてくれた人だけでなくトークを聞いていない人の目にも触れるチャンスになります。 参考:大津のspeakerdeck また、自分が登壇した内容のスライド一覧は、自分のポートフォリオの一部として活用できます。 ぜひ自身のブランディングのためにもアップロードしていきましょう。 懇親会にはできるだけ参加しましょう 懇親会では、スピーカーは比較的声をかけてもらえやすい立場にあります。 話す内容はトークのFBだったり、質疑応答だったり…トークした内容について貴重なご意見をもらえるチャンスでもありますので積極的に参加しましょう。 ただし体調不良だったり先約がある場合は、そちらを優先しましょう!(無理せず!) 前日まで徹夜でスライドを作っていたりすると、この懇親会がしんどくなり良いチャンスを逃してしまうかもしれません。 懇親会の参加も含めて、余裕を持ったスライド作りのスケジューリングをしましょう。 最後に トークするまでの流れについてご紹介しました! ちなみに弊社のSlackでは、外部カンファレンスへの登壇についてワイワイする #iikanji-conference-toudan というチャンネルがあります。 今後も外部カンファレンスへの登壇に向けてワイワイしていきます!
この3ヶ月で行ったBDIの内容を紹介します こんにちは、デザイナーの河越です。 BASEのデザインチームが月2回行っている社内勉強会「BDI」。リモートワークになってからも継続的に開催しています♪ 今回は4月~6月に開催したBDIの内容をご紹介したいと思います! BDIとは? 『BDI』は「BASE Design Inspiration」の略。 2018年の秋頃から活動している、デザイナーがやりたいことを持ち寄って、 デザインに関する幅広い知見をみんなで楽しく学ぶことを目的とした任意参加の社内勉強会です。 BASEのデザイナーであれば、デザイナーだけでなく誰でも参加することができます。 Inspirationの名の通り新たなひらめきにつながる新しいトピックを取り上げることも多くあります。 1月~3月の振り返りはこちら devblog.thebase.in BASEのデザイナーがどんな活動をしているのか気になっている方に読んでいただきたいのはもちろん、 リモート環境での社内勉強会のネタ探しにもぜひご活用くださいね! 4月 発想メソッド「マンダラート」でアイディアを湧き出させる会 4月最初のBDIは、アイディア出しのフレームワーク「マンダラート」を実践で使う企画からスタートしました。 マンダラートとは、一つのキーワードに対して8つのマス目を作り、そのマス目一つ一つにアイデアを書き込むことで目標を達成するためのアイディアを見つける手法。 あの大谷翔平選手も目標設定のために使っていて、現在注目が集まっているフレームワークの一つだそうです⚾️(私は初めて聞きました!) BDI用にアレンジしたマンダラートでは、デザインリサーチPJメンバーのショップオーナーインタビューの報告をもとに、マンダラートを使って『オーナーズが持っている課題に対してどのような解決策があるのか』をアイディア出ししました! デザイナー以外も参加して楽しめるように、Miroを使ってワイワイと。 最終的には出てきたアイディアを発表し合うことで、オーナーズへの知見が溜まるだけでなくメンバーへの理解も深まった会でした。 社内で「ものづくり」をしている人のお話を聞くLT会 「今年のGWもおうちにいる時間が長そうだね」ということで、ゴールデンウィーク直前に実施した社内LT会。 おうち時間を楽しく過ごすヒントをもらうべく、社内でものづくりをしている3名の方にお話してもらいました! 3名はそれぞれ、ボードゲーム・ペーパーアイテム・家具を作っていて、 なぜものづくりをしているのか どうやって作っているのか 今後の展望は? と、いう角度からのお話を聞きました。 ものづくりへの熱い思いのこもったLTで「売上をやみくもに上げるより、自分の作品を好きになってもらいたい」「角度にこだわり抜いた座椅子を作って自分の生活を豊かにしたい」といった色々な価値観を知ることができました。 今回の3名だけでなく、オーナーさんが持っている考えとも通じるところがありそうで、ユーザーへの理解が深まる会となりました。 何よりLTをしてくれた3名のこだわりを知れて、作品のストーリーをより深く知ることができ、それぞれの作品のファンになりました...! GWでものづくりをしてみようかな、と考える方が増えたBDIになったかなと思います! 5月 Ownersになってみよう第2弾! ブランドのロゴを作ってみよう🎨 3月に開催した『Ownersになってみよう企画』をシリーズ化し、第1段で制作したブランドのコンセプトを元に、ロゴ作りのワークショップをやってみました。 ロゴはどんなプロセスで作られて、どんな所で使われるかのLT ロゴが使われるシーンをブレストしながら、コンセプトのブラッシュアップ ロゴのラフ案をたくさん作成するモクモク会 2回分のBDIの時間を使って、架空のブランドのロゴやシンボルマークを作成しました。 丁寧なLTがロゴ作成未経験者にもわかりやすい内容で、ブランディングの大切さ/大変さを実感できる回となりました。 フォント決めやロゴタイプのモチーフ決めには議論が活発化して2時間で納めるのは大変でしたが、複数人で集まって一つのMiroボードにモクモク作業する工程で「みんなでデザインするって楽しいな」と改めて感じました。 次回以降の『オーナーズになってみよう企画』ではパッケージや商品のデザインを進めていきます! 6月 「ページ追加 App」リリースまでのあれこれを聞く座談会 5月末にリリースされた「ページ追加 App」について、PJメンバーにリリースまでの道のりを語ってもらう座談会を開催しました。 https://baseu.jp/20203 PJを担当したPMやデザイナーからリリース前に悩んだことの振り返りや、リリース後のユーザーからの反応を聞きました。 機能を利用しているショップの事例を紹介するパートでは、想像以上に工夫して使ってくださるショップを知れてみんなで盛り上がりました! 事前に全社からPJメンバーに聞きたいことを募集して質問を選んでおいたので、より内容の濃いウラバナシを聞くことができたし、発表するメンバーの負担も少なくできたのではないかと思います! デザインツール「Figma」を使いこなそう!!!〜基礎編〜 BASEのデザインチームでも利用しているデザインツール「Figma」。 デザイナー以外のメンバーに向けて、Figmaの基本的な使い方や、Auto Layoutの組み方を学ぶ実践型のワークショップを開催しました。 Figmaマスターの渡邊さんがFigma特有の「Frame」という概念や、「Auto Layout」の組み方を丁寧に説明してくださいました。エンジニアからは「Flexboxに似ていてわかりやすい!」という声も。 資料作成にFigmaを使っているPMや、Figmaでデータを受け取るエンジニア陣から大好評の企画となりました! お互いの仕事についての理解が深まることで、よりスムーズなコミュニケーションが取れるようになりそうです◎ まとめ 月2回ペースで開催しているBDI。4~6月はLTや座談会を多く取り入れてみました。 結果としてデザイナー以外のメンバーもROM専で参加しやすくなり、参加者が徐々に増えて大盛況となりました。 PJやチームが違っても、こういった機会でコミュニケーションが取れる機会があるのは良いなと感じています。また、実況コメントを流しながらのLTなど、オンラインでもLT会を盛り上げる知見も溜まってきたので、参考になったら嬉しいです。 これからも楽しくてためになる勉強会を開催していきます。7月以降もお楽しみに!
こんにちは!! BASE BANK 株式会社 Dev Division にてSoftware Developerをしている永野( @glassmonkey )です。 普段はGo/Python/PHPを主に生業に開発・運用から何でもござれの精神でフルサイクルエンジニアをしています。 現在、自分たちのプロダクトである YELL BANK の分析基盤を構築しています。 その際に、BigQueryで扱っているデータをGoogle App Script(以下GAS)、Googleスプレッドシートとデータポータルで簡易CRMをビジネスサイドのメンバーである猪瀬 ( @Masahiro_Inose )と協力して作ったのでそのご紹介です。 いざ実施してみるとハマってる点もそこそこ多く、意外とGASやBigQueryの連携している情報が少なかったので、誰かの助けになれば幸いです。 thebase.in 簡易CRMツールをスプレッドシートベースで作成した背景 弊社で開発している「YELL BANK」では、プロダクト改善のためにユーザーインタビューなど定性情報を顧客に直接聞く形で得ていくスタイルを積極的に実践しています。 その記録を一元管理する場所がなかったこともあり、CRMツールが必要な状況でした。 CRMツールとしてはSalesforceなどが有名ではありますが、様々なデータがBigQueryに集約をする状況でありました。 今回は以下の2点で慣れているスプレッドシートベースのシステムの採用をしました。 プロダクトとして仮説検証フェーズなので見たい内容は定まってないこと データ分析を主として実施しているビジネスサイドのメンバーが自身でカスタマイズ容易なこと 謝辞 一部改変をしておりますが、コードなどは 【GAS/BigQuery】日付に応じて異なるクエリを実行 から大部分を引用させていただきました。 この場を借りてお礼を申し上げます。 構成 GAS周りの基盤を私が作成して、集計のスプレッドシート構成からダッシュボードの構成まではビジネスサイドのメンバーで分担して作成しました。 簡易CRMの構成図 準備 GASのメンテは基本的には普段コードを書かないビジネスサイドのメンバーが扱うので、都度追加するコードはシンプルである必要がありました。 そこで、OAuth認証に関連するところやAPI経由でBigQueryを実行する処理は共通処理として切り出すことにしました。 GCPプロジェクトの準備 GASからBigQueryをOAuth経由で実行するので、その設定が必要です。 今回は内部的に利用するので、UserTypeは内部を選びます。 0auth同意画面 アプリ名とサポートメールとデベロッパーの連絡先情報を入力します。内部用なので適当で良いです。 Oauth設定画面(メールアドレスなど) BigQueryのAPIが有効になっているか確認します。 このあとAPIを利用するので、もし無効になっていたら有効にしておきましょう。 BigQuery API確認画面 GASの共通プロジェクトの設定 まず最初に共通GASプロジェクト設定します。 GASのメンテは基本的には普段コードを書かないビジネスサイドのメンバーが扱うので、都度追加するコードはシンプルである必要がありました。 API経由でBigQueryを実行する処理は共通処理として切り出すことにしました。 認証用のOAuth設定を予め共通プロジェクトとの連携で行っておくことで、ビジネスサイドのメンバーが権限周りを考慮せずにすむようにもしました。 各種画面のスクリーンショットは2021年6月時点のものになります。 1. 新しいプロジェクトから共通プロジェクトを作成する 新しいプロジェクトを作成します。 新しいプロジェクトその すると下記のように無題のGASのエディターが開きます。 GAS エディター 2. ライブラリを追加する GASからスプレッドシートのアクセスを簡易的にしてくれる、gas-underscoreを入れます。 詳細は simula-innovation/gas-underscore を参照ください。 READMEから"プロジェクトキー"である M3i7wmUA_5n0NSEaa6NnNqOBao7QLBR4j を使うと良いとありますが、新エディターの場合使えませんので注意ください。 github.com 旧エディターを利用する場合 (非推奨) Add a libraryから"プロジェクトキー"である M3i7wmUA_5n0NSEaa6NnNqOBao7QLBR4j を入力します。 バージョンは最新の2を選びます。 なお、この方法はいずれ廃止になる可能性があるのでおすすめはしません。 新エディターを使う場合 左カラムのライブラリの + をクリック gas-underscoreの"スクリプトID"は 1PcEHcGVC1njZd8SfXtmgQk19djwVd2GrrW1gd7U5hNk033tzi6IUvIAV なのでそれを入力する。調べ方は後述します。 ] 調べ方としては、旧エディターで得た情報をベースにはなりますが、インストールするとGASのマニフェストファイルからスクリプトIDがわかるのでそれを利用します。 旧エディタの場合は 表示 > マニフェストファイルを表示 を選択すると見ることができます。 他にもプロジェクトキーしかわからないライブラリがある場合はこれを使うとわかるので便利です。 GASマニフェストファイルの一部 余談ですが、公式のリポのREADMEには一応 Pull Request を出しました。古いリポジトリなのでメンテされてない可能性が高いので、forkした 私のリポジトリ も記載しておきます。 github.com 3. サービスにBigQueryを追加する 今回はBigQueryのAPIを利用するので追加します。 1. 左カラムのサービスの + をクリック 2. BigQueryを選びます。バージョンは2021年6月時点で最新のv2を選択しています。 4. GCPプロジェクトと連携させる 歯車マークのアイコンからスクリプトの設定に遷移します。 プロジェクトを変更からGCPプロジェクトの連携設定を追加します。 ここでうまくいかない場合は、連携先のGCPプロジェクトのOAuth認証が正しく設定できていない可能性があります。 ここのプロジェクト番号とはGCPのダッシュボードのプロジェクト情報で確認できます。 プロジェクト情報 5. GASの共通コードを用意する。 集約したいスプレッドシートのID、シートの名前、実行したいクエリは都度変わるとのでそこを外部から渡せるように共通処理を書いておきます。 共通処理内部でBigQueryへのAPIリクエストを秘匿するようにしていので、ここはビジネスサイドのメンバーは意識しなくて済むようしておきます。 プロジェクト番号のところは、前述したOAuth設定したBigQueryを実行するgcpプロジェクト番号に読み替えください。 function run ( spreadsheetId , sheetName , query ) { /** * コンフィグ設定 **/ const projectId = 'プロジェクト番号' ; // 出力先シートSpreadsheet const sheet = SpreadsheetApp . openById ( spreadsheetId ) ; //出力するシート名 const workSpace = sheet . getSheetByName ( sheetName ) ; const request = { query : query , useLegacySql : false } ; execBigQuery ( projectId , workSpace , request ) ; } ; function execBigQuery ( projectId , ws , request ) { let queryResults = BigQuery . Jobs . query ( request , projectId ) ; const jobId = queryResults . jobReference . jobId ; //BigQuery実行 queryResults = checkOnQueryJobStatus ( projectId , queryResults , jobId ) ; //結果を取り出す const rows = getAllResults ( projectId , queryResults , jobId ) ; //結果を書き出す outputToSpreadsheet ( rows , ws , queryResults ) ; } function checkOnQueryJobStatus ( projectId , queryResults , jobId ) { var sleepTimeMs = 500 ; while ( ! queryResults . jobComplete ) { Utilities . sleep ( sleepTimeMs ) ; sleepTimeMs *= 2 ; queryResults = BigQuery . Jobs . getQueryResults ( projectId , jobId ) ; } return queryResults ; } // Get all the rows of results. function getAllResults ( projectId , queryResults , jobId ) { var rows = queryResults . rows ; while ( queryResults . pageToken ) { queryResults = BigQuery . Jobs . getQueryResults ( projectId , jobId , { pageToken : queryResults . pageToken }) ; rows = rows . concat ( queryResults . rows ) ; } return rows ; } // Clear sheet data and output BigQuery results to sheet. function outputToSpreadsheet ( rows , ws , queryResults ) { if ( rows ) { ws . clear () ; // Append the headers and return header cols. const headers = appendHeader ( ws , queryResults ) ; // Append the results and return all spreadsheet data. const data = appendResults ( rows , headers ) ; ws . getRange ( 2 , 1 , rows . length , headers . length ) . setValues ( data ) ; return data ; } else { Logger . log ( 'No rows returned.' ) ; } } function appendHeader ( ws , queryResults ) { const headers = queryResults . schema . fields . map ( function ( field ) { return field . name ; }) ; ws . appendRow ( headers ) ; return headers ; } function appendResults ( rows , headers ) { const data = new Array ( rows . length ) ; for ( var i = 0 ; i < rows . length ; i ++ ) { var cols = rows [ i ] . f ; data [ i ] = new Array ( cols . length ) ; for ( var j = 0 ; j < cols . length ; j ++ ) { data [ i ][ j ] = cols [ j ] . v ; } } return data ; } 詳細は後ほど記載しますが、各種呼び出しは以下のような形になります。 実際にシートに集計クエリを書くビジネスサイドのメンバーとしては、集計クエリをどこのシートに出力するかのみ考慮すれば良いようにしました。 function example (){ //シートID, シート名, クエリを指定する 共通プロジェクトの名前 . run ( SpreadsheetApp . getActiveSpreadsheet () . getId () , "集計結果シート" , `何か集計用SQL` ) } シートごとの設定編 共通プロジェクトのスクリプトIDをメモ 集計シートからBigQueryを実行できるように共通プロジェクト呼び出しをします。 共通プロジェクトのスクリプトIDをメモします。 スプレッドシートに紐づくGASを開く 集約用のデータを扱うスプレッドシートを用意します。 スプレッドシートの ツール > スクリプトエディタ からシートと紐づくGASを用意します。 スプレッドシートと紐付いているGASはコンテナにシート名の記載があるので、念の為確認するといいでしょう。 ライブラリの+から共通プロジェクトを呼び出しをします。 ここで呼び出しをすることで 共通プロジェクトの名前.run で共通処理を呼び出しできるようになります。 BigQueryを共通処理と同様にサービス設定に記載します。 呼び出し関数を用意する 何かの集計結果のSQLを集計結果シートに出力する場合は下記のように書くことで実行できます。 基本的にはここの実装はビジネスサイドのメンバーが書くことになるので、 集計したいクエリを書くことに専念できるような呼び出し方としました。 また、関数として切り出すとボタン配置などやデバッグがしやすいのでおすすめです。 function example (){ //シートID, シート名, クエリを指定する 共通プロジェクトの名前 . run ( SpreadsheetApp . getActiveSpreadsheet () . getId () , "集計結果シート" , `何か集計用SQL` ) } 呼び出し用のボタンを用意する gasの実行には色々方法がありますが、今回は不要な集計処理を走らせたくなかったのと必要なときに実行できるようにボタンを用意する方式にしました。 適当にボタンとなる図形を 挿入 > 図形描画 から配置します。 図形右上の︙からスクリプトの割当を選びます。 用意した関数名を入力します。 これが完了すると用意した図形を押下すると集計クエリが出力されるようになります。 結果と使用感 Google Data Studioの連携部分に関してはGoogle Data Studioで何を出すかで変わってくるので割愛します。 結果としてビジネスサイドのメンバーが作ってくれたダッシュボードの一部ですがご紹介します。 架空の数字ですが、イメージは伝わると思います。 簡易CRMのイメージ図 当初の目的通り、ユーザーインタビューなどの定性情報を得にいく時にも、顧客とのコミュニケーション記録を有効に活用できるようになったとのことでした。 過去にコミュニケーションしたショップがファネルの上位のフェーズになったといった変化を発見できるようになったとのことです。 感想 データ分析もより専門性のあるビジネスサイドのメンバーに委譲する流れができた点も良かったと考えています。このおかげで全体としては2週間ほどでCRMのシステムを構築できました。 BigQueryをGAS経由で呼び出せるといろいろ楽なので、誰かの助けになれば幸いです。 そのようなスピード感を持って一緒に開発するアプリケーションエンジニアやカスタマーサクセスのメンバーを募集中です。 open.talentio.com open.talentio.com
Owners Experience Backend Group で Engineering Manager をしています、炭田( @tac_tanden )です。2021 年 4 月末に『ユニコーン企業のひみつ ―Spotify で学んだソフトウェアづくりと働き方』という本が発売されました。 自分含め、多くのメンバーが「読んでみたい!」と話題になっていた中で、翻訳者の角谷さまのご厚意で献本いただいたので、遅ればせながらレビュー記事を書かせていただきます。 🦄 テック企業のみなさま、テックブログの記事の候補に一冊いかがでしょうか!!!q 🙏 » 🦄 書籍『ユニコーン企業のひみつ』を貴社テックブログでレビューしていただける企業さまを募集します https://t.co/TdfL58TwpS — Kakutani Shintaro (@kakutani) April 7, 2021 著者について この本の著者を見たとき驚きました。 "Jonathan Rasmusson(ジョナサン・ラスマセン)"。自分が好きで何度も読んだ『アジャイルサムライ−達人開発者への道−』の著者の、あのジョナサン・ラスマセンさんが書いた、Spotify での経験をまとめた本ということで否が応でも期待が高まりました。 実際、読み終わった後はジョナサン・ラスマセンさんがつぶさに観察した Spotify での開発チームや文化が完結にまとめられていて、読んでいてすごくワクワクしました。 この本のテーマ 冒頭の『日本の読者の皆さんへ』の章でジョナサン・ラスマセンさんは以下のように書かれています。 『アジャイルは今や「ふつう」になりました。(中略)本書を『ユニコーン企業のひみつ』と題したのは、ものすごく成功しているソフトウェア企業(Spotify、Amazon、Googleなど)は「アジャイルでいつもやっていること」を超えた先のやり方を見つけ出しているからです。』 (vii 日本の読者の皆さんへ) ものすごく成功しているソフトウェア企業(Spotify、Amazon、Google など)はアジャイルを超えたさらに一歩先を進んでいる。その一歩先を Spotify はどう歩んでいるのか。知りたくないかい?という風に自分は理解しました。 これだけでなんだかワクワクしてきますよね。自分は Web アプリケーションエンジニアとしてずっとサービス開発に携わってきた中で、アジャイルでの開発の有効性について理解しているつもりです(スクラムマスターの資格を取りに行ったくらいアジャイルやスクラムの考え方が好きです)。 アジャイルのその先の一旦を垣間見れるということで、冒頭からフツフツとテンションが上がっていき、ページ数がそれほど多くないコンパクトな書籍ということも相まって、一気に 2 時間ほどで読み終えることができました。 アジャイルのその先 〜 Spotifyモデル 〜 Spotify モデルは自分自身はこの本を読むまで恥ずかしながら知りませんでした。2012 年(約 10 年も前なのですね...)に、Henrik Kniberg と Anders Ivarsson によって "Scaling Agile @ Spotify with Tribes, Squads, Chapters & Guilds" として発表されたのが初出のようです。 https://blog.crisp.se/wp-content/uploads/2012/11/SpotifyScaling.pdf Spotify モデルについて他に詳しく解説されている記事がいくつもあるので、ここではこれくらいにとどめ、自分はこの本の中でいくつか気になった Spotify の開発組織やスタイルについて取り上げたいと思います。 スクワッド 日本語で分隊と訳されることが多い squad ですが、もともとの語源は square(四角)です。昔、銃などの武器がなかった時代に、軍隊では四角い陣形を組んで戦ったことが多かったことに由来するそうです。自分はここから、一致団結して集団としてフォーメーションを組んで開発に立ち向かう姿をイメージしました。 また、この本からスクワッドはチームというよりは小さなスタートアップに近いように感じられました。小さなスタートアップ企業として、与えられた領域の全てに責任と権限を持ち、与えられたミッションを達成するために高速に開発イテレーション(リリースと検証)を回していく。理想のチーム形態の 1 つだと思います。 Spotify ではこのスクワッドが何をするにしても基本となり開発が進むようです。色々なスキルや経験をもった良き仲間とスクワッドを作っていく過程は楽しそうですよね。 ちなみに BASE でも企画・機能の策定から動作確認、リリースまで責任をもって行う複数の役割のメンバーで構成されたチーム単位で機能の開発をしているので、近い部分もありつつ違いも多かったので、スクワッドについて解説している第3章は特に興味深く読ませていただきました。 devblog.thebase.in カンパニーベット カンパニーベットは会社で取り組みたい最重要事項を並べた ToDo リストです。 会社で最もフォーカスしたいものを、全社向けの OKR のような形で表明する場合もありますが、「ベット」(賭ける)のほうが重要度に対して生々しさを感じてネーミングとして、素敵だなと思いました。 また、この章で紹介されている DIBB(Data, Insight, Belief, Bet)も、いわゆる OODA ループ(Observe, Orient, Decide, Act)をより具体化したものになっていて、過程がはっきりイメージしやすいので、チームに取り入れやすいのではないかと感じました。 会社やチームに限らず、時間は有限ですべてのことを完了させることはできないので、どこかでやることやらないことを分ける必要がありますが、カンパニーベットと DIBB は得られる結果がとてもシンプルになるはずなので、面白い仕組みだなと率直に思います。 会社やチームだけでなく、個人でも何かの目標ややりたいことに対して試してみるのもありなのではないでしょうか? まとめ 以上が、簡単ではありますが自分が『ユニコーン企業のひみつ』で紹介されている Spotify モデルの一部です。この本では他にもスタートアップとは何かや、開発組織をスクワッドを使ってスケールさせる方法、Google や Apple などとの文化の比較も掲載されていて、単純に読み物として面白かったです。 また、説明も具体的にかかれているので、気になった部分や仕組みを一部取り入れてみることもしやすいのではないかなと感じました。 注意点 この本の英語版が世に出たのが 2020 年 3 月です。また、ジョナサン・ラスマセンさん自身はすでに Spotify を退職されているようで、この本の原型といえるようなブログポストが 2017 年 11 月投稿されていました。 The Spotify Playbook /* */ agilewarrior.wordpress.com つまり、この本で描かれている Spotify の文化や開発スタイルは少なくとも 4-5 年前のもので、2021 年の今では Spotify は別の開発スタイルになっていてもおかしくないですね。 実際、冒頭で紹介した Spotify モデルを初めてまとめた "Scaling Agile @ Spotify" にはこう書かれていました。 Disclaimer: We didn’t invent this model. Spotify is (like any good agile company) evolving fast. This article is only a snapshot of our current way of working - a journey in progress, not a journey completed. By the time you read this, things have already changed. Scaling Agile @ Spotifywith Tribes, Squads, Chapters & Guilds p.1 この記事を読んでいるときにはすでにやり方は変わっているはず。終わりのない旅なんだ。 進化のスピードへの危機感を持つとともに更に刺激を受けました。また、Spotify でもやはり試行錯誤の連続なんだなと再確認できてよかったです。 最後に 今回、翻訳者の角谷さまのご厚意で献本いただき、拝読させていただきました。 この本がきっかけとなり社内でも開発スタイルや組織についての議論が活発になり、学ぶことが多かったです。改めて感謝申し上げます。 本当にありがとうございました。 お知らせ BASE 株式会社では、「BASE」の開発を一緒に盛り上げてくれるエンジニアを募集しています。一緒にかっこいい開発チームを作りましょう! ぜひよろしくお願いします! open.talentio.com open.talentio.com
こんにちは!アプリチームのEMをしている竜口です! 今回はリモート下でチームのコミュニケーションに課題があったので、それをどう改善していったかを紹介していきたいと思います。 初手、どうありたいかを決める やったこと いきなりxxxを始めます/やってみます!!と言ってもチームメンバーとしては意図、どうありたいかがわからないと、どう行動するべきかわからないと思います。 なので、何の為に改善して、どうありたいんだっけ?って部分を明文化しメンバーに伝えました。 実際はもっと肉付けしたものですが、下記がメンバーに伝えたものです。 1. 改善、課題解決していく 2. 開発をより早く、より確実にしていく 3. チームで開発していく 正直内容としては、特に真新しいものでもなく当たり前なものだと思いますが、今どのようなチームを目指しているのかというのを、メンバーと認識合わせました。 効果 個人的には、これが一番良かったかなと思っていて、この共通認識をもつことでメンバーも意識的に改善に向かっていってもらえたかなと思っています。 ツールに頼ってみる、tandem導入 やったこと tandem を簡単に説明すると A virtual office for remote teams を実現させるサービスで、 リモートで一緒に働く上でチーム感での会話のしやすさの実現、お互いの状態を緩く把握することで一緒のオフィスで働いてる感を醸成してくれます。 youtu.be 最初はアプリチームで導入し、その後アプリ開発に関わる他のプロジェクトメンバーも含めて導入しました。 効果 Slack or Zoomでコミュニケーションとってる時と比べ、ツールとしての会話のしやすさであったり会話できそうな状態か否かがわかりやすいのもあってコミュニケーションは増えていきました。 参考までに実際のtandemの様子ですが、各部屋に誰がいるのか分かるとこや、特定の人が緑色の場合はactive/オレンジだとinactiveという風に話しかけて良さそうな雰囲気も醸成できて話しかけるハードルや他の人の話してる雰囲気が伝わったりします。 朝会の後に意味のない時間を作る やったこと 毎日、朝会と言ってやること/困りごとを共有する場を15分程設けています。 その中で朝会後にすぐ抜ける必要のない運用にしてみました。朝会が終わってすぐ抜けてもいいし、話題がなくてもただそこにいてもいいし、何か特定の人に聞きたいことあれば聞いていいし、雑談してもいい。 効果 雑談が増えたのと、朝会でみんなの時間取るほどではないしSlackで聞くほどではない疑問とかが自然と出てきてよいです。 このルールだとtandemからの抜けにくさが出るかなと思ったのですが、お昼ご飯時なのもあり抜けやすさがあり、そこもメンバーのお気持ち的にも良かったのかなと。 作業タイムを作る やったこと ただただtandemつなぎっぱなしで作業する時間を2時間/1週間入れてみました。 ただ同じ空間で働いてる感を作りたかったので、ルールとしては強制参加だけど話したりカメラ繋いだりは強制しない形でやってみました。 効果 雑談や軽い相談も出てきてよかったです。ただずっとオンラインで繋げることによる集中力下がるというフィードバックがあったり時間や運用の仕方は、まだまだ調整が必要な部分もあります。 またtandemに Crosstalk という機能があり、同じ部屋にいる特定の人と話す時、会話に関係ない人にはその会話が小さくしか聞こえない機能があり、会話の指向性がでて話題に関係ない人のストレスにもなりにくくよかったです。 まとめ 今回は比較的導入が簡単なものをやってみて、以前よりチーム内での会話が増えて、一人で問題を抱え込むような形は改善されたかなとは思います。 以下メンバーの生の声です! - 朝会の後、いま取り組んでる課題とかに自然と移行できていい - Tandemは、いまみてるGitHubのURLとかを自動で取ってきて表示してくれる機能があって、IssueやPRの話などをしやすい - 朝回の前はあえて話すことないかなーと思っていたりするけど、意味のない時間に入ると敷居が下がって話せたりする - Tandemは、いざってとき会話するハードルが低い。ちょっと確認したいけど込み入った問題で確認したいぐらいの温度感のときに会話しやすいのが助かる ただまだ改善の余地はあって、チームとしてプロダクトを成長させる為に勉強会等を今後やっていきたいと思います。 そして!今アプリチームでは一緒にプロダクトを成長させていける方を募集しています! iOS 採用情報 / カジュアル面談 Android 採用情報 / カジュアル面談
BASE株式会社 Owners Experience Frontend チームのパンダ( @Panda_Program )です。 BASE では BASE の UI を構築するための社内コンポーネントライブラリ「BBQ」を使ってフロントエンドの開発をしています。 BBQ は Vue2 + Storybook v5 で作成されています。現在、フロントエンドの有志たちで Storybook のバージョンを最新の v6.2 にする対応をしています。 この記事では、Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える方法を紹介します。 なお、本記事ではStorybook v6 自体の機能の説明や、 main.js や preview.js の書き方といった Storybook の環境構築の方法には触れません。 Storybook コンポーネントを v5 から v6 に書き換える ここでは button-group を v5 から v6 に書き換えた例を紹介します。 まずは v5、v6 の書き方をそれぞれご覧ください。その後、変更点をそれぞれ解説をしていきます。 v5 の button-group.stories.js // bbq/stories/elements/button-group.stories.js import { action } from '@storybook/addon-actions' import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import README from '../../elements/buttonGroup/README.md' import { devices } from '../../values/Devices' // Storybook コンポーネント名 const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories // addon-knob .addDecorator(withKnobs) // addon-info .addDecorator(withInfo) .add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // Vue Template template: ` <div : class = "'theme-'+device" > <p> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </p> <p> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </p> </div> `, // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] } , } , // methods methods: { change: action( 'change' ), } , } } , // Parameters { notes: README, } ) v6 の button-group.stories.js // bbq/elements/buttonGroup/button-group.stories.js import { devices } from '../../values/Devices' import ButtonGroup from './ButtonGroup' import README from './README.md' export default { // Storybook コンポーネント名 title: "V6/Elements/ButtonGroup/Vue" , // import したコンポーネントを指定 component: ButtonGroup, // parameters parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , argTypes: { // addon-knob の select で定義していた変数 device: { options: devices, defaultValue: devices [ 0 ] , control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } , // addon-action で定義していた関数 change: { action: 'changed' } } } ; const Template = (args, { argTypes } ) => ( { components: { ButtonGroup } , props: Object .keys(argTypes), template: ` <div : class = "'theme-'+device" > <div> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </div> <div> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </div> </div> ` } ); export const Default = Template.bind( {} ) Default.args = { // Default コンポーネントに与える Props selected: 0, tag: "ul" , icons: [{ icon: 'list' } , { icon: 'grid' }] , labels: [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] , iconsAndLabels: [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] , } ; なお、 devices の定義は const devices = ['pc', 'sp'] です。 Storybookv6の変更点 上記、新旧ファイルの変更点を抜粋してコードを比較します。 Storybook 上のコンポーネント名 Storybook で表示されるコンポーネント名の定義方法の変更点です。 // v5 import { storiesOf } from '@storybook/vue' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) // v6 export default { title: "Elements/ButtonGroup" , // ... } v6 では @storybook/vue を import する必要がなくなりました。その代わりに、default export するオブジェクト内にコンポーネントのメタ情報を記述します。 表示するコンポーネントを指定 コンポーネントを指定する箇所も変更になっています。 // v5 import { storiesOf } from '@storybook/vue' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories.add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // ... } } ) // v6 import ButtonGroup from './ButtonGroup' export default { // ... component: ButtonGroup, } v6 ではコンポーネントを default export するオブジェクトのプロパティに追加します。 parametersを定義する箇所の変更 v5 では add メソッドの第三引数だった parameters の定義箇所が、v6 では default export するオブジェクトに変更になりました。ここではマークダウンファイル README.md を v6 で読み込める書き方を紹介します。 // v5 import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import README from '../../elements/buttonGroup/README.md' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories .addDecorator(withInfo) // decorator で addon-vue-info を活用している .add( 'ButtonGroup' , () => { ... } , { notes: README } ) // v6 import README from './README.md' export default { // ... parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , } v6 では Storybook 上の Docs タブで README.md を表示できます。このため、 @storybook/addon-notes 、 storybook-addon-vue-info は不要になります。 今回は v6 のコンポーネント内で定義しましたが、 preview.js に以下のように記述すると Storybook の全コンポーネントに parameters が追加されるため、 docs を各ファイルで記述すること避けられます( 「Migrating from notes/info addons」 )。 // preview.js import { addParameters } from '@storybook/client-api' ; addParameters( { docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } , } ); なお、今回は既存資産を活かすためにマークダウンファイルをそのまま使いましたが、 MDXを用いる方法も公式で紹介されています。 addon-knob の書き換え v5 では addon-knob を使うと Storybook 上でコンポーネントに与える値を画面上で変更できました。 v6 では addon-essentials に含まれている controls を使えば同様のことができます。 以下では knob の number 、 select 、 text 関数を書き換えています。 // v5 import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { devices } from '../../values/Devices' buttonStories. add( // ... data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , props: { tag: { default : text( 'tag' , 'ul' ) } , } , } ) // v6 import { devices } from '../../values/Devices' export default { // ... argTypes: { // select 関数で作成していた値 device: { options: devices, defaultValue: devices [ 0 ] , // 初期値の設定 control: { type: "select" } // この行は省略可能 } , width: { options: [ "" , "full" ] , control: { type: "select" } // この行は省略可能 } , } ; const Template = (args, { argTypes } ) => ( { ... } ); export const Default = Template.bind( {} ) Default.args = { selected: 0, // number 関数で作成していた値 tag: "ul" , // text 関数で作成していた値 } ; select 関数の代わりになる control: { type: "select" } で定義した値は、 defaultValue で初期値を設定できます。 なお、 export default の中で定義している device 、 width は以下のように Default.args で定義することも可能です。 // v6 Default.args = { device: devices, width: [ "" , "full" ] , selected: 0, tag: "ul" , } ; (参考: Dealing with complex values ) addon-actions の action 関数の書き換え addon-actions を使ったダミーのコールバック関数の定義方法も変更になりました。 // v5 import { action } from '@storybook/addon-actions' buttonStories .add( // ... () => { // ... methods: { change: action( 'changed' ), } , } } ) // v6 export default { argTypes: { change: { action: 'changed' } } } ; (参考: addon-actions ) ただし、以前のように action 関数を用いても問題なく動作するため、書き換えは必須ではありません。 コンポーネントに渡すデータの定義の変更 v5 で記述していた data, props, computed で定義していた値を Storybook の画面上で自由に変更したい場合は、 argTypes や args に集約可能です。 // v5 buttonStories .add( // ... () => { // ... // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] } , } , // ... // v6 export default { // ... argTypes: { // addon-knob の select で定義していた変数 device: { options: devices, control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } , } ; // ... export const Default = Template.bind( {} ) Default.args = { selected: 0, tag: "ul" , icons: [{ icon: 'list' } , { icon: 'grid' }] , labels: [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] , iconsAndLabels: [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] , } ; ただし、data や props、computed をそのまま残すことも可能です。その場合、 props 以外は GUI 上で値を変更できません。Storybook の GUI 上で変更したい値であれば、args で記述すれば良いと思います。 BASE BBQ の.Storybook では様々なパターンがあるため、まずは v6 の書き換えを優先しています。このため、data 等で定義している値は一旦 args に集約し、コンポーネントごとの細かい調整は個別に対応する予定です。 以上のパターンで BASE の BBQ で作成された Storybook コンポーネントの大抵のケースを網羅しています。 その他、より詳しい変更点は、 Storybook 6 Migration Guide をご覧ください。 addon について v6 で利用可能な Essential addons には、今までの主要な.addon の機能がまとめられています。 Docs Controls Actions Viewport Backgrounds Toolbars & globals Storybook で開発するにあたり、 @storybook/addon-essentials は開発体験を向上させてくれるため、v6 からは必須といっても過言ではないでしょう。 ここでは、先程紹介した例で使用している addon のみ取り上げます。 addon-knob v6 では deprecated addon-essentials の controls を代わりに使う addon-info knob と同様に v6 では deprecated addon-essentials の docs を代わりに使う addon-action v7 で deprecated になる予定 v6 ではまだ使えるが、可能なら control に置き換えると次のバージョンアップがスムーズになる v6 で addon-action を使いたい場合は、以下のように記述すれば OK です。 const Template = (args, { argTypes } ) => ( { // ... template: `...`, methods: { change: action( "changed" ), } } ); 表示を確認する v6 の書き方に変更したコンポーネントを実際にStorybook で表示すると以下のようになります。 StorybookのButtonGroup ンポーネント control で値を変更して様々な props の表示ケースを確認できるようになりました。 また、「Docs」というタブをクリックすると README が表示されています。 StorybookのButtonGroupコンポーネントのDocs これで v6 への書き換えが完了しました。 Storybook v6 で向上した開発体験 v5 と異なり、v6 では以下の点で開発体験が向上しました。 コンポーネントに与えるデータを Vue の外(args, argTypes)で定義できる args として定義した値は addon-knob を使わなくても Storybook 上で値を書き換えられる 上記の例では取り上げていませんが、args を変えることでコンポーネントのバリエーションを容易に作成できる( using-args ) インストールする addon の数や、stories ファイルのボイラープレートが減った おわりに 今回は Vue2 + Storybook v5 の環境で Storybook を v6 にアップデートする詳細な方法を紹介しました。 Storybook のバージョンアップにあたり本記事の内容が参考になれば幸いです。 多くの方はお気づきだと思いますが、v5 から v6 への書き換えといってもパターンが決まっています。 このため、手順さえ分かってしまえばプログラムで機械的に置換するだけで対応できます。 この考え方をもとに、TypeScript Compiler API を使ってメタプログラミングで v5 のコンポーネントを v6 に書き換えたので、次回の記事でその方法をご紹介しようと思います。
この度は、5/29(土)にオンラインで開催された PHP カンファレンス沖縄 2021 にゴールドスポンサーとして協賛し、また 4 名のメンバーが登壇しました。 登壇者 4 名から発表内容の補足など、PHP カンファレンス沖縄 の参加レポートをお届けします! phpcon.okinawa.jp 発表内容と補足 杉浦のセッション内容について こんにちは!BASE 株式会社でバックエンドの開発をしている杉浦( yutakasugiura )です。この度、PHP カンファレンス沖縄 で、BASE のスポンサートークとして「変化する時代のエンジニアリング」について発表させていただきました。 発表内容の意図 PHP に関するカンファレンスでしたが、スポンサーセッションの発表内容は PHP 以外のことでも良いとのことでしたので、広い意味でエンジニアリングについてお話ししました。普段の業務におけるエンジニアリングとは PHP のような言語が話題の中心ですが、せっかく頂いた機会なので、より大きな視点でエンジニアリングを語ることにしました。 登壇資料 発表内容の要旨 企業活動におけるエンジニアリングはマーケットニーズに従属的です。2010 年代に CakePHP がフレームワークとして支持されたのも「便利な web サービスを早く使いたい!」というニーズが背景にありますし、これほど AWS が広まった理由も「いつアクセスが爆発的に増加するかわからない!」という課題への対処として有効だったからです。スマホの普及によって、web サービスへのニーズが 2010 年代を通じて爆発的に増加したからこそ、これらのエンジニアリングが時代の潮流となったと言えます。 一方で、注意しなければならないことは、エンジニアリング単体で物事を考えると、ニーズにそっぽを向いてしまう可能性があると言うことです。純粋な研究開発は別として、企業活動の最終目的は、顧客にサービスを提供することにあり、その手段としてエンジニアリングがあります。考えてみれば当たり前の事実ですが、当たり前だからこそ、この単純な事実を忘れやすいというのも、また真理です。 web 業界は誕生してまもないこともあり、ニーズと技術が乖離してしまうという経験値が少ない業界です。ですが、歴史を広く見渡せば、時代の変化によって、顧客ニーズとエンジニアリングが乖離してしまい、ビジネスとしては幸せにならなかった例は枚挙にいとまがありません。 今回のセッションでは、1980 年代の DRAM(ダイナミック・ランダム・アクセス・メモリー)におけるエンジニアリングの失敗例を挙げましたが、1 つの技術に精通するエンジニアほど、ニーズに乖離してしまうというのは往々にしてあります。科学技術の世界ではパラダイムシフト、ビジネスの世界ではイノベーションのジレンマがよく知られますが、専門家であっても、常識の変化を機敏に感じ取ることは容易ではないのです。 2021 年の時点で web 業界は成長産業ですし、2040 年ぐらいまで急成長が続くことは、ほぼ確実な未来です。ですが、この疑いない状況が未来永劫、永遠に続くのか?と問われれば、その回答は「その限りではない」と断言します。 だからこそ、エンジニアは細心の注意を払って「このエンジニアリングによってどんなニーズが満たされるのか?」「顧客ニーズはどのように変化していくのか?」を、常日頃から考え続けなければならないのでしょう。 感謝 技術に関するカンファレンスの登壇は初めての経験でした。貴重な体験をさせていただき、関係者の皆様に御礼申し上げます!ありがとうございました! 炭田のセッション内容について こんにちは!BASE 株式会社の炭田( @tac_tanden )です。今回の PHP カンファレンス沖縄 2021 にて「PHP で throw しない例外ハンドリング」をテーマに 30 分間発表させていただきました。 セッション内容について PHP で throw せずに例外ハンドリングを行う事例をご紹介する前に、そもそも例外とは何かという部分を整理したり、PHP 以外の言語でどのように throw しないで例外ハンドリングを行っているのかを説明させていただいました。 特に、前半の例外の概念を説明するのに苦労し、発表でもわかりにくい部分があったかと存じます。例外については様々な意見や見解があるかと思いますが、みなさまの議論や考えの整理にこの発表が少しでもお役立てれば幸いです。 カンファレンスでの発表について 今回自分は初めてのカンファレンスの発表だったのですが、発表を通して色々な方と関わることができ、とても楽しいカンファレンスでの発表体験になりました! 発表中に twitter で様々なコメントをいただいたのも、すごく嬉しかったです。今後も機会があればぜひ発表したいと思いました。この場をお借りして、運用の皆様、参加者の皆様にお礼申し上げます。ありがとうございました! セッション動画はこちらになりますので、興味のある方ぜひご覧いただけると嬉しいです! https://www.youtube.com/watch?v=kOhsJCW9YIE&t=14093s 大津のセッション内容について 2021 年 2 月から BASE 株式会社に入社しました大津( @cocoeyes02 )です。PHP カンファレンス沖縄 2021 では Git のコミットにまつわるトークをさせていただきました。 また、セッションの動画はこちらになります。 コミットへのFBをもらうとGood 最後のスライドでこのセッションは自戒が含まれていると書いていますが、それは私自身過去にコミットによる FB をたくさん受けたことがあるからです。 例えば NG 例として挙げたコミットメッセージ「一旦コミット」「PR で指摘したので反映」「バグを直した」は、全て過去に僕が書いたコミットメッセージだったりします。このコミットメッセージが何故ダメなのか FB をもらって、ようやく理解できるようになりました。 コミットを見るのは他人と言う話をしましたが、リーダブルなコミットを書くことによるメリットを受け取れたか判断するのも他人です。今回のトークの tips を参考にしつつ他人から FB をもらうと、リーダブルなコミットを書けているのか判断できるので良い思います! スタッフの皆様ありがとうございました! 最初のオープニングから、最後の懇親会まで楽しく過ごすことができました! 今年はコロナ渦ということもあって、直接沖縄の会場へ向かうことは叶いませんでしたが次回は是非現地にて登壇したいと思っています! 東口のセッション内容について はいさい!BASE BANK 株式会社の東口 ( @hgsgtk )です。当カンファレンスでは、プロダクト開発のため E2E テスト環境を整備してきた中で、泥臭くなりがちなテスト環境・テストデータの考え方と工夫について発表させていただきました。 懇親会等、PHP コミュニティの方とも楽しくコミュニケーションさせていただきました。前回の PHP カンファレンス沖縄では直接沖縄現地に伺い、PHP コミュニティの方と直接話したりソーキそばなどを堪能したり楽しい時間でしたので、また来年・再来年とコロナが落ち着いてきた際に、直接沖縄の会場に伺えればいいなと思います。 最後に 今回弊社は計 4 名のメンバーが登壇して発表する機会をいただき、とても充実した時間を過ごすことができました! また自身の発表だけでなく、多くの方々の発表を通して様々な知識にふれることができ、各々が新たな知見や視点を持ち帰って来れたと考えております。 それもひとえに PHP カンファレンス沖縄実行委員会の皆様のおかげです。心より感謝申し上げます。 それでは、来年もまた皆様にお会いできることを楽しみにしております!
BASE株式会社Data Strategyチーム兼 Data Platformチームの楊(@wyang)です。 ショッピングアプリ「BASE」では、 前回公開した記事 の通り、商品検索基盤をCloudSearchからAWS Elasticsearch Serviceへ移行しました。 この記事では、レスポンス速度改善と検索精度改善をメインにご紹介します。 1. レスポンス速度改善について 1-1. ElasticsearchのProfile API この Profile API を利用すると検索クエリを分析し、どんなクエリが発行されたのか、どのくらいの処理時間がかかっているのかなどを簡単に知ることが出来ます。 GET items/_search { "query": { "bool": { "filter": [ { "range": { "date_field": { "gte": "now-180d" } } } ] } }, "profile": "true", // profile: trueで有効になる "size": 0 } BASEではAWS Elasticsearch Serviceを使っている都合上、X-Packの Search Profiler を利用することはできませんが、こちらではより手軽にパフォーマンス分析をすることができます。 1-2. 改善された変更点 1-2-1. 時間Rangeクエリの丸め込み BASEでは長期間でログインされていないショップの制御など、検索の要所要所でdate型データに対する絞り込みをしています。 GET items/_search { "query": { "bool": { "filter": [ { "range": { "date_field": { "gte": "now-180d" } } } ] } } } しかし、 Elasticsearch公式のドキュメント )にもある通り、時刻によるフィルタリングはfilter cacheに載らないため、毎回絞り込みが行われレスポンス速度が低速になる傾向がありました。 ドキュメントのガイドのように時刻を丸め込むことにより、filterクエリがfilter cacheの対象となり、レスポンス速度が大幅に改善されました。 GET items/_search { "query": { "bool": { "filter": { "range": { "date_field": { "gte": "1600819200000" } } } } } } 1-2-2. 必要最低限のフィルターを利用、不要なドキュメントの削除 Profile APIで時間がかかるフィルターにあたりをつけた後、検索インデックスで管理する必要がないフィールドとフィルターを削除しました。この部分は、インデックス更新バッチ側で制御しており、不要なドキュメントをそもそも登録しないなどの対応で、インデックスサイズを軽量化させました。結果として、クエリの軽量化にも繋がり、レスポンス速度が改善されました。 1-2-3. search_analyzerの使用 シノニム辞書を利用した検索を実現するために、インデックス時と検索時でanalyzerを分けて管理しました。 詳しい内容は Elastic社公式の記事 にもありますが、 インデックスサイズに影響が出ない。 用語の統計全体は同じに保たれる。 同義語ルールを変更するにあたり、ドキュメントの再インデックスは必要ない。 など、大きなデメリットはなく、こちらの設計を採用しています。 2. 精度改善について 2.1. A/Bテスト運用 Firebase Remote Config による検索の A/Bテストを実施しています。アプリのアップデートを配布せずとも、Remote Configのコンソール上で設定を修正するだけで、指定の比率でテストサイズを設定できるようになりました。 2-2. A/Bテストの評価指標 実際に改善に繋がったかどうかを以下の指標で判断しています。 商品閲覧につながった検索率 商品閲覧につながった検索のうち、商品閲覧位置(表示順位)の最小値の平均値 検索結果を1件も返せなかった率 検索結果のうち上位k件の商品閲覧率(SERP@k) 商品お気に入りにつながった率 検索実行からレスポンスを返すまでの時間 閲覧率だけでなく他の評価項目(商品の多様性など)も含めました。 2-3. これまでに効果があった変更点 2-3-1. 複数語検索時の絞り込み条件の緩和 ユーザーが複数語で検索した場合、語順を考慮する検索(type: phrase)と、語順を考慮しない検索(type: best_fields)では、語順を考慮した検索のほうが成績が良いことがわかりました。ただし、一部のキーワードの結果に対してはヒット件数が0件になってしまうため、ヒット件数が一定件数以下になった場合に、条件を緩和して語順を考えないクエリで再検索をするようにしています。結果、CTR改善に繋げることができました。 2-3-2. function_score BASEのおすすめ順では、ユーザが検索したキーワードに該当するドキュメントをスコアリングする際にfunction_scoreを利用しています。function_scoreにより、ショップ情報や、商品情報の複数の要素に対して、それぞれ重みづけをしたオリジナルのスコアを設定することができます。 GET items/_search { ... "function_score": { "functions": [ { "field_value_factor": { "field": "score_field_A" // スコアに関するfield } }, { "gauss": { "date_field_B": { // 日付に関するfield "origin": "now", "scale": "180d", "decay": "0.5" } } } ], "score_mode": "multiply", "boost_mode": "multiply" } } 上記のクエリでは、score_field_Aとdate_field_Bのスコアを乗算してスコアリングをしています。 gaussを利用する場合では、origin(now:現在時刻)を基準として、scale(180d: 180日)の日付差の地点に対して、decay(0.5)の減衰をする正規分布のスコアを設定することができます。 その他 日々登録される商品データのサイズや、更新反映までに求められる時間を考慮し、 Bulkで投入するリクエストデータのサイズ refresh_interval に対する調整をそれぞれ行い、効率的にドキュメントを更新できる設定にしています。 おわり 今回は移行後におけるレスポンス速度改善と検索精度改善についてご紹介しました。AWS Elasticsearch Serviceへの移行を行うことで継続的な検索性能の改修、改善をしやすい環境を作ることができました。これからさらなる改善を行なっていきたいと思います。