TECH PLAY

株式会社スタメン

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

231

スタメンエンジニアの井本です。 漏洩チェッカーのWebアプリケーションをフルスタックに領域問わず開発しています。得意な領域はインフラ含むバックエンドです。 本記事では、先月2022年12月にリリースしたサービス、 漏洩チェッカー について、プロダクトと開発体制の紹介をします。 漏洩チェッカーについて 技術について 技術スタック アーキテクチャ 組織について 組織体制 開発体制 まとめ 漏洩チェッカーについて 漏洩チェッカーは情報漏洩を未然に防ぐセキュリティ管理サービスです。 企業にとってセキュリティ事故の影響は甚大であり、近年、情報の漏洩事故の件数は年々、増加の傾向にあります。 情報資産に関するセキュリティ事故には、外部からのサイバー攻撃のほか、従業員などにより会社の内部で引き起こされる、不正行為や悪意のない不注意などによる情報漏洩があります。 漏洩チェッカーは後者、内部で引き起こされる情報漏洩を予防・検知するためのサービスです。 IT部門が存在しない企業においては、IT資産のリスク管理を始めるにしても、どこから始めれば良いのかが分からなかったり、またIT資産に優先順位をつけて部分的に管理を始めようとしても、世の中のほとんどのセキュリティツールは重厚なオールインワン形式のものしかなかったりと、費用面や工数面においても小さく始めることが難しい状況にあります。 漏洩チェッカーはモジュールと呼ばれる機能単位で構成されたサービスで、フォルダー操作の監視やUSBの脱着の検知などといった機能をユーザーが自由に組み合わせて利用することができます。 モジュールというコンセプトにより各企業の事情に合わせて、機能・コストを最適化したリスク管理を実施できます。 またクラウド型のサービスなので、保守・管理の負担を抑えられることも特徴です。 セキュリティ向上を目的としたツールの多くが、セキュリティやITのリテラシーが高いユーザーを前提としており、リスク管理の障壁となっています。 そこで漏洩チェッカーは、セキュリティやITを得意としていないユーザーでも簡単に利用できる、シンプルさを追求したサービスとして開発されています。 管理画面は、自社のセキュリティ管理状況を直感的に理解できるUIにデザインされています。 また監視対象の端末にインストールするエージェントアプリケーションは、煩雑さのない簡単なセットアップで動作を開始するようになっています。 技術について 技術スタック 漏洩チェッカーの技術スタックは下図の通りです。 漏洩チェッカーは大きく2つのアプリケーションで構成されています。 一つは監視対象のPCにインストールして情報を吸い上げるWindowsデスクトップアプリケーション、もう一つは吸い上げた情報を集約・閲覧するWebアプリケーションです。 ログ収集アプリケーション ログ収集用のWindowsアプリケーションは、.NET 6 による WPF(Windows Presentation Foundation) として実装され、MVVM フレームワークとして Prism を利用しています。 モジュールによって機能を組み合わせられる漏洩チェッカーのWindowsアプリケーションでは、Windows の各種情報にアクセスする必要があるため、新旧様々な API を利用することが特徴です。例えば USBドライブの接続状況については WMI(Windows Management Infrastructure) を利用し、Windows ごみ箱 の容量取得にはWin32 API の SHEmptyRecycleBin を利用しています。取得されたログは Entity Framework によってローカルPCに保存され、ネットワークがオンラインのタイミングでサーバーに送信されます。 また、機能追加が頻繁に行われることを想定し、 MSIX でパッケージングし Microsoft Store で配布することで、アプリケーションの簡易インストールと自動更新 (サイレントアップデート)を実現しています。 ログ集約・閲覧アプリケーション ログの保存・閲覧用のWebアプリケーションの開発には、バックエンドフレームワークとしてNest.js、フロントエンドフレームワークとしてNext.jsを使用しています。SPA構成となっており、フロントエンドとバックエンドの間のインターフェースにはGraphQLを採用しました。 クラウドはAWSをメインとし、一部基盤としてGCPやその他SaaSを利用しています。AWSの開発・管理にはAWS CDK in TypeScriptを使用しています。 漏洩チェッカー開発チームは、ユーザーに最速で価値を届けるために、職能や言語にとらわれない開発体制を目指しています。 そこで、言語の違いによるハードルをなくすため、フロントエンドからインフラまでフルTypeScriptで開発できるようにしました。 アーキテクチャ 漏洩チェッカーのアーキテクチャの概要は以下になります。 アーキテクチャは構成アプリケーションと同様に、大きく2つに分けることができます。 一つは、顧客企業の社員のPCの状態を監視し、操作ログなどの情報を吸い上げ、それを弊社ストレージに蓄積するログ収集アーキテクチャです。 もう一つは、蓄積したログ情報を管理者が閲覧・検索する管理画面アーキテクチャです。 ログ収集アーキテクチャ 顧客企業の社員のPC上では監視エージェントとしてWindowsデスクトップアプリケーションが稼働しており、ユーザーの操作やスケジューラをトリガーとして情報を収集します 収集データは一定量ごとにAWS上に構築されたログ収集エンドポイントに送信されます。 受信したログはAWS Lambda経由で一度Amazon SQSにキューイングされ、さらに後段に位置するログ処理サーバーが、Amazon SQSに対してポーリングすることで順に処理されます。 ログ処理サーバーはAmazon ECS(Fargate)にコンテナとしてデプロイされており、ログ種別ごとの仕分けの他、監視対象ユーザーの不審な行動ログを検知してアラートを発報します。 キューイングにより、ログ受信とログ処理を非同期に実行することで、監視対象のPCの負荷の上昇や処理サーバーの負荷変動を抑制しています。 ログはAmazon S3に蓄積され、監視対象のPCのオンライン状態やアラートなどの情報はAmazon RDSに保存されます。 管理画面アーキテクチャ 主に顧客企業の管理者が利用する管理画面アプリケーションのためのアーキテクチャです。 こちらは先程のログ収集アーキテクチャと比較して、シンプルな構成です。 Webアプリケーション自体は前述の通り、SPA構成となっており、フロントエンドはNext.jsと相性の良いVercelに、バックエンドはAmazon ECS上にデプロイされています。 Amazon S3上のログデータはAWS Glueを用いてテーブル定義されており、WebアプリケーションからはAmazon Athenaを経由することで検索・取得が行われます。 また、漏洩チェッカーはマルチテナント方式のサービスです。 マルチテナントは同じDBやサーバーを複数のユーザー(企業など)が共用する方式を指し、サービステナント同士のデータ分離が必須です。 データベースにPostgreSQL互換のAmazon Auroraを採用し、PostgreSQLの RLS(Row Level Security) を活用することでテナント分離を行っています。 アプリケーションレイヤーではなく、より低レイヤーの機能によってテナント分離を実現することで、テナントIDによる絞り込みのwhere条件を書き忘れる、といった情報の漏洩リスクを低減することができます。 その他 認証機能や決済機能など、プロダクトのコアバリューではないが、事業上クリティカルなシステムについては、SaaSを利用することで、開発・運用コストの低減および品質の向上に努めています。 またCI/CDパイプラインとして、CircleCIにおけるテスト実行後、AWS上でコンテナビルド〜各種ECSタスクの更新といった一連の処理を自動化しています。 組織について 組織体制 次のような体制で開発しています。 管理画面などWebアプリケーション全般の開発 2名 Windowsアプリケーションの開発 1名 デザイナー 1名 Webアプリケーションの開発担当者は、フルスタックに開発することでフロントエンドやバックエンドといった職能に左右されず、事業上の優先度に従って次の開発アイテムを選ぶことが可能です。 またマーケ・営業担当のメンバー1名を含めた事業推進チーム全体としても5名の少数精鋭であり、ビジネスサイドのメンバーと密にコミュニケーションを取りながら事業を進めています。 開発体制 1チームのスクラム体制で開発をしています。 日々の進捗共有の場であるデイリースクラムには、ビジネスサイドのメンバーも参加しており日々、共通のKPIと現在値を確認しています。 売り上げやユーザー数などを共通の目標値として追うことで、同じ危機感を持ち、同じ達成の喜びを共有することができます。 顧客の声を直に受けているメンバーから常にフィードバックを受けることにもなり、「本当に顧客にとって価値に繋がるのか」常に向き合い続けています。 漏洩チェッカー事業に関わることで得られる、少数精鋭チームゆえの貴重な経験の一つになっています。 まとめ 漏洩チェッカーは、管理対象のPCにインストールされた.NET製のWindowsアプリケーションによって、ログ情報を収集しています。 収集したログはAWS上のストレージに集約され、SPA構成のWebアプリケーションで閲覧が可能です。 このWebアプリケーションは、ユーザーへの価値提供のスピード向上のために、フルTypeScript&スクラム体制で開発を進めています。 弊社スタメンでは、今後さらに加速してユーザーに価値提供するために、共に漏洩チェッカーの開発に携わる仲間を募集しています。 漏洩チェッカーの技術スタックや開発体制に興味を持っていただけましたら、ぜひ下記のページから採用エントリーいただけると幸いです。 Webエンジニア(Forkwell) Windowsエンジニア(Forkwell) Windowsエンジニア(Wantedly)
アバター
2023年1月11日(水)〜1月13日(金)に開催された、スクラムの国内最大のイベントであるRegional Scrum Gathering Tokyo 2023(以下: RSGT)にスポンサーとして参加してきたのでその報告です。 2023.scrumgatheringtokyo.org 今回はCTO+エンジニア兼スクラムマスター×3人の4人で参加してきました。 会場の雰囲気 会場に入るとスポンサーブースが出展しているエリアがあるのですが、ブースの周りやセッション会場の外など、会場の至るところで話し合いをしているのが印象的でした。 初参加だと入場パスに"First Timer"というシールを付けられるので、そこから「初めてなんですね〜」と会話が広がることも多いですし、セッション中でも「〇〇について周りの人と話し合ってみましょう」という機会があり、初参加で知り合いが少なくても話しやすい環境になっていると思います。 あと、ランチのすき焼き弁当が大人気ですぐになくなります。 スポンサー活動報告 スポンサーセッション枠にて、CTO松谷が『スタメンのLeSSの導入と 事業部全体を巻き込んだ アウトカム文化への道のり』というタイトルで発表をしました。 スタメンとしての取り組みを発信できたほか、LeSSを実践している方とお話できたり、実際にTUNAGを使ってくださっているユーザーの声を聞けたりと良い機会になりました。 RSGT2023 スタメンスポンサーセッション スライドはこちら↓ ブースの出展もしました スタメンスポンサーブース (トートバッグが公式グッズとかぶってしまったのは反省点😣) 参加した感想 河井 OST(Open Space Technology) が強烈に記憶に残っています。 最初に参加者が話したいテーマを皆の前で発表し、興味を持った他の参加者がそれぞれ集まってディスカッションをする、というのを繰り返すセッションでした。 色んなディスカッションをふらふらと渡り歩いてましたが、途中からは「デザイナーがスクラムとどう関わるのか」というテーマのテーブルでじっくりと議論に参加して、参加者の方々と交流できたのが良かったです。 初対面の人も含めて次々に会話が生まれる場を目の当たりにして、自社でもこんな活発な場を作りたいなと思いました。 神尾 スクラムマスター兼エンジニアとしてTUNAGの開発をしている神尾です。 RSGTに参加して一番感じたことは、アジャイルに関わるみなさんがとても暖かいということです。 普段からスクラムマスターとして組織やチームのために活動されている方が多いこともあって、ワークショップに参加した際はスクラムの経験が浅い自分の話も「うんうん!わかる!そういうこと本当にありますよね!」といった感じでとても親身に聞いてくれる方が沢山いらっしゃいました。 また、セッションの時間もとても有意義なものになったので、ここで得た経験を会社に持ち帰って自チームだけでなく他チームにも還元します。 来年のRSGT2024も参加したいと思うので、この記事を読んでくださった方ともお会いできるのを楽しみにしています。 澤田 TUNAGプロダクト部門のモバイルアプリGでスクラムマスターをしているカーキ( @khaki_ngy )です。 今回が初めてのRSGTへの参加になりました。 昨年の10月にいちエンジニアからチームのスクラムマスターとなり、そんな自分が参加して大丈夫なのだろうか?とはじめは不安を感じていました。 ただ今回参加してみたことで、参加者の多くが同じような問題で悩み行動をしていることを知ることができました。 そして、自分自身もスクラムマスターとしての役割を今の組織で発揮していきたい、自分も一歩進んでいこうという勇気をもらいました。 またRGSTはスクラムに関する カンファレンス ではありますが、それ以上に ギャザリング であることを実感しました。 セッションはもちろんですが、ワークショップやイベントの合間や終了後での参加者同士の会話を非常に大切にされており、多くの参加者の方とお話しすることができました。 今回参加して得られた学びを行動に活かして、来年参加する際にはその差分や変化について参加者の方とお話しできるようになりたいと思いました。 まとめ 今回の参加で得られた知見を各自チームに持ち帰り、日々の開発に活かしていきます。 最後に、スタメンでは一緒にプロダクトを作っていく仲間を募集しています。 アジャイルな開発をやっていきたい方、ぜひお話だけでもできたら嬉しいので応募お待ちしております!
アバター
こんにちは!株式会社スタメン/TUNAG事業部プロダクト開発部の手嶋/西川/若園です。 弊社のプロダクト部では2022年から 大規模スクラム LeSS (以下: LeSS)を導入しています。 その中で私たち3人は、 チームプロダクトオーナー (以下:チームPOと呼ぶ)という通常ではLeSSに定義されていない役割を担って様々なことにチャレンジしてきました。 今回のブログでは、実際にチームPOとしてどのように活動をしてきたのかを紹介しながら、 チームPOがスクラムにいるメリットや苦労した点 を書いていきたいと思います。 LeSSとは 前提として、LeSSとは何かご紹介します。 (引用元:大規模スクラム Large-Scale Scrum(LeSS) アジャイルとスクラムを大規模に実装する方法) LeSSは、1つのプロダクトを複数チームで協働するために考えられたスクラムです。 LeSSはスクラムの原理・原則、目的、要素、洗練された状態を大規模な状況にできるだけシンプルに適用したものです。 基本的にLeSSで定義されている役割は以下4つです。 プロダクトオーナー(1人) スクラムマスター(複数人) チーム(複数) チーム代表者 上記の通り、LeSSにおけるPOは1人だけで、このPOが各チームに仕事を与えることができる唯一の役割だと定義されています。 (引用元:大規模スクラム Large-Scale Scrum(LeSS) アジャイルとスクラムを大規模に実装する方法) プロダクトオーナーは1人で、顧客にとって素晴らしいプロダクトのビジョンに責任があって、その影響(ROIなど)を最適化します。プロダクトオーナーとして、あなたはプロダクトバックログを継続的に育て、学習および変化に対する適応にもとづいてアイテムを追加、削除、最優先順位付け(並び替え)します。また、上位マネジメント、チーム、顧客に対して、プロダクトバックログが見える状態にしておくことで、透明性を保ちます。あなたはアイテムが明確になるように、チームや顧客と協働します。新しいスプリントに提示するアイテムは、プロダクトオーナーが適応的に決めますが、どのくらい選択するかはチームが決めます。そして、プロダクトオーナーだけがチームに仕事を依頼することができます。 弊社でもスクラム導入直後は、上記4つのロールでスクラムを回していました。 スタメンでのLeSSの紹介 本来のLeSSの体制を踏まえた上で、弊社のLeSSの体制をご紹介します。 弊社のLeSSは、各チームがメンバーの技術領域ではなく職能で横断され、以下のように編成されています。また各チームには、プロダクトオーナーの役割が移譲された「チームPO」が1人配置されています。 冒頭で述べた通り、チームPOはLeSSでは定義されていないため、スクラム体制に移行した直後にはない役割でしたが、今では基本的に各チームに1人(開発者と兼任)配置することを前提として、以下のような組織体制を目指しています。 以降、「チームPO制」導入の背景やスクラムでの実体験をご紹介します。 プロダクト開発部の組織構成(※一部簡略化しています) 「チームPO制」導入の背景 「チームPO制」導入の目的は主に以下の2点でした。 ① POのボトルネック化の防止 ②チームの「意思決定機能」を強め、自己組織化を一層深める 「チームPO制」導入前は、基本的には各チームのスクラム活動において、受入条件や詳細仕様を都度POに確認し、また同席の上でバックログアイテムを作成していましたが、 その結果、チームの意思決定に関してPOへの依存度が高くなり「待ちのチーム」が発生し、POがボトルネックになることが増えてしまいました。 このボトルネックを解消するために、チームの意思決定を率先していく役割として「チームPO制」を導入することを決めました。 元々は1開発者だった中でチームPOと兼任になったため、苦労した点も非常に多かったですが、スクラムにおける様々なイベントでチームが自立的に動ける場面が増えたと感じています。 スクラムの中での動き 弊社のスクラムでは、基本的に以下スケジュールで活動を行っています。 その中で、実際にチームPOとしてどのような動きをしているのかご紹介します。 【1週間スプリントのスケジュール】 月曜 スプリントレビュー(15:00 ~ 16:00) スプリントレトロスペクティブ(16:00 ~ 17:00) スプリントプランニング1(17:00 ~ 17:30) スプリントプランニング2(17:30 ~ 18:30) 火曜 スクラムマスター会議(11:00~12:00) オーバーオールリファインメント(16:00 ~ 17:00) 水曜 (チーム内)リファインメント 木曜 PO会議(17:30 ~ 18:00) スプリントプランニング スプリントプランニングでは主に、自チームのスプリントゴールの決定とチーム間でのアイテムの調整を主導して行っています。 開発部全体のスプリントゴールに関してPOから共有があった後、各チームPOを中心にゴール達成に向けてどのように1週間活動していくのか議論と決定をします。 スプリントゴールに直接関与しないアイテム(弊社ではSRE関連、臨時の顧客対応、ルーティン業務など)もあり、また各チームのベロシティが違うので、自チームの状況と全体の優先順位の両方を考慮して意思決定を行っています。 以前のスプリントプラニングでは、各チーム毎に発言するメンバーや内容に偏りがあったため、タイムボックス内にスプリントゴールを決めることに非常に苦労していましたが、 今では意思決定をリードするチームPOとPOを中心に優先順位にフォーカスした議論することでスムーズに決定できるようになってきました。 オーバーオールリファインメント オーバーオールリファインメントでは、数ヶ月先を見据えた優先順位付けとリファインメントの担当チームの割り振りについて議論しています。 以下のようにリファイメントの担当や方法に関しては、チームが意思決定を行うことが推奨されています。 (引用元:大規模スクラム Large-Scale Scrum(LeSS) アジャイルとスクラムを大規模に実装する方法) オーバーオールPBRでは、より詳細なリファインメントを「複数チームPBR」で行うか、「単一チームPBR」で行うかを決める。これはPOではなくチームに決めてもらうことで、自己組織化が促されPOの作業を減らします。 以前のオーバオールリファイメントは、基本的にPOからビジネス上の優先順位について共有を受けた上で、アサインチームがなんとなく決まっていきチームが意思決定を行うことが少ない状態でした。 「チームPO制」導入後チームの自己組織化が少しずつ進み、POからの共有内容以外にもチームから開発に関する要望(ex.技術的負債の解消など)がボトムアップで出ることが増えました。 今ではそれらを含め全ての優先順位を検討した上で、各チームがリファイメント対象のアイテムに着手できそうなタイミングを共有し、担当する可能性が高いチームがリファインメントを行えるようにチーム間で調整しています。 同時に、POからは開発のWHY(なぜ今必要なのか、マネタイズ、期日)とWHAT(ユースケース、ストーリー、要件)について共有を受け、後のチーム内のリファイメントで活かしています。 リファインメント リファインメントの前に、オーバーオールなどで共有を受けた内容をもとに、チームPOが受入条件を明記してバックログアイテムを作成しています。 見積もり時はチームPOがファシリテーションを行いながらポイントを算出しています。 開発者の疑問点に対しては、オーバーオールリファインメント等で共有を受けた情報を元にチームPOが解消することで、チーム独力でアイテム作成できることが増えました。 まだまだ改善点はありますが、以前と比べるとPOを介した意思決定が少なくなり、チームが主体的に受入条件や詳細仕様に関して意思決定できるようになりました。 また、以前はHOW(どのように実装をするか)に意識が向きがちでしたが、意思決定の回数や責任が増したことで、チーム全体としてプロダクトのWHYやWHATにフォーカスした議論が活発になり、開発への向き合い方も変わり始めています。 チームPOが開発者と兼任なので、技術的に専門性が高いアイテムに関しても意思決定をスムーズに行える場面が増えたこともメリットの1つでした。 PO会議 PO会議は弊社が独自で行っているスクラムのイベントです。 POと各チームPOが参加者で、チームPOが動きやすくするために情報を同期する場として開催し始めました。 具体的には、直近数スプリントの計画やスプリントレビューの設計について話しています。 直近数スプリントの計画に関しては各チームの情報を把握するために始めました。 スプリントプランニングの場で1週間毎の動きはわかりますが、比較的長いプロジェクトにアサインされると、プロジェクトの終了時期が見えづらく、各チームが中期的にどのように連携していくべきなのか把握し辛かったためです。 スプリントレビューの設計に関しては、スプリントの状況を共有しつつ次のスプリントレビューでどのようなデモを行うのか事前にすり合わせを行っています。 デモの時間が限られているので、各チームが使用する時間や優先順位について議論します。 また、社内のどのステークホルダー(営業部やカスタマーサクセス部のメンバー)をデモに招待するのかも決めています。結果として、スプリントレビューで有益なフィードバックを貰える場面が徐々に増えてきました。 最近では、PO主導でチームPOのレトロスペクティブも始めました。 普段のレトロスペクティブは開発者として参加しているため、なかなか振り返りをする機会がありませんでしたが、この振り返りを通して開発部全体としてPOスキルの向上を図っています。 以上がスクラムでの活動の紹介になります。 その他にも比較的大きめのプロジェクトにアサインされている時は、リリーススケジュールマネジメント/やQA項目の作成などを行っています。 チームPOを経験して感じたこと 良かったこととしては、チーム内で意思決定を完結できることが増えチームとして迅速に開発を行えるようになったことや、チームPOだけでなく開発メンバーも自分たちでプロダクトを作る意識が高まったことが挙げられます。 加えてプロダクトの開発方法よりも、その前段にあるWHYやWHATに意識が向くようになり、よりプロダクトの提供価値にこだわりを持つメンバーが増えたと感じています。 実際に直近では、プロダクトのKPIに開発部全体でフォーカスするために、チームPOが主体となり開発のKPIを可視化するダッシュボードを作成するプロジェクトも始めました。 このような開発の成果を可視化する動きは、チームメンバーの関心も高いため、今後のスクラム活動においてより注力していきたいポイントの1つです。 一方で、開発者と兼任なので開発との時間配分を誤るとチームPOの業務に十分な時間が割けず、チームの動きを停滞させてしまうことがありました。 特にバックログアイテムの受入条件やストーリーが明確になっていないと、リファイメントで時間が押してしまうので、バックログアイテム作成の時間を確保する必要がありますが、スプリント中に開発とのバランスを調整することに難しさを感じています。 また、プロジェクトマネジメントのスキルが不足しているという大きな課題があるので、今後はファシリテーションや仕様の意思決定に関してPOからスキルを学んだり、ステークホルダーとの連携を強めるために社内の連携フローを整えていきたいと思っています。 おわりに 実際にチームPOという役割を経験してみて、意思決定の手法や開発とのバランスの取り方に今でも難しさを感じています。 一方でチーム内で意思決定を完結できることが増え、当初の目的であったPOのボトルネックも解消できたことで、チームとして迅速に開発できるというメリットがありました。 今後はよりチームの自己組織化を進めていくために、これまでPOが担っていた役割を巻き取り さらにチームPOの活躍の幅を広げていきたいと思っています。 最後まで読んでいただき、ありがとうございました。 Lessにおけるチームのあり方として、少しでも参考になれば幸いです。  株式会社スタメンは2023年1月に東京に新たに開発拠点を設立することになり、立ち上げメンバーを募集しています!幅広いポジションで募集していますので、ご興味ある方はお気軽にご応募ください! 東京へ開発の拠点拡大!急成長する大規模SaaSプロダクトのエンジニア募集! また スタメン Engineering Handbook として、スタメンの開発体制について体系的にまとめられたページも公開していますので、こちらも是非ご覧ください。
アバター
目次 はじめに スケルトンスクリーンとは react-content-loaderによる実装 プリセットを使用する方法 Create React Content Loaderで独自のスケルトンスクリーンを作成し使用する方法 おわりに はじめに はじめまして、株式会社スタメンの神尾です。 普段はスクラムマスター兼エンジニアとして弊社が運営しているエンゲージメントプラットフォーム TUNAG を開発しています。 2022年10月末にTUNAG内のチャットに『タスク管理』機能を追加リリースし、タスク一覧のローディングUIとしてTUNAGで初めてスケルトンスクリーンを導入しました。 そこで今回はスケルトンスクリーンがUXにもたらすメリットや実装方法などを紹介したいと思います。 スケルトンスクリーンとは スケルトンスクリーンとは、下の画像のようにローディング中に表示される骨組みのようなデザインのことです。 例) youtube スピナーやインジケータといったローディングUIと比較して、ユーザーがローディング完了後のコンテンツを予測できるというメリットがあります。 その結果、ユーザーはスピナーやインジケータよりも多くの情報を得ることができるため、結果として体感時間が短くなると言われています。 また、スケルトンスクリーンとローディング完了後のコンテンツのサイズや表示位置を可能な限り近づけることでGoogleがWebサイトの健全性を示す指標として定義しているCore Web Vitalsの一つの CLS の向上にも繋がり、SEOにも効果があります。 react-content-loaderによる実装 今回は react-content-loader を使ってスケルトンスクリーンを実装しました。具体的な実装方法を説明する前にreact-content-loaderを採用した理由を簡単に話したいと思います。 cssで実装することも可能でしたが、下記の理由からreact-content-loaderを採用しました。 本格的にTUNAG全体で導入する前に新機能の一部でコスト低く試したかったため チャットのタスク一覧は固定幅のため Create React Content Loader を使用することで、簡単に レイアウトシフト がないスケルトンスクリーンを実装することができるため(Create React Content Loaderの使用方法については後述します) Create React Content Loader を使用することで簡単にスケルトンスクリーンを実装することが可能になりコストやハードルが低いと考えたため ライブラリが非常に軽量であるため ここからは、具体的な実装方法を見ていきたいと思います。 react-content-loaderは、以下の2種類の方法でスケルトンスクリーンを実装できます。 1. プリセットを使用する方法 デフォルトでFacebookやInstagramで使われているものと同じデザインのものが用意されています。それらをパッケージからimportするだけで簡単にスケルトンスクリーンを表示することができます。 import React from "react" ; import { Facebook } from "react-content-loader" ; export const MyFacebookLoader = () => < Facebook / >; 例 ) Facebookのプリセット この他にもプリセットがいくつか用意されているので、気になる方は こちら をご覧ください。 ただこれらのプリセットをそのまま使っても、ローディング完了後に表示されるコンテンツとは見た目が異なることが多いと思います。 スケルトンの中身がコンテンツとは異なっていてもサイズが同じであればUXやCLSを向上させることができますが、実際のコンテンツと同じにすることでよりUX向上や誤タップを防ぐことに繋がります。 そのため、react-content-loaderは次の方法で独自のスケルトンスクリーンを作成することができます。TUNAGでも次の方法を使用しました。 2. Create React Content Loaderで独自のスケルトンスクリーンを作成し使用する方法 react-content-loaderには Create React Content Loader というツールがあり、画面右側でデザインを作成すると、画面左側のコードに反映されるので、それを使用すると簡単に独自のスケルトンスクリーンを作成することができます。 Create React Content Loader 実際には以下の手順で行いました。 Figmaからスケルトンスクリーンを作成したいコンテンツの画像を保存 Create React Content Loader右側の「ファイルを選択」に1で保存した画像をアップロード 背景に表示されるコンテンツの画像に合わせてスケルトンスクリーンを作成 左側の「Copy to clipboard」でコードをコピーし、Reactのコンポーネントに貼り付け、ローディングUIとして呼び出す。 手順をひとつひとつ見ていきます。 1. Figmaからスケルトンスクリーンを作成したいコンテンツの画像を保存 TUNAGはデザイン作成にFigmaを使用しているので、Figma上で、スケルトンスクリーンを作成したいコンテンツを選択しエクスポートして保存します。 ここで保存する画像はスケルトンスクリーンを作成する際にサイズ(width, height)などを参考とするためスクリーンショットではなく実際に表示されるサイズの画像を保存してください。 2. Create React Content Loader右側の「ファイルを選択」に1で保存した画像をアップロード 3. 背景に表示されるコンテンツの画像に合わせてスケルトンスクリーンを作成 4. 左側の「Copy to clipboard」でコードをコピーし、Reactのコンポーネントに貼り付け、ローディングUIとして呼び出す( SkeltonTaskItem としてコンポーネントを作成) import React from 'react' import ContentLoader from 'react-content-loader' export const SkeletonTaskItem = () => ( < ContentLoader speed = { 2 } // グラデーションのスピード width = { 320 } height = { 98 } viewBox = '0 0 320 98' backgroundColor = '#F2F6F9' // スケルトンの背景色 foregroundColor = '#E1E8EE' // グラデーションの色 > < rect x = "248" y = "25" rx = "0" ry = "0" width = "5" height = "0" / > < rect x = "244" y = "28" rx = "0" ry = "0" width = "3" height = "2" / > < rect x = "215" y = "24" rx = "0" ry = "0" width = "1" height = "0" / > < rect x = "16" y = "16" rx = "0" ry = "0" width = "24" height = "24" / > < rect x = "56" y = "21" rx = "4" ry = "4" width = "210" height = "11" / > < rect x = "56" y = "48" rx = "4" ry = "4" width = "136" height = "8" / > < rect x = "55" y = "73" rx = "4" ry = "4" width = "158" height = "8" / > < rect x = "146" y = "23" rx = "0" ry = "0" width = "0" height = "1" / > < /ContentLoader > ) タスク一覧のローディングUIとして使用したかったので SkeltonTaskItem を複数回呼びだし上下に border を当てて表示しています。 おわりに 今回はチャットの一部にスケルトンスクリーンを導入しましたが、いずれはタイムラインや制度といった他の機能にも導入していくことで、TUNAG全体のユーザー体験の向上に繋げていきたいと思います。 バックエンドやフロントエンドのパフォーマンス改善で実際の待ち時間を短くすることも大事ですが、スケルトンスクリーンのように実際の待ち時間よりも短く感じてもらえるようなユーザー体験の改善も積極的にしていきたいと思います。 最後まで読んでいただき、ありがとうございました。 この記事が少しでも皆様の参考になれば幸いです。 株式会社スタメンは2023年1月に東京に新たに開発拠点を設立することとなり、立ち上げメンバーを募集します!幅広いポジションで募集していますので、ご興味ある方はお気軽にご応募ください! 東京へ開発の拠点拡大!急成長する大規模SaaSプロダクトのエンジニア募集! www.wantedly.com また スタメン Engineering Handbook として、スタメンの開発体制について体系的にまとめられたページも公開していますので、こちらも是非ご覧ください。 engineering-handbook.stmn.co.jp
アバター
目次 はじめに インペディメントリストとは 運用方法 インペディメントリストの効果 運用する上で課題となったこと インペディメントリストをもっと活かすために今後やりたいこと さいごに はじめに はじめまして、株式会社スタメン TUNAG事業部 プロダクト開発部 西川、神尾、小松、村中です。 スタメンでは2022年の2月からスクラムを導入しており、私たちはスクラムチームの「チームおでん🍢」として、1週間のスプリントで開発しています。 先日、私たちは、社内で開かれたベスプラ会という業務における改善策を共有するイベントで登壇をしました。発表内容は「チームの一番重要な問題を改善し続ける方法」です。 私たちは4ヶ月ほど前からチームの継続的な課題解決のためにインペディメントリストを導入し運用をしていますが、今ではうまく活用できているものの、導入当初は多くの問題がありました。 そこで今回は導入から現在に至るまでどのように問題を改善してきたか、現在はどのように運用しているか等を紹介していきます。この記事によって、現在スクラムを導入しておりインペディメントリストをうまく活用したいと思っているチームや、具体的な運用事例を知りたい方の参考になれば幸いです。 インペディメントリストとは インペディメントとはプロジェクトのゴールを妨げるものであり、その一覧がインペディメントリストです。 具体的には下記のようなものが含まれます。 ミーティングの参加者が事前に決まっていないので、準備が十分に出来ていない 属人化してしまっている作業があるので、休暇時の対応に困る オフィスが暑い 3つ目の「オフィスが暑い」というのも、チームの課題と一見関係なさそうに見えますが「オフィスが暑い」ことが原因で「集中力が続かない」と考えると十分にインペディメントだと考えられます。 このように明らかにチームの課題と考えられることから一見関係なさそうなことまでインペディメントリストには含まれます。 運用方法 次に私たちが、どのようにインペディメントリストを運用しているかをStepに沿って紹介していきたいと思います。 前提として、私たちは「いつでも誰でも」インペディメントをリストに追加するようにしています。 Step1 レトロスペクティブの前に、インペディメントリストの中から一番重要なインペディメントをチームで話し合って決定します。 Step2 レトロスペクティブの際に、そこで挙がった問題とStep1で決めたインペディメントを比較して1番重要な問題(レトロアイテム)を決定します。 そうすることで、そのスプリントで出た問題だけでなく、過去も含めた全ての期間で一番改善すべき問題をレトロアイテムとして決定することが出来ます。 Step3 レトロスペクティブで改善策を決定し、次のスプリントで改善を行います。 Step4 次のスプリントのレトロスペクティブの際に、本当に改善できたか、改善策は正しかったかなど、再度話すことで確実に1つずつ改善していきます。 インペディメントリストの効果 このようにインペディメントを運用した結果、毎スプリント改善を積み重ねることができるようになり、チームの雰囲気にも良い影響がありました。感覚的なことにはなりますが、以下のような意見がメンバーから出てきました。 インペディメントをポジティブに捉えられるようになった リストに追加すればチームで改善できるという安心感が生まれた 本心で話せるようになった これらはチームが問題に向き合う上でとても重要な考え方だと思います。 毎スプリント、チームの問題に正面から向き合っていると改善が難しいことも出てきます。そのような時に、本心で話すことが根本解決に繋がるということを、経験から学ぶことができました。 運用する上で課題になったこと 現在はチームでインペディメントリストを上手く活用することで継続的に改善を積み重ねていくことができていますが、導入当初はいくつか課題がありました。 今回はその中から下記の2つの課題と具体的に改善した方法を紹介します。 ただリストが並ぶだけ チームの改善よりも開発チケットを優先してしまう 1. ただリストが並ぶだけ これはインペディメントリストが「優先度順」ではなく「記載された順」に並んでいることにより、何が重要なインペディメントなのかがわかりにくいという課題です。 これは「重要度×緊急度のマトリクス」を参考に優先順位を割り振ることで改善しました。 定期的にチームでインペディメントリストを確認する時間をとり、優先順位の「割り振り」と「更新」を行っています。具体的には次のようなことです。 割り振り スプリント中に各チームメンバーが追加した優先順位が割り振られていないインペディメントについて「重要度×緊急度のマトリクス」を使い優先順位を割り振る 更新 過去に優先順位を決めたインペディメントの優先度が変わっていないかを確認し、変わっている場合は再度優先順位を話し合う このようにすることで、レトロスペクティブで一番改善したいアイテムを簡単に選ぶことができるようになっただけでなく、チームのインペディメントについてチームメンバー全員が共通認識を持つことができるようになったため、日々の話し合いなどの活性化にも繋がりました。 2. チームの改善よりも開発チケットを優先してしまう これは期限のある開発チケットをチームの改善よりも優先したくなってしまい、チームの改善が上手く進まないという課題です。 インペディメントリストを運用し始めたばかりの頃は、スプリントの後半に話し合いの時間を設けており、スプリントの進捗が悪いとゴールを優先し話し合いがスキップされることが多くありました。 このような課題は、スプリントの冒頭であらかじめ話し合いの時間を確保しておくことで改善しました。 「チームの改善が何よりも重要」という共通認識をもって、スプリントの初めに話し合う時間を必ず取るようにしています。 具体的には、レトロスペクティブの最後にメンバーのカレンダーを用意し、全員が参加できる時間にスケジュールを確保するようにしています。 その話し合いの中で完全な改善策が見つからない場合もありますが、軽減策やその課題に対する共通認識を持つことが出来るというメリットだけでも毎スプリント欠かさず行うことに大きな意味があると感じています。 インペディメントリストをもっと活かすために今後やりたいこと レトロスペクティブの改善 チームのレトロスペクティブは KPT を使用しています。 これはTUNAGでスクラム開発を導入してから継続的に取り入れている手法ですが、KPT以外にも振り返りの手法は数多くあります。 それらを取り入れるなどしてチームに合ったレトロスペクティブを模索し、インペディメントリストをより上手く活用できるようにしていきたいと思います。 さいごに 最後まで読んでいただきありがとうございます。 インペディメントリストを導入したことで、チームの問題を継続的に解決できるようになっただけでなく、本心で話し合いができるようになり、改めて導入して良かったと感じています。 この記事を読んでインペディメントリストに興味のあるチームや既に導入しているチームの参考になれば幸いです。 そして、株式会社スタメンは2023年1月10日-13日に開催される Regional Scrum Gathering Tokyo 2023 (通称:RSGT)にシルバースポンサーとして参加することが決まりました🎉👏🎈 スポンサーブースの出展やDay2(12日)にスポンサーセッションもありますので是非お越しください👏👏👏 2023.scrumgatheringtokyo.org また、2023年1月に東京に新たに開発拠点を設立することとなり、立ち上げメンバーを募集します! 幅広いポジションで募集していますので、ご興味ある方はお気軽にご応募ください! www.wantedly.com
アバター
こんにちは!TUNAG事業部モバイルアプリGのカーキです。 2022年ももう残りわずかになってきましたね。 最近は、社内のポケモンマスターズトーナメントに向けて、ポケモンの育成に勤しんでいます。 スタメン TUNAG事業部のプロダクト部では今年から 大規模スクラム(通称:LeSS) を導入しています。 今回のブログでは、 大規模スクラムを導入した今年、モバイルアプリGがどのような体制で開発してきたのか 、その変遷をご紹介できればと思います。 スタメンでの大規模スクラムの紹介 スクラム中でのモバイルアプリGの動きを紹介するのに、そもそもスタメンでのスクラムがどのように導入されているかについての前提が必要なので、先に紹介します。 大規模スクラムでは複数の機能横断のチーム(以下:フィーチャーチームと呼ぶ)に分かれています。 フィーチャーチームでは、メンバーの技術領域ではなく、機能で横断されたチームとして編成されます。 ただモバイルアプリの開発領域に関しては、技術の特殊性と元々開発していたメンバーが2人しかいなかったこともあり、現状は技術横断なグループとして存在しています。 そのため、Web領域では機能横断なものの、モバイル領域では技術横断のチームとなっています。 スクラムの導入前までは、こちらのように完全技術横断の組織構造になっていました。 開発一部 アプリケーショングループ SREグループ 開発二部 モバイルアプリグループ フロントエンドグループ そして現在はこのような組織構成になっています。 TUNAG事業部のプロダクト組織図 技術領域に依らないチームになるため、モバイルアプリG以外はユニークな名前がついています。 実際にネーミングの場に立ち会ったわけではないので想像になりますが、メンバーが共通して好きな食べ物から来ているのだと思います(?) 元々、スタメンのモバイルアプリGはモバイルアプリのみを専属で開発しており、開発の途中で発生するAPIの実装に関してはWebのチームが開発していました。 そのため新しい機能の開発がモバイルで発生した場合は、基本的にはAPIが必要になるため、Webのチームと並走して実装する必要がありました。 本格導入前 モバイルアプリのチームへのスクラムの導入は事例が少なく、社内でも手探りの状態から始まりました。 またWebのフィーチャーチームの立ち上げを確実に行いたいという考えから、フィーチャーチームへの移行が優先的に進められました。 その間、モバイルアプリGはある種 外注的 に動いてプロジェクトに関わっていくことになりました。 もちろんモバイルアプリ開発の専任チームとしてモバイルアプリの体験に関しての提案は行っていましたが、一つのプロジェクトの仕様面に関しては担当しているチームが決めていました。 与えられた仕様を実装するだけなので、技術的な挑戦にはトライしやすい環境ではありました。 ただそのような関係では、プロジェクトに入って一緒に進めているという実感が薄く、メンバーのモチベーションの低下につながっていました。 スクラムの本格導入 ただモバイルアプリエンジニアからはその旨みを享受できていないこともあり、モバイルアプリGにもスクラムを導入していった方が良いのではという声は徐々に大きくなっていきました。 そこで今年の夏頃から徐々にチームにスクラムのプラクティスを適応していき、アジャイルな開発体制を目指していきました。 スクラムイベント 8月ごろからモバイルアプリGを一つのスクラムのチームとして扱ってスクラムイベントに参加するようになりました。 この時開発していたチャットのタスク機能はWeb、モバイル両方に新たに追加される機能になり、スクラムを本格導入するには絶好のタイミングでした。 モバイルアプリGの特色として、iOS/Androidの両OSを開発する必要があったり、Webのチームとの連携が必須なため、どのようにスクラムイベントを導入するのかは手探りの状態から、少しずつ改良を加えていきました。 プロジェクト内でスクラムイベントをどのように行ってきたのかを紹介します。 スプリントレビュー スプリントレビューではモバイルアプリGの成果物としてデモとレビューを行なっています。 今まではモバイルアプリGのレビューの時間はなく、開発した機能に関しては一緒に連携して動いていたフィーチャーチームの成果物の延長としてデモを実施していました。 モバイルアプリGも一つのチームとして、独立したことでなぜデモを行うのかや、どのような観点でレビューを行うのかなど、より高い次元でスプリントレビューを行えるようになってきました。 スプリントレビューではプロダクト部全体でデモアプリの配信に利用している、App Distribution 経由で行っており、実際に自身の端末に入れて体験してもらうことを重視しています。 スプリントプランニング スプリントプランニングはフィーチャーチーム全体で実施しているスプリントイベントで他のチームと一緒に行うようにしました。 今まではモバイルアプリGのみ、プロダクト全体とは別のプロダクトバックログで管理を行い、別日にスプリントプランニングを行なっていました。 そのため他のチームからモバイルアプリGがこのスプリントで何を行なっているのかが見えづらい状況でした。 以前まで使用していたモバイルのプロダクトバックログ(Githubのプロジェクトボードで管理) この時のプロジェクトではお互いのチームが何をしているのかに応じて取れるアイテムが変わる可能性があり、全体のスプリントプランニングの場でモバイルアプリGのスプリントプランニングを実施しました。 リファインメント チャットタスク機能全体のリファインメントはWebのメンバーと合同で実施をしました。 リファインメントはアイテムを 分割 し・それを 見積もる 工程になりますが、見積もりの中でそのアイテムの 仕様について議論 する必要があります。また大きなプロジェクトのリファインメントには PO や PdM が同席していることが多く、プロジェクトに関わるメンバーとして参加したことで、プロジェクトの背景やモバイルアプリでの見せ方などの議論も早い段階から行うことができました。 またリファインメントでのアイテム分割もWebのチームと一緒に行い、同名のアイテムとしてWebとモバイルでそれぞれを作成しました。これにより、Webのチームと同じストーリーポイントでアイテムを切ることができ、お互いの進捗を把握しやすくなるという利点がありました。 Webとモバイルとで同じストーリーでアイテムを作成する ただその後の工数見積もりに関してはWebのチームとは分かれて実施をしました。 同じストーリーで作られたアイテムであっても実装する手段がWebとモバイルで異なっているため、見積もるポイントも当然異なるためです。 これらのように合同でリファインメントを実施したことで、一緒に仕様を固めることができ、モバイルアプリGからプロジェクトに参加している実感や開発へのモチベーションも高く行うことができました。 また当時はプロジェクトの中でiOS/Androidで同一の機能を提供するために、モバイルアプリに関するアイテムは iOS/Androidで分けない 方針を取っていました。 この時はプロジェクトで開発をしていたのがiOS/Androidで2名しかおらず、実装できるOSにも隔たりがあったためです(自分は両OS実装できるが、片方はiOSしか実装できない)。 あえて同じアイテムで分けたことで、自分がAndroidの実装を先に終えたら相方のiOSの実装にサポートできるという体制に必然的になり、チームでゴールに向かっている実感がありました。 デイリースクラム チームでのデイリースクラムは朝会として以前から実施しましたが、いくつか改良も加えてました。 朝会では以下の内容をチームメンバーで共有しています。 - 一人ずつ - 昨日やったこと - 今日やること - 困っていることや共有したいこと - 全体で - レトロアイテムの振り返り - 今日のスケジュールの確認 また以前までモバイルアプのプロダクトバックログとして管理していたGithubのプロジェクトボードをスプリントバックログとしてデイリースクラムに利用しています。 GithubのPRの状態と連携させることができるので、現在の作業の進捗状態が分かりスプリントバックログとしては優秀だと感じています。 他には、デイリースクラムの司会を順番に回すなどの工夫を行なっています。 司会が固定されてしまうと、慣れてくると受け身の姿勢で参加してしまうので、チーム全体で主体的に参加できるようにそうしています。 またそれに加えてプロジェクト全体でのデイリースクラムを実施しました。 プロジェクト全体で毎朝進捗の同期が取ることができるため、モバイルアプリGとしても動きやすくなりました。 特にモバイルアプリGではAPIの開発をWebのチームに依存しているので、その辺りの開発の優先度を上げてもらうなど、相互のコミュニケーションを取ることができるようになりました。 また開発をしていて感じた細かい仕様の相談などを早期に共有し、解決できるようになりました。 今後の展望 今後は、今のモバイルアプリGのフィーチャーチーム化を進めていくことを予定しています。 モバイルアプリ開発が主軸のフィーチャーチームでは、現状のチームにWebのエンジニアが数人参画することをイメージをしています。 このようなチーム構成にすることでAPIの改修や追加が必要なタスクであってもチーム単独でWebも含めたインクリメントを生み出すことができるようになります。 最終的にはスクラムの効果を最大限発揮した上でモバイル開発をより加速させることができると考えています。 また来年1月からTUNAG事業部は事業をより加速させていくために、 東京での新規開発拠点の設立 を予定しています。 東京支社の設立により、TUNAGのモバイルアプリエンジニア増え、モバイルアプリを主軸としたチームも近いうちに複数になってくることも想定しています。 そのような状況で、地理的な条件を超えて、モバイルアプリ開発を主軸としたチーム同士がどう自立して・連携をとっていくのかに関しては、まだまだ分からないことだらけです。 ただ一つわかっているのは、スタメンのモバイルアプリ開発はこれから さらに加速して面白くなっていく ということです。 フィーチャーチーム化やチームの増加は新たな課題にはなりますが、アジャイルな組織はそれを乗り越えて、 進化 することができると確信しています。 株式会社スタメンでは、これから面白くなるモバイルアプリ開発組織を一緒に作り上げていくメンバーを募集しています。 詳細は以下のページをご覧ください 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します! また弊社では、オンラインサロン用プラットホーム FANTS というサービスも開発・運営しております。 ご興味がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します! また スタメン Engineering Handbook として体系的にまとめられたページも公開しています。 こちらも是非ご覧ください。
アバター
こんにちは! スタメン TUNAG 事業部 モバイルアプリグループのカーキです。 最近では主として Android アプリの開発に携わっています。 株式会社スタメンでは7月の初めに『TUNAG 受付』という、TUNAGのチャット機能を利用したオフィスの受付アプリをリリースしました(Androidのみ対応) ( ストアリンク ) 『TUNAG 受付』はUIの部分を全て Jetpack Compose で記述しており、アプリ全体の構成もJetpack Composeに合わせたものになっています。 (過去のJetpack Compose導入の経緯などは こちらのブログ をご参照ください) 今回は『TUNAG 受付』のアーキテクチャ及び、その構成について紹介します。 TUNAG 受付について まず初めに『TUNAG 受付』がどのようなサービスかについて紹介します。 「TUNAG 受付」では、主に以下の2つの機能を提供しています。 オフィスに訪れた来訪者が担当の社員を呼び出す 社員はTUNAGチャットでその呼び出しを確認できる TUNAG受付とTUNAGのアプリケーション間の関係 「TUNAG 受付」のアプリとしてはTUNAGのチャットの仕組みを使って、「オフィスの来訪者が担当の社員を呼び出せる」機能を提供しています。 受付アプリを自社で開発をしたきっかけとしては、 今年4月のオフィス移転 があります。 新オフィスでは執務スペースが2階にあり、出入り口のある1階に来訪の方が来られた時の対応を検討していました。 他社のサービスも検討したのですが、TUNAGのユーザー情報とチャットの通知機能を使うことで、スタメンらしさを出すことができるのでは?という話があり、自社で受付アプリの開発を行うことになりました。 アーキテクチャ TUNAG受付は、Androidタブレット端末向けに既にリリースされています。 開発時にはちょうど Jetck Compose を用いた開発が社内でも盛り上がっており、UIの部分に関しては全て Jetpack Compose で開発を行いました。 また今後iOSアプリへの拡張性を考慮し、データ及びドメイン層には Kotlin Multiplatform Mobile (通称:KMM)を採用しています。 今回は JetpackCompose をどのように活用しているかにフォーカスを絞って紹介しますので、KMMに関しては詳しくは触れません。 全体的なアーキテクチャとしてはMVVMを採用しています。 TUNAG受付全体の構成はこのようになっています。 アプリ全体のアーキテクチャ 社内では既に新規のコンポーネントの作成には Jetpack Compose を使っていましたが、Activityをほとんど用いないフル Jetpack Compose の開発は初めてだったので、Google が公開している Jetpack Compose の公式サンプルアプリ のアーキテクチャと Android アプリアーキテクチャガイド を参考にしています。 UI層 Googleのアプリアーキテクチャガイドでは、UIレイヤは「Viewを構成する UI elements 」と「Viewの状態を管理する State holders 」によって構成されています。 Google のアーキテクチャガイドにある UI 層の構成 一般的に UI elements では、View や Compose が、 State holders は ViewModel が該当します。 UI State の不変性 この View の状態管理の方法について、アプリアーキテクチャでは UI State という概念が登場しています。 UI State は UI の表示を一意に決定することのできる状態を持っています。 UI State の使用が推奨されている大きな理由は、その 不変性 にあります。UIレイヤーにおける不変性とは、データの状態により UI が一意に決まることを指します。 ここで UI elements は直接 UI の状態を書き換えることはできず、必ず State holders から受け取った状態のみを UI へ反映します。このようにすることで State holders は確実に UI の状態を把握することができ、UI elements は受け取った状態のみを UI に反映するだけで良いので、関心の分離を実現することができます。 受付アプリのUserListScreenでのUI State は以下のように記述されています。 /** * UserList - UIState */ sealed interface UserListUiState { val isLoading: Boolean val errorMessage: String ? val currentRoom: RoomDetailDto? /** * ルームにユーザーが存在しない */ data class EmptyUser( override val isLoading: Boolean , override val errorMessage: String ?, override val currentRoom: RoomDetailDto?, ) : UserListUiState /** * ユーザーが存在する場合 */ data class HasUsers( override val isLoading: Boolean , override val errorMessage: String ?, override val currentRoom: RoomDetailDto, val userOnCall: UserStateHolder, val isOpenCallDialog: Boolean , ) : UserListUiState } 受付アプリのUserListScreenでは以下の2つの状態があり、 sealed interface で定義することにより、状態を完全に切り分けています。 - EmptyUser - ルームにユーザーが存在しない場合は「ルームにメンバーが存在しません。」のように空の状態を表示 - HasUsers - ルームにユーザーが存在する場合は、メンバー一覧を表示 UI elements における UI State の扱い UI State は sealed class にて状態がただ一つに決定されるため、 UI State は状態に合わせて適切な UI を構成することができます。 UserListScreen のような UI elements を担うコンポーザブル関数では以下のように状態に合わせて UI の状態を変えて表示しています。 @Composable fun UserListScreen( uiState: UserListUiState, ... ) { when (uiState) { UserListUiState.EmptyUser -> EmptyUserList() UserListUiState.HasUsers -> UserList() } } ViewModel内におけるUI Stateの扱い ViewModel内では表示するデータ全般を扱う別のデータクラスの操作を行い、UI Stateの操作はViewModelでは直接は行いません。 ViewModelStateがViewModelでのデータ全般を扱い、ViewModelStateの変更がそのままUI Stateに通知されるようにしています。 このような ViewModelState を使用した ViewModel 内での状態管理は Google が公開している Jetpack Compose のサンプルアプリである JetNews の実装を参考にしています。 ViewModel内における状態管理を全てViewModelStateが担うメリットとしては、 UIState の状態を個別の値の変化によっていちいち変更する必要がないことが挙げられます。 これはアーキテクチャガイドにも 考慮事項 として記述されています。 private data class UserListViewModelState( val currentRoom: RoomStateHolder = RoomStateHolder(state = State.Initial), val userOnCall: UserStateHolder = UserStateHolder(state = State.Initial), val errorMessage: String ? = null , val isOpenCallDialog: Boolean = false , ) { /** * UiStateへの変換 */ fun toUiState(): UserListUiState { return when (currentRoom.state) { is State.Data -> { if (currentRoom.state.data?.users?.isNotEmpty() == true ) { UserListUiState.HasUsers( isLoading = false , errorMessage = errorMessage, currentRoom = currentRoom.state.data !! , userOnCall = userOnCall, isOpenCallDialog = isOpenCallDialog, ) } else { // メンバーが存在しない場合 UserListUiState.EmptyUser( isLoading = false , errorMessage = "呼び出し先がありません。設定してください。" , currentRoom = currentRoom.state.data, ) } } else -> { // 読み込み中・エラーなどでメンバーが表示されていない場合 UserListUiState.EmptyUser( isLoading = currentRoom.state is State.Loading, errorMessage = errorMessage, currentRoom = null , ) } } } } この UserListViewModelState はViewModelの内部での全ての状態と、UI elements へ公開される UI State (ここでは UserListUiState )へマップするメソッドを持っています。 ViewModelの内部では下記のように MutableStateFlow でラップされています。 MutableStateFlowとして扱えることで、StateFlowの update メソッドから、状態の更新を行うことができます。 class UserListViewModel() { private val viewModelState = MutableStateFlow(UserListViewModelState()) fun hoge() { viewModelState.update { it.copy(isOpenCallDialog = true ) } } } またViewModelから外部に公開される UI State はStateFlowの持つメソッドにより、 viewModelState に更新がかかるたびに順次更新されるように実装を行なっています。 /** * 外部に公開されるUiState */ val uiState = viewModelState .map { it.toUiState() } .stateIn( viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState() ) 画面遷移 画面遷移に関しては、 NavHost を利用しています。 こちらに関してはDvelopersのドキュメント通りの実装になるので、軽く紹介します。 NavHost ではコンポーザブルの目的地を指定することで対象となるコンポーザブルを呼び出すことができます 以下は公式ドキュメントのサンプルコードになります。 NavHost(navController = navController, startDestination = "profile" ) { composable( "profile" ) { Profile( /*...*/ ) } composable( "friendslist" ) { FriendsList( /*...*/ ) } /*...*/ } 実際には、画面間の値の受け渡しなどが発生するので以下のように実装を行なっています NavHost( navController = navController, startDestination = startDestination, modifier = modifier, ) { composable(ReceptionDestination.GroupList.route) { val viewModel: GroupListViewModel = viewModel( factory = GroupListViewModel.provideFactory(container) ) GroupListRoute( groupListViewModel = viewModel, navigateToUserList = { navigationActions.navigationToUserList(it) }, ) } composable( ReceptionDestination.UserList.route, arguments = listOf(navArgument( "roomId" ) { type = NavType.LongType }) ) { val viewModel: UserListViewModel = viewModel( factory = UserListViewModel.provideFactory( container, it.arguments?.getLong( "roomId" ) ?: 0L ) ) UserListRoute( userListViewModel = viewModel, back = navigationActions.back, ) } } 遷移先のコンポーザブルが呼び出されるタイミングで ViewModel の注入を行なっています。 まとめ 「TUNAG受付」がどのようにフルJetpack Compose の構成で実装を行なっているのかを紹介しました。 現状「TUNAG受付」は小さなアプリケーションとなるので、複雑さは少なく、Google が提供している公式のツール、方法でやりくりすることができています。 スモールスタートで開発を進める上で、JetpackCompose や Google のアーキテクチャガイドは良い指針となりました。 弊社では、JetpackCompose に興味のある Android エンジニアを募集しています。 気になる方、興味のある方は、ぜひ弊社採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します! また弊社では、オンラインサロン用プラットフォーム FANTS というサービスも開発・運営しております。 ご興味がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します! また スタメン Engineering Handbook として体系的にまとめられたページも公開しています。 こちらも是非ご覧ください。
アバター
目次 はじめに 共通UIコンポーネント とは 共通UIコンポーネント を作り、運用することのメリット デザインの一貫性やクオリティが保たれる 開発コスト&デザインコストを削減できる 実装時に考慮すると良いこと スタイルを適切なpropsで操作可能か コンポーネントのトップの要素にmarginをつけない 親や子の要素をお互いが知っている前提の実装をしない おわりに はじめに はじめまして、株式会社スタメンの神尾です。 普段はフロントエンドにReact、バックエンドにRuby on Railsを用いて、弊社が運営しているエンゲージメントプラットフォーム TUNAG の開発をしています。 TUNAGには多種多様な機能 がありますが、普段のReactを用いたフロントエンド開発において、それら特定の機能に紐づかず共通で使用できるコンポーネントを実装したい時が多々あります。 今回は、そのようなReactコンポーネントを 共通UIコンポーネント と定義し、 共通UIコンポーネント の説明、メリット、実装時に考慮すると良いことを紹介していきたいと思います。 共通UIコンポーネントとは 「 共通UIコンポーネント 」とは、ドメインの知識を持たないコンポーネントのことで、具体的には Button, Modal, Pulldown, Tab, Checkbox 等があります。 これらを提供するライブラリとして有名なものは、以下のようなライブラリがあります。 MUI Ant Design Chakra UI 基本的には、これらのライブラリが提供しているReactコンポーネントが、この記事で言う 共通UIコンポーネント です。 例 ) 共通UIコンポーネント として汎用的なボタンを作成し、様々な機能で使用できるようにしている(投稿、制度、組織、チャットはTUNAGの機能名です) 共通UIコンポーネントを作り、運用することのメリット デザインの一貫性やクオリティを保つことができる 開発コストとデザインコストを削減できる デザインの一貫性やクオリティを保つことができる 共通UIコンポーネント があることで、特定の機能のコンポーネント内で当てるスタイルを減らすことが出来ます。 そうすることで、デザインの軽微な差異を無くすことが出来るので、一貫性やクオリティを保つことができます。 次に、 共通UIコンポーネント がない場合を考えることでメリットが伝わりやすいと思うので、具体例を挙げて考えてみます。 共通UIコンポーネントがない場合 共通UIコンポーネント としてボタンが無い場合、各機能のコンポーネント内でボタンを実装することになります。 上の画像で言うと「投稿」のコンポーネントの中でボタンを実装し、また別の「制度」でもボタンを実装する等、機能毎にボタンを実装することになります。 そのように色々な機能でUIコンポーネントが実装されると次のようなことが起こります。 ボタンにはborder-radiusが当てられることが多くあると思いますが、各機能ごとにボタンを実装する場合border-radiusもそれぞれのボタンで実装されます。 それらの実装は、それぞれ時期も実装者もデザイナーも異なる可能性が高いので、その都度デザイナーがスタイルを決め、実装することになります。 そのため、この機能の時のボタンのborder-radiusは4px、また別のボタンのborder-radiusは6pxなど、微妙な違いが出てしまう可能性があります。 この問題は、デザインルールがあり、かつ浸透していれば、避けられる問題ではありますが、エンジニアが実装する際に誤ったスタイルを当ててしてしまうことや、毎回実装やデザインの確認をするコストを考えると、同じ機能を持ったボタンを複数回実装することは避けた方が良いと考えられます。 さらに、色々な場所で同じボタンが実装されていると共通でスタイルを変更したくなった時にとても厄介なことになります。例えば「ボタンのbackground-colorを変更したい」となった時、各機能のコンポーネント内でボタンを実装している場合、どのようにbackground-colorを変更すれば良いでしょうか。 それは「ひとつひとつ変更していく」しか方法はありません。 「ボタンは投稿で使ってたから変更して、制度でも使ってたから、これも変更して、、」という風にひとつひとつ変更をしていたら「組織」でボタンを使っていたことを失念する可能性は十分あります。 そのようなことが続いていくと「デザインの一貫性」は崩れていきます。 例 ) 各機能で実装していると変更漏れなどでデザインの一貫性は崩れてしまう そこで 共通UIコンポーネント で、これらの問題を解決します。 どの機能でも共通で使用できるボタンがあることで、ボタンのbackground-colorを変更したくなった時は、 共通UIコンポーネント のbackground-colorだけを変更すれば、投稿、制度、組織、チャットの全てのボタンも一緒に変更されるため、変更漏れが起きることはありません。 このように 共通UIコンポーネント を上手く使うことで「デザインの一貫性」を高めることに繋がります。 例 ) 共通UIコンポーネントとしてのボタンがあれば、変更漏れを無くすことができる。 開発コストとデザインコストを削減できる 共通UIコンポーネント があることによって、開発コストやデザインコストが削減できます。 理由は以下のようなものです。どれも大きなメリットだと思います。 CSSを書く量が減る。 共通UIコンポーネント に既にスタイルが当たっているため デザインレビューする箇所が減る。 共通UIコンポーネント はスタイルも共通のため 「ここのpaddingどうしますか」のような細かいスタイルのコミュニケーションが減る。 改めて実装する必要がないので確認する必要がないため 使いまわせるコンポーネントがあるので、一からデザインを作成する必要がない。 同じ機能を持った似たようなデザインを考える必要がないため 以上が自分が感じている 共通UIコンポーネント のメリットになります。 ここからは、実装について見ていきたいと思います。 実装時に考慮すると良いこと ここからは 共通UIコンポーネント を実装する時に考慮した方が良いことについて書いていきます。 共通UIコンポーネント は実装時に考慮するべきことが多いので、気づいたら汎用的でないコンポーネントになってしまうことが多くあると感じています。 言われれば「そりゃそうだ」と思うこともあると思いますが、今回は3点紹介させていただきます。 スタイルを適切なpropsで操作可能か 共通UIコンポーネント と言っても「どこで何のために表示するか」が異なれば、当てるスタイル全てが共通になるわけではありません。 そのような時に備えて、柔軟にスタイルを変更できるようにしておくことは、より良い 共通UIコンポーネント の実装には重要なことです。 もちろん、そのような違いが多すぎると共通のスタイルが少なくなってしまい、 共通UIコンポーネント の意味も薄れ、上記のメリットで上げた「デザインの一貫性」が減ってしまうので気をつけなければならないと思います。 その点に関しては、デザイナーとエンジニアで「どこまでスタイルの違いを許容するか」を話し合って決める必要があると思いますが、一旦ここでは「使う場所によって、ここのスタイルは変えたい」となった場合を考えます。 例えば、以下の画像のようなタブコンポーネントを実装したとします。タブ全体には灰色のborder-bottomがあり、選択されているタブには水色のborder-bottomがあるデザインとなっています。 このコンポーネントを作成した時点では、このスタイルでしたが、別の機能で使用する際にデザインの観点からborder-bottomを表示しないようになったとします。 このような時は isDisplayBorderBottom といったボーダーを表示するかどうかのフラグのpropsで表示・非表示を分けられるように実装しておき、呼び出し側から、このフラグを操作できるようにしておけば、より汎用的な 共通UIコンポーネント を作ることができます。 この時に is機能名 のようなフラグにしてしまうと、また別の機能で使用する時に、 is機能名2 のようなフラグを増やさなければいけないので、避けた方が良いです。 また、例として挙げた isDisplayBorderBottom でも「ボーダーの表示・非表示」の対応はできますが、ボーダーの色を変更したいとなった時に対応できません。 そのような変更の可能性に備えて、以下のようなpropsにすると、さらに汎用的なコンポーネントになります。 borderBottom: 'mainColor' | 'subColor' | 'none' このように、propsひとつで対応できる幅が大きく変わるので、適切なpropsは何かということを意識することが大事になります。 コンポーネントのトップの要素にmarginをつけない コンポーネントのトップの要素にmarginをつけると、使い回しづらいコンポーネントになります。なぜ使い回しづらくなるのかを具体例で考えます。 下記の①では入力フォームとボタンの間にmarginが16px必要だったのでボタンコンポーネントにmargin-leftを16pxつけることにしました。 今回は入力フォームの右にflexboxを使って配置したらデザインが実装できました。 次に②のデザインを実装することになりました。プルダウンとボタンの間は8pxです。 ボタンは同じなので、①で作ったボタンコンポーネントを使い回そうとしましたが、margin-left: 16pxが付いているのでプルダウンとボタンの間が8pxに出来ません。 どうしよう、、、 => このようにコンポーネント自身にmarginが付いていると、他で使う時に邪魔になってしまうことがあります。そもそもmarginはボタンの要素ではなくボタンと他のコンポーネントの間にある空間なのでボタンコンポーネントが持つべきスタイルではありません。 marginはボタンを呼び出しているところで当てるようにすると、使い回しやすいコンポーネントになります。 // もろもろ省略 return ( < Wrapper > < Input / > < PrimaryButtonContainer > < PrimaryButton onClick = { handleOnClick } / > < /PrimaryButtonContainer > < /Wrapper > ) // styled-components const Wrapper = styled.div ` display: flex; ` const PrimaryButtonContainer = styled.div ` margin-left: 16px; ` 親や子の要素をお互いが知っている前提の実装をしないこと コンポーネント自身が「どのようなレイアウトで表示されるか」「親要素が何か」などを知っている前提の実装になっていると使い回しづらいコンポーネントになります。 以下は、UserCardList(親)とUserCard(子)がお互いの要素を知っている例です。 「親や子の要素をお互いが知っている」とはどういうことなのかを説明します。 UserCardListはUlなので、その子はLiでなければなりませんが、以下のUserCardListコンポーネント内だけでは子要素のUserCardがLiで作られたコンポーネントかどうかを判別できません。 今回の例ではUserCardはLiなので問題はありませんが、その「問題ない」かどうかを判別するには、親(UserCardList)が子(UserCard)がLiで作られたコンポーネントであると知っている必要があります。 反対に、Liの親はUlでなければならないのでUserCardもUlの子要素として呼ばれることを知っているとも言えます。 これが「親や子の要素をお互いが知っている」ということです。 const UserCardList = ( { users } : Props ) => { return ( < ul > { users.map (( user ) => ( < UserCard user = { user } / > )) } < /ul > ) } const UserCard = ( { user } : Props ) => { return ( < li key = { user.id } > < p > { user.id } < /p > < p > { user.name } < /p > < /li > ) } このような実装になっていると以下のような理由で使い回しづらいコンポーネントになります。 呼び出し側も呼び出される側も、そのコンポーネントがどのように作られているかを気にする必要があること。 呼び出される親が条件に合っていなければ使いまわせないこと。 上の例のUserCardの場合、Ul以外の親から呼び出したい時に使えません。 これらの問題は以下のように実装すると解決します。 const UserCardList = ( { users } : Props ) => { return ( < ul > { users.map (( user ) => ( < li key = { user.id } > < UserCard user = { user } / > < /li > )) } < /ul > ) } const UserCard = ( { user } : Props ) => { return ( < div > < p > { user.id } < /p > < p > { user.name } < /p > < /div > ) } このように実装すれば、UserCardListは子要素をLiタグでくくっているので、UserCardがどんな要素でも問題なく、UserCardも親がUlでもdivでも問題ありません。 このように「親や子の要素をお互いが知っている」という状況を減らすことができれば、より汎用的なコンポーネントとなります。 おわりに 今回は 共通UIコンポーネント について書かせていただきました。 この内容は 共通UIコンポーネント の実装以外にも活かせる考えだと思います。 コンポーネントが、どこでどうやって使われるのか、今後どのように拡張する可能性があるのか、今の実装だと今後こういう時に困るのではないか?など、実装時点では分からないことは多いですが、デザイナーとコミュニケーションを多く取るなど、出来るだけ多くの情報を集め、対応できる幅が増えるように実装することを自分も意識し続けていきたいと思います。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します! 株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 FANTSの開発技術・開発組織を紹介します! また スタメン Engineering Handbook として体系的にまとめられたページも公開していますので、こちらも是非ご覧ください。
アバター
目次 はじめに 使用量監視の課題 LambdaとCloudWatchを用いた監視方法 名前空間別にSPICE使用量を集計してCloudWatchにメトリクスを送信するLambda関数 CloudWatchアラート、ダッシュボードの作成 まとめ はじめに こんにちは、スタメンの田中、若園です。 こちらの記事 でカスタムダッシュボード機能の全体像を紹介しました。🎉 この記事では、続編その2としてカスタムダッシュボード機能におけるLambdaとCloudWatchを活用したマルチテナント別のSPICEデータ使用量の監視方法と実装について紹介していきます。 使用量監視の課題 名前空間を用いたQuickSightのマルチテナント環境を運用するに当たり、SPICEの使用量に関して以下の課題がありました。 ①名前空間別にSPICEの使用量を把握したい。 1つのAWSアカウントのQuickSightでは、SPICEは全名前空間で共通して使用されるため、QuickSightコンソールや既存のAPIでは、名前空間別のSPICE使用量を取得することができません。 ②意図せず大容量のデータセットが作成され、特定の名前空間でSPICE容量を大幅に専有された際に気づくことができるようにしたい。 そこで、上記の課題が解決でき、SPICE使用量が無料枠内に収まっているかを監視できる仕組みを実装しました。 (ちなみに、QuickSightでは、AUTHORアカウント1人につき、SPICEの10GB分が無料枠 *1 として付与されます。) LambdaとCloudWatchを用いた監視方法 前述の課題の解決策として、下図の構成で監視の仕組みを実装しました。 EventBridgeによる定期実行 ルールを設定し、毎日6時間毎にLambda関数を起動しています。 名前空間別にSPICE使用量を集計してCloudWatchにメトリクスを送信するLambda関数の実装 ListNamespaces APIとListDataSets APIを用いて、全名前空間および全データセットの情報を取得します。 DescribeDataSet APIとDescribeDataSetPermissions APIを使用し各データセット毎のSPICE容量と属している名前空間を確認します。 ListUsers APIを用いてAUTHORアカウント数を取得します。 上記で取得した情報から名前空間別にSPICE使用量を集計、AUTHOR1人あたりのSPICE使用量を算出し、CloudWatchにメトリクスを送信します。 CloudWatchアラートの設定・ダッシュボードの作成 閾値を超えた場合のアラートの設定とダッシュボードでの見える化を行っています。 名前空間別にSPICE使用量を集計してCloudWatchにメトリクスを送信するLambda関数 Lambda関数のコードを記載しておきます。 require ' json ' require ' aws-sdk ' def handler ( event : nil , context : nil ) @quicksight_client = Aws :: QuickSight :: Client .new( region : ENV [ ' REGION ' ]) @cloudwatch_client = Aws :: CloudWatch :: Client .new( region : ENV [ ' REGION ' ]) # 名前空間の一覧を取得 list_namespaces = get_list_namespaces # データセット一覧を取得 list_data_sets = get_list_data_sets data_sets_per_company = {} list_namespaces.each do |namespace| # 名前空間は company-#{company_id}の形式で作成している company_id = match[ 0 ].match( /\d+/ )[ 0 ] data_sets_per_company[company_id] = [] end # company_idをkeyとして、企業毎のデータセットの一覧を配列で持つ、Hashを作成する # DescribeDataSet APIでデータセット毎の使用量を取得することが出来る data_sets_per_company = classification_per_company(list_data_sets, data_sets_per_company) data_sets_per_company.keys.each do |company_id| total_bytes = data_sets_per_company[company_id].map do |data_set| data_set[ :consumed_spice_capacity_in_bytes ] end .sum total_gigabytes = convert_gigabytes(total_bytes) author_count = get_author_count( " company- #{ company_id }" ) put_metric_data(company_id, total_gigabytes, author_count) end end def classification_per_company (list_data_sets, data_sets_per_company) list_data_sets.each do |data_set| response_data_set = @quicksight_client .describe_data_set( aws_account_id : ENV [ ' AWS_ACCOUNT_ID ' ], data_set_id : data_set.data_set_id ).data_set response_data_set_permissions = @quicksight_client .describe_data_set_permissions( aws_account_id : ENV [ ' AWS_ACCOUNT_ID ' ], data_set_id : data_set.data_set_id ).permissions next if response_data_set_permissions.empty? # principal: arn:aws:quicksight:{region}:{aws_account_id}:user/#{namespace}/user_nameの形式で取得できる namespace = response_data_set_permissions[ 0 ].principal.split( ' / ' )[ 1 ] data_set_hash = { data_set_id : response_data_set.data_set_id, data_set_name : response_data_set.name, consumed_spice_capacity_in_bytes : response_data_set.consumed_spice_capacity_in_bytes } company_id = namespace.match( /\d+/ )[ 0 ] data_sets_per_company[company_id] << data_set_hash end data_sets_per_company end def get_author_count (namespace) response_list_data_sets = @quicksight_client .list_users( aws_account_id : ENV [ ' AWS_ACCOUNT_ID ' ], namespace : namespace ) response_list_data_sets.user_list.count { |user| user.role == ' AUTHOR ' || user.role == ' ADMIN ' } end def convert_gigabytes (bytes) bytes.fdiv( 1024 * 1024 * 1024 ) end def put_metric_data (company_id, total_gigabytes, author_count) data = { namespace : ' tunag/quicksight ' , metric_data : [ { metric_name : ' spice_usage ' , value : total_gigabytes, unit : ' Gigabytes ' , dimensions : [ { name : ' company_id ' , value : company_id } ] } ] } data[ :metric_data ] << { metric_name : ' spice_usage_per_author ' , value : total_gigabytes.fdiv(author_count), unit : ' Gigabytes ' , dimensions : [ { name : ' company_id ' , value : company_id } ] } @cloudwatch_client .put_metric_data(data) end CloudWatchアラート、ダッシュボードの作成 下図のようにアラートとダッシュボードを作成しました。 アラート ダッシュボード まとめ 以上が、LambdaとCloudWatchを活用したマルチテナント別にSPICEデータ使用量の監視方法の内容となります。 QuickSightを利用していて運用を効率化したいと考えている方にとって、少しでも参考になれば幸いです。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア 弊社開発部門では、開発体制や開発の流れ、採用している技術をエンジニアリングハンドブックにまとめております。ご興味がある方は下記のリンクからぜひご覧ください。 stmn, inc. Engineering Handbook *1 : Amazon QuickSight の料金
アバター
目次 はじめに データ更新のニーズと課題について API経由でのSPICEデータの更新方法 自動更新のアーキテクチャ Lambda関数のコード まとめ はじめに こんにちは、スタメンの田中、若園です。 こちらの記事 でカスタムダッシュボード機能の全体像を紹介しました。🎉 この記事では、続編その1として、カスタムダッシュボード機能におけるLambda関数を活用したQuickSightのSPICEデータ更新の方法と実装について紹介していきます。 データ更新のニーズと課題について 分析の元となるデータは、 処理速度やコストのメリット を考慮しSPICEにデータを格納しています。 データ更新においては下記のニーズがありました。 ① 日次の定期更新 毎日 TUNAG 上に利用データが蓄積されるため、最新の利用データがSPICEデータに毎日反映されるようにし、分析できるようにしたい。 ③「更新ボタン」からの即時更新 ユーザー情報、部署の所属情報、 セグメント機能 で設定したセグメントの対象の変更などが発生した場合に即時に分析に反映したい。 即時の更新をTUNAGの管理画面からユーザー操作で行えるようにしたい。 これらのニーズがある中で、QuickSightコンソールで提供されている機能を利用してデータ更新が行えないか検討しましたが、下記のような課題がありました。 ① 日時の定期更新 スケジュール更新機能 は、QuickSightコンソールから都度設定を行う必要があり、工数がかかる。 データセット同士の結合が頻繁に発生するため、スケジュール設定の漏れが懸念される。 ②「更新ボタン」からの即時更新 即時で変更を反映するためには、QuickSightコンソールで手動更新を行う必要があり、工数がかかる。 今後導入の拡大を狙う上で、QuickSightコンソール上での作業による工数がボトルネックになることが懸念されました。 そのためQuickSightから提供されているAPIを使用し、独自で自動更新の仕組みを実装することとしました。 API経由でのSPICEデータの更新方法 API経由でのSPICEデータの更新方法は下記の2通りあります。 ① CreateIngestion API SPICEデータの更新処理を開始する。 24時間あたり32回までの サービスクォータ が設定されている。 ② UpdateDataSet API データセット更新処理後に、SPICEデータの更新が実行される。 サービスクォータを考慮して、 UpdateDataSet API を使用して自動更新を実行するLambda関数を実装しました。 (サービスクォータを引き上げて欲しい。。。) 自動更新のアーキテクチャ 自動更新の実現に向けた課題 自動更新の実現に向けて下記の課題がありました。 ① 親子関係を持つデータセット群の更新において、親データセットを更新した後に子データセットを更新するようにしたい。 カスタムダッシュボード機能では、例えばユーザー情報などのマスターデータと投稿などのトランザクションデータを結合して、ユーザー別の投稿数などといったデータセットを作成しています。 結合済みのデータセットとマスター/トランザクションデータとの間に下図のような親子関係が発生します。データの中身を最新にするに当たり、親データセットを更新した後に、子データセットを更新する必要がありました。 ② SPICEデータ更新時にAuroraもしくはAthenaへの多重リクエストを最小限にしたい。 更新時にAuroraもしくはAthenaに対してデータをクエリするため、全てのデータセット更新を実行した場合の負荷や同時接続数が懸念されました。 Lambda関数を活用したアーキテクチャの概要 前述の課題解決にあたり、下図のように2つのLambda関数を組み合わせた構成で自動更新フローを構築しました。 2種類の更新方法について、それぞれ流れを紹介します。 日時の定期更新 日時の定期更新はEventBridgeでルールを設定し、毎朝早朝に更新が実行されるようにしています。 EventBridgeから、ECSタスクを起動し、一部データをQuickSightで扱う形式に変換する処理の実行を行います。 その後、QuickSight上のデータセット同士の依存関係の解決を行う関数(以下、関数その1)が起動されます。 関数その1では、データセット同士の依存関係を解決し、データセットIDを更新順に配列化します。 その後、データセット更新用のLambda関数(以下、関数その2)を起動し、更新順の情報を渡します。 関数その2は、1番目に更新するデータセットの更新をUpdateDataSet APIで実行し、更新が完了するまで待ちます。 更新が完了したら、更新順の配列情報と次の更新対象を示すインデックス情報を渡し、次のデータセット用の関数その2を起動します。 そして、最後のデータセットの更新が終わると、SNS経由でTUNAGに更新完了を通知します。 「更新ボタン」からの即時更新 即時更新は、TUNAG上の更新ボタンを押下することで実行されます。その後の流れは定期更新と同じとなります。 即時更新の場合では、更新対象がマスターデータと結合済みのデータセットの2種類のみとなるように絞り込みをしており、データ量が多いトランザクションデータは、日時で定期更新される前日分までの利用データを分析で使用するようにしています。 このように関数その1, その2を組み合わせて、データセット同士の依存関係の解決と更新順に順次実行することで、前述の課題を解消しました。 Lambda関数のコード 最後にLambda関数のコードを記載しておきます。 関数その1 require ' json ' require ' aws-sdk ' def handler ( event : nil , context : nil ) @quicksight_client = Aws :: QuickSight :: Client .new( region : ENV [ ' REGION ' ]) # 企業IDを元にデータセットをフィルタリングする company_id = event[ ' company_id ' ] # 日時の定期更新なのか、「更新ボタン」からの即時更新なのかを判定するため # daily: 日時の定期更新 # manually: 「更新ボタン」からの即時更新 update_type = event[ ' update_type ' ] # ListDataSets APIから企業毎にデータセットを絞り込む list_data_sets = list_data_sets_for(company_id) # 依存関係をDescribeDataSet APIから取得して、Hashを生成 unresolved_hash = generate_unresolved_hash(list_data_sets) # 依存関係を解決する resolved_data_set_ids = resolve_recursively(unresolved_hash) # トランザクションデータはdata_set_idの末尾に、transactionを付与している # 即時更新の場合はマスターデータと結合済みのデータセットに絞り込む if update_type == ' manually ' resolved_data_set_ids = resolved_data_set_ids.reject { |data_set_id| data_set_id.end_with?( ' transaction ' ) } end # 更新処理を行う関数その2を呼び、配列の0番地から開始する invoke_lambda_function(resolved_data_set_ids, company_id, update_type) end private def generate_unresolved_hash (list_data_sets) unresolved_hash = {} list_data_sets.each do |data_set| resp = @quicksight_client .describe_data_set( aws_account_id : ENV [ ' AWS_ACCOUNT_ID ' ], data_set_id : data_set.data_set_id ) dependent_ids = resp.logical_table_map.map do |_, v| # 未結合のデータセットはdata_set_arnがnilが返る next if v.source.data_set_arn.nil? # 結合で生成されたデータセットは、arnを取得することが出来るため、id値のみ取得する v.source.data_set_arn.split( ' / ' ).last end .compact! unresolved_hash[data_set.data_set_id] = dependent_ids end unresolved_hash end def resolve_recursively (unresolved_hash) resolved_array = [] unresolved_hash.each do |data_set_id, dependent_ids| resolve(data_set_id, dependent_ids, resolved_array, unresolved_hash) end resolved_array end def resolve (data_set_id, dependent_ids, resolved_array, unresolved_hash) return if resolved_array.include?(data_set_id) # 更新順序の依存関係を持たないデータセットの場合 return resolved_array.unshift(data_set_id) if dependent_ids.empty? # 更新順序の依存関係を持つデータセットの場合、依存先のデータセットを依存配列に含めるように再帰処理を行う dependent_ids.each do |dependent_id| if resolved_array.include?(dependent_id) && !resolved_array.include?(data_set_id) resolved_array.push(data_set_id) next else resolve(dependent_id, unresolved_hash[dependent_id], resolved_array, unresolved_hash) end end resolved_array.push(data_set_id) unless resolved_array.include?(data_set_id) end def invoke_lambda_for_start_to_update_datasets (data_set_ids, company_id, update_type) lambda_client = Aws :: Lambda :: Client .new( region : ENV [ ' REGION ' ]) lambda_client.invoke( function_name : ' 関数その2 ' , invocation_type : ' Event ' , log_type : ' None ' , payload : JSON .generate( index : 0 , # 最初は0番地を指定 data_set_ids : data_set_ids, company_id : company_id, update_type : update_type ) ) end 関数その2 require ' json ' require ' aws-sdk ' def handler ( event : nil , context : nil ) company_id = event[ ' company_id ' ] # データセットの更新順の配列 data_set_ids = event[ ' data_set_ids ' ] # 更新対象を示すインデックス情報 index = event[ ' index ' ] # 日時の定期更新なのか、「更新ボタン」からの即時更新なのかを判定するため # daily: 日時の定期更新 # manually: 「更新ボタン」からの即時更新 update_type = event[ ' update_type ' ] # 更新対象のデータセット data_set_id = data_set_ids[index] @quicksight_client = Aws :: QuickSight :: Client .new( region : ENV [ ' REGION ' ]) # データセットの存在確認 response_describe_data_set = @quicksight_client .describe_data_set( aws_account_id : ENV [ ' AWS_ACCOUNT_ID ' ], data_set_id : data_set_id ).data_set # データセットの更新 resp_update_data_set = @quicksight_client .update_data_set( aws_account_id : ENV [ ' AWS_ACCOUNT_ID ' ], data_set_id : data_set_id, name : response_describe_data_set.name, import_mode : ' SPICE ' , physical_table_map : response_describe_data_set.physical_table_map, logical_table_map : response_describe_data_set.logical_table_map ) # SPICEインポートの進捗確認 ingestion_status = wait_ingestion( resp_update_data_set.data_set_id, resp_update_data_set.ingestion_id ) if ingestion_status == ' COMPLETED ' success_update( index, data_set_ids, company_id, update_type, { data_set_id : data_set_id, name : response_describe_data_set.name, company_id : company_id, lambda_function_name : context.function_name, lambda_request_id : context.aws_request_id, lambda_log_stream_name : context.log_stream_name } ) else # SNS経由でエラー通知 end end def wait_ingestion (data_set_id, ingestion_id) waiting = true while waiting resp_describe_ingestion = @quicksight_client .describe_ingestion( aws_account_id : ENV [ ' AWS_ACCOUNT_ID ' ], data_set_id : data_set_id, ingestion_id : ingestion_id ) ingestion_status = resp_describe_ingestion.ingestion.ingestion_status case ingestion_status when ' FAILED ' , ' COMPLETED ' waiting = false else sleep 3 end end ingestion_status end # 最後のデータセット更新の場合は、SNS経由で更新完了を通知 # それ以外は、次のデータセット更新を実行する def success_update (index, data_set_ids, company_id, update_type, message) if last_update?(data_set_ids, index) # SNS経由で更新完了を通知 else invoke_lambda_for_next_dataset(index, data_set_ids, company_id, update_type) end end def last_update? (data_set_ids, index) data_set_ids.length == index + 1 end def invoke_lambda_for_next_dataset (index, data_set_ids, company_id, update_type) lambda_client = Aws :: Lambda :: Client .new( region : ENV [ ' REGION ' ]) lambda_client.invoke( function_name : ' 関数その2 ' , invocation_type : ' Event ' , log_type : ' None ' , payload : JSON .generate( index : index + 1 , data_set_ids : data_set_ids, company_id : company_id, update_type : update_type ) ) end まとめ 以上が、QuickSight SPICEデータのLambda関数を用いた自動更新処理の内容となります。 現在QuickSightの利用していて運用を効率化したいと考えている方にとって、少しでも参考になれば幸いです。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア 弊社開発部門では、開発体制や開発の流れ、採用している技術をエンジニアリングハンドブックにまとめております。ご興味がある方は下記のリンクからぜひご覧ください。 stmn, inc. Engineering Handbook
アバター
目次 はじめに カスタムダッシュボードの概要 カスタムダッシュボードのアーキテクチャ データセットの結合 アプリケーションへの埋め込み セグメント機能 まとめ はじめに こんにちは、スタメンのチームねぎまの近藤、滿本です。 以前、「 名前空間を用いたQuickSight上でのマルチテナントの実現 」というブログでご紹介した技術を用い、カスタムダッシュボード機能を開発・リリースしました 🎉( TUNAG「カスタムダッシュボード」の提供を開始!!利用データを最大限活用し、エンゲージメント経営の実践を支援! ) 今回はその概要とアーキテクチャをお伝えさせていただきます。 カスタムダッシュボードの概要 カスタムダッシュボードの背景 弊社では TUNAG という組織向けのSNSを提供しており、投稿やコメントなどの利用データを扱っています。 導入企業様の利用年数が長くなるに連れて利用データは蓄積され、利用状況として可視化することでその価値はより一層高まります。 TUNAGでは管理者様向けにそれらの利用状況をグラフ化したダッシュボード機能を提供していますが、全ての企業で同じ指標となっており、各社で異なる「○○の指標を△△の対象者で絞り込んでグラフ化したい」というニーズに応えられていない状況でした。 そのような背景から、企業ごとに分析したい指標・対象者をカスタマイズできるカスタムダッシュボード機能の開発に着手しました。 カスタムダッシュボードで出来ること 前述のとおり、カスタムダッシュボードでは会社ごとに異なる指標を特定の対象者に絞り込んでグラフにできます。 また、QuickSightはグラフの種類が豊富でフィルタリングもできるため、利用状況を最適な形で表現できます。 カスタムダッシュボードのアーキテクチャ カスタムダッシュボードをどう実現するか議論する中で、出来る限り早く機能提供したいという思いからBIツールを利用することにしました。 いくつかのツールを比較検討する中で、コスト面に加えて、TUNAGが既にAthenaやAuroraを利用しており親和性が高かったため QuickSight を採用することに決めました。 構成は以下のとおりです。 Athena、Auroraから各データ(投稿、コメントなど)のデータセットを作成 グラフ用にデータセットを結合(部署 ✕ 投稿 👉 部署別の投稿数) それらのデータセットを元に分析、ダッシュボードを作成 TUNAGの管理画面に、QuickSightのコンソールを権限別で埋め込み データセットを最新にするため、更新をEventBridgeにより日次で定期実行 or 手動で実行(Lambdaにリクエスト) Lambdaによりデータセットを更新 この構成は、カスタムダッシュボード機能を数社で検証する中で見えてきた以下のニーズを基に、徐々に固めていきました。 出来る限り柔軟に指標と対象者を組み合わせたい 👉 データセットの結合機能を利用 TUNAG上でグラフを編集したい 👉 アプリケーションへの埋め込み 対象者を細かく絞って分析したい 👉 セグメント機能(後述します)の開発 順番に説明します。 データセットの結合 検証では、「ユーザー別の投稿数」といった指標を1データセットとして用意していました。しかし、カスタムダッシュボードを運用する中で、新しい指標の要望が常に挙がってきます。それらの要望に答えるために、QuickSightのデータセットの結合機能を用いることにしました。これにより自由にデータセットを組み合わせて、グラフの基になるデータセットを作成できるようになりました。データセットは2つに分類して運用しています。 マスターデータ トランザクションデータ マスターデータ 一般的に、マスターデータとは基礎情報となるデータを指します。ユーザー、部署、役職といったデータが該当します。データソースはAuroraを使用し、SPICEにデータを保存しています。 トランザクションデータ 発生した出来事の詳細を記録したデータは、トランザクションデータと呼ばれます。トランザクションデータの例は、「投稿数」や「投稿の既読数」などです。トランザクションデータは、データベースの負荷を考慮し、AuroraではなくAthenaをデータソースとして使用しています。 マスタデータとトランザクションデータをQuickSight上で結合させることで、より多くの指標の組み合わせを表現できるようになりました。 アプリケーションへの埋め込み TUNAGアプリケーションへのQuickSightの埋め込みは AWS SDK for Ruby、Amazon QuickSight Embedding SDK を利用しました。 埋め込み用のURLを取得 埋め込み用のURLは、 generate_embed_url_for_registered_user を用いて取得します。 def generate_embed_url_for_registered_user () resp = @client .generate_embed_url_for_registered_user({ aws_account_id : AWS_ACCOUNT_ID , user_arn : user_arn, session_lifetime_in_minutes : 600 }) resp[ :embed_url ] end 取得したURLをAmazon QuickSight Embedding SDKを用いて埋め込む 取得したURLは、 Amazon QuickSight Embedding SDK を用いて埋め込みます。 export const QuickSightEmbed = () => { useEffect(() => { const containerDiv = document .getElementById( 'embeddedCustomDashBoardContainer' ) QuickSightEmbedding.embedDashboard( { url: "取得したURL" , container: containerDiv, scrolling: "yes" , height: "700px" , width: "1000px" , locale: "ja-JP" , } ) } , [] ) return ( <EmbedContainer id= "embeddedCustomDashBoardContainer" /> ) } ) カスタムダッシュボードでは、2種類のアカウントを提供しています。 READER:ダッシュボードの閲覧が可能 AUTHOR:ダッシュボードの閲覧及び新規グラフの作成・編集が可能 ユーザーは権限に応じて、TUNAGアプリケーションに埋め込まれたQuickSightを利用できます。 セグメント機能 セグメント機能とは、データを特定の対象者に絞り込んで分析できる機能です。 例えば「投稿数」というトランザクションデータを「新卒者」というセグメントと掛け合わせると、「新卒者の投稿数」がグラフとして表現できます。 アプリケーションで任意のセグメントをマスターデータとして作成 セグメントのマスターデータを基にQuickSight上でデータセットを作成 投稿数などのトランザクションデータと結合して利用 TUNAGでは各社ごとに利用状況から課題を抽出し、新たなTUNAGの利活用の方針を立てるため、こうした任意のセグメントによる利用状況の分析が重要になります。 まとめ 以上が、カスタムダッシュボード機能の概要とアーキテクチャになります。最後までご覧いただきありがとうございました。この記事がQuickSightとアプリケーションの連携を検討されている方にとって、少しでも参考になれば幸いです。 弊社ではエンジニアを募集しています。 やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア 開発体制や事業内容をまとめた Engineering Handbook も併せてご覧ください。 参考 データの結合 Amazon QuickSight コンソールへのアクセスのカスタマイズ 登録済みユーザー向けの Amazon QuickSight コンソールの全機能の埋め込み
アバター
こんにちは、TUNAG事業部のカーキ ( @khaki_ngy )です。 普段は TUNAG の iOS/Android などのモバイルアプリの開発を行なっています。 今回は TUNAG の Android アプリ開発における Android View の開発での Jetpack Compose を使ったレイアウトプレビューの活用について、Jetpack Composeの導入理由と併せて紹介していきます。 Jetpack Compose の導入 TUNAG の Android アプリでは Jetpack Compose の安定版がリリースされた2021年末から Jetpack Compose の導入を開始し、新規の View に関しては極力 Jetpack Compose を利用して UI の構築を行なっていました。 当時の背景としては、下記の3点を狙いとしていました。 よりスピーディーな新機能の提供 レイアウトプレビューによる開発体験の向上 今後のUI開発のバラダイムが変わっていくことを見越して よりスピーディーな新機能の提供 また Jetpack Compose により宣言的にUIを作成することができるため、直感的に記述することができるようになります。 TUNAG アプリは以前から画面の部品単位でコンポーネントに分けて開発を進めていましたが、Android View を使った開発では一つのコンポーネントを作成するのにレイアウトファイルをいくつも作成しないといけないなど、煩雑さを感じていました。 レイアウトプレビューによる開発体験の向上 Jetpack Compose を導入したことにより作成したコンポーネントをプレビューすることができるようになりました。 以前までの Android View を使ったコンポーネント開発では、作成したコンポーネントの振る舞いが意図した通りになっているかどうかの担保は、テスト用の Activity を作成して、そこに配置してテストをおこなっていました。そのためコンポーネントの開発にプラスして、コンポーネントの確認のために時間と工数を費やしていました。 Jetpack Compose のプレビューには、インタラクティブプレビューやアニメーションプレビューなどの動的な動きを確認する仕組みもあり、かつておこなっていたような検証方法は不要になりました。 今後のAndroidのUI開発のパラダイムが変わっていくことを見越して 宣言型UIフレームワークはモバイルでは Flutter や SwiftUI 、Webフロントエンドの領域では React など、現代のフロントエンド開発における技術トレンドになってきています。 ここ数年での技術の成長や技術コミュニティの成熟などを見るに、この傾向は不可逆なものだと考えています。 導入と現状 現在、新しい画面や新しいコンポーネントの開発には積極的に Jetpack Compose を採用しています。 ただ TUNAG のモバイルグループの開発メンバーのリソースは限られ、既存の View を Jetpack Compose に全て置き換えることはできていません。 本来であれば、既存機能の改修のタイミングで既存コンポーネントのComposable 関数への移行もおこなっていきたいのですが、現状そこまでは取り組むことはできていません。 そんな中で Android View を使用した開発でもJetpack Composeによるプレビューによる恩恵を得たいと考えたのが今回の事例になります。 次項では、ブログタイトルにもある Jetpack Compose のプレビューで AndroidView で作成したコンポーネントをプレビューした事例に関して、背景を踏まえて紹介していきます。 チャット返信機能プロジェクト 直近で私達のチームで Android View のプレビュー化を行ったのは、春ごろにリリースをおこなった TUNAG のチャットの返信機能プロジェクトでした。 TUNAG では制度の利用やその閲覧を行うタイムラインの他にビジネス用のチャット機能も提供しています。この春に追加機能としてチャットでの引用返信や動画送信の機能の開発を行いました。 TUNAGチャットのデモ画像 このプロジェクトでメッセージの表示パターンに引用返信が増えるのに当たって、既存のメッセージのレイアウト自体を改修することになりました。 ここでの改修の際に全て Composable 関数で書き換えるという方法もありましたが、開発期間が短く、着手当時は自分たち自身もそこまで Jetpack Compose に詳しくなかったため、現状動いている Android View をベースに改修を行うことになりました。 Jetpack Compose のプレビューを使用した開発 チャットのメッセージコンポーネントにはそのメッセージの状態や選択された時、引用元のメッセージを含む場合とで数十通りのパターンが存在していました。 今までは実行環境でその状態を作り、意図した通りに表示できているかを確認していましたが、これ以上パターンが増える場合は対応できないと判断しました。 そこで目をつけたのが以前から試験的に導入していた Jetpack Compose のプレビュー機能でした。 Android View で作成したものを Composable 関数でラップしプレビューを可能にすれば、現状の Android View で作成したものを活かしながら、プレビューにより網羅的に状態を把握することができます。 Android Viewで作成されたコンポーネントの Composable 関数として利用するのは、Jetpack Compose の公式ドキュメントとして公開されているこちらの Android Developers ドキュメントの相互運用 API を参考にして進めました。 TUNAGチャットで使用されるテキストメッセージのプレビュー 上の画像はチャット返信機能着手前の時点で36通りですが、これが返信機能によって倍の72通りに増えています。 プレビューの機能を使用したことで、これだけの状態のパターンを全て網羅的に把握することができるようになりました。 このメリットとしては、「View の網羅的な状態の確認ができる」に尽きると思います。 今回、Composable 関数でのプレビューが導入できたことで、デグレを発生させることなく、安心してリリースを行うことができました。 Compose のプレビューのみを使用するデメリット これまで良い面を紹介してきましたが、開発時や運用してしばらく経ってみて感じたデメリットがいくつかあるので紹介します。 Android View での状態の変化を随時 Composable 関数の方にも反映させないといけない 今回私たちが行なった手法では、実際にコンポーネントとして使用しているのでは AndroidView であり、プレビューしている Composable 関数自身ではありません。 そのため、Android View で状態が増えた場合に、随時 Composable 関数の方にも反映して同期を取っていかないと正しい状態をプレビューできているとはいえなくなってしまいます。 今後基本的にコンポーネントに関わる変更は、プレビューでの正しい表示により確認をしていくので、問題にはなっていないのですが、二重で管理する必要はあります。 ビルド時間が長い そもそも今回のプロジェクトで進めていたモジュール自体がかなり大きいということにも原因があると思いますが、今回の対応によりビルド時間が長くなりました。 正確に測っているわけではないですが、体感で感じるレベルで長くなりました。 これは純な Composable 関数ではなく、Android View をラップしたものを扱っているところにも原因があると感じています。 上記のようなデメリットはありますが、それを踏まえた上でも Android View で開発中のコンポーネントの確認ができる今回の手法は有用だと思っています。 開発中にこのプレビュー機能を使用したことで、何度も View の不整合に気付くことができ、即座に修正に取り掛かることができました。 それは今回のような改修プロジェクトで、 以前までの表示状態を担保しなければいけない という状況にも合っていました。 今後の展望 今後の展望としては、チャットメッセージに関わる全てのコンポーネントを Composable 関数に置き換えたいと考えています。 もちろん開発プロジェクトとの調整を取りながらにはなりますが、コンポーネント自体を Composable 関数にすることで上で紹介したデメリットもある程度解消されます。 また最初に Jetpack Compose をプロジェクトに導入した経緯でも紹介したように、Composable 関数として扱えることで管理が楽になり、より早く新機能・改善を行うことができると確信しています。 TUNAG のチャット機能はまだまだ機能追加される余地が残っているため、早期に Composable 関数への置き換えができれば、今後の開発をより効率的進めることができます。 今回は Android View で作成したコンポーネントを Composable 化してプレビュー可能にすることで開発を加速させた事例の紹介しました。 途中でも述べましたが、新規作成するコンポーネントに関しては Jetpack Compose を使用して作成しています! Jetpack Compose を使ってガシガシ開発をしたいという方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します! また弊社では、オンラインサロン用プラットホーム FANTS というサービスも開発・運営しております。 ご興味がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します! また スタメン Engineering Handbook として体系的にまとめられたページも公開しています。 こちらも是非ご覧ください。
アバター
はじめに こんにちは。株式会社スタメンで FANTS のエンジニアをしている @0906koki です。 今回の記事では、以前社内で実装した、デシリアライズする前の JSON:API フォーマットの型を、デシリアライズ後の型に変換する方法について書きたいと思います。 目次 はじめに 目次 TL;DR (概要) 👇 この JSON:APIフォーマットの型を... 👇 こう デシリアライズした型に変換する! JSON:API jsonapi-serializer でJSON:APIフォーマットのJSONを返却 返却されるレスポンスの例 なぜ実装したか 1. デシリアライズ後のフォーマットを考えて、型を実装する必要がある 2. MSW でモックデータを作成する際、型安全でないモックデータになる 最終的な型 型の説明 リレーションがない場合 リレーションがある場合 実際に運用してみて 最後に TL;DR (概要) 👇 この JSON:APIフォーマットの型を... type RawResponseType = { data: { id: string ; type : "user" ; attributes: { name: string ; } ; relationships: { drinks: { data: { id: string ; type : "drink" ; }[] ; } ; books: { data: { id: string ; type : "book" ; }[] ; } ; } ; included: | { id: string ; type : "drink" ; attributes: { name: string ; image_url: string ; created_at: string ; } ; }[] | { id: string ; type : "book" ; attributes: { title: string ; image_url: string ; published_at: string ; } ; }[] ; } ; } ; 👇 こう デシリアライズした型に変換する! type ResponseType = JsonApiRelationshipDeserializedType < RelationShipsApiType >; const Sample3: ResponseType = { id: '1' , name: "Nagai Koki" , drinks: [ { id: '1' , name: "Coke" , imageUrl: "https://avatars.githubusercontent.com/u/50698194?v=4" , createdAt: "2021/01/01" , } , ] , books: [ { id: '1' , title: "TDD" , imageUrl: "https://avatars.githubusercontent.com/u/50698194?v=4" , publishedAt: "2021/01/01" , } , { id: '2' , title: "DDD" , imageUrl: "https://avatars.githubusercontent.com/u/50698194?v=4" , publishedAt: "2021/01/02" , } , ] , } ; JSON:API FANTS ではサーバーサイドを Rails で実装しており、その API のシリアライザとして jsonapi-serializer を使用しています。 例えば、Movie モデルが has_many として Actor モデルを持ち、Movie の title と year、Movie のリレーション先である Actor の name を JSON で返したい場合、以下のようなコードを記述することで、 JSON:API のフォーマットで JSON 返却することができます。 jsonapi-serializer でJSON:APIフォーマットのJSONを返却 render json : MovieSerializer .new(movie).serializable_hash.to_json 返却されるレスポンスの例 { " data ": { " id ": " 1 ", " type ": " movie ", " attributes ": { " title ": " アナ雪 ", " year ": 2013 } , " relationships ": { " actors ": { " data ": [ { " id ": " 1 ", " type ": " actor " } , { " id ": " 2 ", " type ": " actor " } ] } } , " included ": [ { " id ": " 1 ", " type ": " actor ", " attributes ": { " name ": " クリステン・ベル " } } , { " id ": " 2 ", " type ": " actor ", " attributes ": { " name ": " イディナ・メンゼル " } } ] } } https://jsonapi.org/ に詳しく書かれていますが、JSON:API フォーマットでは、 type にリソース名、 attributes にそのリソースのデータ、 relationships と included はリレーション先のリソースデータを返します。 API のレスポンスが上記のような JSON の形で シリアライズされて返却されるため、クライアントサイドではこのフォーマットをデシリアライズして扱いやすい形に変換する必要があります。JavaScript であれば jsonapi-serializer 、swift であれば、 Japx など、簡単にデシリアライズできるライブラリが各クライアントに存在するので、基本的にはそれらを通してデシリアライズします。 ちなみに上記の JSON:API フォーマットの JSON データを jsonapi-serializer を使ってデシリアライズすると、以下のようなオブジェクトに変換されます。 const movie = { id: "1" , title: "アナ雪" , year: 2013 , actors: [ { id: "1" , name: "クリステン・ベル" , } , { id: "2" , name: "イディナ・メンゼル" , } , ] , } ; なぜ実装したか FANTS のフロントエンドでは、上記のように、 jsonapi-serializer を通して、JSON:API フォーマットのレスポンスデータをデシリアライズしていますが、主に 2 つの観点で、型の安全性の問題がありました。 デシリアライズした後のフォーマットを考えて、型を実装する必要がある MSW でモックデータを作成する際、型安全ではないモックデータになる 1. デシリアライズ後のフォーマットを考えて、型を実装する必要がある サーバーサイドが Rails で実装されており、API スキーマの型の自動生成、クライアントとの共有が出来ないので、フロントエンドエンジニアはサーバーサイドエンジニアと連携して、API スキーマを自前で実装する必要があります。この実装においてハブとしているのが、 stoplight という OpenAPI 定義ファイルを GUI で作成できるツールです。 stoplightのイメージ サーバーサイドエンジニアは、この stoplight 上で API スキーマを定義することで、フロントエンドエンジニアは stoplight を確認してフロントエンドの API レスポンスの型を実装します。 しかし、あくまで stoplight 上で定義されるのはデシリアライズされる前の JSON:API フォーマットであり、フロントエンドエンジニアが実際に使用するのはデシリアライズした型であるため、stoplight 上のスキーマからデシリアライズした型を以下のように自前で実装する必要があります。 type DeserializedResponseMovieType = { id: string ; title: string ; year: number ; actors: { id: string ; name: string ; }[] ; } ; 具体的に言うと、以下は axios の例ですが、axios の interceptors を使って、レスポンスとして返されるデータを先程上げた jsonapi-serializer を通してデシリアライズしており、そのデシリアライズされたデータをアプリケーション内で使います。 axiosInstance.interceptors.response.use (async ( response ) => { // ... const deserializedData = await new Deserializer ( { keyForAttribute: "camelCase" , } ) .deserialize ( response.data ); return { ...response , data: deserializedData } ; } ); jsonapi-serializer のデシリアライズ処理の返却データの値に型が付けば良いのですが、以下の通り Promise<any> で返ってきます。 export interface Deserializer { deserialize ( data: any , callback: Callback ) : void ; deserialize ( data: any ) : Promise < any >; } このように、デシリアライズした型を自前で実装するので、その変換した型にミスがある可能性があったり、実装自体にコストもかかるなど、安全性や実装コストの面でデメリットが存在します。 2. MSW でモックデータを作成する際、型安全でないモックデータになる FANTS では、フロントエンドのモックサーバーとして MSW を使用しています。MSW とは Service Worker を立てて、実際に API リクエストがあった際に、それに合致するモックデータをネットワークレベルでインタセプトして返却するライブラリです。 MSW ではテストを書く場合や、実際に API がない場合でもフロントエンドだけ先行して開発したい場合など、様々なシーンで使用していますが、MSW のモックデータは「ネットワークレベルでのモック = デシリアライズする前の JSON:API フォーマットの JSON データ」であるので、以下のように記述しており、型安全ではない状態になっていました。 const MOVIE_MOCK_DATA = { data: { id: "1" , type : "movie" , attributes: { title: "アナ雪" , year: 2013 , } , relationships: { actors: { data: [ { id: "1" , type : "actor" , } , { id: "2" , type : "actor" , } , ] , } , } , included: [ { id: "1" , type : "actor" , attributes: { name: "クリステン・ベル" , } , } , { id: "2" , type : "actor" , attributes: { name: "イディナ・メンゼル" , } , } , ] , } , } as const ; export const fetchMovie200Handler = rest. get( `/api/v1/movies/:id` , ( _ , res , ctx ) => res ( ctx. status( 200 ), ctx.json ( MOVIE_MOCK_DATA )) ); 例えば、レスポンスの型に変更があり、1 で書いたデシリアライズした後の型に変更を加えても、上記のモックデータには型が当たっていないので、静的チェックが出来ずコンパイルが通ってしまいます。 デシリアライズする前の型にも型注釈を書けば良いですが、レスポンスのデータに変更があった場合に、デシリアライズした後の型にも変更を加える必要があり 2 重メンテになるため避けたいです。 このように、デシリアライズする前と後でそれぞれ型安全なコードを書く必要があり、またレスポンスの変更に静的チェックで気付けるように、それぞれの型が相互に結びついている必要があります。 最終的な型 それらの問題点を解消するために、デシリアライズする前のレスポンスの型を、デシリアライズした型に変換する型を実装しました。 以下が最終的なコードです。 import { SnakeObjectToCamelType , ExtractArrayType , NarrowUnionObjectType , } from "@/types/utils" ; import { BaseJsonApiArrayType , BaseJsonApiType , BaseRelationshipApiType , BaseRelationshipApiArrayType , } from "./base" ; // JSON:API(Relationなし / Metaデータなし)の型変換 export type JsonApiDeserializedType < T extends BaseJsonApiType | BaseJsonApiArrayType > = T extends BaseJsonApiType ? SnakeObjectToCamelType < T [ "data" ][ "attributes" ] > & { id: string } : T extends BaseJsonApiArrayType ? SnakeObjectToCamelType < ExtractArrayType < T [ "data" ] > [ "attributes" ] & { id: string } > [] : never ; // JSON:API(Relationなし / Metaデータあり)の型変換 export type JsonApiDeserializedWithMetaType < T extends BaseJsonApiType | BaseJsonApiArrayType , MetaType extends Record < string , unknown > > = { data: JsonApiDeserializedType < T >; meta: SnakeObjectToCamelType < MetaType >; } ; // JSON:API(Relationあり / Metaデータなし)の型変換 export type JsonApiRelationshipDeserializedType < T extends BaseRelationshipApiType | BaseRelationshipApiArrayType > = JsonApiDeserializedType < T > & ( T extends BaseRelationshipApiType ? SnakeObjectToCamelType < { [ K in keyof T [ "data" ][ "relationships" ]] : SnakeObjectToCamelType < NarrowUnionObjectType < ExtractArrayType < T [ "data" ][ "included" ] >, "type" , ExtractArrayType < T [ "data" ][ "relationships" ][ K ][ "data" ] > [ "type" ] > [ "attributes" ] & { id: string } > [] ; } > : T extends BaseRelationshipApiArrayType ? SnakeObjectToCamelType < { [ K in keyof ExtractArrayType < T [ "data" ] > [ "relationships" ]] : SnakeObjectToCamelType < NarrowUnionObjectType < ExtractArrayType < ExtractArrayType < T [ "data" ] > [ "included" ] >, "type" , ExtractArrayType < ExtractArrayType < T [ "data" ] > [ "relationships" ][ K ][ "data" ] > [ "type" ] > [ "attributes" ] & { id: string } > [] ; } > [] : never ); // JSON:API(Relationあり / Metaデータあり)の型変換 export type JsonApiRelationshipDeserializedWithMetaType < T extends BaseRelationshipApiType | BaseRelationshipApiArrayType , MetaType extends Record < string , unknown > > = { data: JsonApiRelationshipDeserializedType < T >; meta: SnakeObjectToCamelType < MetaType >; } ; // JSON:APIのベース(Relationなし / dataがObject)の型 export type BaseJsonApiType = { data: { id: string ; type : string ; attributes: Record < string , unknown >; } ; } ; // JSON:APIのベース(Relationなし / dataがArray)の型 export type BaseJsonApiArrayType = { data: BaseJsonApiType [ "data" ][] ; } ; // JSON:APIのベース(Relationあり / dataがObject)の型 export type BaseRelationshipApiType < MetaType extends Record < string , unknown > = Record < string , unknown > > = { data: { id: string ; type : string ; attributes: Record < string , unknown >; relationships: Record < string , { data: { id: string ; type : string ; }[] ; } >; included: BaseJsonApiType [ "data" ][] ; } ; meta?: SnakeObjectToCamelType < MetaType >; } ; // JSON:APIのベース(Relationあり / dataがArray)の型 export type BaseRelationshipApiArrayType < MetaType extends Record < string , unknown > = Record < string , unknown > > = { data: BaseRelationshipApiType < MetaType > [ "data" ][] ; meta?: BaseRelationshipApiType < MetaType > [ "meta" ] ; } ; // SnakeCaseをCamelCaseへ変換(string) export type SnakeStringToCamelCaseType < T extends string > = T extends ` ${ infer R } _ ${ infer U } ` ? ` ${ R }${ Capitalize<SnakeStringToCamelCaseType<U>> } ` : T ; // SnakeCaseをCamelCaseへ変換(object) export type SnakeObjectToCamelType < T extends Record < string , unknown >> = { [ K in keyof T as ` ${ SnakeStringToCamelCaseType< string & K > } ` ] : T [ K ] extends Record < string , unknown > ? SnakeObjectToCamelType < T [ K ] > : T [ K ] extends Array < any > ? SnakeObjectToCamelType < ExtractArrayType < T [ K ] >> [] : T [ K ] ; } ; // NOTE: UnionのObjectから絞り込む // UnionObject : Union型のObject // UnionObjectKey : Objectを識別するプロパティ // UnionObjectKey : 絞り込みたいObjectのUnionObjectKeyの値 export type NarrowUnionObjectType < UnionObject extends Record < string , unknown >, UnionObjectKey extends keyof UnionObject , UnionObjectValue extends UnionObject [ UnionObjectKey ] > = UnionObject extends { [ Key in UnionObjectKey ] : UnionObjectValue } ? UnionObject : never ; // Array<T>のTを取得 export type ExtractArrayType < T > = T extends ( infer U ) [] ? U : T ; 例として、先程のデシリアライズされる前のムービーのレスポンスの型を、以下のように上記の型を通してデシリアライズした型を生成します。 // デシリアライズする前のJSON:APIフォーマットの型 type RawMovieResponseType = { data: { id: string ; type : "movie" ; attributes: { title: string ; year: number ; } ; relationships: { actors: { data: { id: string ; type : "actor" ; }[] ; } ; } ; included: { id: string ; type : "actor" ; attributes: { name: string ; } ; }[] ; } ; } ; // デシリアライズした後の型 type DeserializedMovieResponseType = JsonApiRelationshipDeserializedType < RawMovieResponseType >; // 👇 静的チェックが無事通る const movie: DeserializedMovieResponseType = { id: "1" , title: "アナ雪" , year: 2013 , actors: [ { id: "1" , name: "クリステン・ベル" , } , { id: "2" , name: "イディナ・メンゼル" , } , ] , } ; 型の説明 まず、FANTS で使用している JSON:API フォーマットとして、大きく分けて 4 つありました。 対象リソースが単一で、リレーションがない場合 JSON:API フォーマットで言う、 data がオブジェクトで、 relationships , included がない場合 対象リソースが複数で、リレーションがない場合 JSON:API フォーマットで言う、 data が配列で、 relationships , included がない場合 対象リソースが単一で、リレーションがある場合 JSON:API フォーマットで言う、 data がオブジェクトで、 relationships , included がある場合 対象リソースが複数で、リレーションがある場合 JSON:API フォーマットで言う、 data が配列で、 relationships , included がある場合 JSON:API フォーマットとしては、 links やリレーション先が単一リソースである場合 など、上記以外にも様々なエッジケースが想定されますが、FANTS では上記の 4 つが主なユースケースであるため、今回デシリアライズする対象としても 4 つに絞りました。 また、前提として、サーバーサイドで返却される JSON データはスネークケースであり、フロントエンドで扱うフォーマットは基本的にキャメルケースであるので、スネークケースからキャメルケースへ変換する処理も加える必要もあります。 それらを踏まえた上で、まず最初に、1, 2 の「リレーションがない場合」の変換について説明します。 リレーションがない場合 リレーションがない場合は、以下の型がデシリアライズする型となります。 export type JsonApiDeserializedType < T extends BaseJsonApiType | BaseJsonApiArrayType > = T extends BaseJsonApiType ? SnakeObjectToCamelType < T [ "data" ][ "attributes" ] > & { id: string } : T extends BaseJsonApiArrayType ? SnakeObjectToCamelType < ExtractArrayType < T [ "data" ] > [ "attributes" ] & { id: string } > [] : never ; リレーションがない場合は単純で、ジェネリクスで JSON:API 形式の型を受け取り、Conditional Types を使って、data がオブジェクトであるか、配列であるかの条件によって分岐しています。 オブジェクトである場合は、 attributes の型と id の型 を intersection して、それらをキャメルケースへ変換しています。 キャメルケースへ変換する処理に関しては、Mapped Types の Key Remapping を使ってオブジェクトの Key をスネークケースからキャメルケースへ変換し、それに対応する値は、オブジェクトか配列の場合に Recursive Conditional Types を使って再帰的にスネークケースの key をキャメルケースへ変換しています。 // SnakeCaseをCamelCaseへ変換(string) export type SnakeStringToCamelCaseType < T extends string > = T extends ` ${ infer R } _ ${ infer U } ` ? ` ${ R }${ Capitalize<SnakeStringToCamelCaseType<U>> } ` : T ; // SnakeCaseをCamelCaseへ変換(object) export type SnakeObjectToCamelType < T extends Record < string , unknown >> = { [ K in keyof T as ` ${ SnakeStringToCamelCaseType< string & K > } ` ] : T [ K ] extends Record < string , unknown > ? SnakeObjectToCamelType < T [ K ] > : T [ K ] extends Array < any > ? SnakeObjectToCamelType < ExtractArrayType < T [ K ] >> [] : T [ K ] ; } ; リソースが複数 ( data が配列) である場合も基本的には同じです。 以下のような、 ExtractArrayType を定義して、 T["data"]<U> の U を取得することで、リソースが単一である場合と同じように T["data"]["attributes"] と { id: string } を intersection させています。 type ExtractArrayType < T > = T extends ( infer U ) [] ? U : T ; 以下がサンプルデータとなります。 // デシリアライズする前のJSON:API(Object)フォーマットの型 type RawMovieResponseType = { data: { id: string ; type : "movie" ; attributes: { title: string ; year: number ; } ; } ; } ; // デシリアライズする前のJSON:API(Array)フォーマットの型 type RawMoviesResponseType = { data: { id: string ; type : "movie" ; attributes: { title: string ; year: number ; } ; }[] ; } ; // 👇 静的チェックが無事通る const movie: JsonApiDeserializedType < RawMovieResponseType > = { id: '1' title: 'アナ雪' , year: 2013 } // 👇 静的チェックが無事通る const movies: JsonApiDeserializedType < RawMoviesResponseType > = { id: '1' title: 'アナ雪' , year: 2013 }[] リレーションがある場合 リレーションがある場合は、以下の型がデシリアライズする型となります。 export type JsonApiRelationshipDeserializedType < T extends BaseRelationshipApiType | BaseRelationshipApiArrayType > = JsonApiDeserializedType < T > & ( T extends BaseRelationshipApiType ? SnakeObjectToCamelType < { [ K in keyof T [ "data" ][ "relationships" ]] : SnakeObjectToCamelType < NarrowUnionObjectType < ExtractArrayType < T [ "data" ][ "included" ] >, "type" , ExtractArrayType < T [ "data" ][ "relationships" ][ K ][ "data" ] > [ "type" ] > [ "attributes" ] & { id: string } > [] ; } > : T extends BaseRelationshipApiArrayType ? SnakeObjectToCamelType < { [ K in keyof ExtractArrayType < T [ "data" ] > [ "relationships" ]] : SnakeObjectToCamelType < NarrowUnionObjectType < ExtractArrayType < ExtractArrayType < T [ "data" ] > [ "included" ] >, "type" , ExtractArrayType < ExtractArrayType < T [ "data" ] > [ "relationships" ][ K ][ "data" ] > [ "type" ] > [ "attributes" ] & { id: string } > [] ; } > [] : never ); リレーションがある場合は、リレーションがない場合と比較してかなり複雑となっています。以下のようなリレーションありの JSON:API フォーマットの型をサンプルとして、順々に説明していきます。 type RawMovieResponseType = { data: { id: string ; type : "movie" ; attributes: { title: string ; year: number ; } ; relationships: { actors: { data: { id: string ; type : "actor" ; }[] ; } ; directors: { data: { id: string ; type : "director" ; }[] ; } ; } ; included: | { id: string ; type : "actor" ; attributes: { name: string ; } ; }[] | { id: string ; type : "director" ; attributes: { name: string ; } ; }[] ; } ; } ; まず始めに、リソースの attributes を取得する際は、先程見たリレーションがない場合と同じであるので、 JsonApiDeserializedType を使って取得します。 export type JsonApiRelationshipDeserializedType < T extends BaseRelationshipApiType | BaseRelationshipApiArrayType > = JsonApiDeserializedType < T >; const movie: JsonApiRelationshipDeserializedType < RawMovieResponseType > = { id: '1' title: 'アナ雪' , year: 2013 } 次に、リレーション部分ですが、大枠の方向性としては、 relationships にある type と、 included にある type を突合させて、リレーションとして返す型を決定してあげます。 data がオブジェクトの場合ですが、以下のように実装しています。 // ... SnakeObjectToCamelType < { [ K in keyof T [ 'data' ][ 'relationships' ]] : SnakeObjectToCamelType < NarrowUnionObjectType < ExtractArrayType < T [ 'data' ][ 'included' ] >, 'type' , ExtractArrayType < T [ 'data' ][ 'relationships' ][ K ][ 'data' ] > [ 'type' ] > [ 'attributes' ] & { id: string } > [] } > // ... keyof T['data']['relationships'] で RawMovieResponseType の key である actors と directors のそれぞれを Mapped Types で分配して展開しています。 NarrowUnionObjectType の箇所にある type の型パラメータが突合の部分ですが、 ExtractArrayType<ExtractArrayType<T['data']>['included']> で、 included のユニオン(今回で言うと included の actor と director のユニオン型)を、分配された K の relationships にある type を使って絞り込んでいます。 例えば、 K が actors の場合、以下のように included が絞り込まれます。 type NarrowUnionObjectType < UnionObject extends Record < string , unknown >, UnionObjectKey extends keyof UnionObject , UnionObjectValue extends UnionObject [ UnionObjectKey ] > = UnionObject extends { [ Key in UnionObjectKey ] : UnionObjectValue } ? UnionObject : never NarrowUnionObjectType < { id: string ; type : "actor" ; attributes: { name: string ; } ; } | { id: string ; type : "director" ; attributes: { name: string ; } ; } , 'type' , "actor" > // 👇 typeがactorを持つincludeに絞り込まれる { id: string ; type : "actor" ; attributes: { name: string ; } ; } 最後に絞り込んだ included の attributes と id を intersection させたオブジェクトをキャメルケースへ変換し、リレーション先の型を決定しています。 最初に見た対象リソースの attributes とリレーション先の intersection させると、リレーションありの場合の型生成が完成します。 // 👇 最終的に以下のような型になる SnakeObjectToCamelType < { title: string ; year: number ; } > & { id: string ; } & SnakeObjectToCamelType < { actors: SnakeObjectToCamelType < { name: string ; } & { id: string ; } > [] ; directors: SnakeObjectToCamelType < { name: string ; } & { id: string ; } > [] ; } >; type ResponseMovieType = JsonApiRelationshipDeserializedType < RawResponseMovieType >; // 👇 静的チェックが通る const movie: ResponseMovieType = { id: "1" , title: "アナ雪" , year: 2013 , actors: [ { id: "1" , name: "クリステン・ベル" , } , { id: "2" , name: "イディナ・メンゼル" , } , ] , directors: [ { id: "1" , name: " クリス・バック" , } , { id: "2" , name: " ジェニファー・リー" , } , ] , } ; 実際に運用してみて 今までは、stoplight 上のレスポンスの型を見て、デシリアライズした型に変換して記述していましたが、今回デシリアライズした型を実装した後では、stoplight 上で記述されている型をそのまま TypeScript の型で書くだけで良くなり直感的になった他、レスポンスの変更があった場合でも、静的チェックでデシリアライズする前と後で矛盾がないかを検知できるようになりました。 しかし、今回書いた型は JSON:API におけるベーシックな部分のみをカバーしたものであるため、今回のデシリアライズ変換でカバーできないエッジケースに対しては随時拡張していく必要があります。 また、stoplight 上で OpenAPI を管理しているため、OpenAPI から自動で TypeScript の型を生成できるようにできれば、Rails API とフロントエンドは、より型安全な API スキーマによって連携できるため、今後検討していきたいです。(OpenAPI がどれくらいの精度で TypeScript の型を生成できるか、運用上のコストを天秤にかけた上で) 最後に 今回実装した内容は、github で公開しているので、よりよい書き方がある場合やエッジケースの拡張など、PR お待ちしています! json-api-typescript-deserializer 最後まで読んでいただきありがとうございました!
アバター
Work illustrations by Storyset こんにちは、スタメンの滿本、若園、田中、近藤です。スクラムでのチーム名は、チームねぎまです。 (2022年冬から スクラム開発に移行しました ) 本記事では、QuickSightの概要、マルチテナント構成とその運用方法について紹介します。 QuickSightとは QuickSightは、AWSが提供しているクラウドBIサービスです。 本項では、QuickSightの主な構成要素であるユーザーとアセットについて紹介します。 ユーザー QuickSightでは、下記の3種類のユーザー種別があります。 ロール 権限範囲 エンタープライズ版の費用 *1 管理者(ADMIN) 各種設定の変更, ダッシュボードの作成 24ドル/月 作成者(AUTHOR) ダッシュボードの作成 24ドル/月 閲覧者(READER) ダッシュボードの閲覧 最大5ドル/月 アセット QuickSightでは、データ、分析、ダッシュボードの3つをアセットと呼びます。 データ データソースとデータセット グラフなどを用いた分析を行う前に、データソースへの接続もしくはアップロードを行い、データセットと呼ばれるデータ群を準備する必要があります。QuickSightは、 様々なデータソース に対応しています。データセットは、データソース内の指定した任意のテーブルを元に作成することができ、テーブルの中身をそのまま使用したり、SQLを用いてカスタマイズした状態でデータセット化することができます。 ※サンプルデータです。 分析 分析は、データセットを元に作成したグラフやピボットテーブルなどの要素群のことを指し、作成した分析をダッシュボードに公開することで分析結果をダッシュボード化することができます。 ※サンプルデータです。 ダッシュボード ダッシュボードは、複数シート化・Eメール送信・印刷・PDF出力ができ、APIを用いると埋め込み用のURLを生成することもできます。 ※サンプルデータです。 QuickSightを用いたマルチテナント構成 QuickSightには、ユーザーをまとめる概念として、 グループ と 名前空間(Namespace) の2つがあります。 グループを用いる場合、グループを作成しユーザーを所属させ、そのグループ単位でアセットへのアクセス権限を付与することできます。 この場合、QuickSightにデフォルトで存在している名前空間(名前空間名はdefault)上に、全ユーザー・アセットが存在している状態になります。 同一の名前空間上に存在しているため、アクセス権限付与のミスオペレーションなどでグループ間で意図しないアセットが共有されてしまう恐れがあり、マルチテナント構成には向いていないことが分かりました。 一方で名前空間(Namespace)を用いる場合、名前空間別に環境を分けることができるため、確実にユーザー・各アセットを隔離された空間に分離できます。これにより自分が所属している名前空間以外のユーザーとアセット共有することが出来なくなります。 今回の検証では下図のようなマルチテナント構成を準備しました。 AWS CLI *2 によるユーザーとデータの作成 以下の手順でQuickSightのマルチテナント構成を構築しました。 名前空間の作成 ユーザーの作成・登録 データソースの作成 データセットの作成 名前空間の作成 QuickSightの名前空間を作成します。 $ aws quicksight create-namespace \ --aws-account-id {aws-account-id} \ --namespace {namespace} \ --identity-type QUICKSIGHT ユーザーの作成、QuickSightへの登録 IAMでユーザーを作成し、QuickSightへ登録します。分析を作成する権限を持つユーザーは user-role を AUTHOR に指定し、閲覧のみのユーザーは READER とします。 $ aws quicksight register-user \ --aws-account-id {aws-account-id} \ --namespace {namespace} \ --identity-type IAM \ --email {email} \ --user-role {role} \ --iam-arn {iam_arn} データソースの作成 今回はAthenaからデータソースを作成しました。permissionsで quicksight:UpdateDataSource 、 quicksight:DeleteDataSource などの必要な権限を付与しています。 $ aws quicksight create-data-source \ --aws-account-id {aws-account-id} \ --data-source-id {data_source_id} \ --name {data_source_name} \ --type ATHENA \ --data-source-parameters AthenaParameters={WorkGroup=primary} \ --permissions Principal={user_arn},Actions={permitted_actions} データセットの作成 クエリが複数行にわたるため、cli-input-jsonオプションでjsonファイルを指定してデータセットを作成します。 $ aws quicksight create-data-set \ --cli-input-json {cli_input_json_file_path} // 読み込ませるjson { " AwsAccountId ": aws_account_id, " DataSetId ": data_set_id, " Name ": data_set_name, " PhysicalTableMap ": { " AthenaPhysicalTable ": { " CustomSql ": { " DataSourceArn ": data_source_arn, " Name ": custom_sql_name, " SqlQuery ": sql_query, // Athenaで実行するSQL " Columns ": columns , // データセットとして利用するカラムの Name と Type を指定 } } } , " ImportMode ": 'SPICE' , // SPICE または DIRECT_QUERY " Permissions ": [ { " Actions ": permitted_actions, " Principal ": user_arn } ] } 上記の手順でQuickSightコンソールから分析を作成できる環境を用意しました。 日々のSPICEインポート更新の設定と管理 QuicksightではSPICEと呼ばれる、高速なインメモリエンジンをデータ格納先として使用することができます 。 データセットの作成・編集においてSQLを用いる場合、「直接クエリ」と「SPICEへのインポート *3 」という2種類のクエリモードのいずれかを選択できます。 下記の比較の通り、直接クエリと比較してメリットが大きかった為、SPICEを使用することが決まりました。 SPICEと直接クエリの比較 SPICEのメリット ①処理速度 SPICEにデータをインポートすることで、分析作成時やダッシュボード閲覧時において明らかな高速化が確認出来ました。 ②コスト 直接クエリの場合、データセットを参照する毎に、データのクエリに対して課金されます。一方で、 SPICEのデータはインポートするデータの容量 *4 で決まるため、一旦インポートすればコストは固定です。 SPICEのデメリット 容量管理 使用可能なSPICEの残容量は、QuickSightのコンソール上で確認することになります。容量は事前購入の必要があり、オートスケールさせることが出来ません。その為、容量が不足している場合は更新が失敗します。 SPICEデータの更新 SPICEの「スケジュールに基づいたデータセットの更新 *5 」を利用することで、毎日の早朝に自動で最新のデータをSPICEにインポートし更新しています。更新のスケジュールは、データセットごとに設定することが可能です。またもし更新が失敗した場合は、メールで通知を受け取ることが出来ます。 リポジトリとスクリプトによるデータ管理 運用する中で、既存のデータセットを更新したいという要望が頻繁に発生しました。 また、都度QuickSightコンソール上でカスタムSQLを更新してしまうと、誰がどのような理由でデータセットのカスタムSQLを変更したのかが把握しづらくなるという課題がありました。 これらの問題を解決するために、カスタムSQLは下記のようにyamlファイルとして管理するようにしました。 ID : users Name : ユーザー一覧 SqlQuery : |- SELECT name, age, ..., FROM users LEFT JOIN ... WHERE .... GROUP BY ... Columns : - Name : ユーザー名 Type : STRING - Name : 年齢 Type : INTEGER - ... また、 update-data-set のコマンド引数に渡す --physical-table-map を都度コマンド入力するのが大変だったので、スクリプト化することでより運用しやすくなりました。 # cmd/update_dataset_by_id # 特定のデータセットを更新するスクリプト #! /usr/bin/env ruby # frozen_string_literal: true require ' json ' require ' optparse ' require ' yaml ' require_relative ' ../src/aws/config ' require_relative ' ../src/aws/quicksight/update_data_set ' class UpdateDataSetById def initialize option = CmdOptionParser .new.execute @indicator_file_id = option.indicator_file_id end def execute puts ' ===== START Update Data Set ===== ' puts " === Update Indicator ID: #{ @indicator_file_id }" # 対象のyamlファイルの読み込み indicator = YAML .load( File .read( " ./indicators/ #{ @@indicator_file_id } .yml " )) data_source_arn = " arn:aws:quicksight:ap-northeast-1: #{ Aws :: Config :: AWS_ACCOUNT_ID } :datasource/ #{ @indicator_file_id }" Aws :: Quicksight :: UpdateDataSet .new.execute(indicator, data_source_arn) puts ' ===== FINISH Update Data Set ===== ' end end class CmdOptionParser Option = Struct .new( :indicator_file_id ) def initialize @option = Option .new end def execute :: OptionParser .new do |o| o.on( ' -i ' , ' --indicator-file-id [SQL_DEFINED_FILE_ID] ' , ' SQL Defined File ID (Required) ' ) { |v| @option .indicator_file_id = v } o.on( ' -h ' , ' --help ' , ' Show help. ' ) do |_v| puts o exit end o.banner = ' Usage: update_data_set ' o.parse!( ARGV ) return @option if valid? warn ' indicator_files required. ' puts o.help exit ( 1 ) end end private def valid? unless @option .indicator_file_id warn ' Argument indicator-file-id required ' exit ( 1 ) end true end end UpdateDataSetById .new.execute # src/aws/quicksight/update_data_set.rb # aws cliを実行するクラス require_relative ' ../../aws/config ' require_relative ' ../../utils ' module Aws module Quicksight class UpdateDataSet def initialize ; end def execute (indicator, data_source_arn) File .open( ' tmp-update-data-set.json ' , ' w ' ) do |file| JSON .dump(cli_input_json(indicator, data_source_arn), file) end ` aws quicksight update-data-set \ --cli-input-json file://tmp-update-data-set.json ` ` rm tmp-update-data-set.json ` unless $? .success? warn ' ===== Command failed: aws quicksight update-data-set ===== ' exit ( 1 ) end end private def cli_input_json (indicator, data_source_arn) { " AwsAccountId " : Aws :: Config :: AWS_ACCOUNT_ID , " DataSetId " : "#{ indicator[ ' ID ' ] }" , " Name " : "#{ indicator[ ' Name ' ] }" , " PhysicalTableMap " : { " AthenaPhysicalTable " : { " CustomSql " : { " DataSourceArn " : data_source_arn, " Name " : ' CustomSQL ' , " SqlQuery " : indicator[ ' SqlQuery ' ], " Columns " : indicator[ ' Columns ' ] } } }, " ImportMode " : ' SPICE ' } end end end end 参考 運用設計 -きめ細かなユーザー権限とアクセス管理におけるポイント- アカウント/ユーザー管理におけるポイント まとめ 以上が、QuickSightを用いたマルチテナント構成とその運用方法を効率化した内容となります。 BIツールとしてQuickSightの利用を検討している方、現在利用していて運用を効率化したいと考えている方にとって、少しでも参考になれば幸いです。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します! また株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 ご興味がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します! *1 : エンタープライズ版の価格表 *2 : AWS CLI Command Reference quicksight *3 : SPICE へのデータのインポート *4 : SPICE 容量の管理 *5 : スケジュールに基づいたデータセットの更新
アバター
こんにちは、スタメンエンジニアの手嶋です。普段はRuby on RailsやReactなどの技術を用いて開発しています。最近は フィーチャーチーム体制に切り替わったこともあり 、AWSなどの技術にも触れる機会が増えました。 これまで複数のプロジェクトにおいて React(TypeScript) で開発を行ってきました。そんな中でやはり型の恩恵を感じることが多かったのですが、バックエンドも含めて TypeScript でアプリケーションを作成してみたいという想いが湧いてきたので、個人開発として Next.js と NestJS で構築したアプリケーションをモノレポで運用し本番環境で動かしてみました。 モノレポはその名の通り、単一のリポジトリ(git等)で複数のプロジェクトを管理することです。 主に以下のようなメリットを享受できないかと思い、モノレポを採用しました。 ESLint / Prettierなどの設定を一元管理できる frontend/backendを同時に変更する時に管理しやすくなる 本記事では Next.js/NestJSとモノレポ で実際に運用するまでに試した方法やリリースまでに苦戦した点を中心に紹介したいと思います。 ※上記に焦点を当てているため、 Next.js や NestJS 等個々の技術に関する詳細説明は割愛しております。 目次 完成イメージ/技術スタック フロントエンドバックエンド共通 フロントエンド バックエンド まとめ 完成イメージ 早速ですが、モノレポでの大まかな完成形イメージは以下のようなディレクトリ構成となります。ルート配下に frontend と backend というディレクトリを持ち、それぞれの中で Next.js と NestJS を作成しています。 // アプリケーション全体の構成 root─┬─ frontend #Next.js │ ├─ package.json #frontend固有のpackageを管理 │ └─ その他のファイル │ ├─ backend #NestJS │ ├─ package.json #backend固有のpackageを管理 │ └─ その他のファイル │ ├─ package.json #共有で扱うpackageを管理 └─ yarn.lock #全体で1つ 各ディレクトリ配下で個別に package を管理するために、それぞれ package.json を作成していますが、ルートでも package.json を管理しています(計3つ)。ルートの package.json で管理するのは eslint や prettier など、アプリケーションで共通となるパッケージです。 ※yarn.lockに関しては、全てのpackageの依存を管理した上で、アプリケーション内に1つだけ生成されるようです。 技術スタック 詳細に触れないものもありますが、以下のような技術を使用しています。 Backend NestJS Prisma GraphQL / Apollo Frontend Next.js / React GraphQL Code Generator / Apollo Client その他 yarn workspace ESLint / Prettier / husky Docker Vercel(Next.jsをホスティング) heroku(NestJSをホスティング) フロントエンドバックエンド共通 最初にフロントエンドバックエンドで共通となる部分の紹介です。 上述のように同じアプリケーション内でフロントエンドとバックエンドをそれぞれを管理するために、 Workspaces という Yarn の機能を使用しています。(追加のpackageなどは特に必要ありません。) こちらを使用することで、ひとつのリポジトリで複数のパッケージを開発できるようになり、効率的に作業を進められるようになります。 例えば、ルートの package.json を以下のような記述にする事で、その配下の frontend と backend という複数の package.json の管理をすることができます。 またルートから各ディレクトリを参照できるようになるので、 backend に追加のパッケージをインストールしたい場合は yarn backend add パッケージ名 というコマンドをルートで実行するだけでよく、各ディレクトリへ移動する必要がありません。 ※注意点として、ここで package というキーに指定する値はディレクトリの名前ではなく、各ディレクトリ内の package.json のname属性の値と統一させる必要があります。 nohoist に関しては必須ではないと思いますが、実際の運用にあたって必要だったため導入しています。詳細はバックエンドの章で後述します。 { "name": "app-name", "private": true, "workspaces": { "packages": [ "frontend", "backend" ], "nohoist": [ "**/backend", "**/backend/**", "**/frontend", "**/frontend/**" ] }, "scripts": { "backend": "yarn workspace backend", "frontend": "yarn workspace frontend" }, // その他devDependenciesなどを記述する } バックエンド バックエンドは NestJS で構築し、ホスティング先には無料で使える heroku を選びました。クライアントと GraphQL で通信するために、 NestJs 側にも GraphQL / Apollo をインストールしました。 モノレポで管理するにあたってポイントとなったのは以下2点です。 1つ目はdefalutブランチにマージした際に backend 配下のみの変更を検知して heroku へのデプロイを開始することです。( frontend の変更のみであればデプロイをしない) こちらに関しては github/actions を使って実現しました。 プロジェクト内に以下のようなファイルを作成した上で、今回は heroku へのデプロイなのでHEROKU_API_KEYのAPIを github のsettingに設定しました。 name : Deploy backend on : push : branches : - main paths : - "backend/**" jobs : build : runs-on : ubuntu-latest steps : - uses : actions/checkout@v1 - name : Release app backend uses : akhileshns/heroku-deploy@v3.0.4 with : heroku_api_key : ${{secrets.HEROKU_API_KEY}} heroku_app_name : "app_name" heroku_email : "hoge@gmail.com" env : HD_APP_BASE : "backend" 2つ目はルートの package.json における nohoist の設定です。 ORMとして Prisma を導入したのですが.envに記載する DATABASE_URL を参照できず Prisma を起動できないという問題に直面しました。調査の結果、プロジェクト(ルート)レベルの node_modules に Prisma がインストールされてしまうと正常に動作しないということが分かりました。 解決策として、 Workspaces の nohoist の設定で各ディレクトリを指定することでプロジェクトレベルではなくパッケージレベル(ここでは frontend と backend を指します)の node_modules にインストールすることでこの問題を解決しています。 ※ディレクトリの名前ではなく、ディレクトリ内の package.json のnameの値と統一する必要があります。 { // その他の記述 "workspaces": { "packages": [ "frontend", "backend" ], "nohoist": [ "**/backend", "**/backend/**", "**/frontend", "**/frontend/**" ] }, // その他の記述 } このように Workspaces はその性質上、デフォルトではすべての package がルートの node_modules に入ってしまいますが、各ディレクトリレベルで管理したい package はそのディレクトリで管理した方が問題が起きにくいと考えています。 フロントエンド 続いてフロントエンドです。フロントエンドは Next.js/React で構築し、最もメジャーな Vercel にホスティングしました。バックエンドに GraphQL でリクエストするために Apollo Client を導入しています。またバックエンドを含めた型の統一管理や、型を記述するコストを削減するために GraphQL Code Generator を試してみました。 実際の運用にあたってのポイントになったのは以下2点です。 1つ目は Vercel の Root Directory 設定の変更です。 上述のように各ディレクトリで、それぞれに必要なパッケージをインストールしています( Next.js は frontend )が、 Vercel のデプロイ等の各設定はルートに Next.js アプリケーションが構築されることを前提としている為、その部分の手当てをする必要がありました。 2つ目は余分なデプロイ処理を行わないようしたことです。 前章(バックエンド)の内容と重複する部分がありますが、基本的にデプロイは以下のように、各ディレクトリに対して差分があるPRがマージされた時のみ行いたいという考えました。 frontend の差分のみ -> Vercel へデプロイ backend の差分のみ -> heroku へデプロイ 両方差分がある場合は2つのデプロイ処理を実行 この2つを実現する為に Vercel 上で以下二箇所を変更しました。 1.Root Directoryの変更 Settingsタブ/Generalで表示されている Root Directory を Next.js をインストールしているディレクトリに変更します。 今回は frontend という命名にしている為、そちらに合わせる形にしています。 2.Ignored Build Stepの設定 Vercel では Ignored Build Step を設定することで、特定の条件においてcommit及びマージ時のBuild処理をスキップすることが可能です。こちらはSettingsタブ/Gitで設定可能で、ファイルの実行を指定することなどもできますが、今回は単純にディレクトリ指定としています。 まとめ 以上が、 Next.js × NestJS をモノレポで実際に運用する方法と、リリースまでに苦戦した点です。実際には他にも色々な躓きポイントがありましたが、今回は TypeScript×モノレポ という点に絞って紹介させていただきました。 Next.js と NestJS 共にホスティング先は様々な選択肢があるため、完全に同じような構成にはならないと思いますが、 TypeScript×モノレポ でアプリケーションの構築及び運用を目指す方にとって少しでも参考になれば幸いです。 今回は個人開発で扱う技術スタックに関する紹介となりましたが、弊社の事業も拡大や多角化に伴い、開発組織が直面する課題や求められる能力/技術スタックが日々変化しています。 加えて、弊サービスのTUNAGでは、お客様である企業の成長にエンゲージメントという側面から貢献するために、まだまだ開発すべきことがたくさんあります。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します! また株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 ご興味がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します! 参考にさせていただいた資料 https://qiita.com/mikan3rd/items/3f46b1edcb79ad63deef https://tech-1natsu.hatenablog.com/entry/2020/10/06/221500 https://numb86-tech.hatenablog.com/entry/2020/07/21/155343
アバター
はじめに こんにちは。 takumma といいます。株式会社スタメンで一ヶ月半ほどインターンをさせていただいたので、そこで体験したことを文章として残そうと思います。 本当はインターンでお世話になった社内の方達に向けて社内ドキュメントに投げようと思っていたのですが、その旨を CTO の松谷さんに話したら、是非社外向けにも公開してほしいとのことだったので、スタメンに興味を持った人のことも意識して書こうと思います。 僕は短い間しか居なかったので、スタメンはこういう会社だ!というよりは、僕の体験ベースで素直に感じたことを書いていければなと思います。 スタメンとは? stmn.co.jp スタメンは、名古屋にあるITベンチャーです。2016年創業、2020年に東証マザーズへの新規上場を果たしていて、事業としてはエンゲージメント経営プラットフォーム TUNAG などを行っています。 TUNAGとは? tunag.jp 僕がインターンで関わった TUNAG は、エンゲージメント経営の実践を通じて会社の成長や成功を支援するサービスです。エンゲージメント( ≒ 会社と従業員、従業員同士の相互信頼関係)を重視した組織づくりを通して強い組織を作っていくことを支援するプロダクトです。 少し分かりづらいかも知れませんが、簡単に言うと、社内の信頼関係を強くし、組織を強くしていくための社内 SNS のようなプロダクトです。 僕について 改めて、僕は takumma と申します。 高専に通っている5年生(インターンに行っていた時は、ギリ4年生)です。 目次 インターンの概要 インターンを通じて感じたこと インターンに参加して良かったこと まとめ インターンの概要 最初に僕がインターンでやっていたことを軽く触れようと思います。 僕は TUNAG 事業部の開発チームの一つに一ヶ月半ジョインさせてもらい、そこでチームメンバーらとともに様々なタスクに取り組ませていただきました。 情勢もあり、社内の体制に合わせて前半はオンライン、後半はオフラインで参加していました。 取り組んだ内容 僕の入ったチームでは、TUNAG のプレミアムチャット機能を実装していました。既存のチャット機能に加え、引用返信機能と動画チャット機能の二つの機能を実装していました。 最初の方はチームがメインで取り組んでいる上記の機能開発を行うタスクではなく、既存機能の改善などを行うタスクがほとんどで、改善タスクをこなしながらコードに触れていくというようなことをしていました。しかしインターンの中盤になると、チームが取り組んでいる新規機能の開発にも入っていくようになり、より一層チームの一員としてタスクに取り組んでいくようになっていきました。 TUNAG で使っている詳しい技術スタックは、 こちら に詳しく書かれています。 インターンでは、 フロントエンドからサーバーサイド、インフラ 周りまで幅広く触らせていただきました。僕はフロントエンドメインのスキルセットなので、インターンでもフロントエンドのタスクをやることになると思っていました。しかし、スクラムチームだったこともあって、 領域横断 でタスクに取り組むことができました。チームでは基本的に ペアプロ・モブプロ でタスクに取り組んでいたので、その中で教えていただいたりしながら進めることで、あまり経験のない領域にもチャレンジしていくことができました。 また、社内の企画部の勉強会にも少し混ぜていただき、 ビジネスや企画、デザイン などの領域に関して学ぶ機会もありました。 振り返ると、本当に幅広い経験を積ませていただいたなと感じています。 インターンを通じて感じたこと 激しい変化 僕が入った時期は特に社内の変化が激しい時期で、 オフィス移転 オンライン → オフラインへ 開発体制がスクラム → 大規模スクラムへ など、様々な変化がありました。 オフィス移転 前のオフィスは名古屋の亀島の高架下にオフィスがあって、一度だけインターンが始まる時にお邪魔させてもらいました。そしてその2, 3週間後くらいに新しいオフィスに変わりました。新しいオフィスも高架下にあって、おしゃれで階段の多いオフィスです。 高架下にあるオフィス 中はこんな感じ 働いているオフィスが移転するということもそうそうあることではないと思うので貴重な体験だったし、新しい新築のオフィスで働くのは気分が上がってとても楽しかったです。 ちなみに オフィスの目の前に Zepp Nagoya がある ので、業務終わりにライブに直行することもできます。最高。 大規模スクラム(LeSS)への移行 僕が入ったときのスタメンの TUNAG 開発チームは、フロントエンドやSREなどの領域ごとのチームからクロスファンクショナルなスクラムチームに変化し、 スクラム開発を導入している真っ最中 でした。 https://www.wantedly.com/companies/stmn_inc/post_articles/379461 そしてインターン中には、個々の開発チームのスクラムから、LeSS へと体制が変化していきました。またその中でも、例えばスクラムイベントの時間が変化したり、デザイナーさんがチームに加わったり、スクラムマスターの役割がチームのリーダーに任されるようになったりと、 毎スプリント変化があって、インターン中は全く同じ体制のスプリントが無かった ほどでした。 またインターン期間中にオンラインからオフラインへの移行もあったので、使うツールやスクラムイベントの開催方法も変化していました。例えば僕のいたチームではレトロスペクティブに使うツールを、 Miro → スプレッドシート → ホワイトボード(物理) というふうに変えたりしていました。個人的にはホワイトボードがスクラム感?があって良いなと感じていました。 人とプロダクトの熱 スタメンという会社を知ったとき、まず創業事業の TUNAG に興味を持ちました。 突然ですが、僕は熱のあるプロダクトが大好きです。ハッカソンなどで企画・開発をするときも、ゆとりや人の温もり・感情を大切にするプロダクトのアイデアがとても好きだし、そんな熱のあるプロダクトを開発したりそのプロダクトのことを考えているのがとても好きです。 また熱のあるプロダクトを仲間と開発していく中で、作る人の想いに触れることも好きになりました。 僕は TUNAG に対しても同じように熱を感じたのでとても興味を持ったし、「熱のあるプロダクトを創る」ということを実務レベルでもやってみたいとずっと思っていました。 インターンにいく中で、特に熱を感じることができた出来事があります。TUNAG のプロダクトオーナーの方とお話した時のことです。 プロダクトオーナーとの会話 インターンの中で、TUNAG のスクラムチーム全体のプロダクトオーナーをされている二階堂さんという方に、TUNAG のオリエンテーションをしていただくという機会がありました。オリエンの目的はプロダクトゴールを理解することだったので、二階堂さんに色々と説明をうけるという形だったのですが、話を聞いていく中で「ここはこういう思いがあるんだ、じゃあここにはこういう施策があるんじゃないか?」というように TUNAG に対する理解が能動的に深まっていく感覚があって、僕も二階堂さんもヒートアップして熱く語り合うみたいな状態になったのがすごく記憶に残っています。普段の企画でもたまにある、プロダクトに対して熱くなるような体験を業務の場でもできたということが僕にとって印象的でした。 また、プロダクトに対する想いが強く、作りたい世界やユーザに届けたい体験があるからこそ、ただ創るだけじゃなく どうやったらユーザに使ってもらうか、作りたい世界を広げるか を考えていくということの大切さを改めて学びました。 TUNAG に関しては業務中もずっと社内で利用していたので、エンジニアとユーザの目線から色々と感じたり、考えたりしていました。 インターンに参加して良かったこと 自分の興味が広がった まず一つは、自分の興味が広がったことです。 スクラムチームへ変化したことで領域横断でタスクに取り組んだので普段あまり触らない領域(僕の場合はインフラやサーバーサイド)にも触れることができました。 興味のあるところにチャレンジしていける環境と雰囲気があったのもとても良かったポイントです。例えばチームに降ってきた AWS の cdk を新たに導入するタスクに、インフラの知識が全くない僕が手を挙げたりとかしていましたし、そこに心理的な障壁を感じたりすることはありませんでした。 また、企画部の勉強会に参加したことがきっかけでデザイナーの方におすすめしていただいた本を会社から借りて読んだりもしていました。 興味はあったけれどなんとなく触らないでいたことを、インターンというきっかけでトライできた。というようなことが色々あって、今回のインターンが いろんなことを勉強したりチャレンジする良いきっかけになった と感じています。 会社全体の人や雰囲気を感じ、想いに触れられた 基本はチームの中で業務に取り組んでいたのですが、役員の人と話す機会を与えていただいたりしたので、いろんな人と話すことができました。というか、普段から役員の方とも距離が近いので気軽に話せる雰囲気がありました。 またオフラインでオフィスにいた時は、ベンチャーというのもあってか営業や CS の人たちのデスクも近く、顔を見たり電話で話す声をよく聞いていたので、会社全体の雰囲気も感じていました。 特に役員の方と話す中で、スタメンや TUNAG に対する想いを聞いたりすることができたので、とても良かったです。 スクラムの考えがより深まった スクラムの導入も初めてだったため、スクラムに関わる一人ひとりがスクラムの在り方を常に考えていました。どうすればより価値のあるプロダクトを提供できる組織が作れるかということを導入期に一緒に考えることができたので、とても貴重な経験になりました。 アクティブにスクラムへチャレンジしていく組織の中にいることで、 なんとなくでいたスクラムの考え方が、繰り返し考える経験を経てより深まった と自分の中で感じています。 まとめ 様々な領域でたくさん学びがあったこと。今のスタメンでしかできない経験ができたこと。いろんな人の思いを、話したり一緒に仕事をすることで感じ、刺激を受けられたことがとても良かったです。 インターンで関わってくれたスタメンの方達に本当に感謝しています。ありがとうございました。
アバター
TL;DR (概要) Deviseにおける認証ロジックの実装 認証処理の流れ カスタムストラテジーの実装 カスタムストラテジーの呼び出し default_strategiesとして呼び出す ストラテジー名を指定して呼び出す その他 Tips FailureApp(エラーハンドリング用のクラス) hook model 参考 まとめ TL;DR (概要) こんにちは、スタメンエンジニアの井本です。普段はRuby on RailsやAWSなどサーバーサイド寄りの技術を用いて開発しています。最近は フィーチャーチーム体制に切り替わったこともあり 、React入門中です。 さて今年の1月下旬、弊サービス TUNAG にて、新機能として「2要素認証」をリリースしました。 TUNAGバックエンドはRuby on Railsを用いて開発されていることもあり、認証機能はDeviseを用いて実装しています。 今回は元々のパスワード認証に加えて、OTP(One Time Password)認証をあわせて行うことで、2要素認証としました。 新しく追加するOTP認証をDeviseの設計に沿った形で実装しました。 本記事では、Deviseの実装の解説と、実装に則った新しい認証方法の追加について、その他Tips含めてお話しします。 ※Deviseで認証するモデルがUserモデルである前提で進めます。 Deviseにおける認証ロジックの実装 DeviseはRackの認証ミドルウェアであるWarden上に実装されています。Wardenは認証ロジックをストラテジーパターンで置換可能なものとしており、Deviseはパスワード認証をWardenのストラテジーとして実装しています。またRememberableを有効化した場合のRememberトークン認証についてもストラテジーとして実装されてます。 2要素認証実装においては、新たなストラテジーとしてOTP認証を実装しました。 認証処理の流れ まずはDevise認証処理の流れについて解説します。 コントローラにて warden.authenticate! のように、 Warden::Proxy#authenticate! を呼ぶことで認証処理が実行されます。deviseで用意されている認証用のアクションである sessions_controller#create でも呼ばれていますね。 Warden::Proxy#authenticate! は次のような順番で処理を実行します。 1. 実行する認証ストラテジー名をシンボル配列で取得します。 2.それぞれのストラテジー名からストラテジークラスを取得し、取得したクラスに対して authenticate! ※メソッドを呼び出します  ※ Warden::Proxy#authenticate! ではなく、ストラテジークラスの authenticate! メソッドです。 strategies.each do |name| strategy = _fetch_strategy(name) strategy.authenticate! end 3. ストラテジーの認証が成功すると、対象のユーザーをsessionに格納し、ユーザーオブジェクトを返します。 4. ストラテジーの認証が成功しなければ、sessionの格納はされず、ユーザーも返しません。 ここで実行されるストラテジーは、あらじめ default_strategies として設定されたものです。Deviseの場合は、パスワード認証用の database_authenticatable ストラテジーが default_strategies として、設定されています。加えてRememberableを有効にしている場合は、Rememberトークン認証用の rememberable ストラテジーも default_strategies に設定されます。 puts strategies # => [:rememberable, :database_authenticatable] 特定のストラテジーを呼び出したい場合は、引数にストラテジー名を渡します。 warden.authenticate!( :custom_strategy , :custom_strategy2 , ...) カスタムストラテジーの実装 カスタムストラテジーを実装は authenticate! メソッドを定義する必要があります。定義した authenticate! メソッドの中に認証ロジックを実装します。また先程の説明では省略しましたが、 authenticate! を実行するか判定を行うための valid? メソッドを実装する必要があります。 strategies.each do | name | # ... next unless strategy.valid? strategy.authenticate! end valid? メソッドをうまく記述することで、「特定の条件だけ、ある認証ストラテジーを無効化する」といった実装ができるようになります。 さて、以上よりカスタムストラテジーは次のような実装となります。 module Devise module Strategies class OtpAuthenticatable < Authenticatable def valid? # ... end def authenticate! resource = TwoFactorAuthentication .get_user(params[ :email ]) if validate(resource) { OtpAuthentication .valid?(user, params[ :otp ]) } remember_me(resource) success!(resource) else fail !( :invalid_otp ) end end end end end OTP認証の実装例です。 実際にはパスワードとセットで認証しなければいけないので、もう少し複雑な実装ですが、今回は省略しています。 以下、補足です。 認証ロジック自体は validate メソッドに渡しているブロック内に記述します。 validate はDeviseで用意されたメソッドで、すべての認証ストラテジーにおいて共通の処理が実行されます。例えば、DeviseのLockableモジュール(※)を有効化した場合の、ログイン試行回数のカウントやリセットなどの処理は、 validate に実装されています。 ※ログイン試行回数が一定数を超えると該当ユーザーの認証をロックする機能 success! と fail! は認証結果をインスタンス変数に格納するメソッドで、認証ストラテジーの呼び出し元で結果を確認するために呼び出すメソッドです。 fail! の引数にはエラー種別を渡します。エラー種別に基づいて、エラーメッセージ等の制御を行います。 次にカスタムストラテジーを呼び出す処理の2パターンについて、それぞれ補足します。 カスタムストラテジーの呼び出し default_strategiesとして呼び出す default_strategies を追加するパターンです。設定はconfig/initializer配下のdevise.rbに書きます。 config.warden do | manager | manager.strategies.add( :custom_database_authenticatable , Devise :: Strategies :: CustomDatabaseAuthenticatable ) # deviseのパスワード認証strategyを上書き manager.default_strategies( scope : :user ).delete :database_authenticatable manager.default_strategies( scope : :user ).push :custom_database_authenticatable end strategies.add でストラテジー名とクラスの関係をマッピングをします。 default_strategies を追加する場合は manager.default_strategies(scope: :user).push で追加します。 default_strategies のリストは配列で保持されており、 manager.default_strategies(scope: :user) は default_strategies の配列の参照を返すため、 shift や delete などによる変更も可能です。 ストラテジー名を指定して呼び出す warden.authenticate!( :custom_strategy ) これだけで呼び出すことができますが、 default_startegies と異なる情報を用いた認証を行う場合、呼び出し元のコントローラのどこかで default_strategies が呼び出しされていないか気をつけてください。 具体的には、次のような処理が想定されます。 before_action: :authenticate_user! をしている。 同じく before_action で、 current_user の返り値を元に判定処理を行っている。 1つ目のケース、 authenticate_user! メソッド内では Warden::Proxy#authenticate! が呼ばれます。この時、Deviseの標準だと default_strategies としてパスワード認証が実行されます。しかし、カスタムストラテジー用の認証情報しかないために認証が失敗します。 authenticate! メソッドは認証が失敗するとカスタム認証が実行される前に、ログイン画面にリダイレクトされてしまいます。 skip_before_action や、デフォルトストラテジーの valid? メソッドを上書きして実行を制御するなど、対策が必要です。 2つ目のケース、 current_user においては内部で Warden::Proxy#authenticate が呼ばれます。 Warden::Proxy#authenticate! と異なりリダイレクトこそ発生しませんが、返り値としてnilが返ることを念頭に実装する必要があります。 なお、認証の成功以降は、Wardenで認証ユーザーが保持されるため、デフォルトストラテジー認証周りのケアは特に必要ありません。また、普通のチーム開発だと上記のようなコードは割と気軽に書かれると思いますので、テストも書いた方が良いでしょう。 以上、カスタムストラテジーを定義し、呼び出すための実装を紹介しました。 その他 Tips ここからは2要素認証の開発において、ストラテジーの追加以外に変更を加えたDeviseの機能について、Tipsをご紹介します。 FailureApp(エラーハンドリング用のクラス) FailureAppクラスはエラー時のHTTPレスポンスを生成するクラスです。 カスタムFailureAppクラスを作成して、 respond メソッドの返り値を変更することで、エラー時の表示画面やリダイレクト先を変更できます。また、カスタムFailureAppクラスを使用する場合は devise.rbに設定 が必要です。 レスポンスの形式によって、redirectしたりやjsonレスポンスを返すなどの挙動をします。 respondの分岐 レスポンス内容 相当するコントローラ上の処理の例 http_auth jsonやxml形式 render :jsonなど recall htmlファイル render :newなど redirect 302レスポンスする redirect_to xxx_path recall は warden.authenticate!(recall: "CustomAuthController#new") のように引数を渡すことで render :new のような動きを実装可能です。エラー種別による分岐をする場合は、 warden_message メソッドで、認証ストラテジーで fail! したときに引数として渡した値を取り出して利用することができます。 また、FailureAppは、 throw(:warden) を catch して呼び出されます。 Warden::Proxy#authenticate! でも認証が失敗して、user変数がnilのときに throw(:warden) しています。 def authenticate! (*args) user, opts = _perform_authentication(*args) throw ( :warden , opts) unless user user end hook Deviseではlib/devise/hooks配下に認証後・ユーザーセット後・ログイン後など、のコールバック処理が定義されています。こちらもstrategyと同様にカスタムhookを作成することができます。 config/initializer/devise.rbにて、 Warden::Manager クラスのコールバックメソッドを呼び出し、ブロック内に実行したい処理を書きます。 Warden :: Manager .after_set_user do | user , warden , options | # hook処理 end 要件に合うコールバックメソッドをお探しの場合は こちら を参照ください。 model lib/devise/models配下にあるファイル群で、modelsと書いていますがユーザーなど認証アカウントクラスで include されるモジュールです。 上書きしたい場合はユーザークラスに同名のメソッドでオーバーライドする形が良いでしょう。deviseのメソッドのうちいくつかは、オーバーライドしてカスタマイズされる前提で処理がかかれています。ちょっとした追加実装であれば、わざわざ新しいストラテジーを定義するまでもなく、実装できることも多いです。 参考 Deviseはgithubのwikiが充実しているので、参考にご覧ください。 heartcombo/devise Wiki - Home メソッドをオーバーライドする以外にも、devise.rbで設定変更できるもの、コントローラで request.env['hoge'] に格納された設定値を書き換えられるもの、等があります。 まとめ 以上が、Deviseの実装の解説と、実装に則った新しい認証方法の追加でした。その他Tipsについてもご参考いただけると幸いです。 今回の実装は、お客様の大切な情報をお守りするため責任重大な機能ということで私自身、非常にやりがいを感じながら設計からテスト、リリースまで進めることができました。 弊サービスのTUNAGでは、お客様である企業の成長にエンゲージメントという側面から貢献し、企業様の社員の皆様がより充実した会社生活を送るために、まだまだ開発すべきことがたくさんあります。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。 採用ページ バックエンドエンジニア TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します! また株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 ご関心がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します!
アバター
こんにちは。CTOの松谷です。現在はCTOとTUNAG開発部部長を兼務しており、CTOとして会社全体の技術統括を行いながら、TUNAG開発部長として開発組織マネジメントを担っています。 本記事では、スタメンの創業事業である TUNAG について、プロダクトと開発体制の紹介をします。 目次 TUNAGについて 開発体制について 技術スタック アーキテクチャ 開発組織 開発組織の変遷 フィーチャーチーム 開発プロセスについて おわりに TUNAGについて TUNAG(ツナグ)は、エンゲージメント経営の実践を通じて会社の成長や成功を支援するプラットフォームです。 昨今は、外部環境の変化により、働き方にもリモートワークなどの変化が生まれ、社員それぞれの価値観も多様化しています。そんな中、リモートワークによって社員間のコミュニケーションが少なくなってしまったり、経営理念が浸透しにくくなってしまったりと、組織における課題にも変化が生じています。 スポーツにおいては、メンバーが相互に信頼しあっているチームは強いのと同じように、ビジネスにおいても経営陣と従業員、従業員間で信頼関係を築けているチームは強く、さまざまな業界において競合他社に勝り、継続的な成長を遂げている企業があります。企業における経営陣と従業員、従業員間のこの信頼関係を「エンゲージメント」と定義し、TUNAGではこのエンゲージメントの醸成を支援しています。一見すると「従業員満足度」と似ている概念に思われますが、エンゲージメントと従業員満足度は似て非なるものです。従業員満足度は外部環境の変化などで待遇や環境が悪化するとともに満足度も下がってしまいますが、エンゲージメントは外部環境に左右されず、一丸となって支え合う強い組織をつくることができます。 TUNAGは企業のエンゲージメントの構築、つまりお互いを知って理解し、信頼し合う組織を作るための社内コミュニケーションを活性化させるプロダクトです。メイン機能としては、「会社や人の今」を知るための「タイムライン機能」や人を深く知るための「プロフィール機能」、会社の方針や文化を知る「社内制度機能」が組み込まれています。 「社内制度」は、サンクスカードや社内報、1on1ミーティングなど、会社ごとの経営方針や組織課題に合わせてさまざまな形で運用されており、その種類は多岐に渡ります。TUNAGではこれらの社内制度をアプリケーション上で利用することができ、その利用結果がタイムライン上にフィード形式で投稿され、会社全体に共有することができる仕組みになっています。 弊社内でもTUNAGを利用しており、スタメンにおけるタイムラインの画面が以下のスクリーンショットになります。人や情報が自然と集まる「コーポレートリビング」のような空間をオンライン上で実現できるよう、タイムラインに会社の情報が集約される機能設計やUI設計をしているのが特長です。 また、メイン機能以外にも業務アプリケーションとして、「ビジネスチャット機能」や「ワークフロー機能」、「組織管理機能」「データ分析機能」などのさまざまな機能を備えており、従業員数1万名超のグローバル企業から十数人の企業まで規模や業種、業態を問わず、さまざまな企業の利用を支えられる大規模なSaaSアプリケーションです。 東海テレビでTUNAGが紹介されました。YouTubeで公開されているので、ぜひ見てください。 www.youtube.com 開発体制について TUNAG開発部は、「プロダクトの価値を最速でユーザーに届ける」ために最適な技術選定や開発プロセス、組織デザインを採用しています。 技術スタック TUNAGの技術スタックにおいては以下の図のように、フロントエンドは React/TypeScript、バックエンドはRuby on Rails、モバイルアプリはSwift/Kotlinを使用しています。クラウドはAWSをメインとし、一部でGCPを利用しています。 これまで開発部は、各技術領域のベストプラクティスを積極的に導入したり、フレームワークやライブラリの最新バージョンをできる限り追従したりするなど、開発効率や運用効率を上げてプロダクトの価値提供のスピードを維持する取り組みを行ってきました。 スタートアップのプロダクト成長の舞台裏とコンテナ化までの道のり Ruby on Rails 5.1から6.1へのバージョンアップをカナリアリリースしました 今後も、このベストプラクティスの導入などの取組みを継続していきたいと考えています。例えば、現状のTUNAGのフロントエンドは、RailsのViewの中にReactが埋め込まれる形式で画面が構成されているため、ReactのバージョンがRailsによって管理されていたりとメンテナンス性が悪く、モダンフロントエンドの強みをプロダクトに取り入れにくい状況になっています。今後、ユーザー体験と開発者体験の両方が、世の中のスタンダードに追従できるように、去年からフロントエンドの分離プロジェクトを開始しました。分離後のフロントエンドはNext.jsを採用し、バックエンドはAPIサーバーに徹する方向へと徐々に改善を進めています。 アーキテクチャ TUNAG本体のアーキテクチャの概要は以下になります。 Amazon ECS上のDockerコンテナで実行された、モノリシックなRailsアプリケーションを中心としたシンプルな構成になっています。データベースにはAmazon AuroraやAmazon ElastiCache、Amazon S3、そして全文検索エンジンとしてはAmazon OpenSearch Serviceを利用しています。CI/CDにはCircleCIを利用しており、RSpecやJestによる単体テスト、そしてCypressによるE2Eテストを実施した後にデプロイをしています。プロダクトのコアな価値以外のシステム(メール・SMS・プッシュ通知・画像配信基盤など)には積極的にSaaSを活用し、開発・運用コストを圧縮しています。 またTUNAGでは、アプリケーション上のユーザーのアクティビティを追跡し、各社ごとにエンゲージメントに関する数値などを集計した結果をダッシュボード機能で提供しています。この機能を支えるデータ分析基盤のアーキテクチャの概要は、以下になります。 データはすべてデータレイク(Amazon S3)で一元管理し、必要なときに必要なデータを取り出せるようにしています。またデータ処理基盤は、Amazon AthenaやAWS Glue、AWS Lambdaなどのマネージドサービスとサーバーレスアーキテクチャを組み合わせたシンプルな構成であり、特に複雑なことはしていません。クラウドの強みをちゃんと活かし、かつ今後のデータ分析における試行錯誤がしやすい、柔軟な基盤になっています。 このように、適材適所でマネージドサービスやサーバーレスアーキテクチャを採用することで運用コストをほぼゼロにし、プロダクトのコア部分に注力できるようにしています。アーキテクチャの詳しい説明は以下を参照いただければと思います。 サーバーレスのデータ分析基盤について スタメンは、TUNAGだけでなくFANTSというオンラインサロンプラットフォーム事業も展開しており、今後も新規事業を積極的に展開していきたいと考えています。 このような組織においては、組織内での車輪の再発明を防ぎ、組織化する強みを生かしていくために、横展開することを想定した設計や基盤の整備、及びナレッジの水平展開を可能にする情報共有・展開の仕組みを考えていくことが大切になります。特にTUNAGはスタメンの創業事業なので、他の事業に先行して知識を獲得することが多く、ノウハウの横展開を意識することが重要になります。それを実現するための施策の一つとして、Infrastructure as Codeによるコードの再利用によってサービス基盤やデータ分析基盤がすぐに立ち上がる状況を整備し、最速でサービスの立ち上げができるようにしています。実際に第二の事業FANTSでは、TUNAGの資産を大きく活かして素早くサービスを立ち上げることができました。 開発組織 TUNAGを運営する事業部には開発部と企画部があり、2022年5月時点で開発部には15人のエンジニアが、そして企画部にはプロダクトオーナーとプロジェクトマネージャー、デザイナーが所属しており、この2つの部署が連携してTUNAGの企画・開発を担当しています。 そしてその開発プロセスはスクラムを採用しており、以下の図のように組織は3つのフィーチャーチームと1つのモバイルチームの合計4チームで構成されています。このフィーチャーチームは機能横断型で、各スプリントで価値を生み出すために必要なすべてのスキル(バックエンド・フロントエンド)を備えています。 2022年5月時点 開発組織の変遷 TUNAG事業が成長するに従い、開発組織が直面する課題や求められる能力は、刻々と変化してきました。現在はフィーチャーチーム体制としていますが、事業の成長段階に合わせて開発組織も変化させてきました。 例えば、TUNAGリリース後の1〜2年間は、プロダクトの仮説を検証する段階だったからこそ、手段は問わずにとりあえずカタチにして素早くリリースすることが求められていたフェーズでした。少人数でお互いの作業を詳しく把握できていたため、チーム分けなどの組織化の必要性はありませんでした。 その後順調に開発組織とプロダクトの規模が大きくなり、またテクノロジートレンドが移り変わるにつれて、技術のベストプラクティスから外れたまま開発をしていくことが、プロダクトの価値向上にブレーキをかけ始めました。このフェーズでは、開発部に「高い専門性」が求められていました。バックエンド領域では、スケーラビリティの課題を解決するためにサーバレスやコンテナなどを活用してアーキテクチャを進化させ、またクライアント領域でも、エンドユーザーの体験価値を最大化できるように技術的負債の返済やフレームワークの導入を進めてきました。チーム構成をバックエンドチームやフロントエンドチームのように技術領域ごとに分け、各技術領域の学習効率を最大化して事業成長を支えました。 しかし、無事に技術的な課題を乗り越えた後には、組織のスケーラビリティの問題に直面しました。技術領域でチームを分けたことにより、1つの機能をデリバリーするまでに「チーム間の依存関係」によって開発時のコミュニケーションコストが増えてしまうことが顕著になっていきました。この組織規模が大きくなったフェーズでは、組織全体として大きな力を出せるような仕組みが求められます。そこで価値をユーザーに届けるために必要なコミュニケーションがチーム内で完結するように、すべてのスキルをもった機能横断型のチーム体制と、それを支える開発プロセス(大規模スクラムLeSS)への転換を決定しました。 フィーチャーチーム TUNAG開発部の主体となるフィーチャーチームは機能横断型で、各スプリントで価値を生み出すために必要なすべてのスキル(バックエンド・フロントエンド)を備えたチームです。そして、「ソフトウェアの価値を最速でユーザーに届ける」ために、自律したアジリティの高いチームを目指しています。 理想としては現時点での「できる」「できない」は別として、「全てのエンジニアがプロダクト全体のオーナーである」という意識を持ち、「ユーザー目線での開発」「当たり前品質を支えるSRE活動」「価値を遅延なく届けるためのプロジェクトマネジメント」「プロダクトの拡張性・スケーラビリティを見越したコーディング・アーキテクト」「ユーザーの損失を最小限に抑える障害対応」「開発体験・開発効率向上への取り組み」などを実現できるようになりたいと考えており、足りないギャップを徐々に埋めて、お互いに学び合える組織にしていこうと、モブプログラミングなどのさまざまな取り組みをしています。 その結果、今までのマネージャーやSREなどの個人のロールへ依存していた状態から、多くの機能をもつ自律したチームへと徐々に変化していきました。例えば、TUNAG開発部のエンジニアには、バックエンド領域(Ruby on Rails)とフロントエンド領域(React)の両方を担当できるメンバーが増えてきました。また、自分自身でプロジェクトマネジメントをするのは当然のこと、リリースした機能のパフォーマンスを自ら監視し、必要があれば自分でパフォーマンスチューニングをすることもしています。また、開発者がAWSなどのクラウドのアラート対応などのSRE業務をしたり、開発フェーズだけでなく運用フェーズも担当できるエンジニアの比率が大きくなってきました。 一人のエンジニアの活躍の範囲が広がれば、当然認知負荷も大きくなります。そのため、コンテナ化やサーバーレス化など、アーキテクチャのモダン化などによる運用コストの圧縮や、社内勉強会の積極的な開催やモブプログラミングによる組織学習がこのアジリティ向上の取り組みを支えています。 一方でフィーチャーチーム体制は、プロダクト開発におけるコミュニケーションを最優先することになり、技術領域ごとにおけるコミュニケーションの優先順位が下がってしまうことを意味します。ただ、そのコミュニケーションの優先順位が下がってしまうことを意識した上で、それをカバーできるような動きを取れば対処できると考えています。例えばSRE領域やフロントエンド領域などは、今まで以上に標準化の推進や分科会などの会議体設計の工夫を行っています。 また、大切な点は、プロダクト開発に最適化されたフィーチャーチームを開発組織の主体にしていきたいということであり、全てをフィーチャーチームで統一したいというわけではありません。事業をプロダクトで伸ばすためには、各技術領域のエキスパートの力が不可欠です。今後はモバイルアプリチーム含めて必要に応じて専門チームを追加していくなど、上手くバランスを取っていきたいと考えています。 開発プロセスについて 技術領域でチームを分けていた頃は、開発プロジェクトごとに必要なスキルを持ったメンバーが各チームから集められ、完了するとチームを解散するといったように「プロジェクト」を中心に開発を進めてきました。そのため、「納期」や「担当」といったようなプロジェクト自体の関心事が大きくなりがちで、プロダクト全体の価値を考える機会が多くはありませんでした。今後はプロジェクト中心ではなく、プロダクト全体における優先順位に関心を向けることで、「なぜこれが重要なのか」「誰のためなのか」といったようにプロダクト全体の価値を追求する「プロダクト文化」を醸成していきたいと考えています。 また、今までは開発タスクに個人が割り当てられていたため、「個人vs課題」という構図になりやすく、同じチームだったとしてもメンバーの開発内容や困っていることを把握しづらいという状況がありました。そのため、今後は開発タスクをチームに割り当てることで「チームvs課題」という構図に変え、より一体感を持って課題に向き合えるような「チーム文化」を醸成できる土台をつくっていきたいと考えています。 2021年冬、これらの狙いを実現できそうな開発プロセスを調査しました。プロダクト全体の価値を追求できるように、「複数チームに対して1つのプロダクトバックログで対応するフレームワークであること」、そして「シンプルなものであること」という観点で、最終的に 大規模スクラム(LeSS) を採用しました。 2022年2月から大規模スクラム(LeSS)への移行を開始したものの、開発部にはこれまでスクラム自体の経験がありません。さらに大規模スクラム(LeSS)ともなると導入難易度も高いので、社外の支援経験が豊富なスクラムコーチに協力を仰ぎ、短期間でスクラムの良さを引き出せる状態になることを目指しています。 まだまだ手探りの状態なので、大規模スクラムの導入とその後については、別途発信してお伝えできればと思います。 おわりに TUNAG開発部は「ソフトウェアの価値を最速でユーザーに届ける」という目的のために、技術スタックや開発組織、開発プロセスを柔軟に変化させてきました。先に述べましたが、事業が成長するに従い、開発組織が直面する課題や求められる能力は刻々と変化します。特にTUNAG事業のように成長が速く、変化率が高い事業ではなおさらです。その変化の中で一人ひとりのエンジニアが急成長し、これまでの事業を支えてきました。そして2022年には、さらに大きく変わっていきます。 今後も続くTUNAG開発部の転換期を一緒に楽しみ、プロダクトで事業を圧倒的に成長させていく仲間を募集しています! TUNAG開発部の技術スタックやアーキテクチャ、開発組織、開発プロセスに興味を持っていただけましたら、下記の採用ページからエントリーいただけると幸いです。 採用ページ フロントエンドエンジニア バックエンドエンジニア モバイルエンジニア インフラエンジニア デザイナー また、松谷の Meety も公開していますので、TUNAG開発部について気になることがあれば気軽に聞いてください😄
アバター
目次 はじめに FANTSとは 技術について 組織について 開発体制 エンジニア おわりに はじめに FANTS事業部の開発部の部長をしている田中( @sukesan_st )です。2021年9月から開発部門の部長としてチームのマネジメントをしています。去年は料理とサウナにハマった1年でした。 私の経歴を簡単に紹介しますと、以下の通りです(FANTSに関連する出来事を抜粋) 2019年4月にスタメンに入社 2020年3月〜6月にFANTS立ち上げの開発メンバーとして参画 2020年7月からFANTS提供開始 2021年1月よりFANTSの開発チームに異動し、マネージャーに就任 2021年9月よりFANTS事業部に開発部が新設され、部長に就任 本記事ではスタメン第2の事業であるFANTSについて、事業や利用している技術・開発組織の紹介をしたいと思います。 FANTSとは はじめに、FANTSについて紹介します。 FANTSは「オンラインコミュニティ運営において必要なプラットフォームシステム」と、「全体ディレクションやコミュニティ運営のコンサルティング⽀援」の2軸をワンストップで提供し、コミュニティのエンゲージメントを⾼めていくサービスです。 プラットフォームとして様々な機能を提供しており、例えばサブスクリプション型の決済システムと会員管理が連動していたり、専用アプリがあったり、タイムラインに様々な内容を投稿できたりします。 先日、運用サロン数が100サロンを突破し、サービスとしても大きく成長しています。 FANTS、運用サロン数100を突破。1年で約10倍に成長 代表的なサロンを紹介すると、以下のサロンがあります。 一覧ページ もあるので、気になる方は是非チェックしてください。 技術について FANTSのアプリケーションはTUNAGのコードを再利用しているため、基本的には弊社の創業事業である TUNAG とほぼ同じです。ここにFANTS固有の機能として、決済サービスであるStripeを利用したり、ライブ配信サービスであるVimeoを利用しています。 スタメンのエンジニア採用サイト より引用 先日TUNAGで行なったRails6.1へのバージョンアップであったり、インフラ環境をEC2からECSに移行したりといった対応がFANTSにも適応されているため、バックエンドやインフラも少しずつ昨今のトレンドに乗りつつあります。 バージョンアップやコンテナ化への道のりを綴った記事があるので、ぜひ見ていただきたいです。 Ruby on Rails 5.1から6.1へのバージョンアップをカナリアリリースしました スタートアップのプロダクト成長の舞台裏とコンテナ化までの道のり また、FANTSではオーナー様向けの機能であるダッシュボードが先日リリースされ、その中ではNext.jsを採用しています。フロントエンドの技術スタックとしては新しいものとなっているので、こちらもぜひ記事を見ていただきたいです。 FANTS ダッシュボードを支えるフロントエンド技術 そして、ダッシュボード開発を進めるにあたり、スキーマ駆動開発にて開発を進めました。開発体験としてとても良く、全体の開発効率が感覚的ではありますが上がったように思います。こちらも紹介記事を見ていただければと思います。 スキーマ駆動開発、はじめました 弊社の技術記事の紹介ばかりとなってしまいましたが、このように開発を行い、その過程や結果によって得られた知見をアウトプットして共有するのは弊社の良い文化の1つだと思っています。 組織について 2022年6月時点ではエンジニアが私含めて6名在籍しています。スタメンのエンジニアが全員で23名なので全体の約4分の1がFANTS事業部に所属している形となります。また、平均年齢が27.8歳と若手のメンバーで開発を進めています。 エンジニア FANTSの技術構成については上記でお伝えしたとおりですが、開発メンバーの職能については以下の通りとなっています。 フロントエンド: 1人 フロントエンド・バックエンド兼任: 3人 バックエンド: 2人 上記のように分かれてはいますが、フロントエンド・バックエンドの両方の開発経験もある人が5名いるため、プロジェクト状況に応じて柔軟に開発を進めています。 開発体制 現在FANTSの開発は2週間を1スプリントとしたスクラム体制で開発を進めています。開発プロジェクトが大小さまざまあるので、現時点では以下のようにスクラムイベントを行なっています。 全員参加 デイリースクラム(毎朝) スプリントレビュー(2週目の金曜日) スプリントレトロスペクティブ(2週目の金曜日) 2グループに分かれて実施 スプリントプランニング(1週目の月曜日) プロダクトバックログリファインメント(2週目の水曜日) タスク管理ツールはAsanaを、スプリントレトロスペクティブについてはMiroを使って行なっています。 以下の画像は2021/11/22〜12/3のスプリントのスプリントレトロスペクティブの様子のスクショです。スプリント内で起こった出来事と共に個人やチームの視点で各項目を振り返っています。 元々は全てのスクラムイベントを2チーム体制で行なっていたのですが、同じ開発チームなのに互いの開発状況が分からなかったり、同じサービスを作っているのに距離が遠く感じることがありました。 そこで、チームとしての一体感の醸成やお互いの情報共有の場が必要だと考え、少しずつですが一緒に行うようにしました。 結果としてはお互いの開発状況はもちろんのこと、何が良かったのか、改善すべき点は何かが全体として認識できるようになったので良かったです。 この辺り、いきなり全てのスクラムイベントを統合するのも手ではありますが、これまでよりも全体的に時間がかかるようになってしまうので、どこに時間を使うべきかと期待する結果を意識して体制を作っています。 おわりに FANTSの事業や開発技術・開発組織について、手短ではありますが紹介しました。 まだまだ少人数のチームですが、1人1人がFANTSをより良いプロダクトにするために日々開発を進めています。 一人でも多くの人に、感動を届け、幸せを広める ために、FANTSをもっと多くの人に利用されるサービスとなるように引き続き成長させていきます。 最後になりますが、この記事を見てFANTSの事業・技術・組織に興味・関心を持っていただいた方は、ぜひ下記の採用サイトからご連絡ください。一緒に FANTS を作っていきましょう! 急成長中のオンラインサロン事業を支える バックエンドエンジニアを募集! 急成長中のオンラインサロン事業を支える フロントエンドエンジニアを募集! オンラインサロン事業の長期開発インターンを募集! エンジニア採用サイト
アバター
こんにちは、株式会社スタメンで FANTS のフロントエンド開発を担当している @0906koki です。 今回の記事では、本日リリースした FANTS ダッシュボードのフロントエンド開発で選定したフレームワークやライブラリ、ディレクトリ構成について解説します。 目次 目次 FANTS ダッシュボードとは? 技術スタック Next.js SWR Styled-Components Storybook ディレクトリ構成 components apis 最後に FANTS ダッシュボードとは? FANTS ダッシュボードの説明をする前に、FANTS というプロダクトについて紹介させてください。 FANTS とはサブスク型のオンラインファンサロンプラットフォームで、オンラインファンサロンを始めたいオーナー様に、サロン開設に必要なシステム・企画等をワンストップで提供します。現状 100 サロン以上のサロンが開設されており、急成長中のサービスです。 FANTS ダッシュボードは、サロンを開設したオーナー様がサロンに関する様々な設定を行うことができる管理画面のことで、例えばサロンのロゴやユーザー管理の設定がダッシュボード上で行えます。サロンオーナー様が簡単にサロンの設定をできるよう、シンプルで分かりやすい UI・UX が重要です。 (FANTS ダッシュボードの画面UI) 元々 FANTS は、創業事業である TUNAG のコードをベースにしてスタートしたため、FANTS の管理画面は TUNAG の管理画面のコードに FANTS 特有の機能を追加する形で提供していました。 ただ、TUNAG 独自の仕様が含まれることによる分かりにくさや、フロントエンドを Rails のテンプレートである erb で実装していることによる拡張の難しさ、開発速度の低下などが課題としてあったため、今回 Next.js で新しくリニューアルするプロジェクトをスタートさせました。 バックエンド部分は、既存の Rails の基盤を用いて API と認証を実装しましたが、フロントエンドは別リポジトリで切り出して erb を Next.js で置き換えるために、1 からコンポーネントを実装し、基盤も整える必要がありました。ただ、今までの開発で感じていたフロントエンド開発での痛み(コンポーネント設計や状態管理)を解消できる形で実装することができたので、開発者体験としてはとても良かったです。 技術スタック FANTS ダッシュボードでは以下の技術スタックを使用しています。 フレームワーク : Next.js フェッチライブラリ : SWR スタイリング : Styled-Components UI 管理 : Storybook 監視 : Sentry ホスティング : Netlify Next.js 今回ダッシュボードでは Next.js をフレームワークとして選定しました。選定理由としては、技術的な観点と組織的な観点が挙げられます。 技術的な観点で言うと、Next.js を使用することでフロントエンドのベストプラクティスを実装コストを掛けずとも享受できる点にあります。例えば、コード分割や prefetch は CRA( create-react-app )でも実現可能ですが、いざ実装しようと思うとある程度コストを支払う必要があります。一方 Next.js では標準でそうした機能が付随しているので、エンジニアとしてはよりコアな機能開発に焦点を当てることが可能になります。 (その他にも、Next.js にはページごとでレンダリング方式を選択できる利点がありますが、今回のダッシュボードでは認証が必要であるのと、SEO を気にしなくて良いアプリケーションであったので、SSR や SG はせずに CSR しています。) 組織的な観点で言うと、弊社では創業当初から React をフロントエンド技術として使用しており、React に精通しているエンジニアが豊富にいます。Next.js 自体が React のフレームワークであるため、Vue フレームワークである Nuxt.js と比較して、学習コストも低く抑えることができます。 SWR SWR とは React のデータフェッチライブラリであり、サーバーデータの管理が簡単になります。SWR という名前ですが、 stale-while-revalidate という RFC 5861 で策定された HTTP の Cache-Control のオプションから来ていますが、実際に HTTP の Cache-Control を使っているわけではなく、SWR 内部でそれと似た実装がされています。 例えば、以下のようなコードがあるとします。 import { VFC } from "react" ; import useSWR from "swr" ; const fetcher = async () => { const res = await fetch ( `/api/users` ); return res ; } ; const Users: VFC = () => { const { data , error } = useSWR ( "/users" , fetcher ); if ( ! data ) return < p > loading... < /p >; if ( !! error ) return < p > error occurred < /p >; // ... } ; useSWR の第一引数で渡している key( /users )があると思いますが、SWR はこの key に対して API のデータをメモリキャッシュします。Users コンポーネントが初回マウントされたタイミングではキャッシュがないので loading fallback を表示しますが、キャッシュがある場合は loading fallback をスキップしてユーザー一覧をレンダリングすることが可能です。 loading fallback をスキップする際に、メモリキャッシュされたデータが古い場合があるので、バックグラウンドで API リクエストを送り、データが更新されていれば mutate してキャッシュも更新し再レンダリングが走ります。 弊社では今まで API のレスポンスデータを Redux で管理していましたが、非同期処理に関わるボイラープレートの実装コストや、上記のようなキャッシュ機構を実装する難しさを課題に感じ、サーバーデータの状態管理として SWR を選定しました。また、今回のアプリケーション特性上、真に管理すべきクライアントの状態は少ないことを理由に、クライアントの状態管理としては useContext + useState を使用しています。 Styled-Components Styled-Components は言わずとしれた css-in-js ライブラリです。弊社では Styled-Components を React のスタイリングとして使用してきたこともあり、今回の FANTS ダッシュボードもスタイリングツールとして選定しました。 zero runtime が売りの Linaria も検討しましたが、JS のランタイムで CSS を生成する css-in-js と比較してどれくらいパフォーマンスが変わるのか不確実だったのと、管理画面という特性上、パフォーマンスがそこまで求められないことも選定から外した理由です。 Storybook 今回のダッシュボードでは Storybook を使用して汎用コンポーネント(Button や Modal 等)を管理しています。Storybook を見ることで、すでに実装されているコンポーネントを視覚的に確認することができたり、外部環境に依存することなく Storybook 上でコンポーネントが動作するため、コンポーネントの実装がやりやすいなどの利点があります。 ホスティングは Chromatic というサービスを使用しており、CI 経由でデプロイしています。 Storybook で管理しているコンポーネントの Visual Regression Testing はまだ出来ていないので、予期せぬ変更を検知するために今後やっていきたいですね。 ディレクトリ構成 次に、FANTS ダッシュボードのディレクトリ構成について紹介します。 └── src ├── apis // Adapter層。外部データとの接続を行う ├── assets // 画像ファイルを置く ├── components │ ├── atoms // 最小のコンポーネントを配置 e.g) Button, Icon, etc... │ ├── layouts // 全ページに関わるレイアウトを規定するコンポーネントを配置 e.g) Header, Footer, etc... │ ├── molecules // 2つ以上の atoms を組み合わせたコンポーネントを配置 e.g) ActionSheet, Cropper, etc... │ ├── organisms // ドメインコンポーネントを配置 │ └── templates // ドメインコンポーネントを組み合わせてレイアウトを行う │ ├── config // SWRConfigなどの設定ファイルを配置 ├── constants // 定数ファイルを配置 ├── contexts // Context API を配置 ├── hooks // カスタム hooks を配置 ├── libs // 外部ライブラリのコンポーネントを配置する層 ├── pages // Next.js の Page コンポーネントを配置 ├── types // 型定義ファイルを配置 └── utils // 汎用的な TypeScript の関数を配置 FANTS ダッシュボードでは、上記のディレクトリ構成をしています。各ディレクトリの責務に関してはディレクトリ名の右に書かれている通りですが、依存関係を図に表すと以下の感じになります。 それぞれのディレクトリの責務に関して詳しくは説明しませんが、components と apis をピックアップして説明します。 components コンポーネントの設計では Atomic Design での命名規則に則っていますが、責務としては Atomic Design を踏襲しておらず、独自の責務をそれぞれに持たせています。そもそも Atomic Design はインターフェースにおける見た目の粒度を示すものであり、システム的な責務は規定していないためです。 Atoms と Molecules は汎用的なコンポーネントとして、抽象化されて実装されています。例えば Button コンポーネントは Atoms 配下で管理しており、様々なコンテクストから使用されるため、色やローディングの有無、ボタン内の文言は抽象化しています。 Organisms はドメインコンポーネントとして、アプリケーションとしてのコンテクストを持ちます。つまり、ユーザー管理画面のユーザーリストや、ロゴアップロード画面のフォームなど、特定の文脈で使用されるコンポーネントのことです。ここでは、hooks や context の注入を許容し、外部データへのリクエストを行う部分でもありますが、Organisms を Container コンポーネントと Presentational コンポーネントとして分割し、Container コンポーネント側で hooks や context の依存を含めるようにしています。こうすることで、依存が局所的になり変更に強くなるほか、Presentational コンポーネント側はインターフェース(props の型)にだけ依存するようになり、再利用性が高まります。 Templates は Organisms を構成するコンポーネントで、ページのレイアウトを担当し、Pages コンポーネントは Next.js でいう getStaticProps やファイルルーティングとしての責務を持ちます。 今回上記のようなコンポーネント設計で実装を進めましたが、コンポーネント粒度が少し細かすぎる点もあるため、ドメインコンポーネント(Templates と Organisms)と汎用コンポーネント(Atoms と Molecules)という 2 つのディレクトリで分けてしまっても良いかもしれません。特に Templates は Organisms コンポーネントをラップするだけのコンポーネントとなっているため、Templates と Organisms は統合しようと考えています。 apis apis のディレクトリでは、Adapter 層として API などの外部データへアクセスする責務を持ちます。例として、apis ディレクトリは以下のような構造になっています。 └── apis ├── base.ts // GETやPUTなどの、httpのベース関数を置く └── users ├── requestFetchUsers.ts // baseの関数を注入してデータにアクセスする └── usersMapper.ts // レスポンスで受け取ったデータを整形する 今回 axios を使用しているので、base では以下のように axios のインスタンスを使って、GET や POST のリクエストを行うベースの関数を管理しています。また、弊社では JSON:API の形式で API のレスポンスが返却されるので、axios の intercept を使ってデシリアライズとキャメルケースへの変換を行っています。 const axiosInstance = axios.create ( { // ... } ); axiosInstance.interceptors.response.use (async ( response ) => { return await deserializer ( response.data ); // deserializerの中でキャメルケースの変換を行っている } ); export const get = async < T >( path: string ) : Promise < T > => { try { const response = await axiosInstance. get< T >( path ); return response.data ; } catch ( e ) { const error: FetchErrorType = { status : e.response. status, } ; throw error ; } } ; // ... 以下POSTやPUTを定義 この base で定義した関数を requestFetchUsers.ts で import し、ユーザー情報を取得するロジックを書きます。基本的にエンドポイントごとにファイルを分割します。 ここで取得したデータをフロントの世界で扱いやすくするためにマッパー層である usersMapper.ts でレスポンスデータを整形しますが、レスポンスが単純な場合はマッパー層を通さずに、API レスポンスの型のまま使用します。 最後に 今回の記事では、FANTS ダッシュボードのフロントエンド技術について解説しました。 FANTS ダッシュボードは、今後もサロンオーナー様が FANTS を長く使ってもらうために様々な機能を追加していく必要があります。その過程でフロントエンドの力がますます必要になってくるので、FANTS に興味のある方は下記の採用サイトをご覧になってください。一緒に FANTS を作っていきましょう! フロントエンドエンジニア採用ページ エンジニア採用ページ ここまで読んでいただきありがとうございました!
アバター