TECH PLAY

株式会社スタメン

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

247

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