TECH PLAY

株式会社スタメン

株式会社スタメン の技術ブログ

231

はじめに こんにちは、スタメンでエンジニアをしている梅村です。20卒として昨年の4月から正式に入社し、もうすぐ1年が経とうとしています。そんな自分が「 達人プログラマー 」を読んでみた感想と、自分にどの部分が活用できるかを紹介していこうと思います。 新装版 達人プログラマー 職人から名匠への道 作者: AndrewHunt , DavidThomas 発売日: 2017/07/14 メディア: Kindle版 なぜ読んだか プログラマーとしての心得を学び、自分自身を1段レベルアップさせたいと思ったからです。 上記にも書きましたが、スタメンに入社して1年弱が経ちました。業務では主に Ruby on Rails で FANTS の開発を行っており、ある程度の知識は身についてきました。ただ、技術は身についてきたものの、その他のことに関してはまだまだ未熟です。そこで、社内の推薦図書にも上がっていた「達人プログラマー」を読み、実践することで成長したいと考えました。 達人プログラマーとはどんな本か ざっくりと説明すると、「より良いプログラマーになるため・より良い仕事を行えるようお手伝いをする本」です。短いセクションを集めたかたちで構成されており、各セクションで特定の話題を扱っています。技術だけでなく、プログラマーとしての仕事の進め方なども話題にし、効率的・生産的に行動するにはどうしたらいいかが記述されている本です。 ここで、いくつかのセクションを抜粋して、内容や感想・自分に活かすにはどうしたらいいかを紹介していきます。 第1章:達人の哲学 ~ソフトウェアのエントロピー~ エントロピーとは物理学の用語で、無秩序な度合いを表す指標のことです。このセクションでは全宇宙のエントロピーが増加していくのと同様に、ソフトウェアも時間とともに無秩序になっていくと説明しています。 無秩序になる理由として「割れ窓理論」を上げており、悪い設計や質の悪いコードを残しておくと、「自分も適当に作業するだけだ」という考え方が忍び込みやすくなる、と記載されています。 自分自身も、進捗によっては深く考えずにコードを書いてしまったことがあり、それが結果として悪いコードとなり、その後の作業に悪影響を及ぼす、という経験をしたことがあります。 まずは自分の手をつける部分から、割れた窓ではない(適切なメソッド名を記述・責任を明確にした設計)コードを記述し、エントロピーを抑えていきたいと思います。 第2章:達人のアプローチ ~二重化の過ち~ このセクションでは二重化が起きる原因、二重化による問題を説明しています。 これを読んでいる皆さんも、二重化に悩まされた経験があるのではないでしょうか。コード上で同じ知識を2箇所以上に記述しているせいで、複数箇所を修正しなければいけなかったり、仕様が複数箇所にまとまっていることで、どれを正としたらいいかわからない、などはやりがちな問題だと思います。様々な二重化の解決方法が示されていますが、中でも「再利用しやすいようにしておくこと」は一つの解決方法です。コード上の話では、DRYを心がけどこからでも利用できるメソッドにしておく、仕様上の話では、仕様をまとめる一つのドキュメントを作成し逐一履歴を追えるようにして更新していく、などをして、二重化から開放されましょう。 第6章:コーディング段階 ~リファクタリング~ このセクションはリファクタリングとは何か、どうやってリファクタリングをするかを説明しています。 この本では「コードの記述のやり直し、再作業、再設計」を総称して「リファクタリング」と呼んでいます。また、リファクタリングを行うタイミングは、コードがなじんでいないと感じたり、まとめるべき2つの事柄を見つけた場合、としています。まさにコードを書いている際に、上記のことを感じる場面は多いですよね。リリースや納期のことを考えると、安易にリファクタリングに着手できないですが、放置していると、将来問題が発生した場合、余計に修正のために大量の時間が必要になります。なので、気づいたタイミングでリファクタリング・こまめなリファクタリングを行い、将来の問題に対処していきたいですね。 第8章:達人のプロジェクト ~どこでも自動化~ このセクションはビルドやリリース手続き、テストなどの作業の自動化について説明しています。 人間はコンピュータほど繰り返し作業が得意ではないので、ヒューマンエラーによる問題や手作業により工数も多くなります。そこで作業を自動化することで、ヒューマンエラーが無くなり、手作業をなくして他の作業を行い生産性を上げることができます。また、この概念はプログラマー以外でも重要な考えですね。日々の業務を振り返り、繰り返し作業があるなら、それを自動化してみる、ということを実践していこうと思いました。 おわりに 今回は「達人プログラマー」を読み、本の内容・感想・自分に活かすには、ということを中心に紹介しました。本の中で紹介されているツールなどは少し古いので、利用は難しいかもしれませんが、概念としてはとても重要なこと記述されていました。ただ、自分は2016年に発売されたものを読みましたが、去年の11月に 第2版として新しいバージョン が発売されているようなので、こちらでは今に即したツールが紹介されているのではないかと思います。 達人プログラマー ―熟達に向けたあなたの旅― 第2版 作者: デイビット・トーマス , アンドリュー・ハント 発売日: 2021/01/18 メディア: Kindle版 各セクションで完結しているとはいえ、分厚い本なので、自分自身まだまだ理解できていない部分もあります。プログラマーとして迷ったときに読み返すなどして、理解を深めていきたいです。 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
アバター
モバイルアプリグループでおもにAndroidアプリの開発を行っている @sokume です。 Android開発者の方であれば興味関心の尽きない、 Android OS 12 Developer Preview 1 が2/18日に公開されましたね。 毎年の事ではありますが、2021年の新OS Android 12への対応にむけて少しずつ検討をすすめていかないとならない時期がやってきました。 この記事では、新OS Android 12への対応や、2021年平行にして気になる更新などをピックアップしていこうと思います。 Android12 公式情報 以下に公式情報が記載されています。 Android 12 Developer Preview Android 12 Behavior changes: all apps Android 12 Features and APIs Overview スケジュール Android 12 のリリーススケジュールは以下のようになるそうです。 https://developer.android.com/about/versions/12 より 一昨年までは 5月の Google I/O や 10月の made by Google といった大きめなオフラインを軸としたイベントがありましたので、リリース時期がイベントと連動していく感がありました。 昨年と同様のスケジュールになりそうというだと感じたので、今年も8月末〜9月にリリースされる流れになりそうです。 アプリへの変更点 OSが進化するので、その環境を利用するアプリも進化を促されます。 targetSDK Update to Android 12 アプリの targetSdkVersion を Android12用に変更した際の変更点については以下の記事になります。 https://developer.android.com/about/versions/12/behavior-changes-12 キーとなるのは以下の点になりそうです。 Foreground service launch restrictions App components containing intent filters must declare exported attribute Unsafe launches of nested intents 内容としては、アプリのLaunch部分に関するセキュリティーやプライバシーの変更がはいるようです。リンク先に詳細がありますので、開発中のアプリがこの変更点の対象となるアプリかどうかチェックしておきましょう。 Update all Apps Android OS 12 上で動作するすべてのアプリに対しての記事は以下になります。 https://developer.android.com/about/versions/12/behavior-changes-all UXに関する点や、フォアグラウンドPushに関する点の変更など、OS全体での変更があるようです。この点もしっかり把握しないとですね。 Android 12 デバイス 今回の発表にあわせて、Android 12の開発者向けプレビュー版がリリースされています。 https://developer.android.com/about/versions/12/download#flash 更新できる機種は以下になります。 Pixel 3 and 3 XL Pixel 3a and 3a XL Pixel 4 and 4 XL Pixel 4a and 4a (5G) Pixel 5 更新方法も Android Flash Tool を利用した更新と、自分のadb環境を利用した更新の2パターンが用意されています。 https://developer.android.com/about/versions/12/download 注意です!いつものことですが、自身の判断でデバイスのバックアップを取った後に更新を行うようにしましょう。クリーンインストールから実行されます。 私もPixel 4 XL を Android 12開発者プレビュー版 に更新してみました。利用した感じは大きくAndroid 11からの大きな変更はそこまで感じませんでしたが。今後いろいろと使って何らかの違いがわかってくるのかなと思っています。 直近の問題は、やはり動かなくなった一部のアプリをどうするかーという点で悩んでおります。 Jetpack Compose は? Android 環境の宣言型UI開発フレームワークとして、Jetpack Compose が昨年は話題になりました。 Jetpack Composeのロードマップ的には今年がリリースの年となる予定です。 正式にリリースとなることで、Androidの開発フレームワークとしてまた大きな変化をもたらす事が考えられますね。 早い段階から技術的なキャッチアップをすすめて置く必要があるでしょう。 Jetpack Compose https://www.youtube.com/watch?v=U5BwfqBpiWU&feature=youtu.be&t=1324 最後に 2月になり、新OS Android 12情報も出てきたので、これまで以上にアンテナ高く、情報のキャッチアップをしていきたいと思っております。 昨年同様 Android 11 Meetup などを通しての技術情報の共有もあるんじゃないかと思います。 昨年のAndroid 11の更新については こちら にまとまっております。ご興味のあるかたはどうぞ。 株式会社スタメンでは一緒に働くエンジニアを募集しています。 ご興味のある方はぜひ エンジニア採用サイト をご覧ください。 Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、 Creative Commons 3.0 Attribution ライセンスに記載された条件に従って使用しています。
アバター
はじめに はじめまして。株式会社スタメンでエンジニアをしております 永井 です。 今回の記事ではReactでメモ化によるパフォーマンスを意識した実装方法について書きたいと思います。 なぜパフォーマンスを意識した実装が大切なのでしょうか。 なぜなら、ユーザーのある操作に対するレスポンスの速度を高めることは、UXの文脈において非常に重要な要素だからです。例えば、100ms未満のレスポンスに関してはユーザーは瞬時に感じられますが、100ms ~ 300msではすでに遅いと感じてしまいます。遅いことにストレスを感じたユーザーは、別のサービスにリプレイスしてしまうかもしれません。 Reactでパフォーマンスを出すには、バンドルサイズを減らすなど、いくつか方法はありますが、基本的な戦略としては不要なレンダリングを抑えることだと思います。 この不要なレンダリングを抑制するためには、Reactがどのように機能するかを理解する必要があります。理解しないまま改善を行うと、却ってパフォーマンスに悪影響が出る可能性もあります。 そのため、パフォーマンス改善に繋がるメモ化等のメソッドを説明する前に、このReactがレンダリングにおいてどのように機能しているかを説明したいと思います。 はじめに 仮想DOMによるReactの更新処理 Reactのメモ化 React.memo useCallback useMemo 最後に 仮想DOMによるReactの更新処理 ReactではDOMの更新処理を、仮想DOMによる差分更新処理に任せることで、パフォーマンスを高めています。 具体的に言うと、実際のDOMをJavascriptオブジェクトの形式に変換したツリーデータをメモリ上に作成し、コンポーネントの状態に変更がある度に、実際のDOMを更新するのではなく仮想DOMを更新します。 更新された仮想DOMと古い仮想DOMを比較し差分を検出することで、実際のDOMにレンダリングを行います。 こうすることで実際のDOMでは必要最低限の箇所のみレンダリングを行うことが可能になります。 例えば以下のようなコードがあるとします。 const Page = () => { return ( < div > < p > Counter App < /p > < Counter / > < /div > ) } const Counter = () => { const [ count , setCount ] = useState ( 0 ) return ( < div > < span > { count } < /span > < button onClick = { () => setCount ( count + 1 ) } > + < /button > < /div > ) } 仮想DOMとしては、初回レンダリング時にPageとCounterコンポーネントで記述されたJSXを元に、Javascriptで構築した仮想DOMツリーを生成します。そして、 + を押すと、 count のstateが更新されてレンダリングが走り、 count の箇所が 2 となった新しい仮想DOMツリーを生成します。そして新旧2つの仮想DOMツリーを比較して差分を検出し、差分があった箇所である <span>{count}</span> だけを実際のDOMに反映します。 このように、仮想DOMの概念によって必要最低限の箇所のみDOMを変更することができます。 Reactのメモ化 仮想DOMによる差分更新によって、変更箇所のみをレンダリングすることができます。しかし、配下のコンポーネントが再描画されるため、不必要な箇所まで再レンダリングされてしまいます。 そこで、Reactによるメモ化によってコンポーネントに変更がない場合はレンダリングされないようにしましょう。 React.memo Reactの高階関数である React.memo はコンポーネントをメモ化する上でよく使われる手法です。 例えば以下のようなコードがあるとします。 const Parent = () => { const [ parentName , setParentName ] = useState ( '' ) return ( < div > < span > parent is { parentName } < /span > < input type= "text" onChange = { e => setParentName ( e. target .value ) } / > < Child / > < /div > ) } const Child = () => { return ( < div > < span > I am Child < /span > < /div > ) } この時、 <input //... /> に文字を入力すると setParentName が発火して name が更新されます。stateが更新されるのでParentコンポーネントはレンダリングされますが、Childコンポーネントはどうでしょうか? 試しに console.log('child') を仕込んでみましょう。 const Child = () => { console.log ( 'child' ) return ( < div > < span > I am Child < /span > < /div > ) } 再び <input //... /> に文字を入力してみましょう。すると... => child 「child」と表示されてしまいました。つまり、文字を入力する度に発生するParentコンポーネントのレンダリングに付随して、Childコンポーネントも毎回レンダリングされてしまっているのです。 本来であればこのレンダリングは不要なので、パフォーマンスを考慮するのであれば防ぎたいレンダリングです。 このレンダリングを抑えるために、 React.memo を使います。 const Child = React.memo (() => { console.log ( 'child' ) const return ( < div > < span > I am Child < /span > < /div > ) } ) このコードでは React.memo でコンポーネントをラップすることで、Childコンポーネントに渡すpropsに変更がない場合に、レンダリングをスキップしています。Parentコンポーネントにある <input //... /> で再び文字を入力してみると、コンソールには何も出力されていないことが確認できると思います。 この React.memo でpropsの前後の値を比較してレンダリングするかを決定している訳ですが、この比較は浅い比較で行われます。所謂、オブジェクトのインスタンスにおける参照が異なるかどうかを見ています。 ※ React.memo は第2引数に何も指定しないと、デフォルトでは浅い比較で行われます。第2引数に比較関数を渡すことでレンダリングをカスタムで制御することができますが、基本的には等価性のチェックにはコストが掛かるので避けたいです。 このように、 React.memo を使用することで、本来変更されていないコンポーネントのレンダリングを抑えることができますが、一つ落とし穴があります。 次のコードを見てみてください。 const Parent = () => { const [ parentName , setParentName ] = useState ( '' ) const [ childName , setChildName ] = useState ( '' ) const childNameHandler = ( e: React.ChangeEvent < HTMLInputElement >) => { setChildName ( e. target .value ) } return ( < div > < span > parent is { parentName } < /span > < input type= "text" onChange = { e => setParentName ( e. target .value ) } / > < Child name = { childName } onChange = { childNameHandler } / > < /div > ) } const Child = React.memo (( { name , onChange } ) => { console.log ( 'child' ) return ( < div > < span > child is { name } < /span > < input type= "text" onChange = { onChange } / > < /div > ) } ) 変更した箇所としては、Childコンポーネントにstateの name と関数の childNameHandler propsで渡しています。 このコードで再びParentコンポーネントにある <input //... /> に文字を入力してみましょう。すると... => child React.memo でメモ化しているのにも関わらず、再びレンダリングされてしまいました。この原因は、 childNameHandler 関数にあります。 アロー関数はレンダリングの度に新しい関数オブジェクトを生成しますが、この関数オブジェクトの再生成によって、propsとして渡している childNameHandler の参照が変更されてしまい、Childコンポーネントがレンダリングされてしまうのです。( (() => {}) !== (() => {}) であるため) これを防ぐ方法として useCallback があります。 useCallback useCallback を用いて改善したコードは以下のようになります。 const Parent = () => { const [ parentName , setParentName ] = useState ( '' ) const [ childName , setChildName ] = useState ( '' ) const childNameHandler = useCallback (( e: React.ChangeEvent < HTMLInputElement >) => { setChildName ( e. target .value ) } , [] ) return ( < div > < span > parent is { parentName } < /span > < input type= "text" onChange = { e => setParentName ( e. target .value ) } / > < Child name = { childName } onChange = { childNameHandler } / > < /div > ) } useCallback は、メモ化された関数オブジェクトを返すhooks APIです。第2引数にしている配列は依存配列で、配列内のいずれかの値が変更されると、新しく関数オブジェクトを生成します。 useCallback を使用する場合は、基本的に React.memo によって最適化されたコンポーネントにpropsとして渡す場合に限定するべきです。 React.memo を使用していないコンポーネントに useCallback によってメモ化した関数を渡したとしても、親コンポーネントがレンダリングされると子コンポーネントはレンダリングされてしまうからです。 また、 React.memo によってメモ化されたコンポーネントに渡さない場合にも useCallback でメモ化するのは避けた方が良いと言われています。これは useCallback の実行コストは関数オブジェクトの再生成のコストよりも高いと言われているからです。 useMemo useMemo は関数の返り値をメモ化する際に使用します( useCallback は関数自体をメモ化します) 例えば、以下のような関数があるとします。(こんなコードは現実には存在しないと思いますがあくまでサンプルということで) const someCalculate = () => { let number = 0 ; while( number <= 1000 ) { console.log ( number ) number ++ } return count * number } 非常にシンプルなコードですが、変数 number が1000になるまで1ずつ足していき、最終的に変数 count と乗算するというものです。 useMemo ではこの計算結果をメモ化することができ、計算自体をスキップすることができます。(以下のコード) const memorized = useMemo (() => { let number = 0 ; while( number < 1000 ) { console.log ( number ) number ++ } return count * number } , [ count ] ) useMemo の依存配列には count を入れています。countが変化すると再計算する必要がありますが、 count が不変の場合は計算結果をメモ化して利用することができます。 最後に Reactでのメモ化について今回書きました。普通にReactで実装していても画面自体は出来てしまうのですが、Reactのレンダリングやメモ化について知らないと、実はかなりパフォーマンスが悪い実装になってしまうことは往々にしてあると思います。 弊社のプロダクトであるTUNAGでも、フロントエンド領域においてパフォーマンスを最適化しきれていない部分がまだまだあるので、徐々に最適化できればと思います。 スタメンでは一緒に働くエンジニアを募集しています。 興味がある方は、ぜひ 採用サイト からご連絡ください!
アバター
こんにちは。スタメンでTUNAGやFANTSのモバイルアプリ開発を担当している @temoki です。 先週、Twitter iOSアプリで使用されているテキストエディタが TwitterTextEditor というOSSとして公開され、iOSアプリエンジニアの間で話題になりましたね。以下がTwitter公式のエンジニアリングブログによる紹介記事です。 blog.twitter.com 私はTUNAG iOS/Androidアプリの開発の中で、メンションや絵文字のショートコード入力を備えたテキスト入力機能の実装に苦労してきていることもあり、このOSSの公開はとても興味をそそられるものでしたので、早速試してみることにしました。今回のブログでは、実際にこの TwitterTextEditor を利用するという観点で書きたいと思います。 以降の内容は TwitterTextEditor v1.0.0 時点での内容となります。TwitterTextEditor の概要につきましては、上記の公式ブログやそれを日本語で紹介されている以下の記事をご覧ください。 TwitterがiOSアプリ向けに新しいオープンソースのテキストエディタAPI「Twitter Text Editor」を発表 - GIGAZINE TwitterTextEditorの機能 公式ブログでは TwitterTextEditor の機能として以下の5つについて挙げられていますので、1つ1つ実際のコードなども交えながら紹介していきます。 Easy delegate-based APIs Robust text-attribute update logic Additional text editing events Safe event handling for text input Support for recent versions of iOS Easy delegate-based APIs TwitterTextEditor は UIKit と同じようなデリゲートベースのAPIが提供されています。例えばテキスト入力の開始・終了のイベントは TextEditorViewEditingDelegate というプロトコルを実装することでハンドルすることができますが、これは UITextViewDelegate のそれとほぼ同じになっていることがわかります。 public protocol TextEditorViewEditingDelegate : AnyObject { func textEditorViewShouldBeginEditing (_ textEditorView : TextEditorView ) -> Bool func textEditorViewDidBeginEditing (_ textEditorView : TextEditorView ) func textEditorViewDidEndEditing (_ textEditorView : TextEditorView ) } というのも TwitterTextEditor は UITextView を内包しているため UITextView で提供されている機能はほぼ網羅されているのです。そのため、既存のプロジェクトですでに UITextView を組み込んでテキスト入力を実装している箇所も容易に置き換えることができそうです。 TwitterTextEditor はイベントの種類に応じて複数のプロトコルに分割されています。 textEditorView.font = UIFont.systemFont(ofSize : 15 ) textEditorView.keyboardType = . default textEditorView.textContentInsets = . init (top : 10 , left : 10 , bottom : 10 , right : 10 ) textEditorView.placeholderText = "メッセージを入力" // プレースホルダー対応😂 textEditorView.editingDelegate = self そして個人的に嬉しいのは、プレースホルダーの表示に対応している点です。同じ UIKit の UITextField はプレースホルダーの表示に対応しているのに、なぜか UITextView は対応していなくて、泣く泣く実装する...ということもなくなりますね。 Robust text-attribute update logic ここからが TwitterTextEditor が本領発揮する部分となります。TwitterTextEditor はテキスト属性を更新するためのAPIを提供しており、例えばシンタックスハイライトのような機能を実装しやすくなっています。 Twitterアプリでいうとメンションやハッシュタグのハイライト表示ですね。例えば Markdown テキストの入力に対する簡単なプレビュー機能なんかにも応用できそうです。試しに二つのアスタリスクで囲んで強調表示する記法( **text strong emphasis** ) で実装してみます。 テキスト属性を更新できるタイミングで TextEditorViewTextAttributesDelegate プロトコルのメソッドが呼び出されますので、ここで属性を更新して返します。更新結果は completion ハンドラを介して返すことになるので、バックグラウンドでテキストの解析と属性更新を行えるのがポイントです。 extension EditorViewController : TextEditorViewTextAttributesDelegate { func textEditorView (_ textEditorView : TextEditorView , updateAttributedString attributedString : NSAttributedString , completion : @escaping (NSAttributedString?) -> Void ) { // バックグラウンドでテキストを解析して属性を更新する DispatchQueue.global().async { // テキストの解析 let regex = try ! NSRegularExpression(pattern : "(\\*+)(\\s*\\b)([^\\*]*)(\\b\\s*)(\\*+)" , options : [] ) let stringRange = NSRange(location : 0 , length : attributedString.length ) let matches = regex.matches( in : attributedString.string , options : [] , range : stringRange ) // テキスト属性の更新 let newAttributedString = NSMutableAttributedString(attributedString : attributedString ) newAttributedString.removeAttribute(.font, range : stringRange ) newAttributedString.addAttribute(.font, value : UIFont.systemFont (ofSize : 15 ), range : stringRange ) for match in matches { newAttributedString.addAttribute(.font, value : UIFont.boldSystemFont (ofSize : 15 ), range : match.range ) } // メインスレッドで更新結果を返す DispatchQueue.main.async { completion(newAttributedString) } } } } 実行結果の動画です。 いい感じですね。ただ、Markdown は多くの記法があり、編集するテキストのサイズも大きくなる可能性が高いです。実際に Markdown エディタを実装する場合、このTwitterTextEditorの機能がすべてを解決するものではないことを理解しておく必要があります。開発者の @niw さんもTwitterで次のようなことをおっしゃっています。 Nothing prevents to support it! (or probably Markdown grammar itself is, tho.) However, a partial attribute update logic may be needed, depends on the expected size of editing text. — Yoshimasa Niwa (@niw) 2021年1月26日 Additional text editing events Twitterは世界中で利用されているアプリですので、あらゆる言語での入力に対応しなければなりません。そのため、TwitterTextEditor には多言語への対応を考慮したテキスト入力イベントが追加されています。以下がそのイベントです。 入力中の言語が切り替わった時のイベント テキスト入力の方向が切り替わった時のイベント(アラビア語などの Right-to-left writing な言語への考慮) これは TextEditorViewTextInputObserver プロトコルとして提供されています。このイベントをトリガーに、入力のためのUIの切り替えなどに使われることを想定されているようです。 public protocol TextEditorViewTextInputObserver : AnyObject { func textEditorView (_ textEditorView : TextEditorView , didChangeInputPrimaryLanguage inputPrimaryLanguage : String ?) func textEditorView (_ textEditorView : TextEditorView , didChangeBaseWritingDirection writingDirection : NSWritingDirection ) } Safe event handling for text input 例えば、入力済の文字数を表示したり、入力中のテキスト内容に応じた入力サジェストなどを行うためには、ユーザーのテキスト入力イベントを使用します。 UITextView でこのイベントをハンドリングするには、 UITextViewDelegate の以下のデリゲートメソッドを使うことになります。 func textView (UITextView, shouldChangeTextIn : NSRange , replacementText : String ) -> Bool func textViewDidChange (UITextView) TwitterTextEditor ではこれらよりも安全にテキスト入力イベントをハンドリングするための TextEditorViewChangeObserver プロトコルが提供されています。 TwitterTextEditor のソースコードの中には // UIKit behavior ~ というコメントで、UIKit の動作の問題点とそのワークアラウンドといった情報がたくさん記述されており、これらをふまえた上で安全なイベントを提供してくれるというわけですね。 public protocol TextEditorViewChangeObserver : AnyObject { func textEditorView (_ textEditorView : TextEditorView , didChangeWithChangeResult changeResult : TextEditorViewChangeResult ) } iOSアプリでのテキスト入力の難しさやその解決方法については、作者の @niw さんが昨年の iOSDC Japan 2020 でも発表されていますので、ぜひご覧いただきたいです。 www.youtube.com このプレゼンテーションの中では、テキスト入力を伴うアプリを開発する場合、ソフトウェアキーボードの表示状態の変更についても気にしながら実装する必要があるという点についても言及されており、これについては同じく @niw さんが公開されている KeyboardGuide というライブラリでスマートに解決することができるので、こちらもオススメです。 GitHub - niw/KeyboardGuide: A modern, real iOS keyboard system notifications handler framework that Just Works. Support for recent versions of iOS TwitterTextEditor はサポートバージョンとして iOS 11 以降という良心的な設定になっています(Twitter iOS アプリの最小バージョンが iOS 12 なのに!)。 そして、サポートされているパッケージマネージャーも CocoaPods、Carthage、Swift Package Manager と一般的なものは全て揃っているので導入で困るということはなさそうですね。 おわりに 世界中で利用される Twitter iOS アプリ。そのテキスト入力を支えるOSS、 TwitterTextEditor についてご紹介いたしましたが、いかがでしたでしょうか。 このブログを書くにあたって TwitterTextEditor のソースコードを眺めてみましたが、とても勉強になる部分が多かったり、作者の実装の苦労が垣間見えたりしました。また、自分が TUNAG アプリの開発で実装したコードに類似しているところをいくつか見つけて親近感が沸いたりもしたので、改めて人のソースコードを読むのは重要なことだなと感じました。 最後になりましたがスタメンでは TUNAG や FANTS そして新しい事業におけるプロダクト開発を一緒に牽引してくれる仲間を募集しています。エンジニアに限らず、デザイナーやプロダクトマネージャー職も募集中ですので、興味のある方は下記の応募からご連絡ください! エンジニア採用サイト インハウスデザイナーWanted!名古屋で注目のベンチャーで活躍しませんか 急成長する大規模 SaaSプロダクトのプロダクトマネージャー募集!!
アバター
スタメンでエンジニアをしている 田中 です。 今回は決済プラットフォームであるStripeのサブスクリプションを扱う際に遭遇した問題について、発生した事象とその原因、および対策方法についてご紹介します。 なお、本記事ではStripeのサブスクリプションについての詳細は説明いたしません。また、対策方法についてはRubyのコードで記載します。RubyでStripeのサブスクリプションを扱う場合については、以下の記事にて紹介しているのでよろしければご参照ください。 【Ruby on Rails】Stripeのサブスクリプションで試したことをまとめてみた また、以前にも同様のタイトルで記事を投稿しましたが、今回は内容が若干異なります。ただ、前回の記事の知識があると理解が早くなると思うので、この機会に読んでいただければと思います。(タイトルの括弧の中身が異なります) 【Stripe】サブスクリプションの支払いタイミングが特定日時においてズレる問題について(月末版) 前提 本記事で扱うサブスクリプションは請求期間が月次のものです サブスクリプションの支払い日について、通常、翌月に同じ日が存在しない場合は自動的にその前の日を指定してくれます 例 5/31 → 6/30 8/31 → 9/30 参考 https://stripe.com/docs/billing/subscriptions/billing-cycle 発生した事象 以下の画像のように、同じ日(1日)にサブスクリプションを開始しましたが、2回目の支払いのタイミングがズレてしまうということがありました。 2回目の支払いが初回支払いと同じ日(1日)に行われているケース 2回目の支払いが初回支払いと異なる日に行われているケース そのため、例えば支払い成功時のWebhookにて何かしらの処理をする場合に、このズレによって影響が発生する可能性が大いに考えられます。 発生原因 先日ご紹介した記事と同様、 billing_cycle_anchor とタイムゾーンの関係によるものだと思われます。 以下は、過去記事の引用です。 ここで、 billing_cycle_anchor について説明します。 billing_cycle_anchor とは支払い開始の起点となる日時のことです。たとえば、毎月1日に決済したい場合、サブスクリプション作成時に billing_cycle_anchor に翌月の1日を指定することで、毎月1日払いを実現することが出来ます。特に指定をしなければ、サブスクリプション作成時刻 = billing_cycle_anchor となります。 参考 https://stripe.com/docs/billing/subscriptions/billing-cycle 発生原因についての詳細は下記の通りです。 Stripeのシステムは、UTC基準で動作する 日本時間(JST)でサブスクリプションを作成する場合に、UTCの時刻から9時間の差がある そのため、UTC基準では月末だが、日本時間だと翌月と判定されてしまうため今回の問題が発生する これだけだとよく分からないため、具体例を挙げて説明します。 具体例 (1)12/1 午前0時にサブスクリプションを作成した場合 ・日本時間「2020-12-01 00:00:00」にbilling_cycle_anchorを指定 支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC) 1回目 2020-12-01 00:00:00 2020-11-30 16:00:00 2回目 2020-12-31 00:00:00 2020-12-30 16:00:00 3回目 2020-01-31 00:00:00 2021-01-30 16:00:00 4回目 2020-03-01 00:00:00 2021-02-28 16:00:00 (2)11/1 午前0時にサブスクリプションを作成した場合 ・日本時間「2020-11-01 00:00:00」にbilling_cycle_anchorを指定 支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC) 1回目 2020-11-01 00:00:00 2020-10-31 16:00:00 2回目 2020-12-01 00:00:00 2020-11-30 16:00:00 3回目 2021-01-01 00:00:00 2020-12-31 16:00:00 4回目 2021-02-01 00:00:00 2020-01-31 16:00:00 (1)に関してはUTC基準の場合にbilling_cycle_anchorが2020-11-30 16:00:00で設定されてしまうため、次回以降の支払いサイクルが以下の通りになってしまいます。 31日がある月は31日に支払い 31日がない月は1日に支払い (2)に関してはUTC基準の場合にbilling_cycle_anchorが2020-10-31 16:00:00で設定されるので、次回以降の支払いサイクルは毎月1日支払いとなります。 上記のことから、日本時間において以下の日時にサブスクリプションが作成されると今回の問題が発生すると考えられます。 前月が31日までない月の1日(3月1日, 5月1日, 7月1日, 10月1日, 12月1日) 午前0時から午前9時の間 対策方法 特定日時でサブスクリプションを作成した場合、 trial_end を用いて次回以降の支払い日時をずらすことで対応します。 詳細については 前回の記事 をご確認ください。 おわりに 今回はStripeのサブスクリプションを扱う際に遭遇した問題についてご紹介しました。 時差および月によって日数が異なることを考慮しないと想定外の挙動が発生する可能性があるので、取り扱う際にはその点を頭に入れて実装していきましょう。 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
アバター
こんにちは、スタメンでVPoE兼プロダクト部の部長 をしている小林です。 1月も前半が終わり、スタメンでは新しいチームやプロジェクトが立ち上がり、本格的に2021年がスタートしています。 昨年末に、CTOの松谷が技術を中心に「 スタメン開発チーム 2020年の振り返りと2021年の展望 」を投稿しましたので、私は組織面を中心に2021年の方向性をご紹介しようと思います。 現在のプロダクト部について プロダクト部は、2021年1月18日時点で、総勢26名となり、エンジニア、デザイナー、プロダクトマネージャーが在籍する「ものづくり」の部門となっています。 2016年、オフィスに一人で rails new から始めたところから、Rails, React, Swift, Kotlin, AWS/GCP と多岐にわたる技術を用いて、デザイナーとプロダクトマネージャー(PdM)も加わって、TUNAG と FANTS の2つの事業を創る部門に成長しました。 組織(会社)で仕事をする意味は一人ではできない大きな仕事をチームで成し遂げるためにあると言われます。そして、スタメンの経営理念は、「一人でも多くの人に、感動を届け、幸せを広める。」です。 私たちプロダクト部にとって、大きな仕事とは、TUNAGやFANTSなど、スタメンが提供するプロダクト(≒事業)によって、より多くの人に感動を届け、幸せを広めることです。 まさに言葉通り、このチームでなければできないことがたくさんあると思えるほど、たくさんのことが実現できるチームになってきました。集ってくれた皆さんに感謝です。 そんなプロダクト部の内訳としては下記になります。 プロダクト部の内訳 このグラフのとおり、スタメンのプロダクトチームの特徴は下記になります。 エンジニア、デザイナー、プロダクトマネージャーで構成されるフルセットのプロダクトチーム。 役員/社員は計24名で、エンジニア20名、デザイナー2名、プロダクトマネージャー2名。 加えて、学生インターン(アルバイト)のエンジニアが2名。 若手と経験者が混ざって、即戦力と育成の両立を重視しているチーム。 若手は、経験を問わずポテシャルの高い人材を採用し、事業とともに成長している。 経験者は、その経験を活かして即戦力として専門性をもたらし、チームを底上げしている。 ただし、まだまだ満足できる専門性や世の中への影響力ではありません。企画、デザイン、実装において、もっと良いチームになり、良いプロダクトを世に提供する必要があります。 以下では、そのためにVPoEとして考えていることをご紹介したいと思います。 機能を増やすのでなくUXを上げる SoE (System of Engagement) と SoR (System of Record) という言葉がありますが、事業として TUNAG は、機能を増やす SoR から UXを向上する SoE の段階に来ており、機能を追加するよりも、各機能の使い勝手や効果を最大化することを意識して施策を実施しています。 ただし、BtoB の SaaS である TUNAG は、対象とする顧客の業態や規模が多様で、HR Tech領域は法律等で決まった形が無いため、解決すべき課題や要望も多岐に渡ります。 また、TUNAG は、300社以上のお客さまに導入いただき、10人程度から1万人以上の規模の組織で利用されています。全体的に、要件定義が難しく、機能も多く複雑で、仕様、デザイン、実装も複雑になりがちで、プロダクトマネジメントが難しいプロダクトです。 そんな中、今年からプロダクトマネージャーが増えて二人となり、プロジェクトの運営に加えて、カスマーサクセスやセールスとの連携、数値分析や深い議論に時間を割くことができるようになってきました。デザイナーの増員も進めており、プロジェクトの早い段階からデザイナーを巻き込んで、仮説検証やUI/UXの向上に取り組んでいます。 まだまだ向上余地がたくさーんありますが、次のような取り組みにより、プロダクトの作り込みが深くなってきています。 カスマーサクセスとセールスとの連携による課題や顧客ニーズの把握 データを用いた定量的な分析に基づく意思決定 デザイナーとプロダクトマネージャーの連携による企画とプロトタイプによる検証 エンジニアや社員も巻き込んだレビューをプロジェクトの節目で複数回開催 使い勝手の良いUIと開発効率を両立するためのデザインシステムの構築 技術的課題と事業推進のバランス 2016にTUNAGを創り始めてから4年半経過しました。途中事業の方針転換(ピボット)もしていますし、相次ぐ拡張によって、システムは複雑となり、技術的負債も蓄積しています。 これまでいくつかのプロジェクトで負債の解消を行っていますが、まだまだたくさんの負債が残っており、障害や不具合の発生や、開発速度の低下など弊害も見え始めています。同時にシステム全体のスケーラビリティやセキュリティを向上するような取り組みも議論されており、近くプロジェクトも立ち上がる予定です。 ただしスタメンのような小さなチームでは、開発リソースにも限りがありますし、技術的な依存関係や各エンジニアの得意な技術も異なり、同時に実施可能なプロジェクトは限られています。 そのため、定期的にロードマップ会議を開催し、この先半年から1年先ぐらいの長期的な視点での議論を行い、機能の改善などの事業を発展させる施策と、技術的負債の解消やアーキテクチャー変更などの技術的な施策の優先順位を役員が集まって意思決定しています。 最近の例だと、TUNAGのメイン画面であるタイムライン(React)を、今年の夏に大きく機能を追加(変更)する前に刷新するプロジェクトを進めることになり、昨年末から着手しています。 このように、技術的課題と事業推進のバランスを取りながら、プロダクト部全体を運営していく必要があります。 プロダクト部の行動指針である Star Code にあるように、様々な「問題を見極め」、事業の成長のために「ユーザー目線で考え」ながら事業を進める。障害や不具合などが発生したら「失敗に向き合い」、技術的負債の解消やアーキテクチャー変更を行う。こんな、チーム運営をしています。 小さなチームと権限移譲 組織面でも発展を続けています。2021年に入り、デザイナーの増員に伴いデザインチームができ、プロダクトマネージャーも増えて、チームとしてプロジェクト運営を行うようになりました。エンジニアは20名となり6つのチームに所属しています。 全体として8チームが部長の下にフラットに配置された「文鎮型組織」になっており、部長の直下にすべてのチームを配置することで、迅速な意思疎通と意思決定を行うようにしています。 チーム編成としては、専門的な小さなチームの連携により、効率と成果の最大化を意識しています。また、小さなチームにすることで、マネージャーがプレイヤーとしても活躍&成長できるようにしています。 フラットな組織にすることで、意思疎通はしやすい反面、部長(小林)が多くのチームを管轄することになりマネジメントが疎かになる懸念がありますが、技術面を CTOの松谷に移譲し 、プロジェクト運営をプロダクトマネージャーと各チームのマネージャーに委ねることで、小林は組織(採用と人事)と事業の意思決定に専念するようにしています。 今後は、さらなる組織拡大に備えて各分野での権限委譲を進め、小林は全体最適にフォーカスしていく予定です。 全員が成長し貢献する組織へ 主に、組織と事業について、課題と感じていることを書いてみました。各分野で課題がたくさんですが、逆に言えば伸びしろに溢れているとも考えています。 前述したようにスタメンのプロダクト部は、若くて経験の浅いメンバーが多い割に、事業とシステムが複雑な大規模プロダクトを作っています。 エンジニアは、大規模SaaSプロダクトの開発を少人数のチームで主力メンバーとして担当することができますし、難易度の高い負荷対策や大規模リファクタリングといった貴重な経験を得られます。 デザイナーは、インハウスデザイナーとしてのPCとモバイルでのUIデザインはもちろん、紙媒体やプロモーションサイト、デザインシステムの構築など様々なデザインと共に、プロジェクト初期からの参加による上流工程に参加することができます。 プロダクトマネージャーは、HR Tech BtoB SaaS という難しい分野のエンゲージメント経営という前例の無いプロダクトに対して、プロダクトマネジメントのすべてを思う存分することができます。 これらの各専門分野とともに、組織面では20代のマネージャーがたくさん誕生し、小林と一緒になって組織(採用、育成、アサイン、評価)と事業のマネジメントを経験しています。 このように、事業も技術も組織も伸びしろがたくさんあり、まだ組織が小さいこともあって、非常に裁量と経験値が大きな環境となっています。良いチームで良いプロダクトを作っており、きっと皆さんにとって人生の代表作となるプロダクトを作っていけると思っています。 責任が大きい分、大変なときもあるかと思いますが、引き続き全員で創意工夫し、ひたむきに良いプロダクトを作っていきましょう! 新しい仲間も募集中です スタメンでは、引き続き、エンジニア、デザイナー、プロダクトマネージャーを採用しています。これまでも、少年マンガの新キャラクターのように、新しい仲間がピンチを救い、チームに新しい力をもたらしてくれました。 今年もたくさんの仲間が加わって、更に強いチームになる予定です。こんな環境に魅力を感じてくださった方は、ぜひ下記から応募していただけないでしょうか。 株式会社スタメン エンジニア採用サイト インハウスデザイナーWanted!名古屋で注目のベンチャーで活躍しませんか 急成長する大規模 SaaSプロダクトのプロダクトマネージャー募集!! お待ちしております!!!
アバター
はじめに はじめまして。株式会社スタメンでエンジニアをしております、永井( @0906koki )です。 以前の記事 では、筋トレを週5でしていると書いていましたが、今は週2に減らして体をメンテナンスしています。 今回の記事ではRailsとWebpack、そしてReactを使って、webpack_dev_serverによるHot Module Replacement(以下 HMR)を実装する方法について書きたいと思います。 軽くwebpack_dev_serverとHMRの説明をすると、 webpack_dev_server とはWebpackを利用した開発環境向けWebサーバーで、Webpack管理内の静的アセットを配信することができます。また、 HMR とはWebpackの提供する仕組みで、ブラウザのリロードをせずにJavascriptの変更内容を画面に反映するツールです。 弊社のプロダクトである TUNAG ではサーバーサイドをRails、フロントエンドをReactとTypeScriptで実装しており、フロントエンドのビルドファイルを、Railsのsprocketsでコンパイルしてerbで読み込ませていました。 プロダクトの成長に比例してWebpackのbundleサイズも肥大化していき、それに起因してsprocketsのアセットコンパイルに掛かる時間も増加し、フロントエンド開発環境化においてスピード感を持って開発することが難しくなってきました。 このままでは、プロジェクトの進行に大きな悪影響を及ぼすことが目に見えてきたので、問題を解消するために、 sprocketsによる無駄なコンパイルをなくし、webpack_dev_serverによるコンパイルのみにする HMRを導入し、リロードせずとも変更内容が反映されるようにする この2つを軸として、フロントエンド開発環境改善プロジェクトをスタートしました。 ※ この改善プロジェクトの内、webpack_dev_serverとRailsの連携部分に関しては、スタディストさんの 「フロントエンド原理主義者が目論んだ脱webpacker」 が非常に参考になりました。 実装の手順 webpack_dev_serverを導入して、HMRを適用する手順は以下の通りです。 webpack_dev_serverのインストール webpack_dev_serverとmanifestPluginの設定 webpack_dev_serverのビルドファイルを読み込むヘルパーメソッドの実装 Railsのプロキシ設定 react-hot-loaderの導入と実装 ※ TUNAGではRailsのWebpackerを使わず純粋なWebpackを元々使用していたため、Webpackで実装する前提で話を進めます。 webpack_dev_serverのインストール webpack_dev_serverに必要なpackageを追加します。 $ yarn add -D webpack_dev_server webpack-manifest-plugin ※ webpack-manifest-pluginは、生成したビルドファイルパスの管理ファイルとして使用します。 webpack_dev_serverとmanifestPluginの設定 設定は以下のようにしています。(loader等の設定は省略しているので、適宜追加してください) const path = require( 'path' ); const WebpackManifestPlugin = require( 'webpack-manifest-plugin' ) const outputPath = path.resolve( '../../public/packs' ) module.exports = (env, argv) => { return ( { entry: { bundle: [ 'webpack-dev-server/client?http://localhost:8080' , './src/index.tsx' ] } , output: { path: outputPath, publicPath: 'http://localhost:8080/packs' , filename: '[name].js' , } , plugins: [ new WebpackManifestPlugin( { fileName: 'manifest.json' , publicPath: '/packs/' } ) ] , devServer: { contentBase: 'http://localhost:8080/packs' , port: 8080, hot: true , headers: { 'Access-Control-Allow-Origin' : '*' , } } , } ); } ; ここでは、ビルドファイルの出力先をpublicディレクトリ配下のpacksディレクトリに指定しています。そしてwebpack_dev_serverのcontentBaseに /packs を指定することで、 http://localhost:8080/packs で出力先されたビルドファイルを取得することができます。 また、Webpackのプラグインである ManifestPlugin を使用して、ファイル名と実際に配置されるファイルパスが記述されたマニフェストファイルを生成します。 devServerとmanifestPluginの各プロパティの説明は以下の通りです。 devServer contentBase: 静的ファイルを配置するパスの指定 port: ポート番号の指定(rails serverが3000を使用するので、8080に) hot: HMRの利用 headers: webpack_dev_serverからのレスポンスに任意のヘッダー情報を含める manifestPlugin filename: 生成されるマニフェストファイル名の指定 publicPath: valueにprefixを付与する これでwebpack_dev_serverからアセットを配信する設定が完了したので、早速webpack_dev_serverを立ち上げてみたいと思います。 立ち上げ方は、以下のコマンドを実行するだけです。(package.jsonのscriptsに設定しておくことをオススメします) $ webpack-dev-server --progress --color これで、 http://localhost:8080/packs/◯◯.js にアクセスすると、出力先されたビルドファイルを取得することができます。 ちなみに、 http://localhost:8080/packs/manifest.json で、以下のようなマニフェストファイルも取得できると思います。 { "bundle.js" : "/packs/bundle.js" , } webpack_dev_serverのビルドファイルを読み込むヘルパーメソッドの実装 次に行いたいことは、上記で配信されたアセットをRails側で読み込むヘルパーメソッドの実装です。 コードは以下のようになります。(jsファイルのみを読み込む設定になっていますが、cssファイルも読み込みたい場合は、専用のメソッドを追加してください) module WebpackBundleHelper class BundleNotFound < StandardError ; end def javascript_bundle_tag (entry, **options) return javascript_include_tag entry unless Rails .env.development? path = asset_bundle_path( "#{ entry } .js " ) options = { src : path, defer : true }.merge(options) options.delete( :defer ) if options[ :async ] javascript_include_tag '' , **options end private def asset_host Rails .application.config.asset_host || '' end def dev_server_host " http://localhost:8080 " end def dev_manifest # webpack-dev-serverから直接取得する OpenURI .open_uri( "#{ dev_server_host } /manifest.json " ).read end def manifest @manifest ||= JSON .parse(dev_manifest) end def valid_entry? (entry) return true if manifest.key?(entry) raise BundleNotFound , " Could not find bundle with name #{ entry }" end def asset_bundle_path (entry, **options) valid_entry?(entry) asset_path(asset_host + manifest.fetch(entry), **options) end end javascript_bundle_tagでは、webpack_dev_serverから配信されているmanifest.jsonを取得し、manifest.jsonの中で引数に合致するファイルパスを取得します。 例えば、javascript_bundle_tagの引数に bundle を指定すると、manifest.jsonで bundle に合致するkeyを見つけて、そのvalue( /packs/bundle.js )を取得します。そして、 localhost:3000/packs/bundle.js へリクエストを送る流れです。 しかし、 localhost:3000 ではなく localhost:8080 でwebpack_dev_serverを立ち上げているので、当然のことながら、この段階ではアセットを取得できません。 なので、プロキシをしてRails側がwebpack_dev_serverからアセットを取得できるように設定してあげます。 Railsのプロキシ設定 プロキシの処理は、rack-proxyというGemをRailsに追加して実装しました。 require ' rack/proxy ' class DevServerProxy < Rack :: Proxy def perform_request (env) if env[ ' PATH_INFO ' ].start_with?( ' /packs/ ' ) env[ ' HTTP_HOST ' ] = dev_server_host env[ ' HTTP_X_FORWARDED_HOST ' ] = dev_server_host env[ ' HTTP_X_FORWARDED_SERVER ' ] = dev_server_host super else @app .call(env) end end private def dev_server_host " localhost:8080 " end end ここで行っていることは単純に localhost:3000/packs/ で来たリクエストを localhost:8080/packs/ へプロキシしているだけとなっています。 開発環境下のみでプロキシを行いたいので、developmentのconfigファイルに以下の設定を追加します。 config.middleware.use DevServerProxy , ssl_verify_none : true これでRails側からフロントエンドのアセットを取得することができるようになったので、 http://localhost:3000/packs/manifest.json にアクセスすると、webpack_dev_serverから配信されているマニフェストファイルを取得することができるはずです。 react-hot-loaderの導入と実装 ここまでで、Rails側がwebpack_dev_serverから配信されるアセットを取得できるようになったので、今まで通り、フロントエンドの開発を進めることができるようになったと思います。 ここからは、ReactでHMRを行う方法について解説します。 まず、HMRを行うために react-hot-loader というpackgaeを追加します。 $ yarn add react-hot-loader そして、 .babelrc にも以下の設定を追記します。 { "plugins" : [ "react-hot-loader/babel" ] } 次に、Reactコンポーネントの実装に移ります。 react-hot-loader にあるhot関数に、Reactプロジェクトのトップコンポーネントを引数として渡します。 import React from 'react' ; import { hot } from 'react-hot-loader' import { Todo } from './todo' const App = () => { return ( <> < Todo / > < / > ) } export default hot ( App ) HMRの確認 上記のReactコンポーネントを管理しているWebpackから出力先されるビルドファイルがbundle.jsとすると、先程定義したRailsのヘルパーメソッドの引数にbundleを指定します。 <%= javascript_bundle_tag( ' bundle ' ) %> これでwebpack_dev_serverを立ち上げて、Chromeのコンソールに以下の内容が出ていれば、HMRが有効になっています。 試しに、先程指定したトップコンポーネント配下のコンポーネントをいじってみてください。HMRによって即時に変更内容が反映されるはずです! まとめ webpack_dev_serverとReactにおけるHMRの導入について解説しました。 RailsのAssets Pipelineの仕組みや無数にあるWebpackの設定など、技術的に理解するべき範囲は広く、難しい部分はありましたが、今回のプロジェクトを通じてフロントエンドのコードを触るエンジニアの生産性改善に貢献できたのは良かったです。 弊社CTOの記事 にあるように、事業の成長に伴いエンジニアの人数も増えていくなかで、メンバー全体に関わる開発環境の問題は、プロジェクトを進めていく上で非常に大きな問題です。 すでに顕在化している問題や今後起きそうな課題に対して、エンジニアがその都度問題を提起し、解決に向けて行動することはとても大切なので、これからもそうした意識を持ち続けたいと思います。 スタメンでは一緒に働くエンジニアを募集しています。 興味がある方は、ぜひ 採用サイト からご連絡ください!
アバター
スタメンの松谷( @uuushiro )です。2020年3月末にスタメンのCTOに就任し、約9ヶ月ほどが経ちました。色々と変化の大きかったスタメン開発チームの2020年を、私の目線で振り返ります。そして期待も込めて来年の展望を共有したいと思います! どんな2020年だったか 事業について TUNAG まず、創業事業である TUNAG はリリースして今年で4年目でした。TUNAG は、2017年~2019年までは、「エンゲージメント経営プラットフォーム」として必要な一連の機能を揃えてきました。プロダクトの機能がカバーできる範囲を増やすことに注力してきたこともあり、その一つひとつの機能に関しては、価値提供ができる最小限のものでした。 2020年は、これまで揃えてきた主要な機能の価値・体験を引き上げるような改善や、新機能の追加においてもリリース時点のプロダクトの作り込みレベルを上げることができるようになってきたことで、開発組織として提供できる機能価値が大きくなってきたと感じています(もちろん改善余地は沢山です)。特に、フロントエンドエンジニア・ネイティブアプリエンジニアは、スキルアップと組織化により TUNAG のユーザー体験を向上させることに非常に大きな貢献をしてくれました。 また、TUNAG のメイン機能である、自社に合わせた社内制度を運用できる機能だけでなく、チャット機能やワークフロー機能といった通常業務を行う上で必須のツールを多くのユーザー様に使っていただくようになった年でもあったので、TUNAG へのアクセス数も負荷の特性も変化してきました。開発チームとしてもユーザー様の業務を止めてしまうことがないように、不具合が無いか、ストレスのない応答速度かなど「当たり前品質」を追求する意識が大きく高まっていきました。そして、エンタープライズ企業様の導入において、これまで経験したことのない規模のユーザー数の利用シーンを想定した負荷対策、及びセキュリティ対応を実施し、今後 TUNAG の導入を検討していただける企業様の幅も大きく広がりました。これらのシステムの信頼性向上の取り組みには、インフラチームがプロダクト開発全体をリードしてくれました。 一方で、プロダクトの作り込みレベルの向上と比例して、機能の複雑度も上がってきました。リリース当初は想像もしていなかったようなプロダクトの進化が何度も発生したので、その変化になんとか合わせてきた分の負債が無視できなくなってきています。その結果、アプリケーションコードが複雑になり、それが結果として不具合やサーバー負荷につながることが予測しづらいということも多々ありました。こちらについては、来年時間を確保し、リファクタリング及び、自動テストの拡充を進めていく予定です。 また、TUNAG事業において TERAS という組織診断ツールの提供を開始し、TUNAG と別のシステムとして0から構築しました。TUNAG本体とはアーキテクチャが異なり、SPA(React)とAPIサーバ(Rails)で作られ、サーバーもコンテナを利用したり、デプロイも Blue Green Deployment を利用したりなど、スタメンの近い将来の技術スタックの検証も兼ねて構築しました( TERASのアーキテクチャ )。ここで得られた知見の一部を来年TUNAG本体に適用することで、システムの安定性・開発・運用効率向上などを獲得していきたいと考えています。 FANTS そして今年は新規事業 FANTS をスタートしました。必要な機能が TUNAG と近いので、ソースコードの転用ができた箇所は多かったのですが、ライブ配信機能、課金機能、サロン管理機能、集客機能などオンラインファンコミュニティサービス固有の機能開発を多く行いました。特に課金機能に関しては、システムがお金を扱う初めての事例だったこともあり(クレカ情報は保持していません)、今までにない緊張感の中で開発になりました。二重決済や誤課金、そして決済プラットフォーム側とFANTS側でデータの不整合が発生しないような仕組みをしっかりと時間を掛けて(冷や汗をかきながら)構築した甲斐があったと思っています。責任重大な仕事だったと思いますが、FANTSチームが責任持ってやりきってくれたおかげで、今は FANTS の事業理念達成に向けた多くの機能をスピーディにリリース出来ています。 その他取り組みについて 2020年の、プロダクトロードマップ以外の取り組みについて、特に印象に残っている4つを紹介します。 アラート管理改善 アプリケーションのアラート通知の整理をしました。整理する前は、一日あたりの通知数が多いため、通知に対する集中力が削がれ重要なアラートを見逃しやすくなっていました。その結果、不具合・障害対応の初動の遅れや漏れが発生してしまう可能性が高くなっていました。この問題を私たちは「オオカミ少年アラート」と呼び、問題解決のために「システム思考」のフレームワークを活用しました( システム思考でアラート運用に関する問題を考える )。これにより、アラートの責任範囲及びアクションが明確化し、新しいメンバーも迷わずに監視をすることができるようになったと思います。そして、半年経った今も新しいエラーに対して反応が早くなったと実感しています。ただ最近は、たまにあの「少年」が遊びにきている気がしないでもないので、また問題になる前に継続的に見直していきたいと思います。 開発環境改善 TUNAG の開発環境において、Railsのアセットコンパイルが遅い問題があったのですが、JavascriptなどのAssetの配信の仕組みを、RailsとWebpackで分離させたことで、sprocketsの無駄なコンパイルを排除し、待ち時間を大きく減らすことができました。またフロントエンド開発において、Webpackが提供するHot Module Replacement (HMR)についても適用することができるようになり、今後開発者体験も向上していきそうです。来年以降、フロントエンド開発はますます加速していくのでこの改善は非常にありがたいです。 一方で課題に感じるのは、ずっと同じ環境で開発していると緩やかに遅くなっていく開発環境に対して鈍感になってしまうということ。そして、RailsとWebpackが依存し合っている仕組みだと、両方の技術を理解していないと問題提起しにくいという難しさも感じました。しかし「開発環境が遅い」という事象に対しては、誰でも多少ストレスを感じるはずです。そこに慣れるのではなくプログラマーの三大美徳の一つ、「短気」なマインドを持ち、目の前の開発環境の怠慢さに怒りを感じ、問題を提起し、解決に導くことは非常に重要です。そういったエンジニアがどれだけ組織にいるか?という指標は中長期的にプロダクト部としてのアウトプット量を大きく左右します。大きなことを成し遂げる上で、斧を研ぐことがどれだけ大事なのか。開発環境を1%でも良くすることが、今後人数が増えていく開発組織においてどれだけ大事なのか。ということをチームカルチャーとして浸透させていき、来年はエンジニア全員が「短気」になっていければと思います。 APIドキュメンテーション標準化 APIドキュメンテーション標準化がされる以前は、APIドキュメントは社内wikiに蓄積されていましたが、機能が増えてきたことによってドキュメントの数も増えて管理が難しくなったり、 フォーマットが明確でないので書く人によってばらつきがあるという問題がありました。そこでAPIを提供する側・使う側の両方の立場のエンジニアが協力して、ツールの選定、運用方法の確立及び浸透を推進してくれました。既存資産であるRSpecというテストフレームワークをそのまま活かすことで実装者にほぼ負担のない形で導入でき、Swagger UIをホスティングすることで簡単にドキュメントにアクセスできようにしてくれました。そして、運用する中で出てきた課題も適宜解決してくれました。 RSpec から API ドキュメントを生成する「rspec-openapi」を試してみた JSON:APIのRequestSpecに、jsonapi-rspecを導入する - stmn tech blog このプロジェクトは、技術領域やチームを横断する良い例だったと思います。ドキュメントのズレをいちいち手動で修正するのがめんどくさいと、プログラマーの三大美徳の一つ「怠惰」なマインドで課題に向き合ってくれたおかげだと思います。このような、技術領域やチーム領域の間を埋めるエンジニア、越境するエンジニアといった存在は事業を推進する上で非常に重要になってくるので、今後も働きかけをフォローしていきたいと思います。 失敗から学ぶ取り組み スタメンのプロダクト部には「失敗に向き合う」という バリュー が定められています。2020年も不具合や障害など失敗をしてきましたが、今年を振り返ると組織全体で失敗に向き合う姿勢のレベルが一段上がったなと感じています。ただ失敗を反省するだけでなく、個々人の原因追求・再発防止・未然防止の質が高まり、同じ失敗をしないよう改善できるようになってきていると感じています。障害振り返り会での議論も活発になってきて、本音でオープンに原因を追求し、組織全体が失敗から学ぶカルチャーが浸透してきているなと感じています。 2021年にやること 私個人として、組織全体として2021年を実行したいことは2点です。 技術戦略を描き実行する 全体最適・基準決めに注力していく 技術戦略を描き実行する 2020年を振り返ると、事業の要求に応えながら多くの開発でプロダクトの成長を支え、進化させてくることができました。 一方で私自身、開発組織における「技術戦略」について言語化・発信が足りなかったなという反省があります。「技術戦略」というものは、事業上の要望に120%応えつつ、プロダクトの品質、改善スピード、低コスト実現、採用力、育成力...など事業戦略を加速させる強みとして、開発組織・アーキテクチャ・カルチャーをどの時間軸で達成したいのか、そのために限られたリソースをどう配分したらよいのか、ということを示したものだと考えていますが、それに取り組む動きが弱かったと感じています。「やるべきリスト」は常に目の前にありますがこれは戦略ではないです。事業を牽引するスタメンならではの「技術戦略」の描き筋があるはずで、今後はより具体的に考え、言語化し、発信していきます。そこで必要になってくることは、目に見える課題だけではなく、まだ目に見えないより重要な課題を発見しアプローチすることです。何かをやったことのコストとリターンは見えますが、「見えていない」「やっていない」ことによる機会損失はそれ以上に重く捉えるべきと思っています。見逃しているチャンスが無いように今一度システム・組織・未来の事業の姿を考えて、引き続きプロダクト部のビジョンである「プロダクトで事業を牽引する」の実現を追求していきます。 全体最適・基準決めに注力していく 2020年は、バックエンド領域に関しては私も中心となって技術的な意思決定をしてきましたが、その他フロントエンドやネイティブアプリ領域に関しては、技術選定やその実行タイミングは各技術領域のチームごとに意思決定をお任せしてきました。2021年も同じく、信頼してお願いしていきたい一方で、意思決定のフォローや全体最適となるようにバランスをとることはCTOとしての重要な責任です。今後、人・チーム・事業・技術領域が増える中で、技術的意思決定の基準・価値観を揃えることは重要になってきます。 正直、TUNAG は自分でも全体の技術を把握しきれない規模に成長しているのですが、例えば、プロダクト開発を一時的に止めてでも投資すべき技術的な意思決定の判断を迫られた時に「分かりません」じゃ話になりませんし、理解して投資が必要と判断できれば、事業責任者に説明して納得してもらえるように働きかける責任がCTOにはあります。実際、私が全ての領域を理解し判断していくことは難しいですが、問題提起や議論のファシリテートをしながら、各意思決定をオープンに残し、エンジニアチーム全体でベストな意思決定に導くフォローはできるはずです。考えてみれば当たり前なのですが、自社のシステムを十分に理解していないことによって起こる機会損失は、最新技術のトレンドや他社の事例知らない以上に大きいと捉えています。なので2021年は全体最適のリードができるように、自分の技術領域を増やす方向でインプットし、まずはシステムの理解を深めていきたいと思います。 また、事業を複数展開し組織化が進む中で、同じような課題や意思決定が増えてくるはずです。組織内での車輪の再発明を防ぎ、組織化していくからこその強みを生かしていくために、横展開することを想定した設計や基盤の整備、及びナレッジの水平展開が可能な情報共有・展開の仕組みも考えてきます。 そして、社内のエンジニア全体へこれらのメッセージを定期的に発信できるように、月一くらいでCTO通信的なものを始めてみようと思います。 振り返ってみると、2020年は組織や技術の変化が沢山ありました。まだ書き足りないですが、ここで終わりにします。年始に良いスタートダッシュが切れるように、年末は来年のイメージを膨らませながらしっかりリフレッシュをします! 皆さん1年間お疲れさまでした! 最後に 株式会社スタメンは、2020年12月15日をもちまして東京証券取引所マザーズへ新規上場いたしました。これまで支えてくださった方々への感謝の気持ちと、またここからリスタートし、次の高い山を目指してより一層プロダクト開発に励んでいきたいという思いです。 来年は、既存システムのアーキテクチャを事業成長の方向に合わせて大きく変えていくアーキテクトや、大規模なシステムの運用やソフトウェア開発をリードするエンジニアなど、幅広く仲間を見つけていきたいと思っています。B2Bの TUNAG だけでなく、オンラインファンサロン事業 FANTS も展開しており、今後も多角的に事業を展開していきたいと思っているので、TUNAG や FANTS に興味を持ってくれた方も、そうでない方も一緒に作っていくエンジニアを募集しています!興味を持ってくれた方は、 Wantedly で応募いただければと思います!技術・会社・事業についてもっと詳しく聞いてみたい方は、松谷( @uuushiro )まで気軽にDMいただければ喜んで返事します!というわけでここまで読んでいただきありがとうございました。
アバター
目次 はじめに ジェネリック型とは 汎用性の高いコンポーネントを作成 おわりに はじめに こんにちは、スタメンでエンジニアをしている手嶋です。普段は、React+TypeScriptでフロントエンドメインで開発をしています。開発の中でReactコンポーネント(以下コンポーネント)を共通的に使いたいことが多々あるのですが、その手法の一つとしてジェネリック型が有効だったので、今回紹介したいと思います。 ジェネリック型とは ジェネリック型は一言で表すと、「型を抽象化したもの」です。 定義だけでは分かりにくいので、理解を早めるために例を挙げます。 以下のようにtextとindexをそれぞれstring型、number型で受け取って、返す関数があったとします。 const showText = ( text: string ) : string => { return text } const showIndex = ( index: number ) : number => { return index } この似たような処理を共通化する際に、ジェネリック型が役立ちます。 以下のように変更することで、関数を呼び出す際に型を指定すれば処理をまとめることができます。 「型のみ異なる」コードを抽象化することで、汎用性を高くすることが出来ました。 const genericFunction < T > = ( arg: T ) : T => { return arg } genericFunction < string >( "hoge" ) // showText()と同じ genericFunction < number >( 10 ) // showIndex()と同じ この考えを応用し、Reactで汎用性の高いコンポーネントを作成します。 汎用性の高いコンポーネントを作成 例として以下コードを挙げます。 DropDownListコンポーネントは、アプリケーション内で汎用的に使うものだと想定してください。 役割としては、親コンポーネントから受け取ったusersをmapで回して子コンポーネント(DropDownItemコンポーネント)に渡すことです。 親コンポーネントではDropDownで選択した単一のuser名をStateとして管理しています。 (スタイルやDropDownItemコンポーネントの中身は割愛しています) ジェネリック型使用前 //types export type UserType = { key: number ; name: string ; } ; // 親コンポーネント import React , { useState , useCallback } from 'react' ; import DropDownList from 'components/common/DropDownList' ; import UserType from 'types/user' ; interface Props { users: UserType [] ; } const User = ( props: Props ) => { const { users } = props ; // DropDownで選択した単一のユーザーの名前をStateで管理 const [ userName , setUserName ] = useState (); const handleSetUserName = useCallback (( name: string ) => { setUserName ( name ) } , [] ); return ( < div > < span > ユーザー一覧 < span > < DropDownList users = { users } setUserName = { handleSetUserName } / > < div > ); } ; export default User ; // DropDownListコンポーネント import React from 'react' ; import DropDownItem from 'components/commmon/DropDownItem' ; import UserType from 'types/user' interface Props { users: UserType [] ; setUserName: ( name: string ) => void ; //親コンポーネントで管理するStateをセットするAction } const DropDownList = ( props: Props ) => { const { users , setUserName } = props ; return ( < div > { users.map ( user => { return ( < DropdownItem key = { user.key } value = { user.name } setValue = { setUserName } / > ); } ) } < div > ); } ; export default DropDownList ; この状態でも問題なくコードは動作し、ユーザーの名前をDropDownListとして表示できます。 しかし、ユーザーの名前以外(例えばNumber型のリスト)を表示したい場合はどうでしょうか。 DropDownListコンポーネントのinterfaceが 具体的 すぎる(users,setUserNameしか許可していない)ので、再利用することができません。 アプリケーション内で同じ見た目を実現する際に、コンポーネントを使い回せないのは勿体ないです。 解決策として、以下のようにジェネリック型を使用します。 ジェネリック型使用後 // 親コンポーネント① import React , { useState , useCallback } from 'react' ; import DropDownList from 'components/common/DropDownList' ; import UserType from 'types/user' ; interface Props { users: UserType [] ; } const User = ( props: Props ) => { const { users } = props ; // DropDownで選択した単一のユーザーの名前をStateで管理 const [ userName , setUserName ] = useState (); const handleSetUserName = useCallback (( name: string ) => { setUserName ( name ) } , [] ); return ( < div > < span > ユーザー一覧 < span > // DropDownListに対してstring型を指定して呼び出す < DropDownList < string > users = { users } setListItem = { handleSetUserName } / > < div > ); } ; export default User ; // 親コンポーネント② import React , { useState , useCallback } from 'react' ; import DropDownList from 'components/common/DropDownList' ; import PriceType from 'types/price' ; interface Props { price: PriceType [] ; } const Price = ( props: Props ) => { const { price } = props ; // DropDownで選択した単一の価格をStateで管理 const [ price , setPrice ] = useState (); const handleSetPrice = useCallback (( price: number ) => { setPrice ( price ) } , [] ); return ( < div > < span > 価格一覧 < span > // DropDownListに対してnumber型を指定して呼び出す < DropDownList < number > listItems = { price } setListItem = { handleSetPrice } / > < div > ); } ; export default Price ; //types export type ListItemType < T > = { key: number ; value: T ; //keyを汎用的なvalueという名前に、valueの型はジェネリックに } ; // DropDownListコンポーネント import React from 'react' ; import DropDownItem from 'components/commmon/DropDownItem' ; import ListItemType from 'types/common' //汎用的な型に変更 //親コンポーネントから渡した型がTに interface Props < T > { listItems: ListItemType < T > [] ; setListItem: ( value: T ) => void ; //親コンポーネントで管理するStateをセットするAction } //親コンポーネントから渡した型がTに const DropDownList = < T ,>( props: Props < T >) => { const { listItems , setListItem } = props ; return ( < div > { listItems.map ( listItem => { return ( < DropdownItem < T > key = { listItem.key } value = { listItem.value } setValue = { setListItem } / > ); } ) } < div > ); } ; export default DropDownList ; 上記のようにDropDownListコンポーネントの interfaceを抽象的 にしてあげることで、複数の(型が違う)親コンポーネントから呼び出す事が可能になりました。 DropDownListコンポーネント内で扱うpropsも汎用性の高い名前にすることで、アプリケーション全体で共通利用しやすくなると思います。 おわりに 今回はジェネリック型を用いて、汎用性の高いReactコンポーネントを作成する方法を紹介しました。 是非、コンポーネントを共通化する際の選択肢の一つとして考慮してみてください。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
アバター
目次 はじめに jsonapi-rspecのinstall 既存のRequestSpecによく見られるテストケースの例 jsonapi-rspecで置き換えてみる さいごに はじめに こんにちは、株式会社スタメンでエンジニアをしている ワカゾノ です。 4月からサーバーサイドエンジニアとして、弊社プロダクト TUNAG の開発を行っております。 TUNAGでは、ユーザビリティの向上を目的に、既存機能のReact化、Native化が進められています。 その際に、 jsonapi_serializer というGemを使用してAPI実装を行っています。responseはJSON:API形式で取得することが出来ます。 また、弊社では APIドキュメント化 を進めており、RequestSpecによる結合テストを必ず追加し、APIインターフェースに対してドキュメントを作成、追加するようにしています。 TUNAGの既存のRequestSpecには下記のような問題点があります。 responseのstatusやID値のみのテストケースが多く、APIドキュメントに記載されている各属性値に関するテストケースが少ない responseのネストが深くなる場合に、 response['data'][0]['a']['b'] のように対象のテストデータを取得する必要があるため、テストを作成した人以外が見た場合に、直感的に分かりにくいテストケースになってしまう 今後、APIを実装する機会が増加する、APIドキュメントの各属性値に関して、正しさを担保する為に、より細かく、そして直感的にRequestSpecを書きたいというニーズがありました。 そのため、これらの問題点を解消するために、今回は jsonapi-rspec というGemを試してみました。 その使用感や感想についてまとめてみようと思います。 jsonapi-rspecのinstall 公式のREADME 通りですが、下記の手順でinstall、設定をします。 Gemfileに追記します gem ' jsonapi-rspec ' プロジェクトにinstallします bundle install spec/spec_helpers.rbに設定を追記します テストケースの中でkeyをstring型、symbol型のどちらも使用したい場合は config.jsonapi_indifferent_hash = true と設定します。 # spec/spec_helpers.rb require ' jsonapi/rspec ' RSpec .configure do | config | config.include JSONAPI :: RSpec # Support for documents with mixed string/symbol keys. Disabled by default. config.jsonapi_indifferent_hash = true end 既存のRequestSpecによく見られるテストケースの例 下記のようなTODOアプリケーションを例として、テストケースを作成しました /api/v1/tasksにアクセスした際に、TODOリスト一覧を取得することが出来る TODOリストの中には、「完了」と「未完了」のタスクがあり、/api/v1/tasksのエンドポイントにパラメータを付与することで、絞り込みを行うことが可能 require ' rails_helper ' RSpec .describe ' Api::V1::Tasks ' , type : :request do describe ' GET /api/v1/tasks ' do # 完了しているTODOタスクデータを作成 let!( :complete_todo_task ) # 詳細は割愛 # 未完了のTODOタスクデータを作成 let!( :incomplete_todo_task ) # 詳細は割愛 # responseから、取得したデータIDを配列へ格納 let( :json ) { JSON .parse(response.body, symbolize_names : true ) let( :response_todo_tasks ) { json[ :data ].map { | task | task[ :id ].to_i } } context ' 正常系 ' do context ' パラメーターが存在しない場合 ' do it ' TODOリストを取得する ' do get ' api/v1/tasks ' expect(response_todo_tasks).to eq [complete_todo_task.id, incomplete_todo_task.id] end end context ' パラメーターが存在する場合 ' do context ' 完了のパラメーターを付与した場合 ' do it ' 完了しているTODOデータのみ取得する ' do get ' api/v1/tasks ' , params : { status : ' complete ' } expect(response_todo_tasks).to eq [complete_todo_task.id] end end context ' 未完了のパラメーターを付与した場合 ' do it ' 未完了のTODOデータのみ取得する ' do get ' api/v1/○○○ ' , params : { status : ' incomplete ' } expect(response_todo_tasks).to eq [incomplete_todo_task.id] end end end end end end TUNAGの既存のRequestSpecではこのように、レスポンスから取得したデータのIDによりIN、OUT値をテストするテストケースが多く存在します。 今回のAPIのresponse例は下記のような構造となりますが、ID値以外の各属性値をテストしたい場合に、対象のデータを取得するまでが大変であり、またAPIのインターフェースが変更された際の、テストの修正点が多くなってしまいます。 { :data => [{ :id => " ○○○ " , :type => " tasks " , :attributes => { :title => " 宿題を終わらせる! " :status => " 完了 " , } }, { :id => " ○○○ " , :type => " tasks " , :attributes => { :title => " 買い物に行く! " :status => " 未完了 " , } } ], :meta => { :total_todo_num => 2 } } jsonapi-rspecで置き換えてみる 先程のテストケースを jsonapi-rspec を使用して置き換えたものが下記になります。 require ' rails_helper ' RSpec .describe ' Api::V1::Tasks ' , type : :request do describe ' GET /api/v1/tasks ' do # データを作成する箇所は割愛 # 属性毎のテストをするため変更 let( :json ) { JSON .parse(response.body, symbolize_names : true ) let( :response_todo_tasks ) { json[ :data ] } context ' 正常系 ' do context ' パラメーターが存在しない場合 ' do it ' TODOリストを取得する ' do get ' api/v1/tasks ' expect(response_todo_tasks[ 0 ]).to have_id(complete_todo_task.id) expect(response_todo_tasks[ 1 ]).to have_id(incomplete_todo_task.id) end # 新規にmetaのテストケースを追加 it ' metaデータを取得することが出来る ' do get ' api/v1/tasks ' expect(json).to have_meta( total_todo_num : 2 ) end end context ' パラメーターが存在する場合 ' do context ' 完了のパラメーターを付与した場 ' do it ' 完了しているTODOデータのみ取得する ' do get ' api/v1/tasks ' , params : { status : ' complete ' } expect(response_todo_tasks[ 0 ]).to have_type( ' tasks ' ) expect(response_todo_tasks[ 0 ]).to have_id(complete_todo_task.id) expect(response_todo_tasks[ 0 ]).to have_attribute( :title ).with_value( ' 宿題を終わらせる! ' ) expect(response_data[ 0 ]).to have_attribute( :status ).with_value( ' 完了 ' ) end end context ' 未完了のパラメーターを付与した場合 ' do it ' 未完了のTODOデータのみ取得する ' do get ' api/v1/tasks ' , params : { status : ' incomplete ' } expect(response_todo_tasks[ 0 ]).to have_type( ' tasks ' ) expect(response_todo_tasks[ 0 ]).to have_id(incomplete_todo_task.id) expect(response_todo_tasks[ 0 ]).to have_attribute( :title ).with_value( ' 買い物に行く! ' ) expect(response_todo_tasks[ 0 ]).to have_attribute( :status ).with_value( ' 未完了 ' ) end end end end end end マッチャとして下記を使用しています。 have_type have_id JSON:APIでリソースの判別に使用される type と id をテストするmatcher have_attribute(key).with_value(value) attributes配下にkeyが含まれるかとその値をテストするmatcher have_meta metaデータに関してkeyと値をテストするmatcher その他にもリソース間の関連をテストする必要がある場合に have_relationship().with_data() などのmatcherも用意されています。 APIのインターフェースに沿った形でテストケースを書くことが出来るため、より直感的にテストを書くことが出来るようになりました。 また、テスト作成者以外が見てもテストの意図が明確であり、インターフェースの変更に伴うテストの修正が容易になると思います。 さいごに APIドキュメントは既存機能をReact化、Native化するにあたり、多くのチームが参照するため、APIのインターフェースの正確性を担保することが重要であり、なるべく詳細にテストを書く必要があると考えています。 JSON:APIのRequestSpecをより書きやすく、分かりやすくするために有用なGemだと思いますので、是非試してみてください! スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
アバター
目次 はじめに アップロードの流れ Google Cloud Storage の準備 実装 おわりに はじめに こんにちは、スタメンのミツモトです。 スタメンでは TUNAG 、 FANTS というサービスを提供しており、画像の保存先として Amazon Web Services のS3(Simple Storage Service)を採用しています。 Google Cloud Storage(以下、GCS)の場合、どういう流れで画像を保存するか知りたいと思い、自分の学習として GCS を用いた署名付きURLによる画像のアップロードを実装したので紹介させていただきます。 アップロードの流れ 今回はクライアントから直接GCSへ画像をアップロードします。 通信の流れは以下になります。 Google Cloud Storage の準備 サービスアカウント 最初に、GCSのバケットにアクセスするためのサービスアカウントを作成します。 Google Cloud Platformにアクセスし、サイドバーから「IAMと管理 > サービスアカウント」を選択してください。 「サービスアカウントの作成」をクリックします。 サービスアカウント名を入力し、サービスアカウントの権限として「Cloud Storage > Storageオブジェクト管理者」を選択します。 アプリケーションで作成したサービスアカウントを利用するためキーを追加します。サービスアカウントの一覧で対象アカウントの「編集」をクリックしてください。 サービスアカウントの詳細から「鍵を追加」をクリックし、JSON形式で鍵を作成してください。ここで作成した鍵をサーバーサイドの実装時に利用します。 バケット 続いてバケットの作成をします。 サイドバーから「Storage > ブラウザ」を選択してください。 バケット名の入力、データの保存場所やストレージクラス等を選択し、バケットを作成します。 以上で Google Cloud Storage の準備は完了です。 実装 サーバーサイド 署名付きURLを発行するためのモジュールを作成します。 先程作成したサービスアカウントを用いてクレデンシャルを生成し、それを用いてバケットに対する署名付きURLを発行します。 content_typeを指定しないとクライアントからのアップロードが上手くいかないため、署名付きURLを発行する時点でcontent_typeを指定しておきます。 require ' google/cloud/storage ' module Utils :: Gcp :: Storage GCP_SA_CREDENTIALS = { private_key : ' サービスアカウントのprivate_key ' , client_email : ' サービスアカウントのclient_email ' } GCS_PROJECT_ID = ' GCPのプロジェクトID ' GCS_BUCKET_NAME = ' GCPのバケット名 ' class << self def pre_signed_url (path, content_type, expires) @storage = Google :: Cloud :: Storage .new( project_id : GCP_PROJECT_ID , credentials : GC_SA_CREDENTIALS ) expires = expires.to_i @storage .signed_url( GCS_BUCKET_NAME , path, method : ' PUT ' , content_type : content_type, expires : expires) end end end Resourceであるインスタンスからモジュールの pre_signed_url メソッドを呼び出し、レスポンスとして返却します。 class Resource < ApplicationRecord def pre_signed_url (filename【 GCS上のファイル名 】, content_type) Utils :: Gcp :: Storage .pre_signed_url( " resouces/ #{ id } /images/ " + filename, content_type, 5 .minutes.from_now) end end クライアントサイド 最初のシーケンス図通り、以下の順番でリクエストを送ります。(エラーハンドリングは省略しています。) 署名付きURLの取得 GCSへの画像アップロード アップロードされた画像の情報をDBへ保存 実装としては以下のようになります。 const uploadImage = async ( file , resourceId ) => { // 署名付きURLの取得 const res = await fetch ( "署名付きURL取得エンドポイント" + `?content_type= ${ file . type } ` ) const resJson = await res . json () ; // 署名付きURLを用いて Google Cloud Storage へアップロード await fetch ( resJson . preSignedUrl , { method : "PUT" , headers : { "Content-Type" : file . type } , body : file }) // アップロードされた画像の情報をDBへ保存 fetch ( "画像の情報をDBへ保存するエンドポイント" , { method : "POST" , headers : { "Content-Type" : "application/x-www-form-urlencoded; charset=utf-8" } , body : `uniq_filename= ${ resJson . filename } &filename= ${ file . name } &content_type= ${ file . type } &byte_size= ${ file . size } ` } ) } おわりに GCS を用いた署名付きURLによる画像のアップロードについて紹介させていただきました。想定していたより簡単に画像のアップロードを実現できたので良かったです。今回はアップロードの流れを書きましたが、機会があれば画像配信についても記事にできたらと思います。 スタメンでは一緒に働くエンジニアを募集しています。 興味がある方は、ぜひ 採用サイト からご連絡ください!
アバター
はじめに 本記事では RSpec の request spec から OpenAPI 仕様のドキュメントを出力する Gem、rspec-openapi を紹介します。 ドキュメンテーションツール導入にあたっての負担を少なくしたい、実装とドキュメントが乖離しないようにしたい、という場合に参考になるかもしれません。 背景 これまで弊社では、 API ドキュメントは社内 wiki に蓄積されていました。 最初はこれでも問題にならなかったのですが、機能が増えてきたことによってドキュメントの数も増えて管理が難しくなったり、 フォーマットが明確でないので書く人によってばらつきがあるという問題がちらほら出てくるようになります。 API ドキュメント標準化といえば OpenAPI が思いつくものの、記述のために DSL を理解する必要があるとか追加で何か学習が必要となると、全員に浸透させるのはハードルが高そうです。 もちろん OpenAPI 仕様に則ることの価値をチームとして合意できていたら話は別だと思います。今回はこれまでまったく使っていない状況から導入する、という前提なので少しでも簡単に導入できるほうが望ましいです。 rspec-openapi はアウトプットが OpenAPI フォーマットかつ既存の request spec がそのまま利用できるということで、上記の条件に適しているのではないかと思い試してみました。 使い方 Gemfile に以下のように追記して Gem をインストールします。 gem ' rpsec-openapi ' , group : :test OPENAPI=1 と環境変数をセットして request spec を実行すると、doc 配下に openapi.yaml というファイルが生成されます。 $ OPENAPI = 1 rspec path/to/request_spec_file Rails でなくても使えますが、内部で Rails かどうかの判定で処理を分岐している箇所がいくつかあるので、細かい部分など違いがあるかもしれません。 例を見てましょう。 以下のようなコントローラーとテストケースを用意しました。 class Api :: PostsController < ApplicationController def index posts = [ { title : ' foo ' } ] render json : { posts : posts } end end require ' rails_helper ' RSpec .describe ' Api::Posts ' , type : :request do describe ' GET /api/posts ' do it ' get posts ' do get api_posts_path, params : { title : ' f ' } expect(response).to have_http_status( :ok ) end end end テストを実行すると OPENAPI = 1 rspec spec/requests/api/posts_spec.rb doc/openapi.yaml に以下のような内容が生成されます。 --- openapi : 3.0.3 info : title : rspec-openapi-sample paths : "/api/posts" : get : summary : index tags : - Api::Post parameters : - name : title in : query schema : type : string example : f responses : '200' : description : get posts content : application/json : schema : type : object properties : posts : type : array items : type : object properties : title : type : string example : posts : - title : foo このファイルを Swagger Editor などに読み込ませると、問題なく生成できているのがわかるかと思います。(version だけ手動で追記しました) 実行結果から生成するという仕様上、実行結果に含まれない情報は取得できないので、その点については手直しが必要です。 例えば必須パラメータやレスポンスのフィールドが nullable なのかどうかなどの厳密な型情報は、テストケースだけだと判別がつきません。 出力ファイルには追記はされても上書きはされないので、手で直した箇所を保持しながらドキュメントを拡充していくことができます。 まとめ RSpec からドキュメントを生成する rspec-openapi を紹介しました。 既存資産(request spec)をそのまま活かすことで実装者にほぼ負担のない形で導入できるという点は、類似のライブラリと比べても優れているところだなと感じます。 良ければ試してみてください。 参考 rspec-openapi OpenAPI Specification
アバター
こんにちは。フロントエンドエンジニアの 渡邉 です。 普段はReactとTypeScriptを書いています。 今回の内容はタイトルにも記載されていますが、Next.jsを使った画像の最適化です。 画像の最適化でやるべきことは多々あります。 例えば、画像のサイズ・重さ調整や、フォーマット、遅延読み込みなどあります。 Next.js 10で発表された next/image を使えば誰でも簡単に画像の最適化が行えるようになりました。 この記事では以下のような方が対象者となります。 - 画像の最適化?したことないや - 自動でやってくれるなら試してみたい ただし、Next.jsの使い方などは今回は記載していないので、ご了承ください。 目次 画像最適化するとなにがいいの? 実際にnext/image使ってみる まとめ 画像最適化するとなにがいいの? 以下のようなメリットがあります。 - 画像を最適化することによりページの読み込み速度を改善できる - SEOの改善 画像を最適化することによりページの読み込み速度を改善できる 画像を多用したページでは最適化してあるか、していないかで大きく読み込み速度に差が出てきます。 例えば、ECサイトであったり、ランディングページに置いて読み込み速度が遅いとユーザーが離脱して売上にもかなり影響出てきます。 以下の画像はGoogleがページ表示速度がユーザーにどのくらい影響するかを調査した内容です。 引用元: Find Out How You Stack Up to New Industry Benchmarks for Mobile Page Speed 1秒から3秒で32% 1秒から5秒で90% 1秒から6秒で106% 1秒から10秒で123% 直帰率 が増加してしまいます。 SEOの改善 SEOの改善?と思われた方もいると思います。 どういうことかというと、Googleが、Webページの表示速度が遅いと検索のランキングに影響が出るよと言っています。 詳細は こちら ということもあり画像の最適化がされていない表示速度が遅くなり検索ランキングが低下し、そもそもユーザーに見に来てもらえない可能性もでてきます。 上記の他にも最適化することによってのメリットは多々あります。 ただ、もちろん最適化するには工数もかかるうえに大変です。 そこで、Next.jsを使えば画像の最適化を自動で行ってくれるということです。 実際にnext/image使ってみる nextのサンプルアプリケーションを作成し起動させます。 npx create-next-app image-pra cd image-pra yarn dev アプリケーションが起動したら public 配下に今回お試しで使用する画像を配置するのと、 page/index.js を以下のように書き換えます。 import Image from 'next/image' function Home() { return ( <> <h1>Image optimisation</h1> <Image src= "/sample.jpg" // publicに配置した画像 alt= "sample picture" width= { 500 } height= { 300 } /> </> ) } export default Home これだけで最適化された画像が表示されます。 基本的に使い方は既存の <img> と同じです。 ただ、 height と width を設定しないとエラーが出ます。 ここでは、アスペクト比にあった数値をいれてもらえれば、あとはImageコンポーネントがレスポンシブに画像サイズを調整してくれます。 実際にImageコンポーネントは以下のように変換されています。 変換された <img> でsrc属性の /_next/image というエンドポイントが気になったので説明していきます。 今回srcで指定されている /_next/image はNext.js 10〜から予め用意されているエンドポイントです。 _next/image のリクエストを受けると next-server はimageOptimizerを実行しています。 https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/next-server.ts 抜粋 { match: route( '/_next/image' ), type: 'route' , name: '_next/image catchall' , fn: (req, res, _params, parsedUrl) => imageOptimizer(server, req, res, parsedUrl), } , image-optimizer.ts その呼び出されている image-optimizer.ts で様々な最適化処理が行わています。 例えば、ブラウザがWebPに対応している場合にはWebPに変換するなどの対応がされています。 このように、コードの中を読むと実際にどのように最適化しているかがわかって面白いので是非お時間ある方は読んでみてください。 話がすこしそれてしまいましたが、 Vercel にデプロイすれば特に設定する必要なく使えます。とても楽ですよね。 外部サービスで最適化する場合 imgixやCloudinaryの外部のサービスで画像の最適化を行う場合は next.config.js で設定します。 例えばimgix使う場合は以下のようになります。 module.exports = { images: { loader: 'imgix' , path: 'https://example.com/myaccount/' , } , } 詳しく知りたい方は image-optimization#loader を御覧ください。 <img> とImageコンポーネントの比較 上の sample.jpg が <img> で表示している画像で、下の image?url=~~ のほうが Image コンポーネントで表示した画像です。 画像の読み込み時の詳細を見てみると、読み込み速度とサイズが大幅に改善されています。 デフォルトでここまで改善できるが素晴らしいです。 例えば、ある画像だけ品質を下げてもいいなというときには、 quality に数値を渡してあげるだけで調整ができます。 <Image src= "/sample.jpg" alt= "sample picture" width= { 500 } height= { 300 } quality= { 50 } // ここで調整する(デフォルトは75) /> こうすることで、よりSizeを抑え込めます。 その他propsについては next/image#usage を御覧ください。 画像最適化の全体設定 画像最適化の全体での設定は next.config.js で行えます。 module.exports = { images: { // ここに記載 } , } こちらも詳細は image-optimization#configuration を御覧ください まとめ Next.jsのImageコンポーネントを使うだけで画像を簡単に最適化することができます。 今回紹介しきれなかったんですが、初期表示で表示される画像が画面の外にある場合は、viewportを計算して遅延読み込みまでしてくれます。最近のNext.jsは成長がとてつもなく速く、質が高いので今後も楽しみです。 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
アバター
目次 はじめに 使用する関連付け preload、eager_load、includesの挙動 includesはどのような場合にpreloadとeager_loadの挙動となるのか preload、eager_loadの使い分け さいごに はじめに こんにちは、株式会社スタメンでエンジニアをしている ワカゾノ です! 4月からサーバーサイドエンジニアとして、弊社プロダクト TUNAG の開発を行っております。 先日、弊社CTOの 松谷 とペアプロを行いました。 パフォーマンス改善のタスクを行いましたが、タスクを通してN + 1問題に複数回直面しました。 Active Recordにおいて、N + 1問題を解消する方法として、関連テーブルのデータを事前に読み込んでおき、キャシュしておくという方法を取ると思いますが、Railsではその方法としてpreload、eager_load、includesメソッドが用意されています。 それぞれのメソッドの挙動の違いについて、都度調べ理解するようにしていましたが、ペアプロを通して理解が浅いと感じた為、今回はpreload、eager_load、includesの挙動の違いや使い所についてまとめていきます。 検証環境 Ruby version 2.5.1 Rails version 6.0.3.4 使用する関連付け 下記のモデル間の関連付けを前提として各メソッドの挙動の違いを説明していきます。 class Company < ApplicationRecord has_many :users has_many :departments end class User < ApplicationRecord belongs_to :companies has_many :departments , through : :deparment_user_maps end class Department < ApplicationRecord belongs_to :company has_many :users , through : deparment_user_maps end # ユーザーが部署に所属しているかを扱う中間テーブル class DepartmenUserMap < ApplicationRecord has_many :users has_many :deparments end 各メソッドの挙動 preload 指定した関連テーブル毎に別クエリを作成、関連テーブルのデータ配列を取得し、キャッシュします。 引数に多対多の関連先を渡した場合は、中間テーブルを介して取得しキャッシュします。 preloadした関連先で絞り込みを行った場合は、例外を投げます。 Company .preload( :users , :departments ) → SELECT ` companies ` .* FROM ` companies ` SELECT ` users ` .* FROM ` users ` WHERE ` users ` . ` company_id ` IN ※※※ SELECT ` departments ` .* FROM ` departments ` WHERE ` departments ` . ` company_id ` IN ※※※ Department .preload( :users ) → SELECT ` departments ` .* FROM ` departments ` SELECT ` department_user_maps ` .* FROM ` department_user_maps ` WHERE ` department_user_maps ` . ` department_id ` IN ※※※ SELECT ` users ` .* FROM ` users ` WHERE ` users ` . ` id ` IN ※※※ # 例外を投げる Company .preload( :users ).where( users : { id : 1 }) → Mysql2 :: Error : Unknown column ' users.id ' in ' where clause ' : SELECT ` companies ` .* FROM ` companies ` WHERE ` users ` . ` id ` = 1 eager_load LEFT_OUTER_JOINで指定したデータを結合して関連テーブルのデータ配列を取得し、キャッシュします。 引数として渡した関連先の要素で絞り込みを行うことが出来ます。 Company .eager_load( :users ).where( users : { id : 1 }) → SELECT ` companies ` . ` id ` AS t0_r0, ` companies ` . ` name ` AS t0_r1, ` users ` . ` id ` AS t1_r0, ` users ` . ` name ` AS t1_r1, FROM ` companies ` LEFT OUTER JOIN ` users ` ON ` users ` . ` company_id ` = ` companies ` . ` id ` WHERE ` users ` . ` id ` = 1 includes 後述しますが、デフォルトではpreloadと同じ挙動、関連先のテーブルの要素で絞り込みを行った場合などはeager_loadと同じ挙動をします。 Company .includes( :users ) → SELECT ` companies ` .* FROM ` companies ` SELECT ` users ` .* FROM ` users ` WHERE ` users ` . ` company_id ` IN ※※※ Company .includes( :users ).where( users : { id : 1 }) → SELECT ` companies ` . ` id ` AS t0_r0, ` companies ` . ` name ` AS t0_r1, ` users ` . ` id ` AS t1_r0, ` users ` . ` name ` AS t1_r1, FROM ` companies ` LEFT OUTER JOIN ` users ` ON ` users ` . ` company_id ` = ` companies ` . ` id ` WHERE ` users ` . ` id ` = 1 includesはどのような場合にpreloadとeager_loadの挙動となるのか ActiveRecordには下記のように各メソッドが定義されています # activerecord/lib/active_record/relation/query_methods.rb:150 def includes (*args) check_if_method_has_arguments!( :includes , args) spawn.includes!(*args) end def includes! (*args) self .includes_values |= args self end # activerecord/lib/active_record/relation/query_methods.rb: 166 def eager_load (*args) check_if_method_has_arguments!( :eager_load , args) spawn.eager_load!(*args) end def eager_load! (*args) self .eager_load_values |= args self end # activerecord/lib/active_record/relation/query_methods.rb:180 def preload (*args) check_if_method_has_arguments!( :preload , args) spawn.preload!(*args) end def preload! (*args) self .preload_values |= args self end この時点ではクエリを発行せず、それぞれincludes_values、eager_load_values、preload_valuesに値を格納しているだけです。 その後、exec_queriesメソッド内でeager_loading?メソッドにより判定を行い、preloadとeager_loadを使い分けています。 # activerecord/lib/active_record/relation.rb:667 def exec_queries (&block) @records = if eager_loading? find_with_associations do | relation , join_dependency | if ActiveRecord :: NullRelation === relation [] else rows = connection.select_all(relation.arel, " SQL " , relation.bound_attributes) join_dependency.instantiate(rows, &block) end .freeze end else klass.find_by_sql(arel, bound_attributes, &block).freeze end preload = preload_values preload += includes_values unless eager_loading? preloader = nil preload.each do | associations | preloader ||= build_preloader preloader.preload @records , associations end @records .each(& :readonly! ) if readonly_value @loaded = true @records end # activerecord/lib/active_record/relation.rb:597 def eager_loading? @should_eager_load ||= eager_load_values.any? || includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) end eager_loading?メソッドは下記の場合にtrueとなります。 eager_load_valuesが存在している(eager_loadメソッドを使用している) includes_valuesが存在している(includesメソッドを使用している) かつ 別テーブルをjoinsしている か 関連先のテーブルで絞り込みを行っている includesメソッドの場合は2つ目の条件に当てはまる場合がeager_loading?がtrueとなります。 詳細は割愛しますが、eager_loading?がtrueの場合はfind_with_associationsメソッド内で結合処理がされていきます。 まとめるとincludesは デフォルトではpreloadと同様の挙動 他テーブルを結合処理しているか、関連先のテーブルで絞り込みを行っている場合はeager_loadと同じ挙動になる メソッドの実装の通り、複数のアソシエーションを引数で渡したとしても、どちらか片方のみpreload、もう片方はeager_loadを行うという挙動にはなりません。必ずどちらのアソシエーションもpreloadされるか、eager_loadされるという挙動になります。 preload、eager_loadの使い分け 上述の内容を踏まえると、includesはクエリを制御しにくいため、基本的に優先してpreload、eager_loadを使用する方が良いと考えられます。 その上でpreload、eager_loadの使い所を考えてみます。 preload has_manyの関連を持つデータの事前読み込みを行う場合 複数の関連先の事前読み込みを行う場合 eager_loadでは常に結合処理を行うため、データ量が大きいと考えられる条件下では、重いクエリを実行することになってしまう。クエリを分割して事前読み込みを行った方がレスポンスが早くなると考えられます。 ※主テーブルのレコード数が多い場合は、IN句が大きくなってしまうため、DB側での設定値を確認する必要があるようです。 eager_load 関連先の要素で絞り込みを行いたい場合 has_one、belongs_to関連など1クエリでデータを取得した方が効率が良いと考えられる場合 関連先が上記のように○対1で関連付けられている場合、外部結合しても取得するレコード数に変わりは無く、1クエリでデータが取得出来るためpreloadより効率が良いと考えられます。 最後に preload、eager_load、includesの挙動や使いどころについてまとめてみました。 パフォーマンスの問題はレコード数が増加するにつれて顕在化してくるため、未然に防止する為にも、日頃からよりパフォーマンスを意識したコードを書くことが出来るように意識していきたいと思います。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
アバター
みなさんこんにちは! 株式会社スタメンで主にTUNAGのモバイルアプリを開発している カーキ です。 スタメンに正式に入社して11月で約半年が経ちました。 スタメンでは自己研鑽としての読書が推奨されており、社内でお互いに本をオススメし合う「必読Book」という制度や、役員から直接 献本してもらえる「役員献本」という制度があります。 この記事では、そんなスタメンで半年を過ごしてきた新卒エンジニアが、入社後半年でどんなビジネス書を読んできたのかを紹介したいと思います。 この記事ではビジネス書の紹介のため、技術書は一切登場しません。エンジニア向けの読み物も今回は省きました。 思考系 ライト、ついていますか―問題発見の人間学 ライト、ついてますか―問題発見の人間学 作者: ドナルド・C・ゴース , G.M.ワインバーグ 発売日: 1987/10/25 メディア: 単行本 タイトルにもあるように本質的な問題の発見を主題においた本です。問題を頑張って解いてもそもそもの問題が本質的でなく誤っていれば、意味がありません。本書が示す「問題を見極める」は プロダクトチームのVALUE(価値観や行動指針) にも含まれており、スタメン プロダクトチームの血となり骨となっています。 イシューからはじめよ――知的生産の「シンプルな本質」 イシューからはじめよ ― 知的生産の「シンプルな本質」 作者: 安宅和人 発売日: 2014/09/01 メディア: Kindle版 知的生産の本質を指し示してくれる本。この本を読んでから「今まで自分がなんと非効率に仕事をしていたのか」と思い知りました。この本のはじめにある「『悩む』と『考える』の違い」はとても印象に残っていて、自分が『悩んでいる』と自覚するたびに本書を思い出して『考えよう』と切り替えられるようになりました。また『問題解決の5ステップ』は常に確認できるよう、メモして卓上に置いています。 エッセンシャル思考 最少の時間で成果を最大にする エッセンシャル思考 最少の時間で成果を最大にする 作者: グレッグ・マキューン 発売日: 2014/12/12 メディア: Kindle版 自分にとって本当に必要な仕事だけをしようという本。入社したてということもあり、お仕事をもらうことが嬉しくてなんでも引き受けてしまいがちな自分には刺さりました。「受けること」「受けないこと」の線引きの章はとても印象に残っています。 SINGLE TASK SINGLE TASK 一点集中術――「シングルタスクの原則」ですべての成果が最大になる 作者: デボラ・ザック 発売日: 2017/09/01 メディア: Kindle版 マルチタスクはやめて一つのことに集中しようという本。今年はリモートで業務を行う期間が長く、家だとなかなか集中力が続かないと思い読みました。本書ではただシングルタスクをしよう!と言うだけではなく、そのための環境づくり(周囲の人への配慮、習慣化)もしっかり説明されていて、実践的でとても良かったです。 思考中毒になる 思考中毒になる! (幻冬舎新書) 作者: 齊藤孝 発売日: 2020/07/29 メディア: Kindle版 思考する習慣のメリットやその方法について書かれています。後述する 仕事ができるとはどういうことか を読んだときに、センスを身につけるには思考し続けるしかないんじゃないか、と思いこの本を手にしました。『笑いとクリエイティビティ』について書かれた章が個人的には著者の斎藤孝さんらしくてとても好きでした。 コミュニケーション 人を動かす 人を動かす 文庫版 作者: D・カーネギー 発売日: 2016/01/26 メディア: Kindle版 円滑な対人関係について書かれた言わずと知れた名著。初めはオーディオブックとして購入していましたが、体系的に読み直したいと思い文庫版を購入し直しました。書いてあること一つ一つは当たり前のことが多いですが、汎用性がとても高く、噛めば噛むほど味の出るスルメのような本でした。月一でパラパラめくって読み返したいです。 雑談の一流、二流、三流 雑談の一流、二流、三流 (アスカビジネス) 作者: 桐生 稔 発売日: 2020/03/05 メディア: 単行本(ソフトカバー) 一流、二流、三流の雑談が段階的に書かれている本。自分はエンジニアではあるものの、社内でのちょっとした雑談から交流が生まれることもあります。リモートだからと言って『雑談』を蔑ろにできないと思い読み始めました。自分の雑談のパターンが結構「二流」のものが多く、読んでいて驚きと発見がありました。読了した後は雑談への「恐さ」が「楽しみ」に変わりました。 Team Geek Team Geek ―Googleのギークたちはいかにしてチームを作るのか 作者: Brian W. Fitzpatrick , Ben Collins-Sussman 発売日: 2013/07/20 メディア: 単行本(ソフトカバー) エンジニア集団におけるチームの作り方・あり方を書いた本。自分の中でモヤっていた『心理的安全』と言うワードが謙虚・尊敬・信頼の三本柱で支えられていたんだ!と分かり納得した本。今は新人ということで『謙虚・尊敬・信頼』を保てているけど、これから後輩が入ってきたときにもこの柱は大事にしたいと思います。 働き方 小さなチーム、大きな仕事 小さなチーム、大きな仕事 働き方の新しいスタンダード (ハヤカワ文庫NF) 作者: ジェイソン フリード , デイヴィッド ハイネマイヤー ハンソン 発売日: 2016/12/15 メディア: Kindle版 Ruby on Railsの開発者であるDHHなどが著者として参加している本。ベンチャーなど小さなチームにおける仕事の方法や思考法について書かれた本で、入社する直前に読んで士気を高めていました。エンジニア界隈で有名な Getting Real をより一般化した本ですが、日本語の文庫本として読めるのはこの本のみなのでオススメです。 早く正しく決める技術 早く正しく決める技術 作者: 出口 治明 発売日: 2018/02/26 メディア: Audible版 ライフネット生命創業者の出口さんが著者の本。決断力のない自分をなんとかしなきゃと思って購入しました。本書では決断は「数字」「ファクト」「ロジック」で決まるとあります。自分の中でふわふわと浮いていた『決断力』という曖昧な言葉が、「数字」「ファクト」「ロジック」の3つに落とし込まれたことで、「今の状況では『ファクト』が足りていないのでは?」などと、より具体的に思考することができるようになりました。 「仕事ができる」とはどういうことか 「仕事ができる」とはどういうことか? 作者: 楠木健 , 山口周 発売日: 2019/12/16 メディア: Kindle版 「仕事ができる」とはスキルではなく、センスの問題であるという本。本を読むことでスキルは手に入るかもしれないけど、センスとなると自分の中で思考して熟成させるしかないのかなと思わされました。センスを磨くために具体と抽象を反復的に思考するだったり、そもそも思考が足りてないんじゃないかなど、いろいろ考えるきっかけになりました。 一番になる人 一番になる人 作者: つんく♂ 発売日: 2013/11/01 メディア: Kindle版 今年になってハロープロジェクトにどハマりしたので、ハロプロ創始者のつんく♂が執筆した、この本を読みました。シャ乱Qのボーカル、そしてモーニング娘。のプロデューサーとして活躍したつんく♂の仕事哲学が詰まっています。書かれていること自体は目新しいことはないですが、それらがシャ乱Qやモー娘。プロデュース時代の出来事に上手くはまっていて説得力があります。 岩田さん-岩田聡はこんなことを話していた。 岩田さん: 岩田聡はこんなことを話していた。 (ほぼ日ブックス) 作者: ほぼ日刊イトイ新聞 発売日: 2019/08/05 メディア: Kindle版 任天堂の元社長であり、天才プログラマーでもあった岩田さんの考えていることや、インタビューがまとまっている本。この本を読むまで岩田さんは「DSとかWiiの発表動画に出てた人」くらいの認識だったものの、この本から岩田さんの誠実さが滲み出ていて、「あぁ、自分もこんなエンジニアになりたい」と思わさました。 ビジネス THE MODEL THE MODEL(MarkeZine BOOKS) マーケティング・インサイドセールス・営業・カスタマーサクセスの共業プロセス 作者: 福田 康隆 発売日: 2019/01/30 メディア: Kindle版 マーケティングやインサイドセールス、営業、カスタマーサクセスから構成される新しい営業のあり方、エコシステムについて書かれた本。まさにスタメンのTUNAGの事業体制が本書を参考に構成されているので、TUNAGのビジネスモデルをもっと知るために読みました。 グレイトフルデッドにマーケティングを学ぶ グレイトフル・デッドにマーケティングを学ぶ (日経ビジネス人文庫) 作者: ブライアン・ハリガン , デイヴィッド・ミーアマン・スコット 発売日: 2020/04/02 メディア: 文庫 グレイトフルデッドという米国のバンドが、いかにしてファンとの関係を築き、安定的な収入を得ているかをマーケティングの視点でみた本。社内でこれから FANTS事業 が始まる際に、主催者とファンとの関係の事例をもっと知りたいと思い読んだ本です。 ファンダム・レボリューション ファンダム・レボリューション SNS時代の新たな熱狂 (早川書房) 作者: ゾーイ フラード=ブラナー , アーロン M グレイザー 発売日: 2017/12/15 メディア: Kindle版 有名人やアーティスト自身の活動ではなく、その周縁のファンの活動が近年になって変化してきていることについて書かれた本。これもFANTS事業が始まるときに、ファンの活動や思いが何かサロン内での施策にどう影響してくるのかが気になり読んだ本です。 ノンフィクション 爆速経営-新生ヤフーの500日 爆速経営 作者: 蛯谷敏 発売日: 2013/11/07 メディア: Kindle版 2012年に宮坂学さんがヤフーの代表取締役になってからの500日をまとめた本。入社したての僕は社長って一体何をしているんだろう?事業が動くのは現場の人間が動いているからなんじゃないか?と思っていましたが、これを読んで、社長含め経営陣の手腕で会社はいくらでも変化をしていくということを学びました。そしてヤフーという大きな組織の改革を爆速で行った宮坂さんの経営者としての凄みを感じました。 21歳の学生が、200万人を呼び込む「どまつり」を作り上げた! 人も街も動かす!巻き込み力 21歳の学生が、200万人を呼び込む「どまつり」を作り上げた! 人も街も動かす!巻き込み力 作者: 水野 孝一 発売日: 2020/04/01 メディア: Kindle版 名古屋で20年開催されている ニッポンど真ん中祭り (通称: どまつり)の初開催そして拡大における『巻き込み力』について書かれた本。社内で勉強会などを開催するにあたって、巻き込み力が必要だなと思い読みました。自分は愛知に住んでいながら一度もど祭りを経験したことがないんですが、当時の大学生が発起人となって主催されていたことに驚きました。相手に理解してもらうために「なぜやるのか」の想いを伝えるなど、ストーリーだけでなく、アクションに注意して読むと自分にも活かせそうなところがいくつかありました 心の持ち方 EQ 2.0 (「心の知能指数」を高める66のテクニック) EQ 2.0 (「心の知能指数」を高める66のテクニック) 作者: トラヴィス・ブラッドベリー , ジーン・グリーブス 発売日: 2019/02/25 メディア: 単行本(ソフトカバー) EQ(Emotional Intelligence Quotient)は心の知能指数とも呼ばれ、自分の精神状態をよりよく保ったり、仲間と協同で作業をする際に必要とされています。そのEQを4つに分類をし、それぞれを高めるための方法が書かれています。本書についているコードでEQのテストをうけることもでき、そこから自分に足りないことを抽出して教えてくれるので、自分がどこに注力していけばいいのかが分かりとても良かったです。 パフォーマンス・マネジメント-問題解決のための行動分析- パフォーマンス・マネジメント―問題解決のための行動分析学 作者: 島宗 理 発売日: 2000/03/01 メディア: 単行本 行動分析学を自分たちの行動に応用して改善していこうという本。本書は全て「自分攻撃をやめよう」から始まっています。「自分のせいだ」と思ってしまうと次には何も改善されず、本当の問題が解決できません。そこでしっかりと自分の行動を分析し、改善するために行動するノウハウがまとめられています。自分だと技術の勉強が続かないときにふと「自分はなんて怠惰なんだ」と思ってしまうときがあるのですが、本書を思い出して「どうしたら良かったのかな」と分析的に考えられるようになりました。 最後に これまで半年間に読んできた本を振り返ってみて、「この本こんなこと書いてたな〜」など忘れかけていたこともあり、とてもいい機会になりました。 スタメンは行動指針に Take Ownership があり、自己研鑽を推奨する文化があります。冒頭にも述べましたが、スタメン社内では多くの人が読書を通じて自己研鑽を行っています。 それはエンジニアでも同じで技術書だけでなく、いち社員として円滑に仕事を進めるための読書を日々行っています。 スタメンではそんな自己研鑽をして自分に磨きをかけているエンジニアの方を募集しています。
アバター
こんにちは。スタメンで開発者をしている 津田 です。IntelliJ IDEA系のIDEでリモート共同開発と、ペア・プログラミング行うためのツール、 Code With Meが9月末にEAPリリース されたため、同僚と試用してみました。 Ruby on Railsアプリケーション開発のペア・プログラミングを、隣の席で行ったのですが、リモート・ワークにも非常に便利なツールだと思ったので、使用感を紹介します。 Code With Meとは 「 Code With Me EAP リリース 」によると、 Code With Me は IntelliJ IDEA と他の IntelliJ ベースの IDE でバージョン 2020.2.x から使用できる新機能です。 ローカルで実行中のプロジェクトを分散チームと共有できるようにします。 チームがすばやくコードにアクセスし、リアルタイムに問題の調査やレビュー、コーディング作業を共同で行えるようになります。 また、JetBrains IDE 製品で使用できるコードの自動補完、高度なナビゲーション、リファクタリング、各種デバッグ機能、および組み込みターミナルのメリットを最大限に活かしながらこれらすべての作業を実施できます。 とのことです。対応しているのは、以下のIDEで、今回はホストにRubyMineを使用しました。 IDEA Community および Ultimate / WebStorm / PyCharm Community および Professional / PhpStorm / GoLand / RubyMine / CLion / AppCode 大雑把にいうと、「ホスト側のIDEを、専用クライアントを使ってリモートから操作できる」機能です。ファイルは全てホスト側にあり、ゲストはホストにあるファイルを編集することになります。常に画面が同期されるわけではないため、ホストとゲスト(複数可)は別々のファイルを編集出来ます。 ホスト(RubyMine)の画面 ゲスト(IntelliJ Client)の画面 お互いが今どのファイルで作業しているかは、↑の画面のように、ファイルのタブに表示され、同じファイルを表示している場合は、それぞれのカーソルも表示されます。 画面左のProject Paneにはすべてのファイルが表示されていますが、 FAQ about Code With Me によると、ファイルをコピーして同期しているわけではなく、また、 通信はホスト・ゲスト間のend-to-endで暗号化してるそうです。 導入・セッション開始/参加 導入は Code With Me EAP リリース で説明されているとおりで、非常に簡単でした。 ホストとなる側のIDEでCode With Meプラグインを導入し、「Enable Access and Copy Invitation Link」するとセッションのURLが発行される ゲストはIntelliJ Clientをインストールし、発行されたURLでセッションに参加する という手順になります。今回は双方RubyMineがインストールされた状態ではじめたのですが、ゲストとして参加する側は、必ずIntelliJ Clientを使用することになります。 IntelliJ Clientは他のIntelliJ ベースの IDEと似た画面構成ですが、あくまで別途にインストールされるアプリケーションであるため、普段使っているIDEと同じように使うために、事前設定を行ったほうが便利でした。 自分の場合、(どういう仕組みでそうなったかわからないですが)キーバインドはRubyMineのものが引き継がれてましたが、IdeaVim等のプラグインは改めてインストールする必要がありました。 自分の作ったセッションに自分で参加することもできるため、はじめて利用する前にローカルで試して設定を調整するとスムーズだと思います。 Code With Meの良い点 ペア・プログラミング時に環境の違いが吸収できる ペア・プログラミング中、ちょっとキーボードを借りて入力する、というような際に、キーボードのレイアウトの違い、キーアサインの違いetcで困惑するケースがあると思いますが、これが完全に防げます。 今回は、同僚がMac、自分はUbuntuとOSから異なる環境でしたが、それぞれ自分の親しんだ環境で作業できました。 ゲストが、ホスト側のRun Configurationやターミナルを使える ファイルの編集だけでなく、ホストのRun Configurationを利用したテストの実行や、ターミナル経由でログのtailなんかも可能です。 今回は同じオフィスで作業しており、ゲストのマシンから、ホストのローカルIPを参照してWebページとして表示することも可能だっため、Railsアプリケーションの開発に普段必要な作業はだいたい網羅できそうでした。 試していませんが、デバッグセッションも使用できるようです。 Jump To, Syncモードが便利 ツールバーに表示されている他のユーザーをクリックしたり、Code With Meのボタンから他のユーザーを指定することで、Full Sync、Followモードに移行できます。これらのモードの場合は、開いてるファイル、場所などの、エディタの状態が同期されます。Jump Toであれば、そのユーザーが現在開いているファイルに飛ぶことが出来るので、一旦別れて作業、調査していた場合でも簡単に合流できます。 不便だった点 検索が遅い ゲスト側からの、ファイル名を入力してのファイルオープンや、ワードによる検索のレスポンスが、かなり遅く感じます。Syncモードでない場合は、ホスト側で開いたファイルが勝手に開かれるわけでもないため、ファイルを探すのに若干手間取ります。VCS(Git)タブも無く、modifiedファイルに簡単にアクセスすることも出来ないように思えるため、ゲスト側は編集したファイルは開きっぱにして作業するほうが快適かも知れません。 入力が消えてしまう ゲストのファイル編集が反映されない、というケースが時々ありました。特に、セッション開始直後に多かった印象です。ここは結構困ったので早期に修正されるとありがたいです。 ゲストが色々できすぎる これは不便というか、仕方がないのですが、プロジェクト全体が共有されるので、scratchファイルにのこしてあるメモ等も全て共有されてしまいます。 特に、ターミナルは、ホスト側のユーザーで何でも出来てしまうため、注意が必要なケースもありそうです。ターミナルの共有は、セッションへの参加とは別に、個別に許可、却下でき、以下のようなダイアログが出ます。 まとめ Code With Meを、同じ場所でのペア・プログラミングで使用しました。ファイルの状態は共有しながら、完全に別のクライアントで作業できるため、別々の環境で作業できるのが便利でした。リモート・ワークの際は、画面の共有だけでは難しい、共同での作業が行いやすく、便利なのではないかと思うので、リモートでの共有も試してみようと思います。
アバター
こんにちは。スタメンでエンジニアリングマネージャーをしている @temoki です。 前回、 プロジェクトマネジメント入門以前 という記事でプロジェクトマネジメントとは何なのか?について次のようにまとめました。 プロジェクトとは 独自の目標を達成するために、決まった期間の中で、集団で活動すること です。 プロジェクトというのはとても困難であることが多いと思いますが、これを なんとか対処して 目標を達成することに導くのがプロジェクトマネジメントなのです。 今回は、プロジェクトをなんとかするための方法について、特に重要なポイントについてお伝えしたいと思います。 なんとかする領域 前回お伝えした PMBOK では、プロジェクトでなんとかする対象を10個の知識領域(エリア)として定義しています。 そしてこれらは以下に列挙するとおり、プロジェクトの ゴール に関するエリアと プロセス に関するエリアの二つに分類されます。 ゴールに関する3つのエリア Quality / 品質(成果物の品質) Cost / 原価(材料費、人件費、などなど) Time(Delivery) / 納期、スケジュール プロセスに関する7つのエリア Scope / スコープ(なにをやるのか) Human Resource / 要員(どのメンバーでやるのか) Communication / コミュニケーション(定例会、座席、ツール) Risk / リスク(目標の達成を阻害しうるものと、その予防) Procurement / 調達(必要なヒト・モノ・カネを外部から調達) Stakeholders / ステークホルダー(プロジェクトに関わるすべての人) Integration / 統合(↑のエリアすべてを統合してみないとね) たくさんありますが、プロジェクトを成功に導くために(プロジェクトの大小に関わらず)あらかじめ考えておく必要があることばかりです。 今回はこの中でも私が最も重要だと考える、プロジェクトのゴールに関する3つのエリアに絞って考えていきたいと思います。 プロジェクトのゴール プロジェクトのゴールを考えるにあたって、まずは仕事での日常的なシーンを例に挙げてみたいと思います。 仕事上でのとあるシーン ある日のこと... 🙋‍♂️「この書類、スタメンの👨‍💼さんに送っておいて!」 🙆‍♀️「わかりました!」 後日... 🙎‍♂️「こないだお願いした書類の件で👨‍💼さんに怒られちゃったよ!」 🙎‍♀️「え?わたしちゃんとその日の夕方には送りましたよ?」 🙎‍♀️「ちゃんと丁寧な送付状も添えましたし。」 💁‍♀️「それと、これ送った時のレターパックの領収書です。経費精算お願いします。」 🤦‍♂️「なんと...」 さて、🙎‍♀️さんはちゃんと対応したように見えますが何がいけなかったんでしょうか? 問題は認識のずれ どうやら🙎‍♀️と🙎‍♂️との間で次のような認識のずれがあったことが問題のようです。 🙎‍♀️の対応 🙎‍♂️の期待 送るもの 書類の原本 書類のコピー 送る手段 レターパック FAX 受けとる日 2日後 当日 費用 ¥520 ¥0 この中で👨‍💼を怒らせてしまった理由は、その書類を 必要な日に受け取れなかった ことです。 プロジェクトのゴールに関するエリアとして Quality ・ Cost ・ Time(Delivery) の3つがあることを先程お伝えしました。 これは主に製造業において昔から重要とされているエリアで、いわゆる QCD と呼ばれるものです。 この QCD の観点でふりかえってみると、🙎‍♀️は Quality を最優先した対応であったと言えます。 もし書類がとても重要なもので、確実に原本を👨‍💼に届けなければならない状況だったとしたら、🙎‍♀️の対応は100点です。 しかし、実際には👨‍💼が最も優先していたのは Delivery であり、🙎‍♀️の対応ではそれが満たされなかったために、🙎‍♂️は👨‍💼に怒られることになってしまいました。 ゴールのすり合わせ 今回の問題がなぜ起きたかを一言で言うと、ゴールのすり合わせが足りなかったということです。 依頼者である🙎‍♂️が「今日中に欲しいらしいからFAXで送って!」と一言添えていれば問題は起きなかったでしょう。 そして依頼を受ける🙎‍♀️からも「いつまでに届けばよいですか?」と不明点を確認しておくことでも防げたと思います。 ここでお伝えしたいのは、依頼する側、される側のどちらかが一方的に悪いということはあまりないということです。 ソフトウェア開発プロジェクトにおける裁判 少し重い話になりますが、ソフトウェア開発プロジェクトにおいて発注元と発注先の間で裁判が行われることがあります。 訴訟に至る主な原因は、プロジェクトの遅延、完成したものの品質、費用のふくらみなど、QCD というゴールのズレが原因です。 裁判における論点は、発注元と発注先のどちらに問題があったか、となります。 ソフトウェア開発の 発注先 はソフトウェア開発のプロとみなされるため、ソフトウェア開発のプロジェクトマネジメント義務を全うしているかどうかがチェックされます。 そして 発注元 は発注するソフトウェアで解決するユーザー業務に関するプロとみなされるため、発注先への必要な情報提供や要件定義への参加など、ソフトウェア開発への協力義務を果たしているかどうかが問われます。 つまり、プロジェクトに関わる人すべてが協力しないとプロジェクトの成功は難しいということですね。 プロジェクトのゴール設定 私は プロジェクトの成功はゴール設定が最も重要である と考えています。 プロジェクト開始時に設定したゴールが誤っていると、その後のプロセスをいくらうまくやっても間違ったゴールに向かってしまうからです。 プロジェクトが始まる時、誰もがまずは理想的なゴールを想像します。 Quality : 高い Cost : 安い Delivery : 早い これが理想的なQCDです。しかし、このようなゴールがソフトウェア開発の中で成り立つことはまずありません(旨い、安い、早いの成り立つ牛丼は偉大です)。 なぜなら、予算が決まっている、リリース日ををプレスリリースしてしまっている、などプロジェクトには必ず QCD のどこかに制約があるからです。 プロジェクトごとにどのような制約があるかを抽出し、その中で最良バランスの QCD でゴール設定することが重要です。 ソフトウェア開発プロジェクトでのゴール設定 ソフトウェア開発プロジェクトにおける QCD を具体的にすると次のようになります。 Quality / ソフトウェアの品質(=バグが少ない、だけではないのでまた別の機会に) Cost / インフラ費用、人件費(=工数) Delivery / 納期、リリース日 ソフトウェアのエンジニアであれば、これらのどれかを優先すると、別のものが犠牲になるというのは感覚としてわかると思います。 ですのでプロジェクトの制約に応じて最も優先すべきものを設定しましょう。例えばこんな感じですね。 医療現場で利用される投薬管理システムの開発 👉 Quality 運用中のシステムで発生した影響度の大きい障害の対応 👉 Delivery コロナで収益の減ってしまった観光施設のPRウェブサイトの改修 👉 Cost もちろんこんなにわかりやすい状況はないので、依頼者としっかりすり合わせてゴールを設定し、ステークホルダー間で合意をとってからプロジェクトを開始しましょう(これはステークホルダーマネジメントというエリアの話に関連しますね)。 まとめ プロジェクトマネジメントにおいて最も重要なのは QCD というゴール設定にあります。 そしてプロジェクト固有の制約の中で、そのプロジェクトの独自の目標を達成するための最良バランスの QCD を設定することが、プロジェクト成功への必要条件です、というのが今回の記事のまとめです。 今回の内容を実践するのはプロジェクトマネージャーだけではありません。 例であげた日常的な仕事のシーンのように、小さなタスクであってもゴール設定はとても重要です。 大きなプロジェクトでもそれは小さなタスクの集合です。 プロジェクトに関わるメンバー全員が、それぞれのタスクが最良のゴールとなるように意識して行動できるかどうかが、プロジェクトの進捗に大きな影響を与えることになります。 これから実施する予定のタスクについて改めてゴールのすり合わせをしてから着手してみてください。 それがプロジェクトマネジメントの最も効果的な入門となるはずです。 最後になりますが、スタメンでは自社プロダクトの開発プロジェクトを一緒になんとかしてくれる仲間を募集しています。興味を持ってくれた方は、ぜひ下記の採用サイトをご覧ください。 スタメン エンジニア採用サイト デザイナー募集ページ サーバーサイドエンジニア募集ページ フロントエンドエンジニア募集ページ インフラエンジニア募集ページ モバイルアプリエンジニア募集ページ
アバター
はじめに こんにちは。株式会社スタメンでフロントエンドエンジニアをしております @0906koki です。 React Hooksが2019年にリリースされてから、Reduxの実装でコンポーネントがstoreと接続する方法に選択肢が増えました。 具体的に言うと、今までconnect関数でstoreにあるstateやactionをpresentational componentに対してpropsとして渡していましたが、hooks時代ではuseSelectorとuseDispatchによってconnect関数を書かなくても、storeとの接続が可能になりました。 実装の広がりが出たと同時に、 connect関数 と useSelector + useDispatch のどちらを使えばいいのか、いざ実装しようと思った際に悩むかもしれません。 現在、弊社では後者のuseSelectorとuseDispatchを使った実装方法でプロジェクトを進めており、今回の記事ではcontainer componentをuseSelectorとuseDispatchで置き換える実装の知見を共有したいと思います。 container componentの課題 container componentを実装するにあたって難しい問題は、どの粒度でcontainer層を注入するかだと思います。 大抵は、親コンポーネントに対してcontainer componentを入れ、その親コンポーネントから子供や孫にpropsを渡していくと思います。 しかし、子供や孫が増えてきた時に、大量のpropsバケツリレーが発生して一気に見通しが悪化していきます。 そこで、その親と孫の間に中間層としてcontainer componentを注入し、propsのバケツリレーを回避することもできますが、container componentの実装はかなりのコストが発生します。できれば、アプリケーションに大量のcontainer componentは書きたくありませんし、container componentを注入するタイミングを毎回考えたくありません。 useSelectorとuseDispatch そこで、react-redux v7.1.0のhooks対応で導入された、 useDispatch と useSelector でこの問題に立ち向かいたいと思います。 軽くuseSelectorとuseDispatchの説明をすると、 useSelector storeのstateをpresentational componentへ持ってくることができる actionがdispatchされると実行される useDispatch actionをdispatchするために使用する storeを変更しない限り、返り値のdispatch関数は変更されない 実装方法 今回container componentsをuseDispatchとuseSelectorに書き換えたアプリケーションを実装したいと思います。 例えば、以下のようなcontainer componentとpresentational componentのコードがあるとします。 // containers/Todo.ts import { Dispatch } from 'redux' import { connect } from 'react-redux' import { UserState } from 'types/user' import { TodoState , Todotype } from 'types/todo' import { RootState } from 'reducers' export type TodoProps = { users: UserState , todos: TodoState , addTodo: ( todo: TodoType ) => void } const mapStateToProps = ( state: RootState ) => { return { users: state.users , todos: state.todos } } const mapDispatchToProps = ( dispatch: Dispatch ) => { return { addTodo ( todo: TodoType ) => { dispatch ( addTodo ( todo: TodoType )) } , fetchTodos () => { dispatch ( fetchTodos ()) } } } export default connect ( mapStateToProps , mapDispatchToProps )( TodoComponent ) // components/Todos/TodoComponent.tsx import { TodoProps } from 'containers/Todo.ts' const TodoComponent = ( props: TodoProps ) => { const { users , todos , addTodo , fetchTodos } = props const { isLoading , todoItems } = todos useEffect (() => { fetchTodos () } , [] ) if ( isLoading ) return (<>< / >) const todoList = todoItems.map ( item => ( < ChildComponent item = { item } users = { users } addTodo = { addTodo } / > )) return ( <> { todoList } < / > ) } まずは、このコンポーネントをuseDispatchとuseSelectorに置き換えたいと思います。 container componetを削除して、TodoComponentへ直接storeのstateとdispatch関数を持ってきます。 // components/Todos/TodoComponent.tsx import { useSelector , useDispatch } from 'react-redux' // import types import { RootState } from 'reducers' import { TodoState } from 'types/todo' import { UserState } from 'types/user' // import actions import { addTodo , fetchTodos } from 'actions/todo' const TodoComponent = () => { const dispatch = useDispatch () const { users } = useSelector < RootState , UserState >( state => state.users , shallowEqual ) const { todoItems , isLoading } = useSelector < RootState , TodoState >( state => state.todos , shallowEqual ) useEffect (() => { dispatch ( fetchTodos ()) } , [] ) if ( isLoading ) return (<>< / >) const todoList = todoItems.map ( todo => ( < ChildComponent todo = { todo } users = { users } addTodo = { dispatch ( addTodo () } / > )) return ( <> { todoList } < / > ) } container componentがなくなり、すっきりしましたね。ただ、このコードにはいくつかの問題点が存在します。 usersやtodoItemsなどのstateが複数のファイルで必要になった場合に、毎回同じuseSelectorを書かないといけない(型ファイルのimportやdispatch関数も同様) useSelectorの返り値をobjectにしているため、コンポーネントに必要なstate以外が更新された場合でも、レンダリングが走る 2に関しては、意外と盲点かと思います。 サンプルとして以下のようなコードを用意しました。(importやtypeは省略しています) // App.tsx const App = () => { return ( < div > < TodoItem / > < Memo/ > < /div > ) } // components/Todos/Items.tsx const TodoItem = () => { const [ keyword , setKeyword ] = useState ( '' ) const dispatch = useDispatch () const { todoItems } = useSelector ( state => state.todos ) const handleOnChage = ( e: React.ChangeEvent < HTMLInputElement >) => [ setKeyword ( e.target.value ) ] const handleAddTodo = () => { dispatch ( addTodo ( keyword )) setKeyword ( '' ) } const renderTodoList = todoItems.map ( todo => { return ( < li > { todo } < /li > ) } ) return ( < ul > < h2 > TODOリスト < /h2 > { renderTodoList } < input value = { keyword } onChange = { handleOnChage } / > < button onClick = { handleAddTodo } > 追加 < /button > < /ul > ) } // components/Todos/TodoMemo.tsx const TodoMemo = () => { const dispatch = useDispatch () const { memo } = useSelecto ( state => state.todos ) const handleOnChange = ( e: React.ChangeEvent < HTMLInputElement >) => { dispatch ( changeMemo ( e.target.value )) } return ( < div > < h2 > TODOメモ < /h2 > < input value = { memo } onChange = { handleOnChange } / > < /div > ) } App.tsxが親コンポーネントで、その配下にTodo追加・羅列するItems.tsx, Todoのメモを書くTodoMemo.tsxがあります。 TodoMemoにあるchangeMemoをdispatchしてmemoのstateを更新しても、Items.tsxがレンダリングされないことが理想です。 しかし、実際は以下のようにItems.tsxにもレンダリングが走っています。 こうした無駄なレンダリングや冗長なコードを解消するために、以下の様にselectors/todo.tsのファイル作成して、そこのファイルから必要なdispatch関数やstateを取得することにしたいと思います。 // selectors/todo.ts import { useCallback } from 'react' import { useDispatch , useSelector } from 'react-redux' // import actions import { addTodo , fetchTodos } from 'actions/todo' // import types import { TodoType } from 'types/todo' const useTodoDispatchActions = () => { const _fetchTodos = useCallback (() => dispatch ( fetchTodos ()), [ dispatch ] ) const _addTodo = useCallback (( todo: TodoType ) => dispatch ( addTodo ( todo )), [ dispatch ] ) return { fetchTodos: _fetchTodos , addTodo: _addTodo } } export const useSelectTodoItems = () => { return useSelector < RootState , todoType [] >( state => state.todos.todoItems ) } export const useSelectIsLoading = () => { return useSelector < RootState , boolean >( state => state.todos.isLoading ) } // components/Todos/TodoComponent.tsx import { useTodoDispatchActions , useSelectTodoItems , useSelectIsLoading } from 'selectors/todo' // import selectors import { useSelectUsers } from 'selectors/todo' const TodoComponent = () => { const { addTodo , fetchTodos } = useTodoDispatchActions () const todoItems = useSelectTodoItems () const isLoading = useSelectIsLoading () useEffect (() => { dispatch ( fetchTodos ()) } , [] ) if ( isLoading ) return (<>< / >) const todoList = todoItems.map ( todo => ( < ChildComponent todo = { todo } users = { users } addTodo = { dispatch ( addTodo () } / > )) return ( <> { todoList } < / > ) } 上記の様に、selector関数とdispatch関数をまとめたselectorファイルを切り、そのファイルからstateやdispatch関数をimportすることで、そのコンポーネントで本当に必要なものだけ取得できる + レンダリングがそのコンポーネントにだけに閉じたものになります。 コードの解説をすると、useTodoDispatchActions関数でdispatch関数を返却しています。 また、stateに関しては、objectで一括に返却するのではなく、個別のstateをそれぞれexportしています。 そして、TodoComponentにて必要なstateとdispatch関数を上記のファイルからimportしています。 こうすることで、わざわざcontainer componentを実装せずに、selector関数を実装するだけで、それぞれのコンポーネントにて必要なstateとdispatch関数をimportするだけで済みます。 まとめ Reduxの実装に関しては、所属する会社やチーム、プロジェクトの内容によってバラツキが出ると思いますが、弊社ではcontainer componentをuseSelectorとuseDisptchに代替して実装しました。 現時点の感触ですが、container componentを実装するよりも見通しがよく、実装コストも低いと感じています。また、container componentを注入する粒度に関しても考える必要がないので、スムーズに開発ができています。 今まで実装していた痛みが新しい技術で解消されていくことは楽しいので、これからもキャッチアップしていきたいと思います。 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
アバター
目次 はじめに ライブラリ(react-paginate)の導入 ページネーションの実装 おわりに はじめに こんにちは、スタメンでエンジニアをしている手嶋です。今回は「react-paginate」というライブラリを使用し、Reactでページネーションを実装する方法を紹介したいと思います。 ライブラリ(React-Pegination)の導入 まず「react-paginate」」の導入です。ページネーションのライブラリはいくつか選択肢があると思いますが、以下の観点から「react-paginate」を採用しました。 ・実現したいUIに近かった事 ・ライブラリの更新頻度 ・直近のダウンロード数 選定の際には、 openbase が非常に参考になりました。 以下のコマンドで react-paginate をプロジェクトに導入します。 yarn add react-paginate // tsの場合は以下で型も追加。 yarn add @types/react-paginate これで導入が完了です。 ページネーションの実装 続いてコンポーネントの実装です。以下は、ページネーションとユーザー情報(UserTable)を描画するcomponentです。(スタイル等は割愛しています) このComponentからページネーションComponentを呼び出しています。 import React from 'react' ; // componentのimport import UserTable from 'component/UserTable' ; import Pagination from 'component/Pagination' ; // Userの型定義 export type userTypes = { id: number , name: string } interface Props { users: userTypes [] ; //Userテーブルに表示するユーザー情報 userSize: number ; //ページ数を計算するために必要な全ユーザーの数 handleSearchUser: () => void ; // ユーザーを検索する関数 setCurrentPageNumber: ( page: number ) => void ; //ページネーションの番号をセットする関数 } // User一覧とページネーションを描画 const Users = ( props: Props ) => { const { users , userSize , handleSearchUser , setCurrentPageNumber } = props ; return ( < div > < div > { users. length > 0 && < UserTable users = { users } / > } < /div > { userSize > 0 && ( < Pagination userSize = { userSize } handleSearchUser = { handleSearchUser } setCurrentPageNumber = { setCurrentPageNumber } / > ) } < /div > ); } ; export default Users ; ページネーションのComponentは以下です。導入した「react-paginate」に必須であるpropsを渡す事で描画できます。propsの詳細については後述しますが、基本的な流れは以下です。 pageCountに表示したいページ数を渡します。(ここでは全データ件数を、1ページに表示したい数で割った数にしています。) 上記のpropsにより、添付のような数字付きのボタンが生成されます。これをクリックすると「onPageChange」というpropsに渡している関数(例のhandlePaginate)が実行されます。 この関数の引数にページ番号が渡ってくるので、その番号を元にAPIを叩く処理を実行し、サーバーサイドから取得したいデータを返します。(引数は0始まりなので、+1しています。) 取得できたら上記の親コンポーネントである「index.tsx」に取得したデータ(users)を渡すことで、UserTableに描画したいデータが表示されます。 ページネーションComponentは、あくまで指定の番号を返すだけなので、その番号を元にデータ取得する処理が必要です。 その他のpropsは後述しています。それぞれのDOMにClassNameを指定できるので、そのClassNameにスタイルを当てる事も可能です。今回は「pagination.css」というファイルでスタイルを実装し、Componentにimportしました。 import React from 'react' ; import ReactPaginate from 'react-paginate' ; //ライブラリの呼び出し import 'styles/pagination.css' ; //カスタムスタイル用ファイルの呼び出し interface Props { userSize: number ; //ページ数を計算するために必要な全ユーザーの数 handleSearchUser: () => void ; // ユーザーを検索する関数 setCurrentPageNumber: ( page: number ) => void ; //ページネーションの番号をセットする関数 } const ONE_PAGE_DISPLAY_USERS = 20 ; const LAST_DISPLAY_SIZE = 20 ; const AROUND_DISPLAY_PAGES = 5 ; const Pagination = ( props: Props ) => { const { userSize , handleSearchUser , setCurrentPageNumber } = props ; const handlePaginate = ( selectedItem: { selected: number } ) => { const page = selectedItem.selected + 1 ; setCurrentPageNumber ( page ); // APIを叩きに行く処理 handleSearchUser (); } ; const arrowIcon = ( iconName: 'left' | 'right' ) => { return ( < i class= "fas fa-chevron-${iconName}" >< /i > ); } ; // ページ数の計算 const calculatePageCount = () => { return Math .ceil ( userSize / ONE_PAGE_DISPLAY_USERS ) } ; // ページネーションを表示 return ( < div > < ReactPaginate pageCount = { calculatePageCount () } marginPagesDisplayed = { LAST_DISPLAY_SIZE } pageRangeDisplayed = { AROUND_DISPLAY_PAGES } onPageChange = { handlePaginate } containerClassName = "pagination" pageClassName = "page-item" pageLinkClassName = "page-link" activeClassName = "active" activeLinkClassName = "active" previousLinkClassName = "previous-link" nextLinkClassName = "next-link" previousLabel = { arrowIcon ( 'left' ) } nextLabel = { arrowIcon ( 'right' ) } disabledClassName = "disabled-button" / > < /div > ); } ; export default Pagination ; propsの詳細 公式ドキュメントによると、必須なのは上から3つのみです。 props 内容 補足 pageCount 総ページ数 必須 marginPagesDisplayed 終端に表示する件数 必須 pageRangeDisplayed 選択位置の前後で表示する件数    必須 onPageChange ページクリック時にハンドルするメソッド   APIを叩く関数を渡す containerClassName pageのulタグに設定するclass pageClassName pageのliタグに設定するclass pageLinkClassName pageのaタグに設定するclass activeClassName 現在activeなpageに設定するclass previousLabel previousに設定するラベル名 JSXを渡す事が可能 nextLabel nextに設定するラベル名 JSXを渡す事が可能 previousClassName previousのliタグに設定するclass nextLinkClassName nextのliタグに設定するclass previousLinkClassName previousのaタグに設定するclass nextLinkClassName nextのaタグに設定するclass disabledClassName previous・nextが押せなくなった状態の表示 breakLabel pageの省略表示 breakClassName pageの省略表示のulタグに設定するclass breakLinkClassName pageの省略表示のliタグに設定するclass おわりに 今回はページネーションを「react-paginate」を使って実装してみました。自前でも実装できるとは思いますが、ページ数が増えた時の表示方法や、ボタンを押せない時のハンドリング等が簡単に実装できるのが導入のメリットだと思います。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
アバター
スタメン、プロダクト部で主にモバイルアプリ開発(Android/iOS)を行っている @sokume です。 早速ですが、皆さんスマートフォンつかっていますか?スマートフォンは年に1度位、大きなアップデートが実施されています。アップデートが来たらできるだけ更新しましょうね。 ちなみに今年はAndroid 11 が 9月9日 、iOS 14が9月17日でした。 アップデートの時期は知らされていないので、モバイルアプリの開発者はこのくらいにリリースされるかな?という予想をたてて準備を進めています。 今回は、プロダクトのAndroid アプリを、Android 11 へのアップデートするために、どういう情報を調べたかをまとめて行こうと思います。 準備 Developers Preview 今年は2月にAndroid 11のDevelopers Preview版の発表がありました。 このタイミングでリリースまでのスケジュールや、どういった機能が追加されるかというかという情報が見えてきます。 Pixel 2 といった指定の端末があればインストールして動きを確認する事もできます。 自身のプロダクトに大きな変更のある機能があれば、このタイミングでAPIの挙動を確認します。 リリースまでのロードマップイメージ https://developer.android.com/preview/overview 最低限の対応 Beta Release 6月位にはβ版がリリースされました。 このタイミングでAPIが最終版という状態になります。 このあたりから、公式のAndroid 11への移行日本語ドキュメントが出てきます。 移行ガイド 動作変更点 内容を確認し、どういった機能に影響があるかを調査し、影響のある機能はβ版の実機を使い確認を実施しましょう。 11 weeks of Android 今年はGoogle I/O の中止もあり、Android 11 の公式情報は 11 weeks of Android にまとまって発信されていました。(6/15 〜 11週間で実施) この中でAndroid 11へのアップデートという観点ですと、以下の3つの内容が重要と感じました。 特に、 Android 11の互換性 のなかで以下のように記述されていますので、この点を最低限のラインとしておく必要があるでしょう。 11 Weeks of Android:人とID 11 Weeks of Android:プライバシーとセキュリティ 11 Weeks of Android:Android 11 の互換性 When making sure an app is compatible, the goal is to test your app and make the minimum changes to maintain your app’s functionality on Android 11, then publish the compatible version to users by the Android 11 final release. In most cases you should be able to do this without changing your targetSdkVersion or compiling against the new APIs. アプリの互換性の確認は、Android11の実機上でテストを行い、アプリの機能を維持するための最低限の変更を行う必要がある。ほとんどの場合はtargetSdkVersionを変更したり、新しいAPIのコンパイルを行うことなく出来るはずです。 Android 11 Meetups Google と GDG Japan との共催で Android 11の機能についてテーマを決めて、全8回のイベントを実施しました。(6/23 〜 隔週全8回) 内容は 11weeks Android の内容を元に、Googler の方や、Android エンジニアの方が技術の解説やリアルタイムのQ&Aを行いました。 私もGDG Nagoya オーガナイザーを行っているので協力させていただき、MCを務めさせていただきました。(4回目、8回目) 内容も日本語でわかりやすくなっていますので、興味がある方は御覧ください。 Android 11 Meetups 本対応 11 weeks of Android [Learning Topics] 11 Weeks of Android でまとめられた技術情報がまとめられ、ソースコードを元にしたCodeLabが用意されています。 実際のコードを元に、機能の説明や使い方を試すことが出来るので、時間のある方は取り組んで行きましょう。 各回を完了する毎にBadgeがもらえるので、ちょっとしたコレクター要素があり面白かったりします。 アプリはいつまでにAndroid 11本対応をすべき? 11 weeks of Android の記述で最低限の確認について記載がありました。 アプリとしては targetSdkVersion の更新を行い、正式にAndroid 11への対応をしていく必要があります。 Pixel などのGoogle製デバイス以外のデバイスの場合、 AOSP(Android Open Source Project) へのリリース後2ヶ月くらいでアップデートがおこなわれるのが目安かなと思っています。 (Android 11-r01版は 9/9 にリリースされていました) そこから考えると約11月頭には、アプリの本対応が完了していると良いかな🤔 最後に Android 11へのアップデートについてまとめてみました。 来年も新しいOSのリリースはあるでしょうから、今年の内容を元に、上手に対応していけると良いですね。 株式会社スタメンでは一緒に働くエンジニアを募集しています。 ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
アバター