TECH PLAY

株式会社スタメン

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

247

はじめに こんにちは、スタメンでエンジニアをしているミツモトです。 スタメンでは、Web アプリのクライアント側の状態管理に Redux というライブラリを採用しています。 Redux によって API のレスポンスやローカルで扱う値を自由に状態管理できますが、ディレクトリ構成・処理の定義場所など、全体の設計は実装者に委ねられます。 いくつかのプロジェクトで Redux を採用する中で、実装上の冗長な部分、拡張性の低い部分がわかるようになり、所属するフロントエンドG内で設計について話すようになりました。そこからベストプラクティスを学ぼうという流れになり、「Redux Style Guide」を社内のフロントエンド勉強会で取り上げ、プロジェクトへの導入を行いました。 今回はそれについて紹介します。 Redux Style Guide による学び 公式は こちら Redux Style Guide のルールは、必須・強く推奨・推奨の3つのカテゴリに分けられています。 例えば必須だと、「 state に直接変更を加えない」、「 Reducer で非同期処理を行わない」などが挙げられます。スタメンでも必須のルールは守れていましたが、強く推奨・推奨のルールで守れていない部分がいくつかありました。以下がその例です。 Redux Toolkit の採用 Redux Toolkit は効率的に Redux で開発するために、公式が出しているライブラリになります。Redux DevToolsにも対応しており、ベストプラクティスが組み込まれた関数を利用できます。 スタメンではこれまでAction、Reducerを分けて実装していましたが、Redux Toolkit を採用することで、Action、Reducer を slice としてまとめることができ、結果としてコーディング量を減らすことが出来ました。詳しくは後述の「プロジェクトへの導入」で触れます。 機能別またはDucks パターンによるファイル構成 Reduxではディレクトリ・ファイルを自由に構成できます。よく見るのはsrc配下をReduxが提供する機能やRedux-Sagaなどのミドルウェアで分けるタイプ別のパターンです。 /src /actions /containers /sagas /reducers スタメンでもこのパターンを採用していましたが、複数のドメイン(機能)を扱うようになると、各ディレクトリが複雑になります。Style Guideでは、機能別または Ducks パターンでディレクトリを分けることを強く推奨しています。機能別だと、src配下に機能単位で Component と Redux の処理が格納されます。 /src /common(共通のComponent や Hooks など) /features /todos todosSlice.ts todoSagas.ts Todos.tsx /posts postsSlice.ts postSagas.ts Posts.tsx Ducks パターンだと Redux の処理が modules 配下で機能ごとにまとめられます。 /src /components /common(共通のComponent や Hooks など) /modules todos.ts posts.ts これらを参考に、スタメンでもドメイン単位で Redux の処理をまとめるようにしました。こちらも詳細は「プロジェクトへの導入」で触れます。 出来る限り Reducer にロジックを置く Action を dispatch する際、Component の関数内にロジックを書くのではなく、Reducer 内に状態更新のロジックを書くことで、テストしやすくなります。また、Reducer に記述することで Redux DevTools によるタイムトラベルデバッグを行うことができ、予期しない状態変化の原因を早く特定できます。 Style Guide だと以下のように例が挙げられています。 Component内にロジックがある場合 // Component - Click handler: const onTodoClicked = id => { const newTodos = todos.map(todo => { if (todo.id !== id) return todo return { ...todo, id } } ) dispatch( { type: 'todos/toggleTodo' , payload: { todos: newTodos } } ) } // Reducer: case "todos/toggleTodo" : return action.payload.todos; Reducer内にロジックがある場合 // Component - Click handler: const onTodoClicked = (id) => { dispatch( { type: "todos/toggleTodo" , payload: { id }} ) } // Reducer: case "todos/toggleTodo" : { return state.map(todo => { if (todo.id !== action.payload.id) return todo; return { ...todo, id: action.payload.id } ; } ) } ロジックが Component に依存しないことで、 Presentational Component として見通しがよくなり、描画に関する処理に集中できます。また、Component としての再利用性も高まります。 ネスト/リレーショナルによる複雑な状態の正規化 state の構造は自由に設定できるため、API のレスポンスがネストしている場合など、複雑なデータをそのまま state に保存することもできます。しかしネストしているデータは更新しづらく、特定のデータを抽出する処理が毎回必要になります。Style Guide ではデータを正規化した状態で保存する(ネストせず、エンティティごとに状態を持つ)ことを強く推奨しています。 以下が公式で挙げられている例です。 ネストしている場合 state: { posts: [ { id: 'post1' , author: { username: 'user1' , name: 'User 1' } , body: '......' , comments: [ { id: 'comment1' , author: { username: 'user2' , name: 'User 2' } , comment: '.....' } , { id: 'comment2' , author: { username: 'user1' , name: 'User 1' } , comment: '.....' } ] } ] } 正規化している場合 state: { posts : { byId : { "post1" : { id : "post1" , author : "user1" , body : "......" , comments : [ "comment1" , "comment2" ] } , } , allIds : [ "post1" ] } , comments : { byId : { "comment1" : { id : "comment1" , author : "user2" , comment : "....." , } , "comment2" : { id : "comment2" , author : "user3" , comment : "....." , } , } , allIds : [ "comment1" , "comment2" ] } , users : { byId : { "user1" : { username : "user1" , name : "User 1" , } , "user2" : { username : "user2" , name : "User 2" , } , } , allIds : [ "user1" , "user2" , "user3" ] } } このように正規化することで特定のデータを抽出しやすくなり、パフォーマンスとしても良くなります。 プロジェクトへの導入 Style Guideを学び、直近のプロジェクトで最も効果があったのは、 Redux Toolkitの導入 と ファイル・ディレクトリ構成の最適化 です。 Redux Toolkit の導入により、Action と Reducer を slice としてまとめることができ、Action を定義せず、 slice で定義した関数を dispatch できます。 導入前 // Action export const FETCH_DATA = 'FETCH_DATA' export const fetchData = () => { type: FETCH_DATA, } // Reducer const Reducer = (state, action) => { switch (action.type) { case FETCH_DATA: { const { payload } = action return { ...state, data: payload } } } } 導入後 // Slice const Slice = createSlice( { name: data, initialState, reducers: { fetchData: (state) => { return { ...state, data: payload } } } } ) 1つ1つの Action で見ると大きな差はありませんが、アプリケーションの規模が大きくなると Action 数も増えるため、それらを定義する手間が無くなるのは嬉しいです。state の更新処理を追加する時は slice と sagas のみを触れば良いため、機能を追加しやすくなりました。 ファイル・ディレクトリ構成は Style Guide を参考にしつつ、以下のような構成にしました。 /src /modules /domain1 /slice /sagas /domain2 /slice /sagas /common(ドメイン間で共通の処理) /pages /components /domain1 /domain2 /common(ドメイン間で共通のComponent) /containers /utils(共通のHooksなど) Redux の処理を modules 配下の各ドメインで分け、さらに slice や sagas の処理で分けます。state を受け取る Component は pages というディレクトリに分かれており、描画と Redux のロジックを切り離すことで責任を分離しています。このような構成にすることで、既存の処理を修正する時は該当部分を見つけやすくなり、機能追加を行う時も迷わず新しいドメインを定義して実装できるようになりました。 まとめ Redux Style Guide を学ぶことで、より良い Redux の設計を知ることができます。振り返ると、自分たちの設計・実装に疑問を持ち、フロントエンド勉強会で取り上げ、メンバーで議論したことが実践への近道だったように思います。今後も普段触れる技術に疑問を持ちながら、必要であれば勉強会で取り上げることを続けていきたいと思います。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持ってくれた方は、ぜひ下記の募集ページをご覧ください。 フロントエンドエンジニア募集ページ
こんにちは。スタメンの河井です。 RubyKaigi Takeout 2020 が楽しみですね。 Ruby 3.0 から型定義 & 型検査ができるようになると言われていますが、今の段階でもそれに関連した gem は公開されています。 今回は型のある Rails 開発を体験してみようということで、RBS・rbs_rails・Steep の3つの gem を紹介しようと思います。 RBS とは RBS とは、 Ruby プログラムの構造を記述するための言語です。 Ruby のソースコード(.rb ファイル)とは別にファイル(.rbs)を用意して型定義を記述していきます。 たとえば # message.rbs class Message def reply: (from: User | Bot, string: String) -> Message end という定義では Message クラスのインスタンスメソッド reply 引数は User または Bot のインスタンスと String のインスタンスの2つ 返り値は Message のインスタンス といったことを表します。 詳しい文法は こちら を参照ください。 Rails 関連メソッドの型生成 RBS の役割は型を定義することであり、チェックの機能は備えていません。 そこで、Steep という gem(後述)を使用して RBS を読み込んで型チェックをします。 あるクラスの型検査を行うためには、そのクラスが継承しているクラスについても型定義を見る必要があります。 つまり、Rails のソースコードを検査しようと思うと ActiveRecord::Base など Rails が提供しているクラスの定義が必要になってきます。 それらを自前で用意するのはかなり大変なので、rbs_rails という gem に助けてもらいましょう。 rbs_rails は以下の2つの機能を持っています。 Rails が用意してくれているクラスの型定義ファイルの生成 ユーザーが定義したモデルクラスの型定義の生成 rbs_rails#usage に従って Rake Task を実行します。 これによって生成される Rails の型定義ですが、たとえば ActiveRecord_Relation の型はこのようになります。 次はモデルからの型生成です。 たとえば次のような Book モデルの定義からは # app/models/book.rb class Book < ApplicationRecord belongs_to :user end # db/schema.rb ActiveRecord :: Schema .define( version : 2020_08_26_150533 ) do create_table " books " , options : " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 " , force : :cascade do | t | t.string " title " t.bigint " user_id " , null : false t.datetime " created_at " , precision : 6 , null : false t.datetime " updated_at " , precision : 6 , null : false t.index [ " user_id " ], name : " index_books_on_user_id " end end このような型定義が生成されます。 カラム名に応じて動的に定義されるメソッドや、belongs_to で定義されるメソッドの型定義が生成されています。便利… 型チェックによる型エラーの発見 ここからは Steep を使ってソースコードの型チェックを実行してみます。 設定ファイルの作成 まず初めに、Steepfile というファイルで型検査のターゲットや型定義のディレクトリを指定します。ここには rbs_rails と連携するための設定も含まれています。 # Steepfile target :app do signature ' sig ' check ' app ' library ' pathname ' library ' logger ' library ' mutex_m ' end 実行 さきほど出てきた Book モデルに user_name というメソッドを定義します。 まずは型定義から。rbs_rails で生成した book.rbs に追記します。 # sig/app/models/book.rbs class Book < ApplicationRecord ... def user_name: () -> String end book.rb の方で user_name メソッドを定義して、その中で name を naem と間違えてみます。 # app/models/book.rb class Book < ApplicationRecord belongs_to :user def user_name user.naem end end 型検査を実行してみると $ bundle exec steep check app/models/book.rb:5:4: NoMethodError: type=::User, method=naem (user.naem) 検出できたました。user に naem というメソッドは定義されていないよと教えてくれています。 次に、返り値の型を間違えてみます。 # app/models/book.rb class Book < ApplicationRecord belongs_to :user def user_name user.id end end $ bundle exec steep check app/models/book.rb:4:2: MethodBodyTypeMismatch: method=user_name, expected=::String, actual=::Integer (def user_name) ::Integer <: ::String ::Numeric <: ::String ::Object <: ::String ::BasicObject <: ::String ==> ::BasicObject <: ::String does not hold これも検出できました。継承関係をたどった結果、返り値が String クラスではないと判断されたことがわかります。 この他、Rails ではないものの Steep のリポジトリの smoke ディレクトリ にサンプルがたくさんあるのでここを見てみるのも面白いと思います。 エディタによるサポート 静的型があることにメリットのひとつとして、エディタによる補完が強力になるというのがあると思います。 現時点では、VSCode では Steep type checker というエクステンションをインストールすることで型情報を表示できます。 ↓gif を作ってみました vscode でのコード補完 ActiveRecord のメソッドを型情報付きで表示してくれています。 また補完とは関係ないですが、ruby-signature というエクステンションを入れることで RBS ファイルにシンタックスハイライトが効くようになるのでこちらもおすすめです。 まとめ Rails × 型についてかんたんに紹介してみました。 今回のコードは Github に上げてあるのでよかったら遊んでみてください。 最後に、株式会社スタメンでは一緒にプロダクトを作っていくメンバーを募集しています。 ご興味のある方は エンジニア採用サイト をご覧ください。 参考 RBS rbs_rails Steep Steep VSCode
はじめに スタメン、プロダクト部で主にモバイルアプリ開発(Android/iOS)を行っている @sokume です。 先月に弊社のブログで プロダクト部の個人モバイルアプリたち という、記事が投稿されましたがお読みいただけましたでしょうか? プロダクト部メンバーが個人で作っているモバイルアプリの一部を取り上げた記事ですが、みなさんの中にも個人でモバイルアプリを作っているよ!って人はたくさんいると思います。 そして作りたいなーって思っている人もたくさんいると思っています。 この記事では、モバイルアプリをつくる際に知っておくときっと良い デザインガイドライン について書いてみようと思います。 ディズニーランドの世界観 唐突ですが「ディズニーランドに行く!」となったらあなたはわくわくしてきませんか? 公式ページをみて、キャラクター、アトラクション、グッズ、ショーの時間も調べたり。パークの中で何を食べたいとか、どのアトラクション乗りたいなとか、アトラクションの順番を想像したり。 そして(今は気軽に行けませんが)ディズニーランドに遊びに行くと、パーク、キャスト、アトラクション、ショーなど見るものすべてが想像を超えていて、細部へのこだわりや、空気感からディズニーという世界に調和していますよね。 これがディズニーの作りだしている世界観だと思っています。 モバイルアプリの世界観は? モバイルアプリとして主なプラットフォームはiOS(iPhone)とAndroidが挙げられます。 この両プラットフォームにはデザインガイドラインとして Apple / Human Interface Guidelines と Google / Material Design があります。 このガイドラインがプラットフォームにおける世界観を定めています。 デザインガイドラインとは? 正しい規定はありませんが私は以下のように捉えています。 プラットフォーム全体で一貫性のあるデザインを作成するために、アニメーション、UI、デザイン要素・原則などの指針などを定めたもの。 噛み砕くと、アプリの動き方・アプリの見せ方・アプリを作る上で守って欲しい事などが記載されている、ガイド的なものです。 ただ、ガイドラインですから、強制的に従わなければならないという決まりはありません。 なぜガイドラインを知る必要があるのか? iPhoneやAndroidを日常的に使っている人たち(もちろん自分も)は、意識はしていないですが、ガイドラインに沿った使い方や表現のされ方に慣れています。 iPhoneを使い続けている人がAndroidが使いにくくてしょうがない(逆もまた然り)というのも、こういう世界観の違いが一つの要因なのかもしれません。 例えば、 Touch Target というガイドラインは以下のように定義しています。 Human Interface Guidelines Provide ample touch targets for interactive elements. Try to maintain a minimum tappable area of 44pt x 44pt for all controls. Material Design For most platforms, consider making touch targets at least 48 x 48 dp. A touch target of this size results in a physical size of about 9mm, regardless of screen size. The recommended target size for touchscreen elements is 7-10mm. It may be appropriate to use larger touch targets to accommodate a larger spectrum of users. 両ガイドラインとも表示要素への、最小で確保すべきタッチエリアを明確に指定しています。このようにユーザーが利用しやすくするためにどう実装すべきかという点を知っているかどうかで、ユーザーの使いやすさに大きく影響してくるでしょう。 ユーザーが使いやすい物・使っていて違和感のないアプリを作り出すためには、どうすれば良いのかと考えた場合、まずは多くの人が慣れている物を考えて行くと良いでしょう。 そのためにはデザインガイドラインを理解してアプリを作る事で、ユーザーが利用しやすいアプリに近づいていくんじゃないかと思っています。 そこから、新しいアイデア・自分の作りたい機能・自分の考えたデザインを乗せていく事で、唯一無二のアプリが出来上がって行くのかなと思っています。 ディズニーの世界観とは毛色が異なる点ももちろんありますが、各プラットフォームには考えられた世界観があります。(表現の仕方、反応の仕方、利用の仕方、表示されるフォントなど) このプラットフォームで利用できる1つのアプリ(ディズニーのアトラクションやショー🤔)として、ぜひとも楽しんでもらえるアプリを目指していきたいですね。 まとめ デザインガイドラインはプラットフォームの世界観を表している。 ユーザーが喜ぶアプリへの道標はデザインガイドラインにある。 参考 Google / Material Design Apple / Human Interface Guidelines モバイルアプリのデザインガイドラインの中の話はまた今度機会があればしたいと思っています😃 最後に 株式会社スタメンでは一緒に働くエンジニアを募集しています。 ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
みなさんこんにちは! スタメン プロダクト部 モバイルアプリチームで iOS と Android の アプリ開発 を行っているカーキです。 先月の29日に ConstraintLayout2.0-rc1 がリリースされましたね! リリースノート には「安定版前の最後のリリース」とあるので、ConstraintLayout2.0安定版のリリースもかなり近いのではないかと思います。楽しみですね! そこで今回のブログではConstraintLayout2.0で新たに追加される Flow を紹介し、1つの例として自動で折り返しされるようなタグ表示を実装していきます。 Flowとは何か ConstraintLayout2.0から追加されるFlowとは、ConstraintLayoutのChainの概念を用いて自動折り返しなどを行ってくれるヘルパー ウィジェット です。 Flexbox のような折り返し表示や GridLayout のようなカラム表示をシンプルに実現することができます Flowを使用するメリットとしては 複数のビューをネストさせずに整列させることができる GridViewなどRecyclerViewを使うと少し手間なところを手軽に実行できる などが考えられます。 代表的なattribute Flowでタグを実装する際に使うであろうattributeについて先に紹介します。 flow_wrapMode flow_wrapMode はFlowを使って表示するレイアウトの表示形式を指定します。 この形式には以下の3種類あります。 none chain aligned それぞれについて説明します none 単一のチェーンに沿って水平もしくは、垂直に配置することができます。 改行は行われないため、以下の画像のように要 素数 が多いと見切れてしまいます。 noneでの整列 chain チェーンに沿って水平もしくは、垂直に配置することができます。 Flowの大きさに従って要素が入りきらない場合に、自動で改行を行います。 →Flexboxのようなレイアウトを実現できます chainでの整列 aligned 改行されるという点ではchainの場合と同じです。 ただchainではなく行と列のセットで配置されるため、chainにあるような水平・垂直で個別にattributeを指定することができません。 →GridLayoutのようなレイアウトを実現できます 続いて、他のflowの主要なattributeを紹介します constraint_referenced_ids Flowによって制約を加えるビューのidを指定します 複数のidを指定する場合はidの間にカンマを入れて指定します flow_horizontalGap, flow_verticalGap 水平・垂直方向の要素同士の間隔を指定することができます flow_horizontalStyle, flow_verticalStyle 水平・垂直方向のChain Styleを指定することができます ChainStyleには3種類ありますが、それぞれのStyleによってどのように配置されるかは ConstraintLayoutのドキュメント に詳しく記載されています。 flow_horizontalBias Flowの内部で使用できる layout_constraintHorizontal_bias のようなもので、要素全体の重心を指定することができます "0"で指定すれば start の方向、"1"に近づくほど end の方向に重心が寄ります。 Chainを使ってタグ表示を行う( XML ) 上記のattributeを使用してタグを表示してみたものが以下になります chainを使用したタグの表示 <androidx.constraintlayout.helper.widget.Flow android:id="@+id/flowLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="20dp" app:constraint_referenced_ids="tag1, tag2, tag3, tag4, tag5, tag6, tag7, tag8" app:flow_horizontalBias="0" app:flow_horizontalGap="8dp" app:flow_horizontalStyle="packed" app:flow_verticalGap="8dp" app:flow_verticalStyle="packed" app:flow_wrapMode="chain" app:layout_constraintTop_toTopOf="parent" /> 上記の xml には記述していませんが constraint_referenced_ids に指定しているidには並べたいビューのidを指定しています 動的にFlowにアイテムを追加する タグを実装する場合、タグ自身を動的に追加することになります 動的に追加する際は、レイアウトファイルにおいたflowの referencedIds に整列したいViewのidリストを渡すことで行うことができます class MainActivity : AppCompatActivity() { private fun setup(parentView: ViewGroup, tagTitles: List<String>) { val referenceIds = IntArray(tagTitles.size) for (i in tagTitles.indices) { val textView = TextView(this) textView.text = tagTitles[i] textView.background = getDrawable(android.R.color.holo_green_light) textView.id = View.generateViewId() parentView.addView(textView) referenceIds[i] = textView.id } // 生成したtextViewのidをreferenceIdsに設定する flowLayout.referencedIds = referenceIds } } あとは呼び出し側から親ビューとタグのタイトル配列を渡すだけです setup(parentView, listOf("たぬき", "きつね", "うさぎ", "かめ", "いぬ", "さる")) 動的にタグを表示する 動的にアイテムを追加する場合の注意 ここで一つ注意しておきたいことがあります。 動的にアイテムを追加する場合、 addView したアイテムに対して整列をかけるため、ロードするたびに レンダリング の処理がかかります。 静的な表示であれば問題にはなりませんが、RecyclerViewの内部のアイテムそれぞれにflowが存在すると、RecyclerViewのスクロールのたびにflowの整列処理が走るため、スクロールがカクカクするなどパフォーマンスの低下が見受けられました。 今後のFlowの改善により整列のパフォーマンスが改善されるかどうかは分かりませんが、現状ではRecyclerViewの内部で動的にFlowを使用するのは現実的ではなさそうです。 最後に どうだったでしょうか? Flowを使用すれば簡単に折り返し可能なタグレイアウトを実装することができます またGridLayoutに関しても同様にして容易に表示ができるため、ちょっとしたセグメント表示などにはかなり使えるのではないでしょうか ConstraintLayout2.0 安定版のリリースがますます楽しみになってきますね 最後になりますが、スタメンでは iOS / Android エンジニアを大募集しています 興味のある方は エンジニア採用サイト をご覧ください
スタメンの松谷( @uuushiro )です。この記事ではアプリケーションのアラート(エラー通知)運用に関する問題を「システム思考」で構造的に捉え、どのように改善していこうとしているのか、ということを紹介します。「システム思考」についても記事内で簡単に説明を入れています。 システム思考とは? 複数の要素が相互に作用することで、ある機能を構成し、特定の結果を生み出すものをシステムと言います。一見シンプルに見える要素単体での働きも、他の要素と相互作用することで、全体のシステムとして想像以上に複雑で思わぬ結果を生み出すことがあるのがシステムの特徴です。例えば、人体も植物も市場の動きもシステムです。一つの要素だけを見ると単純な働きに見えますが、それらが相互作用することで全体としてとても複雑な働きになっています。 システム思考とは、なにか問題を認識した時に、反射的・局所的に対処をするのではなく、まずは全体を俯瞰して捉え、要素の複雑な繋がりを見極め、どのようなシステムを構成しているのかを理解することです。そのシステムに対して効果的に働きかけ、問題解決を図ろうというものがまさに今回やりたいことです。 背景と課題 弊社が提供しているサービス TUNAG では、アプリケーションでエラーが発生した際にはSlackに通知される仕組みがあります。アラート運用の理想としては、一つ一つのアラートがアラートとしての意味を為し、受け取ったエンジニアがそれに対して適切なアクションを取れることが理想です。しかし現状は、 一日あたりの通知数が多いため、通知に対する集中力が削がれ本当に重要なアラートを見逃しやすくなっています。その結果、不具合・障害対応の初動の遅れや漏れが発生してしまう可能性が高くなってしまいます。この問題を私たちは「オオカミ少年アラート」と呼んでいます。 今までも過去に何度かこの「オオカミ少年アラート」問題を解決しようとして、アラート担当を置いて一時的に改善したこともありましたが、開発プロジェクトの状況によっては一時的にアラート対応の優先順位を下げなければならない状況も発生します。ですので、特定の開発リソースを期待して、継続的にアラート状況を改善していくことは基本的には難しいと考えています。また、アラートを減らせば、アラートに集中できるようになり、結果としてアラート数を低く保つことができるのではないか?という仮説を元に、エンジニア複数人で一気にアラートに対応し、一時的にアラート数を減らしたことも何度かありました。しかし、数ヶ月経つとまた元のアラート状況に戻ってしまいました。このように同じ問題が何度も繰り返し発生してしまうことを考えると、「アラートを減らせば、アラートに集中できるようになり、結果としてアラート数を低く保つことができる」 という仮説は間違いだったということになります。アラート数が増える方向へなにか他の「見えない力」が働いているようにみえます。この見えない力は、なんらかの「構造」が生み出しているはずです。その構造を理解するために、システム思考で問題を考えてみます。 氷山の一角モデル システム思考で問題を捉える際に、「氷山の一角モデル」をイメージするとわかりやすいです。氷山と同じく水面上に見えている問題「アラート数の増加」は全体のほんの一部であってその下に本当の問題があります。なので、アラートが減った・増えたというものは、システムが生み出した事後的な結果であり、局所的な最適化を図ったとしても本質的な問題解決にはならないのです。氷山の一角である「アラート数が増えた・減った」という出来事に一喜一憂し、出来事を抑え込む解決策に安易に飛びつかず、出来事の下に潜んでいる構造・メンタルモデルを見抜き適切な打ち手を講じていこうという取り組みが今回の記事の話になります。 氷山の一角モデル どんなシステムが「アラート数の増加」を引き起しているのか? 今までの過去の出来事をよく観察し、どのようなシステムを構成しているのかを考えてみました。もちろん誰が悪いとかではなく、僕ら以外のチームを観察したとしても同様の結果に帰着する「システム」の話です。 結論から言うと、僕は以下4つの「ループ」が相互に影響を及ぼすことで一つのシステムを構成し、「アラート数の増加」という事象を引き起こしているのではないかと考えました。「ループ」とは、影響が回り回って輪のように閉じている因果関係のことを指しています。システム思考ではこのループを図式化することでシステムの構造を捉えることが多いのでここでもそれに倣います。 続いて下図の説明です。システム思考では因果関係のある要素と要素の繋がりを矢印で繋げ、その因果関係が同じ方向への変化なのか?逆方向への変化なのか?を区別します。同じ方向への変化なら「同」、逆の方向への変化なら「逆」と印をつけています。(この図の書き方については、「なぜあの人の解決策はいつもうまくいくのか?―小さな力で大きく動かす!システム思考の上手な使い方」※2を参考にしています。) 各ループについて簡単に説明します。 割れ窓ループ(心理的) まさに割れ窓理論のこと アラートを放置すると、誰も注意を払っていないというサインになり、やがて他の通知も全て放置される 見逃しのループ(物理的) 全体のアラートの流量が多いと見逃す確率が大きくなる そして見逃すと全体のアラート数が増え、さらに見逃す確率が大きくなる 曖昧のループ アラート対応について十分な情報共有がされていないため、メンバーがどう動けばいいのか把握することができない インフラ監視・アプリケーションエラー通知、SaaSのエラー、チャット機能...etc が混ざっており、対応の責任の所在があいまい。 自分はどこの通知を受け取ってどのようにアクションすればいいのか分からない。 なので新しいメンバーに教えられない。 人数が増えれば増えるほど、チーム内の曖昧さが大きくなっていく 学習のループ 自分の行動の結果のフィードバックを受け取ることで、次の行動を改善しようとする心理が働く。 しかしアラート数が多い状況では、アラートを見逃す可能性が高く、結果として自分のリリースによるアラート通知に気が付けず、フィードバックを得ることができない。 そうすると、通知数を出さないようにしようという心理が働きづらいし、学習も進まなく、1リリースが増加させるバグアラート数が減少しない。 このように、4つのループが相互に強め合い・弱め合い、自分のシステムを強化していく方向で永遠とループしていることが分かりました。このため、どれだけ一時的にアラート数を減らしたとしても、時間が経ち、新しいメンバーが入ってくるたびに曖昧のループが強化され、結果的に「見逃しのループ」も「割れ窓のループ」も強化され、そして「学習のループ」が弱まり・・・元の状態に戻ってしまいます。まだ仮説ではありますが、「見えない力」の正体はこのシステムのことだったとして話を進めます。 システムにどう働きかけていくか? 先程も言及したように、表面上の問題「アラート数の増加」を生み出しているシステムそのものを改善しない限り、状況は変わらず元に戻ってしまいます。システムとしてこの問題を捉えると、「アラート数の増加」を押さえるためにするべきことは、「割れ窓ループ」「見逃しのループ」「曖昧のループ」という3つのループを弱め、「学習のループ」を強めていくことになります。 まだ仮決めの段階ではありますが、具体的には以下3つのアクションを考えています。 一気にアラート数を減らす アラート数を減らすことで、「見逃しのループ」と「割れ窓のループ」を弱めることができる 今まさにチームで取り組んでいます。 責任範囲及びアクションを定め明文化する 責任境界や対応方針を明文化することで曖昧のループを弱める(断つ) 責任境界を明確にチームで分ける 担当領域に基づいて適切なチームメンバーに割り当てることで責任境界を明確に。 自分の担当領域の通知のみ受け取ることで、不要なノイズを最小限に抑える。 各責任境界内で方針を定めドキュメント化する アラートの受け取り方や通知設定の徹底(PC/スマホともに有効に) 一時対応者 動き方 重要度の判定方法 ドキュメントを、新メンバー加入のオリエンテーション時またはチーム所属変更の際に共有する 自分自身の行動に対するフィードバックを強める 自分自身の行動の結果を受け取り学習のループを強めるために、アラート内容に関連性の高いメンバーにアサインする 責任境界外のメンバーが対応したほうが良い場合などは、別チームにアサインするなど柔軟に対応する 古いエラーなどで、関連が高い人を特定できない もしくは 既に退職している場合、チャンネルに責任を負うチームで対応をする。 効果について まだ「一気にアラート数を減らす」という施策に取り組んでいる最中なので、これらの取り組みがどれほど効果があるのかは現時点ではわかっていません。もしシステムの理解が間違っていれば、これらに注力したところで結果は出ないので、その時は改めてシステムを見直して理解の修正をしたいと思います。また面白い結果がでれば改めてブログで紹介したいと思います。 まとめ 今回システム思考の題材にした「アラート数の増加」自体がそれほど複雑ではないこともありますが、図に書いたシステムやアクションリストを眺めると、当たり前な感じがしませんか?しかし、システム思考で問題を捉える前には、「アラート数の増加」という表面的な出来事から、チームのドキュメント文化や学習のフィードバックの強さが相互に影響を及ぼしているのではないか?という仮説も含めた観点は生まれませんでした。 今後のアクションについても、個人の積極性や、特定の開発リソースに頼るなどの長期的に持続しない施策ではないというところも重要なポイントだと思います。まさに小さな力で大きな成果を生み出す、システム思考ならではの可能性です。少ないリソースで戦うベンチャーだからこそ、表面的な解決策に飛びつかず、本質的な問題解決をして、二度と同じ問題を発生させないようにしていきたいと思います! また、今回のようにシステムを図で示すことで、議論が空中戦になりませんし、全員で問題の全体像のイメージを揃えやすくなるはずです。引き続きチームの皆と話し合いながら、システムの理解を深め、あらゆる問題を正確に捉えていきたいと思います。(弊社プロダクト部の行動指針に( StarCode )に「問題を見極める」というものがあるのですが、システム思考はまさにその行動指針にぴったりな思考法ですね!) 最後に 私自身、「システム」といえばソフトウェアを想像していたのですが、システム思考を知ってからは、世の中のあらゆるものはシステムなのかもしれないという新しい視点を持てるようになりました。この視点を持っていると、身近なソフトウェアアーキテクチャもチーム構成も他部署についてもそれぞれ単体で機能が完結しているものではなく、相互作用する要素として、複雑なシステムを型作っているとイメージできそうですね。ソフトウェアに限らずあらゆるものはエンジニアリング対象なんだなあと思うとワクワクしてきませんか? 最後に、スタメンでは、エンジニア、デザイナー、プロダクトマネージャーを大募集中です。 もし弊社に興味を持っていただけましたら、こちらの Wantedly をご確認ください! 参考資料 ※1. 世界はシステムで動く ―― いま起きていることの本質をつかむ考え方 ドネラ・H・メドウズ , Donella H. ※2. なぜあの人の解決策はいつもうまくいくのか?―小さな力で大きく動かす!システム思考の上手な使い方 枝廣 淳子 、 小田 理一郎
目次 概要 はじめに 各サービスの紹介 まとめ 概要 こんにちは。スタメンでエンジニアをしている梅村です。今回は、スタメンのプロダクト部のエンジニアが個人開発しているモバイルアプリについての紹介を行っていこうと思います。 はじめに スタメンのプロダクト部のエンジニアは、日々の自己研鑽に励んでいる人が多いです。社外の勉強会に参加している人や勉強会を主催している人、業務では使わない プログラミング言語 を学んで技術力を向上させている人や個人サービスを開発している人など、様々です。 今回はその中でも、個人サービス、ひいてはモバイルアプリでの個人サービスの紹介をしていこうと思います。 2020年の上半期で、プロダクト部の中から5つのモバイルアプリがリリースされました。5つのサービスをそれぞれ紹介していくので、どのようなサービスが開発されているのだろう、とワクワクしながら読んでいただけると幸いです。 各サービス紹介 開発者本人の言葉を用いて、 サービス名 アプリの説明 技術スタック 開発経緯、開発にこめた思い ストアへのリンク 画像 という流れで紹介していきます。 1つ目 [サービス名] Munro books [アプリの説明] 表紙を眺めながら選べる絵本屋さん Munro books です。 [技術スタック] 技術的な話をすると、昨年のWWDC2019で発表されたSwiftUIで全画面のUIを構築しています。絵本データは 楽天ブックス API 、バックエンドはFirebaseオンリーで、ウェブサイトの ホスティング まで任せています。表紙が似ている本を出す機能がありますが、そこは Python の OSS ニューラルネットワーク ライブラリKerasと、画像認識の学習済みモデルVGG16を使ってそれっぽくやっています。 [開発経緯、開発にこめた思い] 誰かに絵本をプレゼントしたい、でも売れているもの、有名なものはもう持ってたり、どこかで読んだことがあるかもしれない。そんな時は本屋で表紙の気に入った絵本を選びます。でも、そういった本は本棚に背差し陳列された中から選ばないといけません。背表紙を頼りに取っては戻し、取っては戻し。こんな状況を解決したいと思って作ったアプリです。 アプリ名やアイコンのイラストは、僕が小さい頃に幼稚園の図書室で見つけて大好きになった、 マンロー・リーフ の絵本のオマージュです。そして最近ハマっている絵本作家 ブルーノ・ムナーリ もひっそりと。 [ストアへのリンク] サイト: https://munro.hiraku.space ストア: https://apps.apple.com/jp/app/id1508880540 1つ目は、モバイルアプリグループで iOS エンジニアをしている @temoki のサービスです。最新技術も取り入れたサービスになっています。私は絵本を手にする機会はありませんが、お子さんがいる方などは使ってみてはいかがでしょうか。 2つ目 [サービス名] 通りの達人 [アプリの説明] チームを作成し、チーム内でご飯屋さんのレビューができるアプリです。 [技術スタック] 言語はKotlinを使用し、 アーキテクチャ にはMVVM+Clean Archtectureを採用しています。mBaaSにはFirebaseを使用しています。 iOS 版はKotlin/Nativeを用いて開発中。 [開発経緯、開発に込めた思い] 知らない人のグルメレビューよりも知り合い間でのグルメレビューの方が信頼できるし、どこかに行ったことが話のきっかけにもなるので開発しました。 [ストアへのリンク] iOS : 開発中 Android : https://play.google.com/store/apps/details?id=com.takhaki.schoolfoodnavigator&hl=ja 2つ目は、同じくモバイルアプリグループで iOS 、 Android エンジニアをしているカーキのサービスです。友達が普段何を食べているか知るにはいい機会かと思いました。カーキは「通りの達人」以外にも様々なサービスをリリースしているので、ぜひ他のサービスも触れてみてください。 3つ目 [サービス名] おごりあい [アプリの説明] 「おごりあい」は親友同士や恋人同士の奢り合いを管理するアプリです。 [技術スタック] マルチプラットフォーム 対応が可能かつ使用したことがあった言語なので、 フレームワーク としてReact Nativeを採用。 React Native開発において、ビルドや実機検証といった開発する上で必要となる部分を支援してくれるExpoを利用。 ログイン認証はFirebaseのAuthentication、データ管理はFirebaseのCloud Firestoreを採用しています。 [開発経緯、開発に込めた思い] よくご飯に行ったり出かけたりする人なのに、会計ごとに割り勘をしたり、会計別支払いをすることで、毎回お金のやり取りや会計待ちが発生して面倒だなと常々思っていました。 そこで、1回の会計ごとにどちらかがまとめて支払いを行い、それをお互いにし合えば、この問題を解決できるのではないかと思い、「おごりあい」を開発しました。 この「奢り合い」の関係は全ての人に推奨できるものではありません。信頼できる人同士で、出かけた際のちょっとした不便を解決できればと思います。 [ストアへのリンク] iOS : https://apps.apple.com/jp/app/%E3%81%8A%E3%81%94%E3%82%8A%E3%81%82%E3%81%84/id1510976430 Android : 絶賛開発中 3つ目は、アプリケーショングループで Rails エンジニアをしている 田中 のサービスです。私はおごりおごられの中で発生した金額を忘れがちなので、「おごりあい」を使うことでトラブルを避けていこうと思います。 4つ目 [サービス名] Wanago [アプリの説明] Wanagoは現在位置から周辺のお店をランダムで表示してくれるアプリです。 [技術スタック] React Native + Expo + TypeScriptを使用。お店の情報は、 Foursquare のPlaces API を使用。 [開発経緯、開発に込めた思い] 自分が友人と遊んでいるときにご飯をどこに食べに行くか迷って、なかなか決まらないことがよくあったので、なんとかしたいなと思い、このアプリを開発しました。 [ストアへのリンク] iOS : https://apps.apple.com/jp/app/wanago/id1509016873 Android : 開発中 4つ目は、フロントエンドグループでReactエンジニアをしている 渡辺 のサービスです。何を食べるかで迷うのは結構な時間の無駄遣いなので、このアプリを駆使して時間を有効活用していきたいですね。 5つ目 [サービス名] マスクル [アプリの説明] 仲間とグループを作り、グループ内で筋トレの共有ができるアプリです。 [技術スタック] 業務でReactを使っている+マルチフラットフォーム対応ということでReact Nativeを使用しています。またReact Nativeの開発支援ツールとしてExpo、DBにCloud Firestoreを利用しています。 [開発経緯、開発に込めた思い] 一人では筋トレが続かない人でも、仲間と励まし合いながら筋トレをすれば、筋トレが継続できるのでは?と思い、このアプリを作成しました。 ぜひ、みなさんもマスクルを使って理想の体を目指しましょう! [ストアへのリンク] iOS : https://apps.apple.com/jp/app/%E3%83%9E%E3%82%B9%E3%82%AF%E3%83%AB/id1509482384 Android : https://play.google.com/store/apps/details?id=jp.masukuru 最後は、フロントエンドグループでReactエンジニアをしている 永井 とアプリケーショングループで Rails エンジニアをしている私のサービスです。私自身、一人では筋トレが続きませんでしたが、このアプリを使うことで仲間の筋トレ内容も知ることができ、筋トレを継続できています。 まとめ いかがでしたでしょうか?今回は、スタメンのプロダクト部の個人モバイルアプリたちを紹介しました。 このように同じ組織で各個人がサービスを開発していると、知見の共有もできますし切磋琢磨もできるので、とても良い経験になったなと感じました。 最後になりますが、スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています。興味を持ってくれた方は、ぜひ下記のエンジニア採用サイトをご覧ください! スタメン エンジニア採用サイト
こんにちは、スタメン VPoE(開発部門の責任者) の小林です。 2020年7月に、スタメンのプロダクトを作っているプロダクト部の全員で合宿を行い、Star Code の改定を行いました。今回は改定した Star Code とその背景をご紹介したいと思います。 なお、はじめて Star Code を決めた 2019年の合宿の記事 、スタメンのプロダクト部が作っている 『TUNAG』の技術的な解説 と合わせると、スタメンのプロダクトチームの現在と将来をご理解いただきやすいと思います。 Star Code とは? プロダクトで事業の成長を牽引する これは、株式会社スタメンの経営理念である『一人でも多くの人に感動を届け、幸せを広める。』を実現するために、プロダクト部として目指している姿( VISION )です。 私たちが創ったプロダクトが事業の成長を牽引し、一人でも多くの人に感動を届け、幸せを広げたいと考えています。 このビジョンを実現するために、大切にしたい価値観や行動指針( VALUE )を合宿でプロダクト部の全員で議論し、定めた内容が以下の Star Code になります。 改定した理由 2019年3月に プロダクト部の行動指針である Star Code を定め、プロダクト部の目指すビジョンとして「エンジニアリングで事業の成長を牽引する。」を決めました。 当時は、初めての(待望の)デザイナーが部に加わった直後でほぼエンジニアのみの組織でしたが、現在はデザイナーは、内定中を含めると3人のチームになりましたし、企画職(プロダクトマネージャー)も加わり、組織として多様化しました。 また、エンジニアチームも4つのグループとなり、規模も専門性も増しています。事業面でも、この一年間で、 TUNAG の拡大に加えて、 TERAS と FANTS のリリースが行われました。 人数も、前回は10名でしたが、現在のプロダクト部は21名と倍になりました。メンバーの半分にとって、Star Code は自分たちで決めたコトではなく、入社時に決まっていたコトになります。Star Code の改定に参加することで、多様な意見を取り入れ、当事者意識を持つことで、チームとしても団結したいと考えていました。 このような背景で、改めて合宿を開催し、全員で原点回帰して議論し、Star Code アップデートすることにしました。 新しい Star Code 合宿での議論を経て、Star Code は下記の5つになりました。順に解説していきます。 ユーザー目線で考える。 問題を見極める。 失敗に向き合う。 枠を越えて巻き込む。 自分の意思を持つ。 ユーザー目線で考える。 多くの人に感動を届け、幸せを広めるためには、ユーザーが抱くプロダクト(事業)に対しての期待値を超える必要があります。そのためには、プロダクトに作り手としての意思を込めつつも、常にユーザー目線で考える必要があります。 問題を見極める。 プロダクトは、ユーザーの問題(課題)を解決するために存在しますし、仕事とは社内外の顧客の問題を解決することです。どんな仕事であっても、問題を見つけ、理解し、どうすべきか見極めることは非常に大切です。様々な視点から物事の本質を捉えられるチームでありたいです。 失敗に向き合う。 プロダクトを作る際には、仕様策定、デザイン、不具合、障害など、いろいろな失敗をします。失敗には真摯に向き合って対応し、同じ失敗をしないように振り返り改善する。失敗から学びを得られるプロフェッショナルでありたいです。 枠を越えて巻き込む。 組織と事業が拡大すると事業、プロジェクト、組織、担当、専門領域など、様々な「枠」ができ、相互連携を阻害します。枠にとらわれず、枠を超えて巻き込むことによって、各自の専門領域を超えてコラボレーションし、想定を超える相乗効果を生む組織でありたいです。 自分の意思を持つ。 プロダクトを作ることは、仕様やデザイン、実装に自分の意思を込めること。意思を持った行動は強く、意思の込もったプロダクトが世の中を変えます。何事に対しても主体的に取り組み自分の意思を持って行動したい。 まとめ 今回 Star Code の改定の背景と改定後の内容をご紹介させていただきました。 前回の Star Code は、プロフェッショナルとしての行動指針が中心でした。今回アップデートによって、組織や事業が大きくなり、今後課題となることがいくつか含まれています。 正直なところ、合宿前はもっとすんなり決まると思っていましたが、思ったより、多様な意見と価値観がでてきて、すぐ決まりませんでした。それこそ、チームが多様・多才になったことであり、今回合宿を開催する意義なのだと思います。 下の写真は議論しながら、デザイナーさんがリアルタイムにイラスト書いてくれました。こんなところにも組織として多才になってきたなぁと実感し、嬉しくなります。 Star Code ユーザー目線で考えて、問題とイシューを見極め、枠を超えて巻き込み連携し、自分の意思を持ってプロダクトを作り、失敗した際には真摯に向き合って改善する。 困難ですが、これができていれば、間違いなく「一人でも多くの人に、感動を届け、幸せを広める。」に近づくと思います。 各事業の成長を牽引できるように、Star Code を常に意識して、良いプロダクトを創っていきたいと思います! 最後に、スタメンでは、エンジニア、デザイナー、プロダクトマネージャーを大募集中です。 株式会社スタメンの募集 - Wantedly から、募集中の職種が確認できます。 こんな環境でいっしょに働きたいなと思っていただけましたら、 @lifework_tech か Wantedly から、ぜひご連絡いただけないでしょうか。未来の新しい仲間からのご連絡をお待ちしております!
はじめまして。株式会社スタメンでフロントエンドエンジニアをしている 永井 です。週5で筋トレをしています。 弊社のプロダクトである TUNAG では、フロントエンドをReact、Redux、TypeScript、サーバーサイドを Ruby on Rails で実装しています。 今回の記事ではReduxのReducerを動的に読み込ませる実装方法について書きたいと思います。 前提として、React、Reduxをある程度理解している人向けに書いています。 目次 はじめに 主な フレームワーク とライブラリ 実装手順 まとめ はじめに 動的なReducer(Dynamic Reducer)の読み込みとはどういうことかを説明します。 ReduxにおけるReducerを読み込ませるタイミングは createStore でstoreを作成する時です。 通常はload時に createStore 関数が実行され、その createStore 関数の引数に、必要なReducerを返す combineReducers 関数を渡すことで、storeが作成されます。 一方で動的なreducerの読み込みでは、読み込ませるReducerを必要なタイミングによって追加します。 なぜ動的に読み込ませることが必要なのでしょうか? 1つの理由としては、読み込み速度を上げることです。 各ページで必要なReducerは共通して使用できるものもありますが、各ページでしか使われないものも沢山あります。各ページで必要なReducerを必要なページ、タイミングで読み込ませることで、一括でReducerを読み込ませるよりも少ない読み込み量を実現することができます。 また、アプリケーションが肥大化するにつれて、combineさせるReducerの数も多くなるので、アプリケーション全体としての複雑性を回避する上でも有益な手段とも言えます。 動的なReducerの実装に関して調べていると、Reduxの作者であるDan Abramovが stack overflow で回答していたものがありました。しかし、彼の回答では「Reducerを追加するタイミング」や「TypeScriptでの実装」を知ることができなかったので、その回答をベースにして、より詳細な実装を今回試みました。 主な フレームワーク とライブラリ React: 16.8.6 redux: 4.0.4 react-redux: 7.1.0 react-router: 5.0.1 typescript: 3.5.3 実装手順 全体的な実装イメージですが、ベースとなるReducerを最初のload時に combineReducers で読み込ませ、必要なタイミングでそのページに必要なReducerを追加していくイメージです。 ※ 実装する際は、動的にReducerが読み込まれているか確認するために、 Redux Dev Tool を使うことをおすすめします。 Reducer 最初にどのページでも必要なReducerを combineReducers にまとめます。ここではどのページでも読み込まれるReducerを layoutReducer 、今後追加されるReducerを homeReducer としています。 import { ReducersMapObject } from 'redux' import { createBrowserHistory } from 'history' // 省略 export const history = createBrowserHistory () export interface RootState { router: RouterState , home: HomeState , layout: LayoutState } const createReducer = ( appendReducers: ReducersMapObject [] ) => combineReducers ( { router: connectRouter ( history ), layout: LayoutReducer , ...appendReducers , } ) export default createReducer ここの createReducer 関数では、 combineReducers によって読み込ませるReducerオブジェクトを作成しています。そして、その引数に appendReducers として、動的に追加する対象のReducerを渡し、 combineReducers に追加します。 storeの作成 import { Store , createStore , Reducer , ReducersMapObject } from 'redux' import createSagaMiddleware from 'redux-saga' import { routerMiddleware } from 'connected-react-router' import { createBrowserHistory } from 'history' import createReducer from '.' import { RootState } from './' export type AppendKeyType = keyof RootState export interface ExtendedStore extends Store { appendReducers?: ReducersMapObject injectReducer?: ( key: AppendKeyType , reducer: Reducer ) => ExtendedStore } const middleware = // 必要なミドルウェア追加 const initializeStore = () => { const store: ExtendedStore = createStore ( createReducer (), middleware ) // storeにreducerを追加する store.injectReducer = ( key: AppendKeyType , reducer: Reducer ) => { store.appendReducers = {} store.appendReducers [ key ] = reducer const { appendReducers } = store store.replaceReducer ( createReducer ( appendReducers )) return store } return store } export default initializeStore 先程定義した createReducer と必要なmiddlewareを、store作成時、つまり createStore の引数に渡します。 上のコードでは initializeStore という関数を作成して、その関数内で createStore を行っています。 ここで特徴的なのはstoreのメソッドに injectReducer と appendReducers を追加していることです。 injectReducer では、引数にkeyとreducerを受け取り、その受け取ったkeyとreducerをもとにして、先程作成した createReducer の引数に渡します。 そして、storeの replaceReducer 関数でReducerの変更を行っています。( replaceReducer はカスタム関数ではなく、Reduxの関数です) storeで injectReducer と appendReducers を受け付けるために、既存のstoreの型を ExtendedStore として拡張しています。 Router これで動的なReducerを受け付ける準備ができました。しかし、先程作った injectReducer をどこで実行するかが問題です。 それを行うのは、Routerで行います。例えば、Routerの /home で指定しているcomponentが呼び出されるタイミングで、必要なReducerを injectReducer で注入します。 例として、 /home で必要なReducerを HomeReducer とします。 const Router = ( props: Routerprops ) => { return ( < Switch > < Route exact path = "/home" render = { props => < HomeRouter { ...props } / > } / > < /Switch > ) } export default Router import HomeContainer from '../../containers/home' import HomeReducer from '../../reducers/home' const HomeRouter = ( props: HomeRouterProps ) => { return < HomeContainer { ...props } / > } export default withReducer ( 'home' , HomeReducer )( HomeRouter ) ここで、新しく withReducer が出てきました。 withReducer の実装は以下の通りです。 import React from 'react' import { Reducer } from 'redux' import { useStore } from 'react-redux' // import types import { ExtendedStore , AppendKeyType } from './initializeStore' const withReducer = ( key: AppendKeyType , reducer: Reducer ) => ( WrappedComponent: ( props: HomeRouterProps ) => JSX. Element ) => { const Extended = ( props: HomeRouterProps ) => { const store: ExtendedStore = useStore () if ( store.injectReducer ) { store.injectReducer ( key , reducer ) return < WrappedComponent { ...props } / > } return null } return Extended } export default withReducer withReducer では引数にkeyとreducerを受け取ります。そして返り値の関数の引数に描画する コンポーネント を指定します。 返り値の関数で行っていることは、react-reduxの useStore() でstoreを持ってきて、先程定義した injectReducer で引数で渡ってきたreducerとそのkeyをstoreに注入しています。 こうすることで、HomeRouterが描画されたタイミングで必要なReducerの注入を行うことが可能になります。 実際に、 /home にアクセスすると、homeReducerが追加されていることをRedux Dev Toolで確認できると思います。 まとめ 今回は動的なReducerの実装方法について書きました。最初にこの実装方法を見た時に、一回では理解できませんでしたが、全体感を掴むことで理解することが出来ました。 また、既存のstoreを拡張するにあたって、reduxがどのようにstoreを実装しているかをコードレベルで調べることで、reduxの構成も深く理解することができたと思います。 動的Reducerですが、npmとしても redux-dynamic-reducer などが存在しますが、スター数が少ないことや、依存性を回避したい理由で、独自で実装しました。参考になれば幸いです。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています!興味を持ってくれた方は、ぜひ下記のエンジニア採用サイトをご覧ください。 スタメン エンジニア採用サイト フロントエンドエンジニア募集ページ
Error こんにちは。スタメンで iOS アプリを開発している @temoki です。 モバイル アプリ開発 に限らずソフトウェアの実装においては必ずエラーハンドリングが必要になりますよね。 iOS アプリを Swift で開発する場合、回復可能なエラーのハンドリングについては次のように do-catch ステートメント を用いることが基本となっています *1 。 do { // `func functionThatCanCauseError() throws -> Int` let value = try functionThatCanCauseError() print(value) } catch let error { print(error) } 他には、Swift 5 で追加された Result 型 *2 を用いて次のように行うことも多いですね。 // `func functionThatCanCauseError() -> Result<Int, SomeError>` let result = functionThatCanCauseError() switch result { case .success( let value ) : print(value) case .failure( let error ) : print(error) } さて、このどちらのケースでもエラーとして取り回されるのが Error です。今回はこの Error について深堀りし、巧く活用することを考えてみようと思います。 Swift のエラー型 Swift の Error は次のように空の プロトコル として定義されています。 public protocol Error { } そして Foundation フレームワーク *3 にて、エラーの ローカライズ された説明を取得するための localizedDescription プロパティが拡張されています。 extension Error { /// Retrieve the localized description for this error. public var localizedDescription : String { get } } さて、この Error プロトコル に準拠したエラーを次のように定義してみます。 struct SomeError : Error { var localizedDescription : String { return "This is SomeError." } } ここでクイズです。下記のコードのようにスローされた SomeError の localizedDescription を出力した結果はどうなるでしょうか? do { // Call function that throws `SomeError`. try functionThrowsSomeError() } catch let error { print(error.localizedDescription) } 答えはこのようになります。 The operation couldn’t be completed. (__lldb_expr_61.SomeError error 1.) なんと This is SomeError ではありません!do-catch ステートメント でスローされたエラーはすべて Error 型として扱われますが、 Error の localizedDescription には Protocol Extension によるデフォルト実装があり、そのデフォルト実装の方で処理されるためにこのような挙動となります。 これは Error に準拠したカスタムエラーを作る場合に陥りやすい罠ですね。では、この例で意図していた結果を localizedDescription で取得できるようにするにはどうすると良いでしょうか? ローカライズ されたエラー説明を提供可能なエラー型 このように ローカライズ されたエラー説明を提供するエラーを定義するには LocalizedError プロトコル *4 を使用します。 LocalizedError は Foundation フレームワーク に次のように定義されています。 /// Describes an error that provides localized messages describing why /// an error occurred and provides more information about the error. public protocol LocalizedError : Error { /// A localized message describing what error occurred. var errorDescription : String ? { get } /// A localized message describing the reason for the failure. var failureReason : String ? { get } /// A localized message describing how one might recover from the failure. var recoverySuggestion : String ? { get } /// A localized message providing "help" text if the user requests help. var helpAnchor : String ? { get } } プロトコル に定義されているプロパティのうち、 errorDescription プロパティが localizedDescription の結果として使われます。試しに先ほどの SomeError を次のように変更してみましょう(すべてのプロパティにはデフォルト実装が提供されているため、実装を省略することができます)。 struct SomeError : LocalizedError { var errorDescription : String ? { return "This is SomeError." } } そうすると先ほどの結果は期待していた This is SomeError. となりました! 思ったような結果が得られたところで、 LocalizedError の他のプロパティに注目してみてください。 failureReason , recoverySuggenstion , helpAnchor 。これらの名前はどこかで見たことはないでしょうか? Cocoa のエラー型 iOS や macOS のアプリを開発したことがあれば必ず目にする Cocoa *5 のエラー NSError *6 に同じような名前のプロパティが存在します。 localizedDescription localizedFailureReason localizedRecoverySuggestion localizedHelpAnchor このことから Swift の Error , LocalizedError はこの Cocoa のエラーとの関連性がありそうです。実際に Foundation フレームワーク には Error プロトコル を継承したエラー型として、この LocalizedError の他に、 RecoverableError , CustomNSError , NSError が定義されており、Swift の Error と Cocoa の NSError には深く関係しています。 NSError と Error NSError の インスタンス を生成するにはエラー ドメイン とエラーコードの2つの要素が必須です。 Cocoa は Objective-C で実装されており、エラーを伴う Cocoa API は次のように引数で NSError を受け取るように設計されています。 *7 NSError *error = nil ; BOOL success = [receiver someMessageWithError:&error]; if (!success) { NSLog(error.domain); } この API は次のように Error をスローする形式で Swift にインポートされます。 do { try receiver.someMessage() } catch let error as NSError { print(error.domain) } ここで注目したいのが Error から NSError へのキャストが常に成功するという点です( error as! NSError のように強制的にキャストしようとすると コンパイラ で Forced cast from 'Error' to 'NSError' always succeeds と警告されます)。上記のような Objective-C のエラーハンドリングを Swift にインポートするために、Swift の Error から Cocoa の NSError に変換される仕組みが Cocoa に用意されているようです。 Error はどのように NSError に変換されるのか? それでは Error はどのように NSError に変換されるのでしょうか?これから Error プロトコル に準拠した様々なエラーオブジェクトを NSError に変換した時にどのように扱われるのかを試していきたいと思います。そのため、 Error を NSError としてコンソールに出力する次のような関数を用意しました。 func printErrorAsNSError (_ error : Error ) { print(String(describing : type (of : error ))) let nsError = error as NSError print( "domain " , nsError.domain) print( "code " , nsError.code) print( "userInfo " , nsError.userInfo) print( "localizedDescription " , nsError.localizedDescription) print( "localizedFailureReason " , nsError.localizedFailureReason ?? "(nil)" ) print( "localizedRecoverySuggestion" , nsError.localizedRecoverySuggestion ?? "(nil)" ) print( "localizedRecoveryOptions " , nsError.localizedRecoveryOptions ?? "(nil)" ) print( "recoveryAttempter " , nsError.recoveryAttempter ?? "(nil)" ) print( "helpAnchor " , nsError.helpAnchor ?? "(nil)" ) } Error as NSError Swift のドキュメントにある Error Handling の例では、Swift の列挙型はエラーの表現に適していると書かれていますので、まずは Error プロトコル に準拠した列挙型で試してみます。以下、エラーの定義とその インスタンス の出力結果です。 enum EnumError : Error { case case1 case case2 case case3 var localizedDescription : String { "EnumError.localizedDescription" } } printErrorAsNSError(EnumError.case3) /* EnumError domain __lldb_expr_7.EnumError code 2 userInfo [:] localizedDescription The operation couldn’t be completed. (__lldb_expr_7.EnumError error 2.) localizedFailureReason (nil) localizedRecoverySuggestion (nil) localizedRecoveryOptions (nil) recoveryAttempter (nil) helpAnchor (nil) */ エラー ドメイン は型の名前になりましたね(これは Xcode Playground での実行結果ですので、 __lldb_expr_*. という プレフィックス がついていますが、ここはモジュール名となります)。エラーコードは 2 となっていますが、これは列挙されたケースが zero-based な番号で割り振られた値となっています。つまり case1 , case2 , case3 はそれぞれ 0, 1, 2 です。ちなみに Swift の列挙型は次のように RawRepresentable *8 に準拠した型の値を割り当てることができます。 RawRepresentable が Int の場合は、各ケースの raw value が NSError のエラーコードとなりますが、それ以外の場合は上記のとおりになります。 enum IntEnumError : Int , Error { case case1 = 123 // code = 123 case case2 = 234 // code = 234 case case3 = 345 // code = 345 } enum StringEnumError : String , Error { case case1 = "CASE1" // code = 0 case case2 = "CASE2" // code = 1 case case3 = "CASE3" // code = 2 } 列挙型をエラーとして使うことは、各ケースにエラーコードが割り当てられるという点でも相性が良さそうであることがわかりましたね。それではクラスや構造体の場合にエラーコードがどうなるのかが気になってきますのでやってみましょう。 struct StructError : Error { var localizedDescription : String { "StructError.localizedDescription" } } printErrorAsNSError(StructError()) /* StructError domain __lldb_expr_7.StructError code 1 userInfo [:] localizedDescription The operation couldn’t be completed. (__lldb_expr_7.StructError error 1.) localizedFailureReason (nil) localizedRecoverySuggestion (nil) localizedRecoveryOptions (nil) recoveryAttempter (nil) helpAnchor (nil) */ エラーコードは 1 となりました。エラーコードを決めるための情報が何もないので常に 1 となるようです。構造体を例にしましたがこれはクラスでも同様です。 引き続き、Foundation フレームワーク の他のエラー プロトコル についても見ていくことにします。 LocalizedError as NSError 先ほど登場した LocalizedError です。これはエラーの内容、理由、回復方法などの説明を含めることができます。 struct StructLocalizedError : LocalizedError { var errorDescription : String ? { "StructLocalizedError.errorDescription" } var failureReason : String ? { "StructLocalizedError.failureReason" } var recoverySuggestion : String ? { "StructLocalizedError.recoverySuggestion" } var helpAnchor : String ? { "StructLocalizedError.helpAnchor" } } printErrorAsNSError(StructLocalizedError()) /* StructLocalizedError domain __lldb_expr_7.StructLocalizedError code 1 userInfo [:] localizedDescription StructLocalizedError.errorDescription localizedFailureReason StructLocalizedError.failureReason localizedRecoverySuggestion StructLocalizedError.recoverySuggestion localizedRecoveryOptions (nil) recoveryAttempter (nil) helpAnchor StructLocalizedError.helpAnchor */ RecoverableError as NSError 次の RecoverableError は、エラーからの回復方法そのものも提供します。 struct StructRecoverableError : RecoverableError { var recoveryOptions : [String] { [ "StructRecoverableError.recoveryOptions.1" , "StructRecoverableError.recoveryOptions.2" ] } func attemptRecovery (optionIndex recoveryOptionIndex : Int , resultHandler handler : @escaping (Bool) -> Void ) { handler( true ) } func attemptRecovery (optionIndex recoveryOptionIndex : Int ) -> Bool { return true } } printErrorAsNSError(StructRecoverableError()) /* StructRecoverableError domain __lldb_expr_7.StructRecoverableError code 1 userInfo [:] localizedDescription The operation couldn’t be completed. (__lldb_expr_7.StructRecoverableError error 1.) localizedFailureReason (nil) localizedRecoverySuggestion (nil) localizedRecoveryOptions ["StructRecoverableError.recoveryOptions.1", "StructRecoverableError.recoveryOptions.2"] recoveryAttempter Foundation.__NSErrorRecoveryAttempter helpAnchor (nil) */ StructCustomNSError as NSError 最後の CustomNSError は NSError に必須のエラー ドメイン とエラーコード、そしてエラーの付帯情報となる userInfo を明示することができます。Swift の Error を NSError として取り扱われることを想定する場合は、この プロトコル に準拠したエラーを定義すると良さそうです。 struct StructCustomNSError : CustomNSError { static var errorDomain : String { "StructCustomNSError.errorDomain" } var errorCode : Int { 123 } var errorUserInfo : [String : Any] { [ "StructCustomNSError.UserInfo.Key1" : 456 , "StructCustomNSError.UserInfo.Key2" : 789 ] } } printErrorAsNSError(StructCustomNSError()) /* StructCustomNSError domain StructCustomNSError.errorDomain code 123 userInfo ["StructCustomNSError.UserInfo.Key2": 789, "StructCustomNSError.UserInfo.Key1": 456] localizedDescription The operation couldn’t be completed. (StructCustomNSError.errorDomain error 123.) localizedFailureReason (nil) localizedRecoverySuggestion (nil) localizedRecoveryOptions (nil) recoveryAttempter (nil) helpAnchor (nil) */ Cocoa が提供するエラー表示と回復の仕組み 少し寄り道させてください。先ほど出てきた RecoverableError にはエラーからの回復方法まで含まれていますが、 iOS アプリの開発者には馴染みのないものですよね。実は Cocoa には macOS 限定となりますが NSError によるエラーの表示と回復の仕組みが提供されており、 RecoverableError はこの仕組みに関するものです。興味がわいた方は Apple のドキュメント Error Handling Programming Guide *9 を読んでみてください。 また、このエラー回復の仕組みを iOS アプリの UIAlertController で実現するという記事 *10 も興味深いのでこちらもオススメです。 エラーログで活用する iOS アプリを正常に動作させるためにはエラーハンドリングを適切にする必要がありますが、すべてのエラーパターンを想定することは難しいのが現実です。そこでエラーの発生をロギングすることでアプリの稼働状況を監視することも重要となります。例えば Firebase の Crashlytics *11 にはクラッシュレポートの他にも、エラーをロギングする仕組みがあります *12 。次のように NSError オブジェクトを指定するだけです。 Crashlytics.crashlytics().record(error : error ) Crashlytics はこの NSError のエラー ドメイン とエラーコードでグループ化し、エラーの一覧に表示してくれます。 Crashlyitcs に記録された NSError 各エラーの詳細情報を覗くと、 NSError のが保持する様々な詳細情報も確認することができます。ここまで記録されていると、エラー発生の原因を解決することもしやすくなりそうですね。 Crashlytics に記録された NSError の詳細情報 この図をよく見てみると NSLocalizedDescriptionKey というキーがでてきます。実は NSError の localizedDescription や localizedFailureReason といったプロパティは userInfo に記録された特定のキーの値へのアクセスを簡易にするものです(そのため、これらのプロパティは読み込み専用です)。つまり、Foundation フレームワーク の LocalizedError などのエラー プロトコル を利用し、各プロパティが適切な値を返すように実装しておくことでエラーログもより意味のあるものにできるのです。 以上から、Swift のエラーハンドリングに Error を使う場合においても、 Cocoa の NSError に変換されることを想定して LocalizedError や CustomNSError を活用することで、有用なエラーログを蓄積することができるということがわかりました。 (オマケ) Error の Undocumented な機能 Undocumented ですが Error プロトコル に準拠したエラーに _domain , _code , _userInfo というプロパティを定義することで、 NSError に変換した時に domain , code , userInfo として動作するようです。 _userInfo は AnyObject? であることに注意してください。 struct UndocumentedError : Error { var _domain : String { "UndocumentedError._domain" } var _code : Int { 123 } var _userInfo : AnyObject ? { [ "UndocumentedError.Key.1" : 456 , "UndocumentedError.Key.2" : 789 ] as AnyObject } } printErrorAsNSError(UndocumentedError()) /* UndocumentedError domain UndocumentedError._domain code 123 userInfo ["UndocumentedError.Key.1": 456, "UndocumentedError.Key.2": 789] localizedDescription The operation couldn’t be completed. (UndocumentedError._domain error 123.) localizedFailureReason (nil) localizedRecoverySuggestion (nil) localizedRecoveryOptions (nil) recoveryAttempter (nil) helpAnchor (nil) */ 最後に iOS アプリ開発 者であれば普段当たり前のように接している Error について深掘りしてみましたが、いかがでしたでしょうか。 エラーハンドリングは地味な作業ですが、これをしっかりしておくことでアプリの信頼性はあがります。また、正しくエラーを把握して改善していくことができれば、その信頼性も向上させることもできます。今回の記事の内容を元に Swift の Error と正しくつきあって、みなさんのアプリがよりよくなれば幸いです。 最後になりましたが、スタメンでは一緒にモバイルアプリを含む自社プロダクトの信頼性向上を牽引してくれる仲間を募集しています。興味を持ってくれた方は、ぜひぜ下記のエンジニア採用サイトをご覧ください。 スタメン エンジニア採用サイト サーバーサイドエンジニア募集ページ フロントエンドエンジニア募集ページ インフラエンジニア募集ページ モバイルアプリエンジニア募集ページ *1 : The Swift Programming Language / Error Handling *2 : Apple Developer Documentation / Result *3 : Apple Developer Documentation / Foundation *4 : Apple Developer Documentation / LocalizedError *5 : Apple Developer Documentation Archive/ Cocoa Cocoa は iOS や macOS の アプリ開発 環境を指します。Foundation フレームワーク はこの Cocoa に含まれています。 *6 : Apple Developer Documentation / NSError *7 : Apple Developer Documentation / Understand How Error Parameters Are Imported *8 : Apple Developer Documentation / RawRepresentable *9 : Apple Developer Documentation / Error Handling Programming Guide *10 : Qiita / Errorをいい感じにUIAlertControllerで表示する @coe *11 : Firebase Crashlytics *12 : Firebase Crashlytics / 致命的でない例外を報告する
スタメンでエンジニアをしている田中です。 今回は決済プラットフォームであるStripeの サブスクリプション について、 Ruby で実際にコードを書きながら調査をしたので、そのまとめを記述していこうと思います。 目次 Stripeの サブスクリプション について 準備 サブスクリプション の生成 テスト用のクレジットカード サブスクリプション の開始時刻の設定 トライアル期間の設定 Webhookでイベントの取得 まとめ Stripeの サブスクリプション について Stripeの サブスクリプション は一度作成すると定期的に自動で決済が行われるようになります。 決済金額や支払間隔はStripeの ダッシュ ボードから設定することが可能です。また、 API 経由で設定することも可能です。 サブスクリプション の作成についても、同様に API 経由で作成することが可能となっています。本記事では、 API 経由で サブスクリプション を作成する方法について説明します。 準備 Stripeのgemをインストールします。 gem ' stripe ' Stripeの API を利用する際には api _keyの設定が必要です。 Stripeの ダッシュ ボードにテスト用のsecret_keyがあるので、initializer配下に以下を配置しておきましょう。 Stripe .api_key = ' secret_key ' 参考: https://github.com/stripe/stripe-ruby サブスクリプション の生成 即時に サブスクリプション の契約を行う場合は Stripe::Subscription.create を使用します。 Stripe :: Subscription .create({ customer : ' customer_id ' , items : [ { price : ' price_id ' , quantity : 1 , } ], default_tax_rates : [ ' tax_id ' ], }) 参考: https://stripe.com/docs/api/subscriptions/create https://stripe.com/docs/billing/subscriptions/examples テスト用のクレジットカード Stripeでは様々なケースのテスト用のクレジットカードを用意しています。 各種ブランド・地域、3Dセキュア対応のカードや異常系を想定したカードなど様々です。 以下のページにて一覧で掲載されているので確認してみてください。 https://stripe.com/docs/testing サブスクリプション の開始時刻の設定 即時に決済を行う場合であれば、先程説明した Stripe::Subscription.create を使用すればよいのですが、事前登録のように サブスクリプション の開始日を未来日で設定したい場合については、以下のように Stripe::SubscriptionSchedule.create を利用することで未来日を指定することができます。 Stripe :: SubscriptionSchedule .create({ customer : ' customer_id ' , start_date : 1592699528 , # unixtime phases : [ { plans : [ price : ' price_id ' , quantity : 1 ], default_tax_rates : [ ' tax_id ' ] }, ], }) 参考: https://stripe.com/docs/api/subscription_schedules/create 注意点として、 サブスクリプション が有効になるタイミングと決済が行われるタイミングには1時間の時間差があります。そのため、決済時のWebhookを利用して処理をしようとする際にはこの時間差を考慮する必要があります。具体的には以下の通りです。 start_date で指定した時刻が サブスクリプション が有効になる時刻 start_date + 1時間後が決済が行われる時刻 参考: https://stripe.com/docs/billing/subscriptions/overview#subscription-events トライアル期間の設定 サブスクリプション によくあるビジネスモデルとして一定期間の無料トライアル後に決済を行うケースがあります。Stripeに関しては、即時に サブスクリプション を開始する場合( Stripe::Subscription )と サブスクリプション 開始日を指定する場合( Stripe::SubscriptionSchedule )のどちらについても、パラメータとして trial_end を渡すことで対応することが出来ます。 Stripe :: Subscription .create({ customer : ' customer_id ' , items : [{ price : ' price_id ' }], default_tax_rates : [ ' tax_id ' ], trial_end : 1593268745 , # unixtime }) Stripe :: SubscriptionSchedule .create({ customer : ' customer_id ' , start_date : 1592699528 , # unixtime phases : [ { plans : [ price : ' price_id ' , quantity : 1 ], default_tax_rates : [ ' tax_id ' ], trial_end : 1593268745 , # unixtime }, ], }) 注意点としては、 サブスクリプション の開始時刻を設定する場合と同様、トライアル終了後1時間後に決済が行われます。時系列としては以下のとおりです。 即時に サブスクリプション を開始する場合 trial_end で指定した日時がトライアル終了日時となる trial_end で指定した日時 + 1時間後に決済が行われる サブスクリプション 開始日を指定する場合 start_date で指定した日時がトライアル開始日時となる start_date で指定した日時 + 1時間後にトライアルの決済処理が行われる。しかし、トライアル期間のため0円の インボイス が作成される trial_end で指定した日時がトライアル終了日時となる trial_end で指定した日時 + 1時間後に決済が行われる。こちらの決済ではプランに設定された金額が請求される 参考: https://stripe.com/docs/billing/subscriptions/trials Webhookでイベントの取得 概要 Rails アプリケーションにWebhookのエンドポイントを作成することで、決済の成功・失敗や サブスクリプション の作成といったイベントの通知をStripeから受け取ることができます。本項では、ローカルでの検証方法やエンドポイントの作成方法をご紹介します。 ローカルでの検証方法 StripeではWebhookの検証のためにStripe CLI を提供しています。 以下の手順を参考にインストールしてください。 参考: https://stripe.com/docs/stripe-cli インストール後、 stripe login を実行してStripeにログインすることで各種コマンドが実行できます。 Webhookを受け取る場合は stripe listen コマンドで受け取ることが出来ます。 また、ローカル環境のエンドポイントの指定や取得したいイベントを指定して起動することも出来ます。 詳細は下記をご参照ください。 https://stripe.com/docs/stripe-cli/webhooks イベント一覧 取得できるイベントの一覧は下記をご参照ください。 https://stripe.com/docs/api/events/types エンドポイントの作成 Webhookを受け取るエンドポイントは、以下の2つの処理を行います。 requestの検証 イベントの種類による処理の振り分け 1に関しては、後述する署名の検証を行います。 2に関しては、イベントに応じて行いたい処理を記述してください。(例えば、決済完了処理を受け取ってデータを作成する・ユーザーのステータスを更新する等) payload = request.body.read event = nil begin event = Stripe :: Event .construct_from( JSON .parse(payload, symbolize_names : true ) ) rescue JSON :: ParserError => e # Invalid payload status 400 return end # Handle the event case event.type when ' payment_intent.succeeded ' payment_intent = event.data.object # contains a Stripe::PaymentIntent # Then define and call a method to handle the successful payment intent. # handle_payment_intent_succeeded(payment_intent) when ' payment_method.attached ' payment_method = event.data.object # contains a Stripe::PaymentMethod # Then define and call a method to handle the successful attachment of a PaymentMethod. # handle_payment_method_attached(payment_method) # ... handle other event types else # Unexpected event type status 400 return end status 200 参考: https://stripe.com/docs/webhooks/build 署名の検証 StripeのWebhookは署名の検証を行うことが出来ます。 本番環境または検証環境であればStripeの ダッシュ ボードの「開発者」 → 「Webhook」からエンドポイントを設定すると、Webhook用のシークレットキーが発行されます。 ローカル環境であれば、Stripe CLI にて stripe listen コマンド実行時にシークレットキーが発行されるので、そちらを設定してください。 payload = request.body.read sig_header = request.env[ ' HTTP_STRIPE_SIGNATURE ' ] endpoint_secret = ' webhookのsecret_key ' event = nil begin event = Stripe :: Webhook .construct_event( payload, sig_header, endpoint_secret ) rescue JSON :: ParserError => e # Invalid payload status 400 return rescue Stripe :: SignatureVerificationError => e # Invalid signature status 400 return end 参考: https://stripe.com/docs/webhooks/signatures まとめ Stripeの サブスクリプション の基本的な作成から、スケジュールやトライアル、処理後のWebhookといった決済サービスを作成する上で必要なところをまとめてみました。Stripeはドキュメントもしっかりとまとめられており、開発者フレンドリーなプラットフォームだと感じました。まだまだ扱いきれていないこともあるので、今後も機会があればまとめていきたいと思います。 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
こんにちは。フロントエンドエンジニアの 渡邉 です。 普段ReactとTypeScriptを書いています。 今回はTypeScriptのUtility Typesについて紹介します。 記事のタイトルが某 大柴さんみたいになっていますが、この記事を読んだ方の力に少しでもなれたら幸いです。 目次 Utility Types よく使うUtility Types その他Utility Types 最後に Utility Types 公式ドキュメント Utility Typesは楽にType Transformするための型で、TypeScriptによって提供されています。 Partial<T> Readonly<T> Record<K,T> Pick<T,K> Omit<T,K> Exclude<T,U> Extract<T,U> NonNullable<T> Parameters<T> ConstructorParameters<T> ReturnType<T> InstanceType<T> Required<T> ThisParameterType OmitThisParameter ThisType<T> よく使うUtility Types 以下のUtility Typesは個人的によく使うので説明と実際の使用例を紹介します。 Pick Omit Parameters ReturnType Pick<T,K> Tに渡した型から指定のプロパティを抽出した型に変換します。 type Hoge = { hoge: string foo: number } type PickFoo = Pick < Hoge , | 'foo' > /* type PickFoo = { foo: number } */ Pickをよく使う理由として、ある型の一部分だけが必要な型を作成する場面がよく出てくるからです。 例えば下記コードのようなUser型があり、その中でnameとgenderだけを使う型が欲しい時に使います。 interface User { id: string name: string gender: string email: string birthday: Date } type ViewUserInfo = Pick < User , 'naem' | 'gender' > Omit<T,K> Tに渡した型から指定のプロパティを除去した型に変換します。 type Hoge = { hoge: string foo: number } type OmitFoo = Omit < Hoge , | 'foo' > /* type OmitFoo = { hoge: string } */ OmitはPickとは逆に、ある型の一部分だけがいらない型を作成するときに使います。 User型から gender だけ使わない型が必要な場合。 interface User { id: string name: string gender: string email: string birthday: Date } type ViewUserInfo = Omit < User , 'gender' > Parameters<T> Tに渡した関数の引数の型をタプルとして抽出した型にします。 type Hoge = { hoge: string foo: number } const hogeFunc = ( arg: Hoge ) => { console.log ( arg ) } type ParametersHoge = Parameters <typeof hogeFunc > /* type HogeParameters = [Hoge] */ postでリク エス トする場面を例に説明します。 型ファイル( types.ts )と api 呼び出し関数をまとめたファイル( apis.ts )、実際に実行するファイル( example.ts )に分けています。 example.tsをまず Parameters を使わず実装してみます。 types.ts export interface PostData { name: string email: string } apis.ts import { PostData } from './types' import axios from 'axios' // PostData型の引数を取る export const postData = async( data: PostData ) => { await axios ( '/users' , data ) return 'success' } example.ts import { postData } from './apis' postData ( { name: '太郎' , email: 'hoge@hoge.com' } ) .then ( res => { // ...処理 } ) 上のコードの様に引数に直接objectをを渡すと可読性が落ちるので、引数に渡す用の変数を宣言します。 ただその時点では型が分からないため補完が効かないのと、dataに何が必要なのか分かりません。 import { postData } from './api' const data = { name: '太郎' , } // dataのプロパティに漏れがあった場合ここで気付きます。 postData ( data ) .then ( res => { // ...処理 } ) なので、types.tsからPostData型をimportしてきても良いのですが、postDataがすでにimportされているので、 Parameters を使って変数dataをpostDataのパラメータの型にします。 example.ts import { postData } from './apis' // 第一引数の型がほしいので[0]で抽出しています。 const data: Parameters <typeof postData > [ 0 ] = { name: '太郎' , email: 'hoge@hoge.com' } postData ( data ) 直感的に「postData関数のパラメータの型だ」ということが分かるので良いです。 ReturnType<T> 関数の返り値の型を返します。 type Hoge = { hoge: string foo: number } const hogeFunc = ( arg: Hoge ) => { return arg.hoge } type ReturnTypeHoge = ReturnType <typeof hogeFunc > /* type ReturnTypeHoge = string */ redux-saga を使って開発しているときに自分は ReturnType を使っています。 redux-sagaについて知りたい方はスタメンでも こちら の記事で紹介しています。 redux-saga/effects の select を使い state から nameInputValue と emailInputValue 抽出してそのデータを axios を使いpostする例で紹介します。 前提条件 - redux-sagaを使うための設定等は省いて、実際に使用するコードの部分だけ記載 - input要素からonChangeイベントで nameInputValue と emailInputValue がstateにセットされている const POST_DATA = 'POST_DATA' ; interface AppState { user: UserState , // その他state } interface UserType { id: string name: string email: string } interface UserState { users: UserType [] nameInputValue: string // input value state emailInputValue: string // input value state } interface PostData { name: string , email: string } // action const postDataAction = ( id: string ) => ( { type : POST_DATA } ); // api呼び出し関数 export const postData = async( data: PostData ) => { try { await axios.post ( '/users' , data ) return { payload: 'success' } } catch { return { error: 'error' } } } // セレクター const userSelector = ( state:AppState ) => state.user ; // saga task function * runPostData () { // ReturnType<typeof userSelector> 型変数を宣言して、代入時に型チェックする const { nameInputValue , emailInputValue } : ReturnType <typeof userSelector > = yield select ( userSelector ) // ここでも Parameters<typeof postData> 型変数を宣言して、代入時に型チェックする const data: Parameters <typeof postData > = { name: nameInputValue email: emailInputValue } const { payload , error } : { payload?: string , error?: string } = yield call ( CompassNoteAPI.requestCompassNote , params ) if( payload && !error ) { yield put ( ... ) // 成功時の処理 } else { yield put ( ... ) // 失敗時の処理 } } // saga task function * handlePostData () { yield takeEvery ( POST_DATA , runPostData ) } yield を使用して変数に代入すると 型推論 でany型になってしまうため、 ReturnType を使い型を指定しています。 その他Utility Types Partial<T> Tに渡した型のプロパティを全て省略可能にします。 type Hoge = { hoge: string foo: number } type PartialHoge = Partial < Hoge > /* PartialHoge = { hoge?: string foo?: number } */ Readonly<T> Tに渡した型のプロパティを全て readonly にして再代入不可にします。 type Hoge = { hoge: string foo: number } type ReadonlyHoge = Readonly < Hoge > /* type ReadonlyHoge = { readonly hoge: string readonly foo: number } */ Record<K,T> Kに渡した型がプロパティとなりTがそのプロパティの型になります。 interface Hoge { title: string ; } type Foo = 'home' | 'about' | 'contact' ; type RecordHoge = Record < Foo , Hoge > /* type RecordHoge = { home: Hoge; about: Hoge; contact: Hoge; } */ Exclude<T,U> Tに渡した型から、Uの型を除去した型に変換します。 type Hoge = { hoge: string foo: number } type Bar = { bar: boolean } type ExcludeHoge = Exclude < Hoge | Bar , Hoge > /* type ExcludeHoge = { bar: boolean } */ Extract<T,U> Tに渡した型から、Uの型を抽出した型に変換します。 type Hoge = { hoge: string foo: number } type Bar = { bar: boolean } type ExtractHoge = Extract < Hoge | Bar , Hoge > /* type ExtractHoge = { hoge: string foo: number } */ NonNullable<T> T型からnullとundefinedを除外した型にします。 type Hoge = { hoge: string | undefined foo: number | null } type NonNullableHoge = NonNullable < Hoge > /* type NonNullableHoge = { hoge: string; foo: number; } */ これには注意点が一つあって、optionalのプロパティは除去されないです。 type Hoge = { hoge?: string foo: number | null } type NonNullableHoge = NonNullable < Hoge > /* type NonNullableHoge = { hoge?: string; foo: number; } */ ConstructorParameters<T> Parametersの コンストラクター 版です。 class Hoge { constructor( a: string , b: number ) {} } type ConstructorParametersHoge = ConstructorParameters <typeof Hoge > /* ConstructorParametersHoge = [string, number] */ InstanceType<T> 型Tのコンスト ラク タの返り値の型を返します。 class Hoge { constructor( a: string , b: number ) {} } type InstanceTypeHoge = InstanceType <typeof Hoge > /* type InstanceTypeHoge = Hoge */ Required<T> 型Tの省略可能のプロパティを必須にします。 type Hoge = { hoge?: string foo?: number } type RequiredHoge = Required < Hoge > /* type RequiredHoge = { hoge: string; foo: number; } */ ThisParameterType thisのパラメータを取得します。(使い所がわからない) function toHex ( this : Number ) { return this .toString ( 16 ); } function numberToString ( n: ThisParameterType <typeof toHex >) { return toHex.apply ( n ); } OmitThisParameter thisのパラメーターを削除します。(使い所がわからない) 注: --strictFunctionTypes が有効になっている場合にのみ正しく機能します。 function toHex ( this : Number ) { return this .toString ( 16 ); } // The return type of `bind` is already using `OmitThisParameter`, this is just for demonstration. const fiveToHex: OmitThisParameter <typeof toHex > = toHex.bind ( 5 ); ThisType<T> objectの中のthisを型Tにします。 interface Hoge { hoge: string ; } interface Foo { foo () : void ; } // objの型はFooであり、obj内でのthisの型はHogeと明示的に指定します const obj: Foo & ThisType < Hoge > = { foo () { console.log ( this .hoge ); // undefined } , } ; 最後に 実際に触ってみてUtility Typesはとても便利ですが、公式ドキュメントの見つけづらいところにあります。 今回の記事で少しでも多くの人に知ってもらえれば幸いです。 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
Amazon Kinesis Firehose 概要 こんにちは。スタメンで開発者をしている津田です。今回は、 Amazon Athena を利用しアプリケーションのリク エス ト処理時間をセグメント別に計測することで、パフォーマンスの低下を検知しやすくしたことについて紹介します。 動機 Webアプリケーションのパフォーマンスについて、以前は主に、 ロードバランサー の平均処理時間を参照していました。しかし、平均処理時間はサーバー全体の負荷状況の手がかりにはなるものの、それ以上の詳細については読み取れません。また、ポーリングや、その他、応答に要する時間の短いリク エス トが多数送られてくる状況だと、平均の処理時間としては下がることになり、アプリケーションパフォーマンス改善の目安としても利用しにくいです。 特別、処理に長い時間のかかったリク エス トに関しては、 bugsnag へ通知、 New Relic 等の APM を使用して対象のリク エス トを調査、というようなこともしていました。これは、極端に遅いリク エス トが発生するケースを発見、改善するのに有用でしたが、 閾値 を下げすぎると調査しきれないほどの量が通知されてしまうため、あまり 閾値 を下げることができません。 上記では検出できない、「特定の状況にあるユーザーのみ、じわじわとパフォーマンスが低下している」ような状況をより早く発見するために、「ユーザーのセグメント」 x 「リク エス トの種類」で分類した平均 応答時間 を算出し、継続的にチェックできるようにしました。 流れ 全体の流れは以下のようになります。 Ruby on Rails アプリケーションからの json 形式ログ出力 Amazon Kinesis によるファイル転送 AWS Glue のクロール Amazon Athena による分析 1. Ruby on Rails アプリケーションからの json 形式ログ出力 アプリケーションは、 Ruby on Rails で作成しています。まずはリク エス ト毎に集計のもととなるログを出力する必要があるため、controllerのbefore_actionで処理開始時の時間を記録、after_actionで所要処理時間、ユーザーの分類に必要な情報、controller、actionに関わる情報を json 形式でログ出力しています。 この時点で、分析や調査に必要と無いと思われるリク エス トではskipして、対象から外しておきます。 2. Amazon Kinesis によるファイル転送 Amazon Kinesis Data Firehose を利用して、ログファイルを各 アプリケーションサーバ ーから、 Amazon S3 の バケット に集約します。 Amazon S3 destination のPrefixに access/YEAR=!{timestamp:yyyy}/MONTH=!{timestamp:MM}/DAY=!{timestamp:dd}/ と設定し、パスに年月日を入れておきます。これは、 Amazon Athena で分析する際に、 パーティション として利用するためです。 Amazon Athena では読み込んだファイルサイズの総量で課金されるため、日次で分析することが多いのであれば、日付単位で パーティション ( Amazon S3 における ディレクト リ)を分割しておくほうがコストを抑えられます。 パーティション については、 Amazon Athena > ユーザーガイド > データのパーティション分割 を参照してください。 3. AWS Glue のクロール S3にファイルを置いただけでは Amazon Athena からDatabaseとして認識されないため、 AWS Glue の クローラの定義 を行い、S3をクロールしてデータソースとして登録します。クローラは json の中身を読み取ってテーブルの定義を作成してくれます。 クロールは基本一度行えば良いのですが、 パーティション が増えたことを検知するために日次で動かしています。データの形式( json のフォーマット)が変わらないのであれば、 ALTER TABLE ADD PARTITION の方が良いのかも知れません。 ちなみに、上記の設定だと GMT 零時で ディレクト リが切り替わるため、日本時間では朝九時過ぎに クローラー を動かすと良いようです。 4. Amazon Athena による分析 ここまでで Amazon Athena のクエリエディターで SQL を利用してログが分析できるようになっているのですが、前述の通り、 Amazon Athena では読み込んだファイルの総容量によって課金されます。試行錯誤したり、いろいろな種類のクエリを実行するのであれば、あらかじめセグメント別に集計したテーブルを作っておいたほうが、時間の節約にもなります。 create table テーブルを作成するため、以下のような感じの SQL を一度実行しました。SELECTの結果を使って CREATE TABLE AS しています。普通に CREATE TABLE を書いてもいいと思いますが、集計したい SQL で直接テーブルを作れるため、こちらのほうが楽でした。項目としてはセグメント別に平均値や総所要時間等を算出しています。 集計のもととなっている access テーブルは上記の json ファイルからなるテーブルなので、各カラムは json ファイルに含まれるオブジェクトのキーにあたります。 CREATE TABLE " access-log " .aggregated_access WITH ( format= ' PARQUET ' , external_location= ' s3://xxx-log/aggregated_access/ ' , partitioned_by=ARRAY[ ' YEAR ' , ' MONTH ' , ' DAY ' ] ) AS SELECT tenant_id, controller_name, action_name, COUNT (*) AS total_count, FLOOR ( SUM (processing_time)) AS total_time, ROUND ( AVG (processing_time), 2 ) AS average_time, MAX (processing_time) AS max_time, MIN (processing_time) AS min_time, APPROX_PERCENTILE(processing_time, 0.9 ) AS ninety_percentile, YEAR, MONTH, DAY FROM access WHERE YEAR = ' 2020 ' AND MONTH = ' 06 ' AND DAY = ' 17 ' -- ここの日付はデータがあれば何でも良い GROUP BY tenant_id, controller_name, action_name, YEAR, MONTH, DAY ORDER BY total_time desc insert 日次では、以下のような SQL で集計を追加します。INSERT文を実行すると、対象のS3 ディレクト リにファイルが生成されます。現状自動化できておらず、間違えて二度実行すると同じデータが2回作られてしまいますが、そうなったらS3から該当のファイルを消せば大丈夫です。 INSERT INTO aggregated_access SELECT tenant_id, controller_name, action_name, COUNT (*) AS total_count, FLOOR ( SUM (processing_time)) AS total_time, ROUND ( AVG (processing_time), 2 ) AS average_time, MAX (processing_time) AS max_time, MIN (processing_time) AS min_time, APPROX_PERCENTILE(processing_time, 0.9 ) AS ninety_percentile, YEAR, MONTH, DAY FROM access WHERE YEAR = ' 2020 ' AND MONTH = ' 06 ' AND DAY IN ( ' 17 ' ) -- ここの日付を変える GROUP BY tenant_id, controller_name, action_name, YEAR, MONTH, DAY select 気になる情報をselectします。aggregated_ access はファイルサイズとして非常に小さくなっているはずなので、PARTITIONもあまり気にせずクエリできます。 上記の SQL でも使用していますが、関数などは Prestoのドキュメント を参照できます。 まとめ 完全な自動化はできていないのですが、日々、各アクションのセグメント別パフォーマンスチェックを行うことができるようになり、いくつか問題点も事前に発見できました。 また、集計したテーブルは、元のデータに比べて圧倒的にサイズが小さくなっています。元のログは、サイズが大きいため、 パーティション を横断するようなSELECT(日付をまたぐような検索)は若干躊躇するようなところがあったのですが、集計後のテーブルであれば全期間検索しても大した読み取りサイズにはなりません。特定のアクションを改善した際など、経時で処理時間を抽出し、改善の成果を確認することができるため、改善のモチベーションも発生しやすくなったのが良かったと思います。
こんにちは、モバイルアプリグループでモバイルアプリの開発をしている @sokume です。 実は日々の開発の傍ら、社内の情シス担当として社内のネットワーク環境の検討や改善に取り組んでいます。 今年の4月〜6月までの間、弊社も新型コロナウィルスに関しての緊急事態宣言にあわせて、全社リモートワークを推奨する期間となっていました。 5月末に緊急事態宣言が解除され、6月1日から本社にまた出勤できる!という数日前に上司からの一言。 という、個人的にも気になっていた Google Wifi の有線対応についての話が! これは良い機会という事で、まだ出社人数の少ない5月の末日に出社して社内ネットワークの見直しに取り掛かります⚡️ 目次 背景 準備 作業開始 結果 まとめ 背景 Gogle Wifi サイコーだよね。早く、安く、便利。 無線メッシュは便利だけど、オフィスが広くて、大本から遠いところで、通信品質が落ちちゃう メッシュ Wifi は安定性は良いけど、速度出ない問題 コロナ禍によるZoomの利用増加により、通信品質の問題が顕著に 社員数増大に伴い、先を見据えたネットワークを構築しておきたい 有線でメッシュにすれば、快適にならないか良くないか🤔 準備 LANケーブル(なるべく長く) 会社の見取り図を見ながら考えよう LANコネクタパーツ(RJ-45) 電源ケーブル 20m 、5m数本 変更前のネットワーク環境 Google Wifi 5台をメッシュ方式で社内に配置する方法。 変更後のネットワーク環境(予想案) Google Wifi の有線接続し、親機と子機がすべてチェーン状にLANケーブルで繋げる 作業開始 よくよく考えると社内のメインのネットワークである Google Wifi をガッツリ切ることは出来ない!ということに気がつく😞 ここから、少しずつすり替えていく案に変更 別親機の Google Wifi を用意 ネットワークの元から、第1設置ポイントへLANケーブルを伸ばし設置 元のネットワークより支障の少なそうな Google Wifi を初期化し、別ネットワークに移行を繰り返す 有線LANを用いて Google Wifi の子機を接続することで、メッシュ構成の時より非常に安定した速度が出るようだ。 すべての Google WIFI を有線で配置する為、社内の上部にある電源エリアにLANケーブルを這わせていきます。 合計で90m(20mx3本、30mx1本)のLANケーブルを使用。 もちろんLANケーブルのコネクタは自作します。(本当に久々の作業😅) 結果的に元ネットワークの Google WIFI は親機と子機1台だけに減らし、新ネットワークに Google Wifi を移行していく 作業中の様子 ネットワークの切り替え Google Wifi を用いたネットワークの切り替えは非常にスムーズに出来ました Google Wifi の設定( Android の Google WIFI アプリ)でネットワーク名の変更から実施 旧ネットワークが network_id 、新ネットワークが network_id_next 新ネットワークの network_id_next を network_id に変更 旧ネットワークの network_id を network_id_old に変更 この際、使用している人が気が付かないレベルですんなり切り替わりました😀 これが社内の Google Wifi の構成が無線メッシュ接続から有線LAN接続に切り替わった瞬間❗️ 結果 社内の無線ネットワークの速度が約3倍になり、オンライン商談や打ち合わせなど、社内のネットワーク環境が改善された😄 まだまだ改善点はあります そもそも社内のデ バイス 数が増えており、1系統の Google Wifi では限界か?という事も😱 そろそろ100台のデ バイス 接続が近づき、流石にハードウェア的に厳しい? まとめ 無線も楽だけど、安定性は有線LANの方が良いです❗️ 今ある機材の構成を見直し、いろいろと試して行くと、良い状況になる😄 スタメンで一緒に働く仲間はどんどん増えています❗️常に試行錯誤して、最適な環境をつくっていく事が大事❗️ 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
Recoil入門 こんにちは。フロントエンドエンジニアの 渡邉 です。 最近フロントエンド界隈で盛り上がっているRecoilについて学びました。 本記事は自分のRecoil入門のついでに記事にしたので、初級者向けになっています。 目次 Recoilとは 使ってみる API Referenceを読む 参考サイト Recoilとは Fecebookが新しく発表したのReactの状態管理ライブラリです。 公式ドキュメント 使ってみる 何からやったらいいか分からない人もいるかも知れないので自分の学習手順を紹介しつつ実際に触っていきます。 公式ドキュメントのGetting Started & BasicTutorialをやる Recoilについて書かれている記事を読む 公式ドキュメントのAPI Referenceを読む みたいな感じで学習しました。 Getting Startedでは入力した文字を出力するのとその入力された文字数カウントを出力するアプリを作っていきます。 この記事でもGetting Startedを一緒にやっていきます。 まずはアプリケーション作成・移動 npx create - react - app my - app cd my - app recoil install npm install recoil or yarn add recoil RecoilRoot recoilを使うにはルートコンポーネントを RecoilRoot でくくる App.js import React from 'react' ; import { RecoilRoot , } from 'recoil' ; function App () { return ( <RecoilRoot > <CharacterCounter / > < / RecoilRoot> ) ; } CharacterCounterでは入力フォームと入力された文字を出力する TextInput と入力された文字数を出力する CharacterCount を呼び出しています。 CharacterCounter.js import React from 'react' ; import TextInput from './TextInput' ; import CharacterCount from './CharacterCount' ; function CharacterCounter () { return ( <div > <TextInput / > <CharacterCount / > < / div> ) ; } export default CharacterCounter Recoilでは複数コンポーネントで共有されるステートはatomと呼ばれます。 今回はTextInputで扱うstateをCharacterCountでも使いたいので、atom関数を用いて作ります。 atomの値を読み取るコンポーネントを暗黙的にサブスクライブされるので、atomに更新が入るとそのatomを使用しているコンポーネント全てに再レンダリングが走ります。 export const textState = atom ({ key : 'textState' , default : '' }) atomを作るにはkey(一意のID)とデフォルト値を設定します。 更に読み取り書き込みしたい場合は、 useRecoilState を使います。 useRecoilState はuseStateみたいな感覚で使えます。 デフォルト値がuseStateと違い、先程定義したatomを引数に受け取ります。 TextInput.js import React from 'react' ; import { atom , useRecoilState } from 'recoil' ; export const textState = atom ({ key : 'textState' , default : '' }) function TextInput () { const [ text , setText ] = useRecoilState ( textState ) ; const onChange = ( event ) => { setText ( event . target . value ) ; } ; return ( <div > <input type = "text" value = { text } onChange = { onChange } / > <br / > Echo: { text } < / div> ) ; } export default TextInput CharacterCountでは先程定義した textState を使ってその文字数をレンダーします。 atomの値から計算し、別の値として出すには selector 関数を使います。 const characterCountState = selector ({ key : 'characterCountState' , get : ({ get }) => { const text = get ( textState ) // getの引数にstateを渡す。 return text . length } }) Recoilでは atom と selector を合わせてstateと呼びます。 どういうことかと言うと、先程使用したuseRecoilStateはatomだけではなくselectorに対しても使えます。 atomとselectorはグローバルな値を提供するという点で共通しています。 違いとしては、atomは自身が値を持っており、selectorはatomから算出された値というところです。 atomと同様に useRecoilState を使い値を取り出せます。 ですが、今回必要なのは値の読み取りだけなのでその場合には useRecoilValue を使います。 逆に書き込みだけしたい場合には useSetRecoilState を使います。 上記3つのhooksをまとめると const hogeState = atom ({ key : 'hogeState' , default : '' }) // useRecoilState const [ hoge , setHoge ] = useRecoilState ( hogeState ) // useRecoilValue const hoge = useRecoilValue ( hogeState ) // useSetRecoilState const setHoge = useSetRecoilState ( hogeState ) // useRecoilStateで値だけ取る場合 const [ hoge ] = useRecoilState ( hogeState ) // useRecoilStateで更新関数だけ取る場合 const [ , setHoge ] = useRecoilState ( hogeState ) 今回は useRecoilValue を使って値を読み取ります。 CharacterCount.js import React from 'react' ; import { selector , useRecoilValue } from 'recoil' ; import { textState } from './TextInput' const characterCountState = selector ({ key : 'characterCountState' , get : ({ get }) => { const text = get ( textState ) return text . length } }) function CharacterCount () { const count = useRecoilValue ( characterCountState ) return ( <p > character count: { count } < / p> ) ; } export default CharacterCount これでGetting Startedのアプリは完成です。 API Referenceを読む Getting Startedだけやっても基本的なことしかわからないので、Getting Startedを終えたらAPI Referenceを読みました。 この記事ではRecoilで基礎的な概念の atom と selector を紹介します。 atom() atom(options) options key : atomを識別する一意の文字列 アプリケーション全体で他のatom,selectorに対して一意のである必要がある default : atomの初期値 selector() selector(options) options key : atomと同じくselectorを識別する一意の文字列 get : 引数からgetを受け取るget関数(ややこしすぎる) get : get関数から受け取ったgetにstateを渡すことでそのstate値を用いることができる。 getにわたしているstateは暗黙的にリストに追加されるのでstateが更新入るとselectorは再評価されます set? : このプロパティが設定されると書込み可能なselectorになります。getとsetをパラメータとしてオブジェクトを渡す関数 get : set関数から受け取るgetは渡したatom/selectorにセレクターをサブスクライブしない set : 他のatom/selectorに書き込むためのset関数 setはReference読んだだけだとちゃんと理解できなかったので簡易なコードを見ながら説明します。 流れを先に記載しておくのでコードと照らし合わせながら読んでみてください。 sCount 初期値0 setSCount(sCount + 1) 実行 set関数で受け取った他のatomを書き込むための(今回の場合はnomalCountに書き込む)set関数と、newValue( 1 )を受け取る atomに書き込むためのset関数にnomalCountを渡し、newValueを10倍にして返す nomalCountが更新されたのでspecialCountのgetが実行されnomalCountの値を返す sCountが更新され、描画 import React from 'react' import { atom , selector , useRecoilState } from 'recoil' ; const nomalCount = atom ({ key : 'nomalCount' , default : 0 , }) ; const specialCount = selector ({ key : 'specialCount' , get : ({ get }) => get ( nomalCount ) , set : ({ set } , newValue ) => set ( nomalCount , newValue * 10 ) , }) ; export default function SpecialCounter () { const [ sCount , setSCount ] = useRecoilState ( specialCount ) ; const addSCount = () => setSCount ( sCount + 1 ) ; return ( <div > <p > specialCount: { sCount } < / p> <button onClick={addSCOunt}>add special< / button > < / div> ) ; } 参考サイト https://blog.uhy.ooo/entry/2020-05-16/recoil-first-impression/ まとめ 普段自分はReduxを使って状態管理をしています。ReduxとRecoilでは、stateの持ち方が異なることをがわかりました。 Reduxでは、ロジックや画面ごとにreducerを持ち、 combineReducers を使って一つのreducerにまとめます。 stateを使う場合は useSelector を使ってstoreのstateから自分が必要なデータを取り出します。 一方Recoilはそもそも局所的にstateをもち、stateを利用するコンポーネント間でのみ共有します。 他にもRecoilではReduxみたいにstoreを作ってreducer作ってaction作ってみたいな作業がなくて、すぐに取り掛かれるのが個人的には嬉しかったです。 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
こんにちは。スタメンで主にバックエンドの開発を担当している河井です。 今回は Firebase Cloud Messaging(以下 FCM)を利用したプッシュ通知の一括送信について書いてみます。 背景 実は以前にも FCM を利用した通知の記事 を書いていて、そこでは各デ バイス への通知1回につき1回 FCM へリク エス トをする方法を紹介しました。 しかしサービスが拡大してくると通知先のデ バイス も増え、個別にリク エス トをしていることによる効率の悪さが目立ってきます。 そこで解決策となるのが複数デ バイス への一括通知です。 複数のリク エス トを一回でまとめて送信することでパフォーマンスの向上を期待できます。 複数デ バイス 通知の仕様について 複数デ バイス 通知の公式ドキュメントは こちら です。 以前のブログ記事でも触れたように、 Ruby には Firebase 公式の SDK はありません。 非公式の Gem もありますが、ここではドキュメントの REST の項目を見て自前で実装してみます。 リク エス ト まずはリク エス トの中身を見てみましょう。 --subrequest_boundary Content - Type : application / http Content - Transfer - Encoding : binary Authorization : Bearer ya29 .ElqKBGN2Ri_Uz...HnS_uNreA POST / v1 / projects / myproject - b5ae1 / messages : send Content - Type : application / json accept : application / json { " message ": { " token ":" bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1... ", " notification ": { " title ":" FCM Message ", " body ":" This is an FCM notification message! " } } } ... --subrequest_boundary Content - Type : application / http Content - Transfer - Encoding : binary Authorization : Bearer ya29 .ElqKBGN2Ri_Uz...HnS_uNreA POST / v1 / projects / myproject - b5ae1 / messages : send Content - Type : application / json accept : application / json { " message ": { " token ":" cR1rjyj4_Kc:APA91bGusqbypSuMdsh7jSNrW4nzsM... ", " notification ": { " title ":" FCM Message ", " body ":" This is an FCM notification message! " } } } --subrequest_boundary-- FCM のドキュメントではこの形式について特に解説していませんが、これは "--区切り文字列" という形式の文字列で始まり 個々のリク エス ト内容を文字列として "--区切り文字列" で連結し 最後を "--区切り文字列--" で閉じた 1つの文字列(テキストファイル) です。改行はすべて CRLF( \r\n ) を使います。 区切り文字列 にはリク エス ト送信時の boundary=" " で任意の文字列を使用できます。 これは mulitpart/mixed という形式で、詳しい説明は RFC 2046 に譲ります。 なお、個々のリク エス トに含める内容としては個別通知のときと同じものなので、こちらについての詳細は 過去の記事 を見てください。 レスポンス レスポンスも同じように、個々のリク エス トのレスポンスが1つの文字列になったものが返ってきます。 --batch_nDhMX4IzFTDLsCJ3kHH7v_44ua- aJT6q Content - Type : application / http Content - ID : response - HTTP /1.1 200 OK Content - Type : application / json; charset = UTF -8 Vary : Origin Vary : X - Origin Vary : Referer { " name ": " projects/35006771263/messages/0:1570471792141125%43c11b7043c11b70 " } ... --batch_nDhMX4IzFTDLsCJ3kHH7v_44ua- aJT6q Content - Type : application / http Content - ID : response - HTTP /1.1 200 OK Content - Type : application / json; charset = UTF -8 Vary : Origin Vary : X - Origin Vary : Referer { " name ": " projects/35006771263/messages/0:1570471792141696%43c11b7043c11b70 " } --batch_nDhMX4IzFTDLsCJ3kHH7v_44ua- aJT6q -- このバッチレスポンスにはバッチリク エス トに含めたリク エス トと同じ順番で対応するレスポンスが含まれています。 先程触れませんでしたが、 Content-ID: foo というフィールドを個々のリク エス トに入れておくとレスポンスに Content-ID: response-foo という形で入ってきます。 各リク エス トにつきユニークな文字列を使用することでリク エス トに対応したものかどうかを確認できます。 注意点として、バッチリク エス ト自体の成否と個々のリク エス トの成否は独立しているということが挙げられます。 例えば、バッチレスポンス自体は正常に動作したので ステータスコード は 200、ただしあるリク エス トの通知先のデ バイス が存在せずそのリク エス トに対するレスポンスコードは 404 となっている、という状況がありえます。 そのため個々のレスポンスに分割してリク エス トの成否を確認する必要があります。 Ruby での実装 一通り仕様については把握できたので、これを Ruby で実装してみます。 HTTP リク エス トには Ruby 標準の Net::HTTP を使います。 リク エス トボディの作成 まずはリク エス トボディを作成します。 上で触れた形式に沿って、以下のように1行ずつ文字列を足していきます。 boundary = " subrequest_boundary " # 区切り文字列 buffer = "" tokens.each do | token | buffer += " -- #{ boundary }\r\n" buffer += " Content-Type: application/http \r\n" buffer += " Content-Transfer-Encoding: binary \r\n" buffer += " Content-Id: #{ token }\r\n" buffer += " Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA \r\n" buffer += "\r\n" buffer += " POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send \r\n" buffer += " Content-Type: application/json \r\n" buffer += " accept: application/json \r\n" buffer += "\r\n" body = { message : { token : token, notification : { title : " FCM Message " , body : " This is an FCM notification message to device 0! " } } } buffer += body.to_json buffer += "\r\n" end buffer += " -- #{ boundary } -- \r\n" 区切り文字列には公式ドキュメントに従って subrequest_boundary としています。 Content_id にはデ バイス の識別 トーク ンを入れてみました。 なお制限として、1回のリク エス トには最大500個まで トーク ンを含めることができます。 バッチリク エス トの実行 curl で書かれているバッチリク エス トを Net::HTTP の形式に変換します。 require ' net/http ' require ' uri ' uri = URI .parse( " https://fcm.googleapis.com/batch " ) request = Net :: HTTP :: Post .new(uri) request.content_type = " multipart/mixed; boundary=\" #{ boundary } \" " request.body = buffer # <= ここでさっき作った文字列を入れる req_options = { use_ssl : uri.scheme == " https " , } response = Net :: HTTP .start(uri.hostname, uri.port, req_options) do | http | http.request(request) end 変換には curl-to-ruby を使いました。 content_type で区切り文字を指定し、request body に先ほど作成したバッチリク エス トボディを入れます。 レスポンスボディの解析 バッチリク エス トのレスポンスはリク エス ト時に指定してた文字列で区切られていて、かつバッチリク エス トと同じ順番でレスポンスを含んでいます。 まずは区切り文字列を使ってレスポンスを分割します。 responses = response.body.split(boundary)[ 1 .. -2 ] レスポンスは --区切り文字 で始まり --区切り文字列-- で終わることを思い出すと、分割した配列は [ '' , res1, res2, ... , ' -- ' ] となるので先頭と末尾の要素は除いておきます。 個々の要素は以下のような文字列です。 Content - Type : application / http Content - ID : response - HTTP /1.1 200 OK Content - Type : application / json; charset = UTF -8 Vary : Origin Vary : X - Origin Vary : Referer { " name ": " projects/35006771263/messages/0:1570471792141696%43c11b7043c11b70 " } ここで 正規表現 で ステータスコード と content_id を抜き出してみます。 code = res.match( / HTTP \/ 1 \. 1 (\d{3})/ )[ 1 ] token = res.match( / Content-ID: response- (.*)\r\n/ )[ 1 ] あとはこれらの情報を元にリトライなりしましょう。 まとめ FCM を利用して複数デ バイス に一括で通知する方法を紹介しました。 一括で送信することでリク エス ト数を大幅に減らすことができます。 今のところ一括送信を利用することによる配信の遅延も感じていないので、パフォーマンスにお悩みの方はぜひ試してみてください。 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。 ご興味のある方はぜひ エンジニア採用サイト や Wantedly をご覧ください。 Photo by Jamie Street on Unsplash
はじめに こんにちは。スタメン エンジニアのミツモトです。 普段は弊社のプロダクトである「 TUNAG 」、「 TERAS 」の開発を行っており、プライベートでは みそかつWeb ( @misokatsu_web )というグループで活動しています。 みそか つウェブの活動として、5/30(土)に「 みそかつ社 エンジニア・デザイナー 転職会議 」の開催を予定しており、そのイベントサイトを Gatsby JSで作成しました。 今回の記事は、そのことについてご紹介させていただきます。 みそか つウェブとは? みそか つウェブは名古屋を拠点として、エンジニアやデザイナーなど、 IT業界に関わる人同士の交流の場を提供するため、今年の2月から活動しています。 似た境遇の仲間を探したり、相談できる友人を作ったり ITというキーワードで繋がれるコミュニティであり、これまでにオンラインの座談会などを行ってきました。 例えば プロダクトマネジメント をしている人が、「社内だと相談できる相手がいない...。」という場合に、 みそか つウェブを通じて、他社の同じような役割を持つ人と気軽に話をすることができます。 活動内容は運営メンバーで随時話をしており、お互いに困っていることを持ち寄ってイベントの企画をしています。そんな中で、どのメンバーも「採用を頑張りたい」という思いがありました。 イベントの経緯 採用を頑張りたいという思いから、「名古屋に拠点をおく企業があつまって、オンラインの採用イベントができないか?」という案が生まれました。そこから各メンバーが名古屋のIT系企業をお誘いし、計8社の参加企業が集まり、「 みそかつ社 エンジニア・デザイナー 転職会議 」を開催することになりました。 5/30の開催に向けて、メンバーで準備を進めています。 参加者への告知を行うためにイベントサイトを作ろうと思い、 実装 工数 を減らせる テンプレートのバリエーションが豊富 細かいところは自分でカスタマイズできる という観点から、GatsbyJSでサイトを作ることにしました。 GatsbyJSとは? GatsbyJS 公式 Reactをベースにした、高速なウェブサイト・アプリをつくる オープンソース フレームワーク 。 Markdown による ドキュメンテーション や投稿、 API などのデータソースをGraphQLで管理し、コード分割・画像の最適化・遅延読み込みなどのパフォーマンス最適化をしてサイトを高速化します。 GatsbyJSは導入も簡単で、 $ npm install -g gatsby-cli を実行することで導入できます。( npm が入っていない場合は先にをインストールしてください。) 実装 今回は gatsby-starter-solidstate というテンプレートを用いてサイトを作成しました。 まずはテンプレートをベースに、デザインの素案を Adobe XDで作ります。 デザインができたら、ローカル開発環境を構築します。 gatsby -starter-solidstateの GitHub ページに従い、以下のコマンドを実行します。 $ gatsby new <サイト名> https://github.com/anubhavsrivastava/gatsby-starter-solidstate $ cd <対象ディレクトリ> $ yarn install $ gatsby develop これだけで、ローカル開発環境でテンプレートのサイトを立ち上げることができます。(楽すぎる。。) ここからイベントサイト用にカスタマイズしたいので、独自のスタイリングをするため、styled-componentsのライブラリを入れます。加えて、本番環境でstyled-componentsが動作するよう gatsby -plugin-styled-componentsも入れます。 $ yarn add styled-components $ yarn add -D gatsby-plugin-styled-components gatsby -plugin-styled-componentsを適用するため、プロジェクトのルートにある gatsby -config.jsの プラグイン に追加します。 module.exports = { plugins: [ 'gatsby-plugin-styled-components' ] }; あとはカスタマイズしたい部分をReact, styled-componentsで実装していきます。 デプロイ GatsbyJSで出来たサイトを ホスティング する方法は、Netlify・ AWS Amplifyなどたくさんあります。(参考: https://www.gatsbyjs.org/docs/deploying-and-hosting ) 今回は試してみたいと思っていた Firebase Hosting でサイトをデプロイしました。 まずは Google アカウントの用意し、 Firebase コンソールへログイン & プロジェクトの作成を行います。加えて、firebase CLI をインストールします。 $ npm install -g firebase-tools 次にfirebaseでログインし、Firebase プロジェクトを初期化します。 $ firebase login $ firebase init initすると、利用するfirebaseの機能・対象のfirebaseプロジェクト等を聞かれるので選択します。 そして最後に以下のコマンドを実行します。 $ firebase deploy これだけでサイトをデプロイすることができます。 完成したものが こちら おわりに 今回はGatsbyJSを用いてイベントサイトを作成しました。GatsbyJSを利用すれば 工数 をかけず、自分の思い描くサイトを作ることができます。また、テンプレートの種類がたくさんあることで、イベントサイトのデザインを考える時の参考になりました。興味がある方はぜひお試しください。 スタメンでは一緒に開発してくれるエンジニアを募集しています!興味を持ってくれた方は、ぜひ エンジニア採用サイト をご覧ください。
概要 はじめまして。スタメンでフロントエンドエンジニアをしている @0906koki です。筋トレを週5でする筋肉エンジニアです。 普段はReactとTypeScriptで開発をしていますが、サーバーサイドのRailsもAPIを作成するタイミングで触ります。 弊社のプロダクトである TUNAG では、フロントエンドをReact、Reduxで実装しており、Reduxにredux-sagaを導入しています。今回は、そのredux-sagaについて書きたいと思います。 ※ 本記事はReactとReduxをある程度理解している人向けとなっております。 経緯 Reduxを触っていると、非同期処理などの副作用を、redux-thunkやredux-sagaなどのmiddlewareが受け持つことが多いと思います。また、Reduxを使わずに、Reactライフサイクル内で非同期処理を記述することもあると思います。 Redux有り無しに関わらず、非同期処理によってコードが複雑化するケースが多く、非同期処理をシンプルに保つことはReactの実装においては重要な課題です。そうした課題に対して、redux-sagaは他の手法に比べて、コードをシンプルに保つことができるので、今回は改めてredux-sagaについて学習してみようと思いました。 主なフレームワークとライブラリ react(16.3.2) react-redux(7.1.0) redux-saga(1.0.5) redux-sagaの基本 redux-sagaは、reduxの同期的なフローの中に、非同期処理などの副作用を簡単に組み込めるようにするライブラリです。また、redux-sagaでは、独自のタスクという概念によって、副作用を並列で走らせることができます。 こうした副作用の処理を、同期的なreduxフローのどこで行うかというと、middlewareが担当します。 大まかなreduxの流れに、redux-sagaを入れるとこんな感じになります。 クリックなどのユーザーイベントにより、ActionをDispatchする そのActionをredux-sagaが検知して、タスクを実行 そのタスク内での非同期処理が成功した場合、成功の結果を通知するActionをDispatchする 通常は、ActionをDispatch → Reducerでstateを更新の流れなのですが、redux-sagaを使用すると、特定のActionがDispatchされたタイミングで、そのアクションに対応したジェネレータ関数を実行します。更に、その関数の中でAPI callを行い、ActionをDispatchすることで、APIで受け取ったデータをもとに、stateに更新をかけることもできます。 redux-sagaが提供するタスク実行コマンド redux-sagaでは、タスクを実行するコマンドを、 redux-saga/effects のimportによって使用できます。主なコマンドは以下の通りです。 put(ActionをDispatchする) take(指定のActionのDispatchの監視を行う) takeEvery(同じActionが複数回来た場合に、並列で処理を行う) takeLatest(処理をキャンセルし、新しい処理を行う) call(非同期処理を呼び出して、終了を待つ) fork(タスクの実行を行う) select(storeからstateを取り出す) こうしたコマンドを、ジェネレーター関数の中で使用して、同期的に記述することを可能にします。 redux-sagaのメリット reduxの中で非同期処理などの副作用を受け持つライブラリとして、redux-thunkがあり、よくredux-sagaと対比されるシーンが多く見受けられます。 redux-sagaでは、redux-thunkと比較して、どういったメリットがあるんでしょうか? redux-sagaを導入することのメリットとしては、主に以下のことが挙げられると思います。 redux-thunkはAction Createrに処理を記述する一方、redux-sagaはsagaコンポーネントに記述できるため、複雑化しにくい テストが書きやすい Async/Awaitなどの非同期処理のネストが深くならずに済む redux-sagaをreduxの中で使用する 基本的なredux-sagaの使い方が分かった所で、実際にreduxの中でredux-sagaを使用してみたいと思います。まずは、redux-sagaをインストールします。 npm install --save redux-saga タスクを作成 今回は、データをサーバーからフェッチしてきて、成功したら成功のアクションをDispatch、失敗したら失敗のアクションをDispatchする処理をSaga内で書いていきたいと思います。 Sagaで使用するアクションとして、データのリクエストを行う REQUEST_FETCH_DATA 、データ取得が成功した場合の SUCCESS_FETCH_DATA 、データ取得が失敗した場合の FAILURE_FETCH_DATA を定義します。 その次に、 src/sagas のディレクトリを作成し、配下に sample.js ファイルを作成します。そのファイルに以下のように記述します。 import { call , select , fork , take , takeEvery , put } from 'redux-saga/effects' import fetchData from '../apis' import { REQUEST_FETCH_DATA , SUCCESS_FETCH_DATA , FAILURE_FETCH_DATA } from '../actions/actionTypes' import { successFetchData , failureFetchData } from '../actions' function* runRequestFetchData () { const { text } = yield select (( state ) => state ) const { payload , error } = yield call ( fetchData , text ) if ( payload && ! error ) { yield put ( successFetchData ( payload )) } else { yield put ( failureFetchData ( error )) } } ここでは以下のような処理を行っています。 select でstoreにある text のstateを取得 このstateをfetchDataの引数に入れて、 call によって非同期処理 取得が成功していれば、 successFetchData アクションをDispatch 取得に失敗していれば、 failureFetchData アクションをDispatch 上記のように runRequestFetchData というタスクを用意しましたが、このタスクはどうやって実行されるのでしょうか? このタスクは REQUEST_FETCH_DATA アクションがDispatchされたタイミングで実行したいので、Actionを監視するように処理を記述しなければいけません。監視するには以下のようにタスクを追加します。 // 省略 function* handleRequestFetchData () { yield takeEvery ( REQUEST_FETCH_DATA , runRequestFetchData ) } これだけで、ActionがDispatchされたタイミングで、 runRequestFetchData を実行することができます。簡単ですね。 ※並列で非同期処理を走らせる場合 非同期処理を並列で実行して、データを取得したい場合は、以下のように書きます。 const { firstData , secondData } = yield all ([ call ( fetchData1 , text1 ) , call ( fetchData2 , text2 ) ]) call all を使用した並列処理の場合、 Promise.all と同様に、すべての処理がresolveされるまでか、どちらかがrejectされるまで、値を返しません。 redux-sagaでのテスト redux-sagaでは、テストの書きやすさをメリットとして挙げましたが、どうやってテストを書くのでしょうか? いくつかredux-sagaのテスト用ライブラリは存在しますが、今回は一番スター数の多い redux-saga-test-plan を使ったUnitテストを、先程のコードを交えて紹介します。 import { expextSaga } from 'redux-saga-test-plan' describe ( 'runRequestFetchData' , () => { test ( 'success' , () => { const successResponse = { payload : 'some data' } ; return expectSaga ( runRequestFetchData ) . provide ([ [ call . fn ( fetchData , 'text' ) , successResponse ] ]) . put ({ type : 'SUCCESS_FETCH_DATA' , payload : { payload : successResponse . payload } }) . run () ; }) ; test ( 'failure' , () => { const failureResponse = { error : 'エラー' } ; return expectSaga ( runRequestFetchData ) . provide ([ [ call . fn ( fetchData , 'text' ) , failureResponse ] ]) . put ({ type : 'FAILURE_FETCH_DATA' , error : failureResponse . error }) . run () }) }) 上記の例では、 runRequestFetchData タスクを実行したときに、 put で最終的に返却されるデータと一致しているかテストしています。 redux-saga-test-planを使ったテストでは、callで呼び出すfetch関数と、その関数を実行した場合の返却データをProviderを使用することで、予めセットしておくことができます。上記の例では、成功した場合と失敗した場合のリスポンスデータを、 successResponse 、 failureResponse にセットしています。 そのデータを元に、 FAILURE_FETCH_DATA アクションと FAILURE_FETCH_DATA アクションをDispatchしています。 storeの接続 それでは、最後にreduxとredux-sagaをつなぎこむ実装をしたいと思います。以下のようにすべてのSagaを集約するrootSagaファイルを作成し、rootSaga関数でタスクをまとめます。 // 省略 export default function* sampleSaga () { yield fork ( handleRequestFetchData ) } import { fork } from 'redux-saga/effects' import sampleSaga from './sampleSaga' export default function* rootSaga () { yield fork ( sampleSaga ) } rootSagaが定義されたので、このrootSagaをstoreに接続して、タスクを実行できるようにします。 import { createStore , applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' // sagaMiddlewareを作成 const sagaMiddleware = createSagaMiddleware () // ストアを作成 const store = createStore ( reducer , applyMiddleware ( sagaMiddleware , someMiddleware )) 以上で、Sagaの一連の実装は完成しました。とても処理の流れを追うのが簡単だと思います。 redux-sagaを使用すると、redux-thunkのコールバック地獄や、Reactライフサイクル内での非同期処理の煩雑性から開放されるので、是非使ってみてください。 最後に 今回の記事ではredux-sagaについて書きました。redux-sagaは特徴的な書き方なので、とっつきにくい部分はありますが、今回の記事で少しでもredux-sagaに詳しくなれたら幸いです。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています!興味を持ってくれた方は、ぜひ下記のエンジニア採用サイトをご覧ください。 スタメン エンジニア採用サイト フロントエンドエンジニア募集ページ
はじめに 20卒の新卒で入社したモバイルアプリチームのカーキです. スタメンでは昨今の 新型コロナウイルス の感染状況を鑑みて4月から原則リモートで業務を行なっています. リモートでの業務ではZOOMを使用し、 インターン 生も含めた全17名が同じ部屋でカメラをオン、マイクをミュートの状態で業務を行っています. 普段のzoomでの様子 今回はスタメンプロダクト部でのリモートワークの取り組みをご紹介しようと思います. プロダクト部の取り組み リモートでの勤務を行った当初、オフィスでの業務と比較して 雑談がなくなる ちょっとした質問がしづらい などのデメリットをプロダクト部全体として感じていました. どれもコミュニケーションに関することですが、この春入社したばかりの僕のような新人エンジニアにはコミュニケーション不足が生産高に関わってきます. そこでスタメンのプロダクト部ではコミュニケーション不足による、生産高の減少を防止するべく以下のような施策を行っています. おやつタイム オフィスにいるときは何気ない会話が息抜きになっていましたが、家で一人でいると雑談は生まれません.そこでお昼の15時には全員ミュートを外して15分で程度のおやつタイムを設けています. おやつタイムではみんなでお菓子を食べながら雑談をしています. 全員でお互いのバーチャル背景をいじりながら過ごすこともあれば、ラブマゲドン方式で話したいメンバーを指名してグループごとに Remo の部屋に分かれて雑談を行うこともあります(今のところ1組も成立していないのですが…) おやつタイムにRemoを使用した時の様子 定時におやつタイムがあることで、自宅での勤務にもメリハリが生まれていると感じます. チームごとに音声を常時通話 これはプロダクト部全体で行っていることではありませんが、アプリケーションチームでは Discord 、フロントエンドチームでは whereby を使用して常時会話ができるようにしています.常時音声をつないでおくことで、チーム内での質問や共有事項を迅速に行うことができています. プロダクト部内には僕も含め入社して間もないメンバーも多いため、いつでも気兼ねなく質問しやすい環境を音声通話の常時接続で実現しています. オンラインでの社内勉強会 プロダクト部内の勉強会もオンラインで開催しています. まだ今月の勉強会から始めたばかりで、反応が分かりづらいなど、改善点もありますがオンラインでも通常通り実施することができました. 社内オンライン勉強会の様子 誕生日会もオンラインで スタメンではメンバーの誕生日のお昼に誕生日会を開いているのですが、今回はオンラインで誕生日会を開催しました. 今回の誕生日会ではRemoを使用して、誕生日以外のメンバーが5人ずつのグループになって順番に誕生日のメンバーを祝いました. Remoのホワイトボード機能を使用し、それぞれのグループごとにイラストや写真・メッセージでデコレーションを行いました. グループごとにボートにも個性が出ていて面白かったです. 誕生日会のRemoでの様子 誕生日会のRemoでの様子② 最後に また会社での取り組みとしてスタメンには テレサ カバ としてオンライン飲み会を推奨する制度があります.リモートワークでもコミュニケーションを活性化するために実施されています. オンラインの飲み会なので東京・大阪支社のメンバーとも飲み会を楽しむことができ、普段以上に他支社のメンバーとの距離が近く感じています. このようにスタメンではリモートでの勤務が余儀なくされる中で、リモートだからできることを見つけ、施策として行っています. この状況がいつまで続くかは分かりませんが、リモートでの勤務も楽しみながらスタメンらしく実施していきたいと思います.
こんにちは、スタメンでディレクターをしている @nicka0001 です! 普段は主に TUNAG の改善施策の企画、設計やプロジェクトマネジメントを行なっています。 今回は、サービスを運用していくにあたっての戦略的な機能企画について書いていきます。 戦略の必要性 仕様やUIを決定することは正解がなく、とても難解で怖いものです。 シンプルに便利・不便/使いやすい・使いにくいではなかなか決められない色んな要素を孕んでいます。 だからこそ、常に考えに考えを重ねて最善を尽くしたいものでもあります。 そのためには、 戦略を立て、 ステークホルダー やプロジェクトメンバーに語り、仕様やUIを戦略をベースに議論し、リリースをしたら戦略をもとに評価を行い、最善を突き詰めていく。 そんな健全で活発な PDCAサイクル を回すために、戦略は必要であると考え、戦略の策定を行うために、UXデザインのフローを一部活用しながら企画を行なっています。 リーン開発を行う組織においても、MVP※1 を評価する上で重要な指標になるかと思います。 戦略を立てるための情報収集 事業戦略を聞き、プロダクトの機能案を立てるため、お客様の一番近くで業務を行なってくれている、カスタマーサクセス※2に ヒアリ ングします。 現状のニーズとそれを満たす為に行なっている作業(タスク) タスクの詳細( 5W1H ) そのタスクが必要な背景 等々 できる限り多岐に詳細に、 ヒアリ ングします。 主観や経験則でない、ユーザーについての事実を明らかにすることで、より効果的なア イデア を出すことができます。 何よりもこの情報は、意思決定や自分のア イデア を語るうえでとても頼りになります。 情報の分析とア イデア の発散 さて、ユーザーについての事実情報が集まったところで、要件に落とし込んでいきます。 ヒアリ ングを行ったタスクは想定フローに落とし込み 最低限、実用最低限機能(MVP)、画面遷移の基準として利用し、 ニーズや環境、背景はKA法※3で価値抽出し、 機能の中の一つ一つのUI案や仕様案とその情報優先度を割り出す為に利用します。 これで、画面遷移とコンセプトレベルのMVP案(UI、仕様案)がそれぞれの ヒアリ ング内容に結びついた形で策定できているかと思います。 これにより、画面遷移から、UI、仕様に至る一つ一つに戦略が結びついた形になり、それぞれが目的を達成できているかといった、詳細な評価を行うことが可能になります。 ユーザーの期待を超えるための仮説設定 MVP案が明らかになったところから、 再度ユーザーのニーズや背景、環境と再度向き合い、 仮説を立ててことでよりスムーズかつ、機能が与える価値ををより増幅させるア イデア を出していくことで、ユーザーの期待を超えたプロダクトの提供が可能になると考えています。 ユーザーの動きや環境などの事実情報からどれだけ、迷いや行動を先回りし仮説を立てられるかという能力は、ものづくりを行う人間に求められていくのではないかと思います。 まとめ プロダクトの細部の一つ一つについて、熱量を持って語り、戦略をチーム一丸で達成していきましょう。 スタメンでは、ユーザーに心から気に入ってもらえるプロダクトを目指し、最適な戦略を共に考え、達成していくプロダクト開発メンバーを募集しています!興味を持ってくれた方は下記の採用サイトをご覧ください! スタメン エンジニア採用サイト サーバーサイドエンジニア募集ページ フロントエンドエンジニア募集ページ インフラエンジニア募集ページ モバイルアプリエンジニア募集ページ 参考記事 ※1 MVP Wikipedia 実用最小限の製品 ※2 カスタマーサクセス ボクシルマガジン カスタマーサクセスとは | 顧客の成功体験づくりで注目、CSとの違い・定義・事例解説 ※3 KA法 UX.txt KA法 見出し画像 : Photo by JESHOOTS.COM on Unsplash
こんにちは。フロントエンドエンジニアの 渡邉 です。 スタメンで開発しているサービスの新機能は、React v16.8で追加された hooks 等を使って開発しています。 その中でも本記事ではuseEffectについて触れていこうと思います。 目次 hooksとは useEffectの第二引数の依存リストを正確に渡す 参考サイト hooksとは フック (hook) は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使えるようになります。 React 公式ドキュメント useEffect useEffectは第1引数にrender毎に実行する関数を。第2引数はrender時にuseEffect内の関数を実行するか判定する値を配列で渡します。 以下コードのように第2引数に何も渡さないとrender毎に実行されます。 const Counter = () => { const [ count, setCount ] = useState(0); const [ count2, setCount2 ] = useState(1); useEffect(() => { document .title = `You clicked $ { count } times`; } ); return ( <div> <span>You clicked { count } times</span> <button onClick= { () => setCount(count + 1) } >Click me</button> <span>You clicked { count2 } times</span> <button onClick= { () => setCount2(count2 + 1) } >count2 Click me</button> </div> ); } countが変わった時限定で実行したい場合は第2引数にcountを渡す。 これでcount以外のstateが更新されても、useEffectが実行されることはなくなります。 const Counter = () => { const [ count, setCount ] = useState(0); const [ count2, setCount2 ] = useState(1); useEffect(() => { document .title = `You clicked $ { count } times`; } , [ count ] ); // countが変わったら実行される return ( <div> <span>You clicked { count } times</span> <button onClick= { () => setCount(count + 1) } >Click me</button> <span>You clicked { count2 } times</span> <button onClick= { () => setCount2(count2 + 1) } >count2 Click me</button> </div> ); } // 初期 render const Counter = () => { // ... // 初期render useEffect(() => { document .title = `You clicked $ { 0 } times`; } , [ count ] ); // ... } // `<button onClick={() => setCount(count + 1)}>Click me</button>`がクリックされると、Counter関数が呼び出される const Counter = () => { // ... // 再renderでcountが変わっているのでuseEffect内の関数が実行される useEffect(() => { document .title = `You clicked $ { 1 } times`; } , [ count ] ); // ... } // `<button onClick={() => setCount2(count2 + 1)}>count2 Click me</button>`がクリックされると、再度Counter関数が呼び出される const Counter = () => { // ... // count2が変わったが、countは変わっていないのでuseEffect内の関数は実行されない useEffect(() => { document .title = `You clicked $ { 1 } times`; } , [ count ] ); // .. } useEffectの第2引数の依存リストを正確に渡す 第2引数に空配列を渡しているパターンは要注意です。 公式ドキュメント引用 関数を依存のリストから安全に省略できるのは、その関数(あるいはその関数から呼ばれる関数)が props、stateないしそれらから派生する値のいずれも含んでいない場合のみです。 例えば、以下コードにはバグがあります。 const dataPage = ( { dataId } ) => { const [ data, setData ] = useState( null ); const getUrl = () => { return `http: //api/data/${dataId}` } const fetchData = async () => { const url = getUrl() const response = await fetch(url); const json = await response.json(); setData(json); } const testFunc = () => { // 処理 } const testFunc2 = () => { // 処理2 } const testFunc3 = () => { // 処理3 } useEffect(() => { fetchData(); } , [] ); // ... } 上のコードの場合 dataId に変更があった場合でも fetchData は実行されず、古い data が表示され続けてしまう。 このように、propsやstateを参照している関数がuseEffectの外で作成されると、propsとstateが実際に扱われているかを把握するには、アプリケーションが肥大化するに連れ難しくなり、バグを生みやすくしてしまう。 const dataPage = ( { dataId } ) => { const [ data, setData ] = useState( null ); const getUrl = () => { return `http: //api/data/${dataId}` // propsで渡ってきたdataIdを使用している } const fetchData = async () => { const url = getUrl() const response = await fetch(url); const json = await response.json(); setData(json); } const testFunc = () => { // 処理 } const testFunc2 = () => { // 処理2 } const testFunc3 = () => { // 処理3 } useEffect(() => { fetchData(); // 実行 } , [] ); // 空配列を渡しているため、dataIdが変更されても古いdataのまま表示されてしまう // ... } なので、依存リストに正確に渡す方法としての基本useEffect内で実行する関数はuseEffect内で宣言するのと、useEffect内で使われている値はすべて記述することです。 なので実際に先程のコードを修正します。 const dataPage = ( { dataId } ) => { const [ data, setData ] = useState( null ); const testFunc = () => { // 処理 } const testFunc2 = () => { // 処理2 } const testFunc3 = () => { // 処理3 } useEffect(() => { const getUrl = () => { return `http: //api/data/${dataId}` // propsで渡ってきたdataIdを使用している } const fetchData = async () => { const url = getUrl() const response = await fetch(url); const json = await response.json(); setData(json); } fetchData(); // 実行 } , [ dataId ] ); // ... } これによりuseEffect内でしか使われていないので、仮に dataId を使わなくなったとしても気づきやすくなります。 useEffect内に関数を移動できない場合 その場合の選択肢として、 コンポーネント の外部にその関数を移動できないかを考える必要があります。 例えば、 testFunc を外部に移動することによりpropsもstateも参照していないことが保証されるため、依存リストに含めなくても良くなるので、空配列を渡しても安全です。 const testFunc = () => { console.log( 'test' ) } const dataPage = ( { dataId } ) => { const [ data, setData ] = useState( null ); // ... useEffect(() => { testFunc() } , [] ); // ... } propsやstateを参照していて外部に移動できない場合の解決策として useCallback を使用する方法があります。 例えば、親の コンポーネント が、自身のstateを変更させるアクションを子 コンポーネント に渡して、子 コンポーネント で発火させる!などなど。 useCallbackを使う useCallbackとは... 関数自体の依存が変わらない限り関数も変化しないことを保証できる(メモ化されたコールバックを返す) 以下のようにParent コンポーネント で自身のpropsを参照している関数を コンポーネント に渡すして実行する場合 const Parent = ( { id } ) => { const [ name, setName ] = useState( '' ); const hogeFunc = () => { console.log(id) } retrun ( //... <Child hogeFunc= { hogeFunc } /> //... ) } const Child = ( { hogeFunc } ) => { useEffect(() => { hogeFunc() } , [ hogeFunc ] ) //... } render内でアロー関数を利用すると常に新規の関数オブジェクトを作成してしまいます。 そのため、その関数を子 コンポーネント に渡した場合render毎に前回渡ってきた関数と違う関数として扱われてしまいます。 なので上のようなコードだとstateの name が更新されて、Parent コンポーネント が再描画されるたびに、Childに渡る hogeFunc は新規の関数として渡してしまうので、useEffect内は毎回実行されてしまう。 そのようなときにuseCallbackの出番です。 useCallbackは「意味的に同じ関数」が返るかどうかを判別して、同じ値を返す関数が返るべきなら新規のアロー式を捨てて、前に渡した同じ結果を返す関数です。 const memoizedCallback = useCallback(() => { doSomething(a, b); } , [ a, b ] ) // useCallbackの第2引数もuseEffectの第2引数と同じ考え方; 実際にuseCallbackを使って先程のコードを修正します。 const Parent = ( { id } ) => { const [ name, setName ] = useState( '' ); // hogeFuncはidに変更が合った場合のみ新規関数として作られる。 const hogeFunc = useCallback( () => { console.log(id) } , [ id ] ) retrun ( //... <Child hogeFunc= { hogeFunc } /> //... ) } const Child = ( { hogeFunc } ) => { useEffect(() => { hogeFunc() } , [ hogeFunc ] ) // Parentコンポーネントのidが変わった場合のみhogeFuncが新規関数として作成されるので、再描画の度に実行されることはなくなる //... } useCallbackを使い常に新規の関数オブジェクトを作成しなくなることにより利点が他にもあります。 Child コンポーネント がReact.memo()されている場合です。 React.memo()はpropsの変更をチェックして、同じ結果の場合は再描画をスキップして、変更があった場合のみ再描画をするので、無駄なレンダーを減らすことができます。 const Parent = ( { id } ) => { const [ name, setName ] = useState( '' ); const hogeFunc = useCallback(() => { console.log(id) } , [ id ] ) return ( //... <Child hogeFunc= { hogeFunc } /> //... ) } const Child = React.memo(( { hogeFunc } ) => { // hogeFuncに変更がない限りChildコンポーネントは再描画されない。 useEffect(() => { hogeFunc() } , [ hogeFunc ] ) //... } ) React.memo()の詳細は今回省くので 公式ドキュメント にてお願いします。 参考サイト useEffect完全ガイド これを読むとuseEffectマスターになれると思います。 少し長いですが、完全に理解したい方にはおすすめです。 まとめ 最近はhooksのお手軽さからFunctionalComponentで書くことが多いですが、きちんと理解した上で書いていきたいと記事をまとめている時に改めて感じました。 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。