TECH PLAY

株式会社スタメン

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

231

こんにちは。スタメンでエンジニアリングマネージャーをしている @temoki です。 私がスタメンに入社した2年前、プロダクト部のエンジニアは10人くらいでしたが、現在はその倍以上のメンバーとなりました。 その中にはエンジニアだけではなく、ディレクター、デザイナーなど、職種も多様になってきています。 そして提供するサービスの規模もどんどん拡大しており、新規事業も立ち上がるなど、プロダクト部でのプロジェクトの難易度がどんどん上がってきているのを感じます。 こうなると、プロダクト部のメンバー全員がプロジェクトマネジメントを意識して振る舞えるかどうかがプロジェクト成功の重要なポイントなってきます。 私はエンジニアとして入社していますが、それ以前のキャリアとしては受託開発でのプロジェクトマネージャーをしておりましたので、社内でプロジェクトマネジメントに入門するための勉強会を開催しました。 今回から数回に分けて、その勉強会で話した内容について書こうと思います。 まず初回は、プロジェクトマネジメントに入門する前に知っておいてほしいことがテーマです。 プロジェクトとは? プロジェクトマネジメントは当たり前ですが、プロジェクトのマネジメントのことを指します。 プロジェクトマネジメントについて学ぶ前に、それぞれの言葉の意味について考えてみることから始めたいと思います。 まずは プロジェクト です。仕事を進める上でこのプロジェクトという言葉をよく使うと思いますが、そもそもプロジェクトとは何なのでしょうか? みなさん自信を持って人に説明することはできますか? プロジェクトの定義 プロジェクトの定義はたくさんあると思いますが、そのうちの1つがこちらです。 独自のプロダクト、サービス、所産を創造するために実施する有期性のある業務 これは、ワールドワイドでプロジェクトマネジメントの標準策定などを行っているプロジェクトマネジメント協会 *1 が発行する PMBOK *2 による定義です。 プロジェクトの3大要素 私はこのPMBOKの定義はやや難しいなと感じています。そこで、この定義を噛み砕いて次の3つ要素を抽出してみました。 達成すべき 独自の目標 がある 期間 が決まっている 集団 で活動する PMBOKの定義の前半「独自のプロダクト、サービス、所産を創造するため」というのを単純に 独自の目標 と表現し、これは達成すべきものであるとしました。 次はあまり聞き慣れない「有期性」ですが(「ゆうきせい」と入力して変換しても出てきませんね)、これは 期間が決まっている という意味です。 最後に残った「業務」は、職業や事業などに関して日々継続して行う活動のことです。一般的に職業や事業に関する仕事は、複数人で行うことがほとんどであるのと、プロジェクトマネジメントにおいて複数人で行うことを前提とした取り組みが重要なポイントになるため、あえて 集団で活動する としてみました。 この抽出した要素でプロジェクトを定義してみます。 独自の目標を達成するために、決まった期間の中で、集団で活動すること いかがでしょうか。プロジェクトという言葉のもやもやが晴れてきたのではないかと思います。 マネジメントとは? プロジェクトの次はマネジメント (management) です。 早速ですが management の動詞である manage をGoogle翻訳で調べてみましょう。 manage = 管理する manage by Google翻訳 これがGoogleの教えてくれる現時点での結果です。当たり前すぎて調べるまでもないと思われたかもしれません。 しかし、これは実は違います。以下が Weblio の結果です。 manage = どうにかしてする、うまくする manage by Weblio ちょっとびっくりする内容ではないでしょうか。 日本人の多くが manage を 管理する と理解していることで、機械学習ベースのGoogleの翻訳もそう理解してしまっているのかもしれません。 Weblioにはさらにこのような記述があります。 「(馬)を手なずける[調教する]」が原義.時に 困難な状況で「なんとか対処する」こと を表す 「管理」はスマートさや、堅苦しさを感じる言葉なので、自然とマネジメントという言葉にも同じ印象を受けます。 しかしながら、実際には 困難な状況でなんとか対処する というような、とても泥臭い印象の意味をもっているのです。 まとめ プロジェクトとは 独自の目標を達成するために、決まった期間の中で、集団で活動すること です。 プロジェクトというのはとても困難であることが多いと思いますが、これを なんとか対処して 目標を達成することに導くのがプロジェクトマネジメントなのです。 今回は プロジェクトマネジメント入門以前 というテーマで、プロジェクトマネジメントとは何なのかをお伝えしました。 おそらくこれまでの認識に変化があったのではないかと思います。次回は プロジェクトマネジメントちょっとだけ入門 というテーマで、プロジェクトを なんとか対処する 部分について書きたいと思います。 最後になりましたが、スタメンでは自社プロダクトの開発プロジェクトを一緒になんとかしてくれる仲間を募集しています。興味を持ってくれた方は、ぜひ下記の採用サイトをご覧ください。 スタメン エンジニア採用サイト デザイナー募集ページ サーバーサイドエンジニア募集ページ フロントエンドエンジニア募集ページ インフラエンジニア募集ページ モバイルアプリエンジニア募集ページ *1 : Project Management Institute *2 : A Guide to the Project Management Body of Knowledge / プロジェクトマネジメント知識体系ガイド
アバター
こんにちは。フロントエンドエンジニアの 渡邉 です。 普段ReactとTypeScriptを書いています。 目次 Lighthouseとは Lighthouseを導入しようとした経緯 使ってみる 最後に Lighthouseとは Lighthouse is an open-source, automated tool for improving the performance, quality, and correctness of your web apps. When auditing a page, Lighthouse runs a barrage of tests against the page, and then generates a report on how well the page did. From here you can use the failing tests as indicators on what you can do to improve your app. 翻訳 Lighthouseは、Webアプリのパフォーマンス、品質、正確性を向上させるためのオープンソースの自動化ツールです。 Lighthouseは、ページを監査するときに、ページに対して大量のテストを実行し、ページのパフォーマンスに関するレポートを生成します。ここから、失敗したテストを、アプリを改善するために何ができるかの指標として使用できます。 要するに、Webサイトのパフォーマンスや品質を計測するツールです。 Performance, Progressive Web App(PWA), Accessibility, Best Practices, SEOの5つの項目からそれぞれ100点が満点として採点されたレポートを生成することができるうえに、具体的な改善案もだしてくれます。 Lighthouseを導入しようとした経緯 Lighthouseを導入しようとした経緯として、以下のような課題がありました。 継続的にパフォーマンス監視ができていない パフォーマンスチューニングに関する知識があまりない そもそもどのコミットでパフォーマンスが悪化したかを知りたい これらの課題を解決してくれるのがLighthouseでした。 Lighthouse CI を使えばコミット単位でパフォーマンスを監視することが可能なので、継続的に監視することができるのと、悪化したタイミングも知ることができます。 さらにパフォーマンスチューニングに関する知識があまりなくても、具体的な改善案を示してくれるので、改善自体のハードルを下げてくれます。 なので、Lighthouseを導入してみようと決めました。 使ってみる まず、チュートリアル用のリポジトリを用意し、ローカルでReactのアプリケーションを作成します。 アプリケーション作成 npx create-react-app lighthouse-ci-pra cd lighthouse-ci-pra Reactアプリケーションを反映 git remote add origin https://github.com/[NAME]/lighthouse-ci-pra.git git push -u origin master ここからは、Lighthouse CIのGetting Startedに従いながらやっていきます。 Lighthouse CIを構成 リポジトリのルートに lighthouserc.js を作成します。 ここにLighthouse CIのオプションを記載します。 module.exports = { ci: { upload: { target: 'temporary-public-storage', }, }, }; より高度な設定をしたい方は ドキュメント を読んでみてください。 一つ紹介すると、下記のように設定することで、パフォーマンススコアが60点を下回った場合、エラーを出してくれます。 module.exports = { ci: { // ... assert: { assertions: { "categories:performance": ["error", {"minScore": 0.6}], }, }, }, } CIプロバイダーの構成 今回はGithubActionsを使ってやりますが、Circle CIなど、他のCIにも対応しています。 ここも同じくルートに .github/workflows/ ディレクトリを作成します。 そこに以下のコードを記載した lighthouse-ci.yml を作成します。 name: CI on: [push] jobs: lhci: name: Lighthouse runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js 10.x uses: actions/setup-node@v1 with: node-version: 10.x - name: npm install, build run: | npm install npm run build - name: run Lighthouse CI run: | npm install -g @lhci/cli@0.4.x lhci autorun これだけで基本的な環境作りは終わりです。 後は実行するだけなので、pushします。 pushしたあとにGithubのリポジトリのActionsタブを見ると以下の画像のようにworkflow一覧の画面がでてくるので、詳細を見るために先程pushしたcommitをクリックします。 次に、CIの項目のLighthouseを選択します。 そうすると以下の画像のようにciが実際に動いているのがわかります。 成功した場合、28行目あたりにOpen the report at https://storage.googleapis.com/.... というURLがあるので飛びます。 そこに計測結果が表示されています。 今回は create-react-app でReactアプリケーションが作りたてのスコアなので高得点です。(Accessibilityがだけが94点なのが気になります) これで、コミット単位でパフォーマンスを計測できるようになったので、悪化したタイミングでLighthouseに従いながらチューニングしていくことができるようになりました。 最後に 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
アバター
スタメンでエンジニアをしている 田中 です。 今回は決済プラットフォームであるStripeのサブスクリプションを扱う際に遭遇した問題について、発生した事象とその原因、および対策方法についてご紹介します。 なお、本記事ではStripeのサブスクリプションについての詳細は説明いたしません。また、対策方法についてはRubyのコードで記載します。RubyでStripeのサブスクリプションを扱う場合については、以下の記事にて紹介しているのでよろしければご参照ください。 【Ruby on Rails】Stripeのサブスクリプションで試したことをまとめてみた 前提 本記事で扱うサブスクリプションは請求期間が月次のものです サブスクリプションの支払い日について、通常、翌月に同じ日が存在しない場合は自動的にその前の日を指定してくれます 例 5/31 → 6/30 8/31 → 9/30 参考 https://stripe.com/docs/billing/subscriptions/billing-cycle 発生した事象 以下の画像のように同じ日付でサブスクリプションを開始しましたが、2回目の支払いのタイミングがズレてしまうということがありました。そのため、ともに5月31日開始のサブスクリプションですが、前者については現在の期間の開始日が1日ズレてしまっています。 2回目の支払いが7/1になっているケース 2回目の支払いが6/30になっているケース そのため、例えば支払い成功時のWebhookにて何かしらの処理をする場合に、このズレによって影響が発生する可能性が大いに考えられます。 発生原因 Stripeのサポートに問い合わせたところ、「 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)午前9時より前にサブスクリプションを作成した場合 ・日本時間「2020-05-31 08:00:00」にbilling_cycle_anchorを指定 支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC) 1回目 2020-05-31 08:00:00 2020-05-30 23:00:00 2回目 2020-07-01 08:00:00 2020-06-30 23:00:00 3回目 2020-07-31 08:00:00 2020-07-30 23:00:00 4回目 2020-08-31 08:00:00 2020-08-30 23:00:00 (2)午前9時以降にサブスクリプションを作成した場合 ・日本時間「2020-05-31 10:00:00」にbilling_cycle_anchorを指定 支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC) 1回目 2020-05-31 10:00:00 2020-05-31 01:00:00 2回目 2020-06-30 10:00:00 2020-06-30 01:00:00 3回目 2020-07-31 10:00:00 2020-07-31 01:00:00 4回目 2020-08-31 10:00:00 2020-08-31 01:00:00 どちらに関してもUTC基準だと翌月の支払いは6/30となっていますが、JSTに変換されると支払日に1日のズレが生じていることが分かります。これが今回発生した問題でした。 上記のことから、日本時間において以下の日時にサブスクリプションが作成されると今回の問題が発生すると考えられます。 毎月29, 30, 31日 午前0時から午前9時の間 たとえば、以下のようなケースです。 12/29 午前2時にサブスクリプションを作成 2月の支払い予定日は本来であれば2/28だが、3/1となる 12/30 午前2時にサブスクリプションを作成 2月の支払い予定日は本来であれば2/28だが、3/1となる 12/31 午前2時にサブスクリプションを作成 2月の支払い予定日は本来であれば2/28だが、3/1となる 4月の支払い予定日は本来であれば4/30だが、5/1となる 以降、31日がない月は1日のズレが発生する 対策方法 方針 今回の問題を解消するための方針として、以下の2つがあります。 特定日時(毎月29, 30, 31日の0時から9時の間)でサブスクリプションを作成できないようにする 特定日時でサブスクリプションを作成した場合、次回以降の支払い日時をずらす 前者に関しては、特定日時ではサブスクリプション契約させないという方法なので、あまり現実的な方法ではありません。そこで、後者に関して説明します。(Stripeのサポートの方にオススメいただいた方法です) なお、設定にてUTC基準からJST基準に変更出来ないかと問い合わせをしましたが、そのような方法は存在しないため、現状は下記の方法で対応するしかなさそうです。 方法 次回以降の支払い日時を変更する方法として trial_end を使用します。 trial_end は本来であればトライアル期間を設定するために使用するパラメータですが、支払い日時を変更する用途でも使用できます。 参考: https://stripe.com/docs/api/subscriptions/update 今回は即時で初回決済する場合とトライアル期間を経て決済する場合の2種類について説明します。 即時決済 サブスクリプションを作成する( Stripe::Subscription.create ) サブスクリプションの開始日時を取得する(StripeのSubscription or Invoiceから取得する) 2で取得した日時が特定日時に該当する場合、サブスクリプションを以下のように更新する # 次回の請求書作成日時 + n時間 = 特定日時を避けた時刻 next_payment_date = period_end + diff_hour.hours # proration_behavior: 'none'で日割り計算を無効にする Stripe :: Subscription .update( ' 該当するサブスクリプションID ' , { trial_end : next_payment_date.to_i, proration_behavior : ' none ' }) 注意点(即時決済のみ) trial_end は本来トライアルを設定するために使用されるので、Stripeのダッシュボードのサブスクリプションのステータスが「トライアル」になります。 trial_end によるアップデートで、以下のイベントが発生します。ステータスがトライアルに変わり、そのタイミングで0円の請求書が作成されるためです。 invoice.finalized invoice.paid invoice.payment_succeeded トライアル トライアルオプション付きでサブスクリプションを作成する( Stripe::Subscription.create ) トライアルオプションで指定した日付が特定日時に該当する場合、即時決済と同様の方法で、サブスクリプションをアップデートする おわりに 今回はStripeのサブスクリプションを扱う際に遭遇した問題についてご紹介しました。この問題を発見できたのは偶然で、発生条件もかなり限られており、最初は何が原因か分かりませんでしたが、Stripeのサポートの方に助けられつつ原因の特定と対応することが出来ました。Stripeをシステムに組み込む際の参考になればと思います。 ※ 追記 本記事では月末について取り扱いましたが、月初にも類似した問題が発生したため別記事にまとめました。ぜひご参照ください。 tech.stmn.co.jp 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
アバター
目次 はじめに 「Incoming Webhook」を使用したMicrosoft Teams (以下、Teams) 側の設定 Railsアプリケーションの実装 おわりに はじめに こんにちは、スタメンでエンジニアをしている ワカゾノ です。 TUNAG では、タイムラインへの投稿が作成される際に、Teamsへ同様の内容のメッセージカードが作成、投稿される「Teams連携機能」を実装しています。 Teamsとは Teamsは、Microsoft社が提供しているビジネスユーザー向けのグループウェアです。 Office製品との連携をスムーズに行うことが可能で、コロナ禍におけるリモートワーク需要の増加に伴い、更にシェアが拡大傾向にあります。 そのため、TUNAGでもTeamsとの連携機能を実装することとなりました。 Teamsでは、例えばプロジェクト毎に「チーム」を作成し、その中で「チャネル」を作成することでチャット機能を利用することが出来ます。 また、Teamsには「Incoming Webhook」という機能があり、Microsoftのドキュメントには下記のように記載されています。 適切に書式設定された JSON に対応し、そのチャネルにメッセージを挿入する HTTPS エンドポイントを公開する このエンドポイントに対してHTTPリクエストをPOSTすることで「チャネル」にメッセージを送信することが可能です。 今回はこの「Incoming Webhook」を利用し、連携機能を実装しましたので、そちらについて説明していこうと思います。 「Incoming Webhook」を使用したTeams側の設定 事前準備 ①Microsoftアカウントを作成します。 ② こちら からTeams無料版にサインアップし、Teamsアプリをダウンロードします。 ③ その後、連携機能を実装したい「チャネル」を作成しておきます。 Incoming Webhookを使用したTeams側での設定 ①「アプリ」を選択します。 ②「Incoming Webhook」と検索、そちらをクリックした後、「チームに追加」をクリックします。 ③連携したい先のチャネル名を選択し、「コネクタを追加」をクリックします。 ④必要に応じて名前と画像を選択、「作成」をクリックします。 ⑤表示されたURLをコピー、アプリケーション実装時に必要となる為保管しておきます。 以上でTeams側の準備は完了です。 Railsアプリケーションの実装 先ほどコピーしたURLに対して、HTTPリクエストをPOSTします。 下記の記事を参考に実装しました。 https://docs.microsoft.com/ja-jp/outlook/actionable-messages/send-via-connectors https://docs.microsoft.com/ja-jp/outlook/actionable-messages/message-card-reference 記事を参照すると分かるように、送信するフィールド項目の種類をカスタマイズすることで、ある程度デザインを選択することが出来ます。(組み合わせにより必須のフィード項目が存在するため注意が必要です。) 今回はPOSTするデータを下記のような構造にしました。 それぞれの値はメソッド内で、TUNAGのタイムラインへ投稿が作成される際にデータを取得しています。 # 先ほどコピーしたURL WEBHOOK_URL = https :/ /outlook.office.com/webhook・・・・・ def request_body { " @type " : TYPE , " @context " : CONTEXT , " themeColor " : COLOR , " summary " : summary, " sections " : [{ " activityTitle " : activity_title, " activitySubtitle " : activity_subtitle, " activityImage " : icon, " title " : title, " text " : text, " markdown " : true , " images " : images }] } end # HTTPリクエストでPOSTする def post_message uri = URI .parse( WEBHOOK_URL ) request = Net :: HTTP :: Post .new(uri.request_uri, { ' Content-Type ' => ' application/json ' }) request.body = request_body http = Net :: HTTP .new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL :: SSL :: VERIFY_NONE response = http.start { | h | h.request(request) } end private def activity_title # 投稿に紐付いた内容を取得 end ・ ・ ・ 以上のような実装を行うことで画像のようにTeamsへ投稿されます。 まとめ 「Incoming Webhook」を使用することで、スムーズにTeamsとの連携機能を実装することが出来ました。 自社のプロダクトに同様の機能を検討中の場合は、是非参考にして頂ければと思います。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
アバター
はじめに こんにちは、スタメンでエンジニアをしているミツモトです。 スタメンでは、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を利用すれば 工数 をかけず、自分の思い描くサイトを作ることができます。また、テンプレートの種類がたくさんあることで、イベントサイトのデザインを考える時の参考になりました。興味がある方はぜひお試しください。 スタメンでは一緒に開発してくれるエンジニアを募集しています!興味を持ってくれた方は、ぜひ エンジニア採用サイト をご覧ください。
アバター