TECH PLAY

レバレジーズ 株式会社

レバレジーズ 株式会社 の技術ブログ

132

はじめに こんにちは。2021年新卒の田中です。現在私は、フリーター・既卒・第二新卒など、20代を対象とした就職・転職支援サービス「 ハタラクティブ 」のシステム開発やユーザー体験の改善に携わっています。 ハタラクティブでは、Laravel + jQueryだったフロントエンド環境をReact/TypeScript + Next.jsにリプレイスしました。その際、コンポーネントシステムとしてAtomic Designを採用し、入社1年目ながら選定から設計、導入プロジェクトの管理、運用ルールの構築を任せていただきました。 この記事では、ハタラクティブでどのような課題を抱えていてAtomic Designを採用したのか、導入を進めるにあたってどのような問題が発生し、どう解決したのか、メリットを含めてご紹介します。 課題は開発コストの削減とデザインの統一 ハタラクティブではフロントエンドをReact/Next.jsで開発していますが、当初はコンポーネント管理に関するルールが決まっていない状態でした。そのため、同じ記述が複数箇所にあったり、同じUIパーツが共通化されていなかったり、無駄な記述や重複が多く、開発コストが余計に増えたり保守性が下がる問題や、修正作業が属人化する問題を抱えていました。 当時は、スタイルガイドが機能していなかったため、チーム内でデザインについての共通認識を取れず、作成したUIパーツを使い回す体制も整っていませんでした。その結果、同じUIでもページによってデザインが異なっている状態が発生していました。 以上のことから、ハタラクティブオウンドメディアチームでは開発コストを小さく抑え、デザインが統一された状態を作る課題があったため、導入事例やドキュメントが多いAtomic Designを採用し、コンポーネントシステムを構築することを提案しました。 導入に際しての懸念 チーム内の知見が少なかった 当時は本格的にAtomic Designを使った開発経験があるメンバーがいませんでした。どのように導入すればいいかのイメージがすぐに湧かず、チーム内でコンポーネント化の共通認識が持てない状況でした。 そのため、私が主導して他サービスのAtomic Design導入事例の記事や社内の他チームへのヒアリングを通じて、Atomic Designに関する知見を集めました。自分で試しにテンプレートファイルのAtomic Design化を実装し、そこで得られた知見に基づき、チームで導入の方針や定義、懸念点の解消方法を明確にするための話し合いの場を複数回設けました。 導入コストが大きかった コンポーネント化を進めることで今までのStyleの記述方法を大きく変える必要があるなど、ハタラクティブへのAtomic Design導入コストの大きさの懸念もありました。 導入コストに関しては、既存コードの整理や将来的な開発コストの削減を目指す方針で認識を合わせ、通常施策とは別のリファクタリングタスクを通じて長期的に進めていきました。Styleに関してはNext.jsでサポートされているCSS Modulesを採用しました。CSS ModulesはCSSをmoduleに分け、JSファイルでimportすることでスタイルを適用する手法です。コンポーネントごとにmoduleファイルを分けることでスタイルをコンポーネントに閉じることができ、class名の干渉を考慮する必要なく記述することができます。 /* Example.module.scss */ .title { font-size : 1.6rem ; color : #222 ; } .box { padding : 8px 16px ; background : #eee ; } // Example.tsx import styles from "./Example.module.scss" const Example: React.FC = () => { return ( <> < h2 className = { styles.title } > タイトル < /h2 > < div className = { styles.box } > { /* 中にコンテンツ */ } < /div > < / > ) } export default Sample CSS Modulesの導入から記述方法の共有を主導し、デザイナーがコンポーネント単位の書き換えを徐々に進めていくやり方を取りました。 方針とルールを決めて、長期的に導入を進めていく 基本を理解した上でコンポーネント分けルールを作る Atomic Designの基本を理解し、ハタラクティブに導入する上での解釈をすり合わせ、コンポーネント分けルールを作成しました。ルールの一部を紹介します。 例えば、MoleculesはAtomsやOrganismsと異なり定義が難しかったため、再利用性を判断軸に置き、分類に迷った場合は一旦Organismsにする運用方針に決めました。 データの取得や保持に関しては、統一性と再利用性を高める目的で、API通信に関する機能や各ページで異なるデータはPagesのレイヤーに、固有のデータはOrganismsに持つようにしました。 準備→試験導入→実装の順にスケジューリング 方針とルールが固まったら、どういったスケジュールで進めるのかを決めました。実際のプロジェクトは準備編、試験導入編、実装編の3つのフェーズに分けて進めました。 準備編では「何をやるのか」「どこまでやるのか」が決まっている状態をゴールに据え、既存デザインパターンの洗い出しとコンポーネント設計を行いました。デザイナーと一緒に既存パーツのバリエーションを精査しページ全体のトンマナやデザインを統一しながら、コンポーネント分けルールに沿って分類と設計を進めました。 試験導入編は「チーム全員がやることを理解し、導入方針に納得している状態」をゴールとし、本格的な実装の前に工数を取って全員がAtomic Designを試す週を設けました。複数の視点から見えてくる問題や疑問を事前に解消し、スムーズに実装を進めることを目的としていました。 実装編は「通常施策への影響を小さく抑えながら導入を完了する」ことがゴールで、Atoms→Molecules→Organismsと小さいパーツから順に実装していきました。 スタイルガイドが機能していない問題に対しては、storybookの導入を決めました。 storybook.js.org storybookは、UIコンポーネントの管理やアドオンを使ったテストなどができるツールで、コンポーネント化と並行してスタイルガイド作成を同時に進めることにしました。 こうして約半年にわたるスケジュールを作成し、導入プロジェクトを進めていきました。 実装時にぶつかった問題 準備を重ねて実装編に入りましたが、いくつか問題があがってきました。 Atoms, Molecules, Organismsだけでの管理ができないケースがある Atoms、 Molecules、 Organismsだけでの管理は、ディレクトリを増やさずに済むのでシンプルさを維持できたり、世の中にリファレンスが多いメリットがあります。しかし、エントリーフォームなど複雑なUIに対しては「Atomsは最小単位で他のコンポーネントを含まない、MoleculesはAtomsを1つ以上含む」などのルールが適用できず、例外が発生してしまうのが避けられない状況でした。 それに対し、ハタラクティブ独自のディレクトリルールを追加するのは、慎重に考える必要がある一方で、ハタラクティブのデザインに合わせて管理しやすい形を実現できるメリットがあります。 そのため、既存のAtomic Designの形を大きく変えないように注意しながら独自のディレクトリルールを作成しました。具体的には、Layouts, Transitions, PageContentsディレクトリなどを追加し、それぞれレイアウトを共通化するコンポーネント、カルーセルやアコーディオンなどのインタラクションを共通化するコンポーネント、ページ全体のStyleやStateに関するコンポーネントを管理するようにしました。 コンポーネントに関する作業が属人化している 導入プロジェクトが始まってしばらく経ったタイミングで作業の属人化が問題となり、2つの問題に分割して対処しました。 1つ目は基準が明確化されていないことです。準備編で作成したルールを見直し、Atomic Designやハタラクティブのデザインパターンに関する知識が少ない人でも理解できるように、コンポーネントの各ディレクトリの役割や実際に分ける際のフローをドキュメント化し共有しました。 2つ目は、慣れの問題です。プロジェクトの進捗を優先していたことで特定の人にAtomic Designタスクが集まっている状態になっていました。限られた人しか出来なければ導入後の維持が困難になります。そのため、なるべく全員が実装タスクを担当するようにし、私がレビューに入るようにしました。 導入した結果 デザインの統一、管理が容易に 導入の過程でデザインを精査したことでデザインの統一が実現し、共通コンポーネントを使うことで微妙に違うデザインが生まれることが防げるようになりました。 storybookを使った管理をすることでデザイナー・エンジニア間のUIパーツの認識合わせや、どんなパーツがあるかの把握が容易になりました。 開発速度の向上と属人化の解消 コンポーネントが共通化されたことにより、変更を加える際の対応箇所が1箇所、ないしは少なくなって開発速度が向上しました。 それまでは修正漏れを避けるために担当者が固定されて作業が属人化していましたが、ファイルが統一されたことで誰でも修正を担当できるようになりました。 大規模なデザイン変更や新規コンテンツ実装にも力を発揮 Atomic Designの導入がほとんど終わってきた段階で、サービスのブランドコンセプト見直しに伴う大規模なデザイン変更がありました。その際、コンポーネント化によって新デザインの反映がスムーズになったり、ルールが作成されていたことで新規UI追加の際も深く悩むことなく実装ができました。 新規コンテンツとして求人検索ページの実装タスクでも力を発揮しました。 求人検索ページ はボタンなどの既存パーツを使いつつ、検索パネルなど新規のUIも多いコンテンツでしたが、storybookやコンポーネントルールにより作業効率を高めることができたことで2週間という短い時間での実装を可能にしました。 終わりに Atomic Designの採用によって、開発コストの削減とデザインの統一という課題を解決し、オウンドメディアとして今後の成長を加速させていく基盤を作ることができました。しかし、Atomic Designを導入さえすれば解決できるというものではありません。職種を超えて課題について議論し、常により良い方法がないかを模索できるチームがあったからこそ実現できたと思っています。 配属から2ヶ月足らず、Reactに対する経験も浅い状態で、今回のAtomic Design採用提案から導入まで担当させていただきました。このように、レバレジーズには年次や経験を問わず、主体的に取り組める環境が整っています。 レバレジーズは「主体的に仕事に取り組みたい」「エンジニアの技術を活かしてサービスの課題を解決したい」と思っている方が活躍できる環境です。 ご興味を持たれた方は下記リンクからご応募ください。 leverages.jp
アバター
はじめに こんにちは、レバレジーズ フリーランスHub開発チームです。 現在、弊社が運営する フリーランスHub というサービスでは検索エンジンとしてElasticsearchを採用しています。 この記事では、エンジニアがサービスの成長速度と工数を考慮した上で、Elasticsearchを採用するに至った背景や理由、そして、MySQLからElasticsearchへ移行した結果どれくらいパフォーマンスが改善したか、などを紹介していきたいと思います。 サービス紹介 フリーランスHub は全国のフリーランスエージェントの保有案件から、10万件以上の案件をまとめて掲載しているエンジニア・クリエイター向けのフリーランス求人・案件メディアです。 2021年4月にリリースした新規事業で、サイト内でフリーランスエージェントの案件を集約して掲載しており、フリーランスを検討される方が案件に一括で閲覧・応募することができます。 また、サイト内で開発言語や職種別に案件の検索ができることに加え、エージェントの詳細ページから各エージェントの特徴やサービスを受けるメリットなどを比較し自分にあったフリーランス案件・フリーランスエージェントを探すことが可能です。 検索機能の説明 検索パネル は開発スキルや職種、業界、こだわり条件(週3など)や、都道府県、路線、駅での検索機能があります。 サービス設計フェーズ 見通しを立てる 事業をスタートする際は様々な軸で数年の見通しを立てます、今回のテーマに影響するものはこの辺です。 フリーランスHubは機能数が少ないので小さくスタートし、短期間で拡張と機能追加をしてサービスを成長させていく方針にしました。 その他見通しの説明は記事の最後で 補足 しておきます。 アーキテクトの選定 AWSを利用している他プロジェクトの導入実績をベースに選定、Elasticsearchでの全文検索は幾つかの事業チームでの運用実績があります。 RDSは2つあり、メインはAurora(MySQL)、データ収集と分析を低スペックのMySQLにしています。 集まってくれた開発メンバーの中にElasticsearchを熟知しているメンバーがおらず、安定性と開発速度の両面で懸念が出てきました。 検討 リリース前に準備しておきたいことは山積しているため甘い判断は出来ない BigQueryでの全文検索は社内導入実績が無いためElasticsearchよりハードルが高い 最終的にElasticsearchは必要になる 開発期間中に実際のデータが無い状態で、振る舞いを熟知し安定運用レベルに達するのは難しい 5-10万件ならAurora(MySQL)FULLTEXTインデックスでも良さそう RDB利用のほうがLaravelデフォルト機能の親和性が高く開発が楽で初期はメリットが多い AuroraはMroongaの利用が出来ないが、Elasticsearch移行するなら大きく問題は無い 決断 Aurora(MySQL)FULLTEXTインデックスで初期開発を行う 機能・拡大フェーズでElasticsearchへ切り替える際、スピードを損なわないシステム設計を行う システム処理設計 まずはRDSだけでシステムを構築し、切替前にElasticsearchにデータを同期した後、検索機能を切り替える方法を選択しました。 リリース前 (3万件〜) 機能追加・拡大 (9〜12万件) 処理説明 案件データ作成 : HTMLクロール等で収集した元データから表示用のテーブルを作成 全文検索データ作成 : タイトル・内容・スキルなどの文字列を検索用テーブルの1つカラムへ 件数作成 : 表示する事の多い全案件数などはカウントテーブルへ事前に入れておく サイトからの検索処理 全ての検索処理は検索テーブル、表示は表示テーブルと役割を分ける ヒットした検索テーブルからidを取り、表示テーブルをidで検索し表示する 例えば検索パネルで PHPで検索した場合 PHPという文字列で検索テーブルを検索する スキルテーブルからPHPを探しテーブルidで検索というプロセスは行わない 一覧だけでなくサジェスト処理なども同じ 検索データを上手に作成しておく事が必要となる代わりに、検索SQLでテーブルをジョインする必要が無くシンプルになります。 Elasticsearch利用時も検索テーブルだけのシンプルなクエリに出来ます。 インフラ構築 Elasticsearchは最終的なスペックでビルドし利用するまでスペックを落として運用していました。 AWSはCDKで構築したのでサイズとノード数を変更し実行するだけで切り替わる構成です。 Elasticsearchに限らず初期は不要なケースも最初から作成しておきました。例えば、MQはRDSでスタートし後でSQSに切り替えました。 インフラ変更は影響範囲が広く、後で追加を行う時の苦労が多いため、構成は最初から作っておき利用時に起動する等により、サービスの安定性と成長スピードを損なわないメリットがあります。 無駄なコストが発生してしまうので、サービス成長速度を上げ無駄となる期間を短くします。 比較 実行結果 早速結果から、クエリキャッシュOFFで実行しています。 MySQL long_query_time は 0.25s を閾値にして運用しています。 案件数が5万件ほどの時期にソート順の要件が増えてきました。 優先 : 一部の案件を上位に表示 募集終了 : 募集終了した案件も一覧の後ろの方に追加 指定した検索条件で関連順の場合はMySQL FULLTEXTインデックスでも高速ですが、独自にソートを追加したSQLは速度低下が見られました。 ダミーデータで作成した32万件のテーブルにクエリ実行した結果、ヒットした件数が増えるほどソート処理に大きく時間がかかる事がわかりました。 契約数・掲載数も順調に伸びているため、早めにElasticsearchに切り替えることにしました。 1週間ほどエンジニア1名をElasticsearchに貼り付け32万件のダミーデータでクエリを発行し大きく改善する事を確認、他の追加開発を止める事もなく、その後3週間ほどで切り替えが完了。 トークナイザーやデプロイの最適化など、経験と専門性が必要な領域は切り替え後に作り込みました。 クエリ テーブル、インデックスは item_search です。 CREATE文は 補足 に記載しておきます。 Aurora(MySQL) 初期 SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE MATCH(search_string) AGAINST( -- キーワードに「東京」が含まれる案件の中で、「Java」もしくは「PHP」の案件の取得 ' +( ""東京"") +( ""Java PHP"") ' IN BOOLEAN MODE ); +優先+募集終了 ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE -- `item_id` = 25000,30000,35000,40000,45000は上位表示する優先データ `item_id` IN ( 25000 , 30000 , 35000 , 40000 , 45000 ) ORDER BY `updated_at` DESC ) UNION ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE `status` = 1 AND MATCH(search_string) AGAINST( ' +( "東京") +( "Java PHP") ' IN BOOLEAN MODE ) ) UNION ALL ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE `status` = 2 AND MATCH(search_string) AGAINST( ' +( "東京") +( "Java PHP") ' IN BOOLEAN MODE ) ); 上位表示する優先データは別のクエリで先に求めています。 上2つのSQLをUNIONしているのは上位表示したデータの重複を防ぐためです。ヒット件数が多い検索条件であるほどソート処理に時間がかかります。なおUNION ALLにしてもヒット件数が多い限りほぼ改善しません。 また、WHERE句でMATCH()を使用すると自動的に関連度順でソートされますが、SELECTステートメントでもMATCH()を使用する事でORDER BY句で関連度順ソートを指定する事ができ、statusと併せてソートする事も可能です。ただ、前者に比べ後者の方がソートのオーバーヘッドがかかるため後者は採用しませんでした。 Elasticsearch +優先+募集終了 GET /item_search/_search { " track_total_hits " : true , -- 表示する案件は1ページあたり30件のためsizeを30で指定 " size " : 30 , " query " : { " bool " : { " must " : [ { " match " : { " search_string " : { " query " : " Java PHP " , " operator " : " or " } } }, { " match " : { " search_string " : { " query " : " 東京 " , " operator " : " and " } } } ], " should " : [ { " terms " : { " item_id " : [ 25000 ], " boost " : 10000 } }, { " terms " : { " item_id " : [ 30000 ], " boost " : 8000 } }, -- <中略> -- { " terms " : { " item_id " : [ 45000 ], " boost " : 2000 } } ] } }, " sort " : [ { " status " :{ " order " : " asc " } }, { " _score " :{ " order " : " desc " } } ] } 上位表示する優先データはスコアをブーストする方法で実現しています。 切替時のプログラム変更 データ作成 LaravelのScoutを用いて、Auroraの検索テーブルを追加・更新した場合Elasticsearchに自動的に同期される作りにしました。 そのため、モデルをElasticsearchと自動的に同期させるモデルオブザーバを登録するために、検索可能にしたいモデルにLaravel\Scout\Searchableトレイトを追加しました。 use Laravel\Scout\Searchable; class Post extends Model { use Searchable; } 検索 以下のクラスを作成し、Elasticsearchで検索できるように設定しました。 app/Providers/ElasticsearchServiceProvider Scoutを使ってElasticsearchを使えるようにする設定を記載 app/Scout/ElasticsearchEngine ScoutからElasticsearchを操作する内容を記載 また、SQLを呼び出す側だったRepository層では、SQLからElasticsearchのクエリに書き換えました。 形態素解析 php-mecab 形態素解析はphp-mecabを利用 スキルテーブルや駅テーブルの名詞をdictに登録 案件データ作成や検索処理以外でも形態素解析の処理がある ES切り替え後もkuromojiだけに出来ない Elasticsearch kuromoji mecab登録した内容はkuromojiにも登録 検索用データの工夫 1文字は単語として認識しないため、検索データ・検索クエリ共に変換して処理しています。 認識しない記号も同じように変換しています。 駅名は検索データに「渋谷」「渋谷駅」の2つを登録し、検索パネルの場合「渋谷駅」に変換し「渋谷区」の別の駅にヒットしないようにしています。 これらの文字列はmecab、kuromojiに登録しておきます。 設定 Aurora(MySQL) 1単語認識文字数 デフォルトは最小4文字ですが「DB」「東京」などあるため2で設定します。 ft_min_word_len=2 innodb_ft_min_token_size=2 変更する場合はDB再起動が必要なので必ず先にやっておきましょう。 ngram MySQL FULLTEXT インデックスはWITH PARSER を指定してINDEXを作ります。 ngramするとアルファベットがバラバラになる "baby" を "ba", "ab", "by"にトークナイズする ngramしないと文章内の日本語がうまく取れない スキル(PHP・Linuxなど)での検索の重要性が高いためngram無しでINDEXを作成しました。 ALTER TABLE `item_searches` ADD FULLTEXT KEY `idx` (`search_string`); 例えば「...業務内容はブリッジSEとして...」を解析しても「ブリッジSE」ではヒットしない挙動となり、 Elasticsearch移行前の大きな課題でした。 Elasticsearch トークナイザー modeをnomalに設定することで、辞書登録した文字列を1単語として単語認識させることができます。 "tokenizer" : { "kuromoji" : { "mode" : "normal", "type" : "kuromoji_tokenizer" } } モードがデフォルトのsearchだと、例えば javascriptを形態素解析した場合、javascriptの他に java script としても分解されるため、検索結果にjavaの案件が含まれる事になります。 今回の事例では辞書登録した単語を1単語として単語認識させることで検索結果の精度を上げています。 Character Filter ※2022/03/10 追記 char_filterで検索文字列の大文字を小文字に統一する設定も行なっています。 "char_filter": { "lc_normalizer": { "type": "mapping", "mappings": [ "A => a", "B => b", "C => c", --- <中略> --- "Z => z" ] } }, Character Filterを使い入力文字列を小文字に正規化することで、大文字・小文字に関係なく(※)辞書登録されている文字列に一致させることができます。 ※ ユーザ辞書を作成する際にアルファベットは小文字限定で作成している、という前提があります。 エイリアス 必要な理由 インデックスはフィールド追加やKuromojiの内容変更などで作り直す事がある プログラムからインデックスを直接参照すると作り直すタイミングでサービスに悪影響が出てしまう 構造 エイリアス item_search インデックス item_search_yyyymmdd 変更時はインデックスを作り直しAliasに紐付け検索データを切り替える リリース手順の変更 artisanコマンドを作成し、以下を実行しています。 スキル・駅などのデータを取得しmecabのdictを作成 Elasticsearchインデックスの変更がある場合のみ以下も実行 定義に従いインデックスを作成 item_search_yyyymmdd スキル・駅などのデータをKuromojiに登録 Aurora item_search から Elasticsearch item_search_yyyymmdd へデータをコピーするプログラムを実行 エイリアスを切り替え 終わりに 簡単ではありましたがElasticsearchを採用するに至った背景や理由、MySQLからElasticsearchへ移行した結果どれくらいパフォーマンスが改善したかを紹介しました。 今回の事例のように、レバレジーズでは事業課題を解決するための技術選定やプロダクト改善をエンジニアが主導して行うことができます。 このような主体的に提案ができる環境で是非一緒に働きませんか? レバレジーズ フリーランスHub開発チームでは一緒にサービスを作ってくれる仲間を募集中です! ご興味を持たれた方は、下記リンクから是非ご応募ください。 leverages.jp 補足 見通し 案件(求人)数 事業モデルが現状だと数年運用しても100万件には達しない スペックアップが簡単など充分コントロール可能なので過度な予測を行う必要がない リリース前期間の見通し 結果的に3ヶ月+12日 システムマネージャー2名と類似サービスを見ながら30分ほど打ち合わせ 楽観でチャレンジングな目標を設定 機能・非機能を洗い出しグルーピングして一覧化 ストーリーマッピングの要領でリリース前のラインを引く 最初は楽観でラインを引く 実際の開発進捗を見てライン・スケジュールどちらかを調整 どうすれば出来るかを常に考える リリース直後が最も忙しくなる 経験則ですが、例えば以下などが良くあります 初めて発生する事案や運用が多い 発見した不具合を即日で潰す 新規の契約や案件(求人)掲載の集中 CREATE MySQL FULLTEXT インデックス CREATE TABLE `item_searches` ( `id` int( 11 ) NOT NULL , `item_id` int( 11 ) NOT NULL , `base_value` int( 11 ) DEFAULT NULL , `status` tinyint( 4 ) NOT NULL , `search_string` text NOT NULL , `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ALTER TABLE `item_searches` ADD PRIMARY KEY (`id`), ADD KEY `item_searches_item_id_index` (`item_id`) USING BTREE, ADD FULLTEXT KEY `idx` (`search_string`), MODIFY `id` int( 11 ) NOT NULL AUTO_INCREMENT; Elasticsearch PUT /item_search?pretty { "aliases" : { "item_search_replace" : { } }, "settings" : { "index" : { "max_result_window" : "500000", "analysis":{ "char_filter": { -- 2022/03/10追記 "lc_normalizer": { "type": "mapping", "mappings": [ "A => a", "B => b", "C => c", --- <中略> --- "Z => z" ] } }, "filter" : { "greek_lowercase_filter" : { "type" : "lowercase", "language" : "greek" } }, "tokenizer" : { "kuromoji" : { "mode" : "normal", "type" : "kuromoji_tokenizer", "user_dictionary_rules" : [ "Java,Java,Java,スキル", "PHP,PHP,PHP,スキル", -- <中略> -- "渋谷駅,渋谷駅,渋谷駅,駅名", "東京駅,東京駅,東京駅,駅名" ] } }, "analyzer" : { "analyzer" : { "type" : "custom", "char_filter" : "lc_normalizer", -- 2022/03/10 追記 "tokenizer" : "kuromoji", "filter" : ["kuromoji_baseform", "greek_lowercase_filter", "cjk_width"] } } } } }, "mappings" : { "properties" : { "id" : { "type" : "integer" }, "item_id" : { "type" : "integer" }, "search_string" : { "type" : "text", "analyzer": "analyzer", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "base_value" : { "type" : "integer" }, "status" : { "type" : "integer" }, "created_at" : { "type" : "date" }, "updated_at" : { "type" : "date" } } } }
アバター
はじめに こんにちは。レバレジーズ株式会社エンジニアの加藤です。 今回は、私が所属しているHRテック事業部における新規SaaSの開発についてご紹介したいと思います。 事業領域について 私たちの事業部が扱う領域はHRTechといい、主に会社の人事業務に対するサービスを展開しています。「人事」と聞くと採用活動をイメージされることも多いですが、ここでは「従業員が入社し、退職するまでに関わる業務の全般」を指します。そのため、採用管理や勤怠管理、タレントマネジメント、給与管理などをはじめとした、さまざまなSaaSが存在しています。勤怠管理サービスなどは使ったことのある方も多いのでイメージしやすいのではないでしょうか。 HRTechの役割 人事業務は人材採用、人材育成、離職対策、人事評価、労務管理など多岐に渡り、従業員が入社から退職するまでなくてはならない業務です。適切に人事業務が提供されない会社では従業員は力を発揮できませんし、当然ながら定着もしません。つまり、人事業務が最適化されているかどうかは「会社の発展や成長」を大きく左右することになります。また、会社規模が大きくなればなるほど、人事業務の複雑性は指数関数的に増していきますので、ますます仕組みとして整っていなければなりません。そのため、手続きや情報の管理を効率的に行い、組織設計、採用、人材育成、離職対策など有効な施策立案に繋げ、人事の価値創出に寄与することがHRTechサービスに求められる役割です。 HR(人事)領域の抱える課題 しかしながら、既存のサービスは機能によって(採用管理・勤怠管理・労務管理といったように)分類化されており、人事業務を包括的にカバーできるものはありません。 例えば、「採用・人材育成・離職対策」といった機能は相互に関係性を持ちますが、それぞれの機能ごとにプラットフォームが分断されていれば、統合された情報を元にした適切な人事オペレーションを提供することは難しくなります。 既存の人事業務を各機能ごとにデジタル化(またはDX化)することはできても、HRTechを利用することで人事の価値創出に繋げることができていないということが、人事業務とHRTechという領域の課題といえます。 私たちのプロダクトの意義 前述の課題に対して、私たちのサービスでは例えば、各人事業務で蓄積された従業員の志向性やエンゲージメント、実績や評価などの情報を元に個別適切な配置転換や離職対策に活用することができます。 昨今の潮流としても、企業は労働者の価値観やワークスタイルニーズの多様化などにより、以前よりもパーソナライズされた労働環境の提供が求められています。 より個に適した形で人事業務を行うには、分断されたプラットフォームではなく、包括的に情報を管理できる環境が必要になります。 チーム体制について 私たちの事業部ではマイクロサービスアーキテクチャを採用しており、開発体制としてはスクラムを採用しています。またスクラムの中でもLeSSと呼ばれるものに近く、4-6人程度を1群としたチームが4チームほどで動いています。それぞれのチームが1つのプロダクトを開発するよう自己組織化されています。 チーム体制のメリット 意思決定の速さ 事業部としての大きな方針はありますが、チームのための意思決定はチーム独自で行っています。人員構成もエンジニアが主体となっているためエンジニア主導でコトが進みその速度も速いです。少人数ゆえメンバー1人1人の意見がダイレクトにチームに反映されます。 業務の範囲に制限がない 担当業務は単純な作業に留まることはなく、フロントエンドやバックエンド、インフラを問わず機能の要否や設計から実装まで行います。 作業範囲が広いため、様々な技術に触れながら成長することができますし、事業計画に直接携わるなど、エンジニアに留まらない仕事の仕方ができる可能性もあります。 もちろん作業範囲が広いだけではありません。例えばプロダクトの仕様についても、単に仕様を満たすための部分的な作業ではなく、プロダクト全体、ひいてはサービスの成長を見据えた上で設計を行うため、事業経営の中核に立って開発を進めることができます。 コミュニケーションの取りやすさ 少人数のチーム構成のため、メンバーと自分の作業関係が把握しやすく、開発に関するやりとりも齟齬なく行うことができます。 開発業務はハードですが、他のチームも含めて非常に安心感のある組織だと自信を持って言えます。 使用技術について TypeScript フロントエンドとバックエンドの使用言語を揃えることで、同じ文法で開発でき、実装者の学習コストを下げ、ソースコードの流用もしやすくなります。 実装者もフロントエンド、バックエンドを問わず実装に参加しやすくなり、フロントエンドもバックエンドも実装したい開発者にとっては魅力だと思います。(ごく一部PHP, pythonもありますがほとんどはTypeScriptです。) GraphQL オーバーフェッチングやエンドポイントの複雑化などREST APIの課題を解決するために生まれました。 AirbnbやNetflix、Shopifyなどをはじめとした多くの企業で使用されています。 クライアントとしてApollo GraphQLを利用しており型安全なAPI開発、利用が実現できています。 Google Cloud Platform 私たちのチームでは、外部サービスの利用を除きプロダクトのバックエンドインフラをGoogle Cloud Platform(以下GCP)の利用に振り切って開発しています。 Cloud RunやCloud SQLをはじめとしたマネージドサービスを利用することで、開発者はコンテナやコンテナ内部のアプリケーションの開発に専念することができます。 Cloud PubsubやCloud Build、Cloud Monitoringなども利用しており、GCPインフラでのDevOpsやSREに興味のある方にとっても魅力の高い環境かと思います。 下記に簡単に仕様技術の一覧をまとめています。 FE React Apollo Client Material-UI BE GraphQL Express/NestJS TypeORM Docker infra GCP Cloud Run Cloud SQL Cloud Storage Cloud Pub/Sub Cloud Build Cloud Memorystore(redis) Cloud Monitoring App Engine Firebase CI/CD Github Actions Cloud Build external-service SendGrid(email delivery) Auth0(IdaaS) Stripe(payment) おわりに いかがでしたでしょうか。 私の所属するHRテック事業部は発足してまだ日が浅く、プロダクトの状況も0-1フェーズにあります。しかし、だからこそ今回ご紹介したような魅力を強く感じられる組織であると思っています。私自身もレバレジーズに入社してまだ1年未満ですが、事業内容や使用技術、働き方など、非常に挑戦的で魅力のある環境だと実感しています。この記事をご覧になり、この事業部を一緒に盛り上げてみたいなと思う方が1人でも増えれば嬉しく思います!
アバター
本文 はじめに  レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。レバレジーズは創業以来事業の成長が続いていますが、ここ最近は事業の拡大にシステムの開発が追いつけていない状態でした。そのため、システム開発が事業の拡大に追いつき、更には加速させるためにマイクロサービス化を主体においた技術スタックの刷新を進めています。この記事では、導入したまたは導入しようとしている主な技術の紹介と、選定した技術が目先の開発だけではなくエンジニア組織全体の課題をどのように解決するかを紹介していきたいと思います。 導入技術紹介  まず、現在導入したまたは導入しようとしている主な技術のご紹介から始めます。 マイクロサービス化 レイヤードアーキテクチャ(クリーンアーキテクチャ、ヘキサゴナルアーキテクチャ) DDD TypeScript gRPC gRPC-web GraphQL Github actions BDD/ATDD(cucumber) コード生成 CDK Docker ECS Fargate Service Mesh DataDog 列挙した技術がざっくりどの領域に関係するかの図はこちらになります。  こちらに紹介しているのは主要な技術のみで、他にも様々な技術検証、導入を進めています。それらはこれからの弊社の技術者ブログを楽しみにしていただければと思います。また、個別の技術の紹介もこちらの記事では詳しく行いませんので、あしからず。 事業成長にシステムが追いつくための課題  それでは、本題の技術導入で解決したい課題を説明していきます。  まず弊社の開発体制ですが、基本的には1サービス毎にチームが付き、サービス毎にコードベースを分けて開発を行ってきました。  レバレジーズ主力事業である『レバテック』は、サービスを使ってくださるエンジニアの方や、企業様が年々増えており順調に成長を続けてきました。また、『レバテックダイレクト』や『レバテックカレッジ』をはじめとするレバテックシリーズの新サービスや、看護師転職支援サービス『看護のお仕事』、ITエンジニア向けQAサイト『teratail』などのサービスも提供をしてきました。しかし、開発サイドでは、長年機能とビジネス範囲の拡張を続けたことによるコードベースの肥大化や、サービス間のデータ連携のために本来独立しているはずのチームが密に結合してしまい、人を増やすだけでは事業成長に追いつけなくなってしまいました。特に 相互依存する、複数の巨大モノリシックサービス 標準化の欠乏 自動化の欠乏 の3つによって、組織としての開発効率が下がってしまっていました。 相互依存する、複数の巨大モノリシックサービス  モノリシックサービスとは、一つのプログラムで一つのサービスを作ることを指します。 弊社もこれまではモノリシックにサービスを開発してきましたが、長年機能拡張を続けてきた結果、PHPで開発してきたことも相まって、コードを一部修正した場合に影響範囲がどこまで及ぶがわかない状態となり、影響の確認のために多大な工数がかかるようになってしまいました。また、サービス間でのデータ連携も行われているため、影響範囲が1つのサービスだけに留まらず他のサービスを巻き込んでバグを出すということも頻発するようになってしまっていました。  さらに、一つのプログラムが受け持つビジネス領域も拡大してきた結果、必要な業務知識も膨らみ、新しいエンジニアがまともに開発できるようになるまでにも多くの時間がかかるようになっていました。 標準化の欠乏  弊社では基本的に1サービス1チームが割り当てられ、コードベースも分けて開発を進めています。しかし、これまではコード規約ぐらいしか標準化されたものが無く、開発スタイルや設計、インフラ構築方法もチームでバラバラでした。そのため、スポットで他チームへヘルプに入ったり、ノウハウの共有が困難でした。  新規サービス開発の際も、機能の流用ができる作りになっていないので、各サービスで同じような機能をバラバラに作るということも多く発生していました。 そのため、チームが増えても相乗効果が発生せず、組織としての開発効率がいまいち上がらない状態になっていました。 自動化の欠乏  CI/CDが入っていないサービスも多々あり、デプロイやインフラ構築もシェルを手動実行するものが数多く残っていました。そのため、開発面ではチーム毎、開発者ごとにコードの品質が揃わなかったり、運用ではデプロイが限られた人しか実行できなかったり、新しいサービスを作る際にもインフラ構築に工数が多くかかる上に、インフラの監視やログ監視も抜け漏れが発生するなど、開発効率の低下や品質の低下を招いていました。 導入技術がどのように課題を解決するか  上記の課題を解決するのは一つの技術だけでは解決できないため、マイクロサービス化を中心においた技術スタックの刷新を行い、技術の組み合わせによって解決を目指しました。 マイクロサービス化とレイヤードアーキテクチャを主体とした分割統治  プログラムの設計において分割統治という考え方があります。大きな問題を小さな問題に分割して、小さな問題をすべて解決することで全体を解決するという考え方です。  サービスのフロント、BFF、バックエンドの3層への分割、ビジネスドメインを再整理しドメインに分割しマイクロサービス化、一つのマイクロサービス内でのレイヤードアーキテクチャによる分割を行っています。それにより、 プログラム変更の影響範囲が限定される 開発者が知る必要があるドメイン知識が減る ユニットテストが容易になる などのメリットを享受でき、結果、長期に渡り開発効率を高い状態で維持することが期待できます。 TypeScript、gRPC, GraphQLなど型システム技術導入によるチーム間コミュニケーションの円滑化  マイクロサービス化、レイヤードアーキテクチャ化することでチームは細分化されることになるので、チーム間のコミュニケーションが重要になってきます。そのため、フロントエンド、バックエンドともにTypeScriptへ切り替え、マイクロサービス間はgRPC、フロントエンドとバックエンド間はgRPC-webまたはGraphQLを導入しました。gRPC、gRPC-web, GraphQLのIDL(interface definition language)が、そのままAPI仕様書となり、IDLからコードを自動生成しコンパイルを行うことで変更点をコンパイルエラーとして検出できるため、仕様変更した際の伝達が容易かつ安全に行えるようになります。 CI/CD、コード生成、BDD/ATDDの活用による品質の担保  レイヤードアーキテクチャ化によるユニットテストの容易化、TypeScript化によるコンパイルエラーによるエラー検知を活用するためCI/CDとしてgithub actionsを導入しソースコードのPush時に自動で検証できるようにしています。  TS化、CI/CDでエラー検知が容易なったことでコード生成も最大限活用できるようになりました。gRPCのIDLからのコード生成の拡張によりレイヤードアーキテクチャのためのボイラープレートの生成や、DBアクセスオブジェクトからの安全なSQL発行コードの生成などを導入することで、開発速度の向上に加えコード品質の安定化も行っています。  一部のチームではユニットテストに加えてcucumberを導入して、マイクロサービスのAPIをBDD(振る舞い駆動開発)、フロントエンドをATDD(受け入れテスト駆動開発)しています。実装前にエンジニア同士やディレクター-エンジニア間で仕様のすり合わせと検討が可能になり、手戻りが減った上に設計品質の向上にも繋がりました。 IaCによるインフラの既製化とDevOps推進  弊社はAWSをメインに使用しているため、インフラの構築はCDKを導入しました。CDKはTypeScriptやJavaなどプログラミング言語を使ってインフラを構築できる技術です。アプリサーバーの構成がすべて、フロント、BFF、バックエンドの3層化しDocker+ECS Fargateでサーバーレス化したことで、同じインフラ構築のコードをどのチームでも使い回せるようになりました。CDKをラップした社内ライブラリを用意したことで、アプリエンジニアがインフラ構築から行えるようになり、かつ、サーバーの構築が共通化されたことで監視の漏れや設定のミスもなくなりました。  現在は更にDataDogの導入も行い、インフラ監視、ログ監視などを強化しています。  また、マイクロサービス化が進み、サーバーも増えてきているため、ServiceMeshも導入を進めており、より緻密で効率的なサーバー管理を目指しています。 便利さによる標準化の浸透  技術選定したはいいものの浸透しないということもあると思います。大体は新しい技術を覚えたり切り替えることに対する不安が原因なことが多いです。今回選定した技術に関してほぼ全て社内ライブラリを用意し弊社の開発に特化させることで、 新しいことを覚えるコスト <<< 選定された技術を使うメリット という状況を作り出しました。一つの技術選定だけでは実現は難しいですが、複数の技術をうまく組み合わせその状況を作り出せれば、あとは自己増殖的に浸透してくれます。 おわりに  レバレジーズは「関係者全員の幸福を追求する」ミッションを達成するため、今回ご紹介したように、開発効率を高め、事業実現を素早く行えるエンジニア組織への変革をすすめています。そのため技術選定も技術的好奇心や目下の開発だけにとらわれない選定を心がけて行っています。しかし、まだまだ改善できるところは多々あります。もし「技術選定を行って組織を変えていきたい」と思っている方がいましたら、ぜひ一緒に働きませんか?エキサイティングな仕事ができると思います。 ご興味ある方は、こちらからエントリーをお願いします! https://recruit.jobcan.jp/leverages/ recruit.jobcan.jp
アバター
はじめに こんにちは! エンジニアの室井です。 近年、レバレジーズは事業の急拡大を続けていますが、その中核となっている「 Dataforce(データフォース) 」と呼ばれる自社プロダクトをご紹介します。 Dataforce(データフォース)とは? Dataforce(データフォース)はレバレジーズが内製化しているSFA ※1 です。 ※1 営業業務の効率化を図り、売上や利益の向上を実現する営業支援システム、Sales Force Automationの略称 Dataforceは現在、看護師の紹介事業・派遣事業、そして介護士の紹介事業・派遣事業の4つの事業で利用されています。 国民の4人に1人が65歳以上という少子高齢社会を迎えた今、医療従事者の需要は日本全体で高まっていますが、依然として十分に供給が追いついておらず、医療・介護業界における人手不足が発生しています。 だからこそ我々SFA開発チームは、Dataforceを通して営業業務の効率化を向上させることで、病院や施設の抱える人手不足問題の解消に貢献することができると考え、日々取り組んでいます。 Dataforceが持つ主な機能は次の3つです。 1. 求職者、事業所管理機能  求職者、事業所に関する基本情報や接触履歴など全ての情報を統合管理する機能です。 顧客から問い合わせが入ったときに担当者が不在でも、顧客管理画面を呼び出せば相手先の情報がひと目でわかり、担当者以外の人の的確かつ素早い対応が可能です。 2. フロー管理機能  求職者に求人を紹介をする段階から、面接日程調整・入職までの流れの中で、個々のフローを管理する機能です。 ひとつひとつのフローについて、顧客情報、求人情報、営業フェーズ、入職予定日など、紐づけられた関連情報を俯瞰することができます。 3. 活動管理機能  営業活動における行動や結果を数値化して管理する機能です。 担当者の案件数、営業実績などの情報を管理・蓄積することができるため、成績の良いメンバーの行動パターンを、他のメンバーに共有すれば、チーム全体のパフォーマンスの底上げにもつながります。 Dataforceの強み Dataforceには多くの強みがありますが、今回はその中から2つご紹介します。 1. 営業業務に必要な機能の集約  LINE to Call や Twillo などの外部連携APIを使用してDataforce内でLINE、SMS、電話といった顧客とのコミュニケーションで使用するツールが利用できます。 これまでバラバラに管理・運用していたツールを一元化することによって、データ統制のみならず、営業業務の効率化や生産性向上を実現しています。 2. 営業向けレコメンド機能  営業業務のサポート機能として、独自のアルゴリズムを基にした求人レコメンド機能があります。この機能は、求職者の希望条件に近い求人をキャリアアドバイザー(営業)へとレコメンドしてくれる機能であり、新人教育や早期立ち上がりなど、さまざまな面で営業組織をサポートしています。 Dataforceの開発チーム 開発チームは、施策の立案から、要件定義 / 仕様設計 / スケジューリングまでを担当するディレクターと、フロントエンドやバックエンドにおけるアーキテクチャ設計 / 実装設計 / 実装 / コードレビューを行うエンジニアで構成されています。 開発手法は、2週間単位でスプリントを設定したオリジナルのスクラム開発で行なっており、スプリントの始まりに工数見積もり会を開き、終わりにKPTを用いてスプリントの振り返り確認をします。 業務内容はエンジニアリングに限らず、基本的に制限がないことから、意欲があれば誰もが色々な業務に挑戦できる組織です。 チームの特徴としてよくあがることが2つあります。 1. 開発サイクルが速い  スクラム開発体制を採用することで、ディレクターとエンジニアを同じチーム内に配し、コミュニケーションコストを削減することができ、システムを企画・開発するサイクルが速いです。 2. 主体的な人が多い  チームやシステムをより良くしたいと主体的に動く人が多いことも特徴の一つです。 日頃から改善点がないか探している人が多いので、エンジニアの業務改善なども頻繁に行われています。 エンジニアの業務改善にとどまらず積極的に施策を立案したり、UI/UX改善を行ったりなど、様々なことにチャレンジしています。 技術スタック ■Laravel  Dataforceを利用する事業が多岐にわたることから、営業業務プロセスを理解しなければ開発ができないのはよくあることです。しかし、それではプロジェクトに参画した人が即戦力として活躍することが難しくなってしまいます。 そのため、Laravelのサービスコンテナの仕組みやパッケージ開発の容易性を活用し、ルールや事業知識がなくても良い基盤を設計し、パッケージをリポジトリに切り分けることで効率的な開発ができるだけでなく、誰でもすぐに即戦力となり活躍することができます。 ■AWS  Amazon TranscribeやAmazon Comprehendを使用して、顧客とのヒアリングなどで得られる情報をシステムに自動入力することで、営業業務を削減したり、リアルタイムで行われる対話から顧客の感情を解析しCX最適化を行うなど、音声分析に取り組んでいます。 【その他利用技術(一部)】 開発言語 PHP JavaScript TypeScript Python フレームワーク Laravel Vue.js インフラストラクチャ AWS : EC2,ECS,S3,RDS,ElastiCache,Lambda ミドルウェア nginx DB、検索エンジン PostgreSQL Elasticsearch 構成管理ツール CloudFormation CI/CD CircleCI GitHub Actions 監視ツール Cloudwatch その他ツール、サービス Docker GitHub sendgrid(email delivery) Twillio LINE to Call おわりに Dataforceチームのエンジニアとして働くことには2つの魅力があります。 1. エンジニアとして大きく成長できること  Dataforceチームでは、様々なAPIを用いた開発やレコメンド機能を用いた開発など、色々なプロジェクトに携わることができます。そのため、幅広い経験をすることができ、大きな成長ができます。 2. プロダクト設計など開発以外の仕事にも挑戦できる  Dataforceチームのエンジニアは本来の開発業務以外の動き方も希望に合わせてできます。 社内に利用ユーザーがいるため、ユーザーにヒアリングを行いながら新規施策の立案やUI/UX改善などに挑戦することが可能です。 立案した施策で業務効率や売上が向上した時は、大きなやりがいを得ることができます。 弊社の情報公開ポリシーのため、機能について深くお話はできませんでしたが、ブログに公開できないことでも、会社説明会やカジュアル面談などではお話ができることもありますので、Dataforceについてご興味がある方はぜひ質問してみてください! レバレジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です! ご興味のある方は、以下のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/ recruit.jobcan.jp
アバター
はじめに こんにちは、20卒でレバレジーズ株式会社に入社した古賀です。 現在私は、若年層領域の事業を複数展開するヒューマンキャピタル事業部に所属しており、営業支援システムの開発に携わっています。現在は、リプレイスに取り組んでいます。私を中心にバックエンド、フロントエンドの技術選定を行い、GraphQL、React、Apollo Clientを採用しました。 この記事では、Apollo Clientを採用した理由、デメリットを補うために行った設計上の工夫をお伝えします。なお、GraphQL、Reactを採用した理由については詳細に説明はしない予定です。 前提 バックエンドにはGraphQLを採用することにしました。採用した目的は下記となります。 営業支援システムや周辺サービスに存在する、似たようなロジックをGraphQLで抽象化し実装速度を向上するため クライアントファーストなAPI設計にすることで、フロントエンドリソース不足を解消するため Apollo Clientとは、GraphQL専用APIクライアントを兼ねた状態管理ライブラリです。下記のような特徴があります。 取得データ、ロード、エラーの状態のロジックがApollo ClientのフックAPIにカプセル化されており、これらに関して宣言的に記述可能 フェッチしたデータが自動的に正規化されキャッシュされる Apollo Clientでは、キャッシュをReduxでいうstoreとして利用し状態管理することが可能です。詳しくは 公式 をご覧ください。 状態管理で抱えていた問題 フロントエンドはjQueryとReact/Reduxが混在。jQueryからReact/Reduxに書き換えていたため、「サーバーからデータ取得→store格納までの処理の記述量が多い」問題が発生していました。 状態管理ライブラリの選択 上記問題を解決するために、状態管理ライブラリをApollo Clientに選択しました。 選択の流れについて説明します。候補は、既存のReduxに加え、バックエンドがGraphQLなのでApollo Client、Relayとなり、Relayは情報が少ないこともあり除外。次に、Apollo Clientのメリット・デメリットを調査しました。 メリット キャッシュ機構をstoreとして利用することで、サーバーから取得したデータをstoreに格納する処理が不要になり、コード量が削減される storeの設計が不要になり設計工数が削減される デメリット どこからでもAPIリクエストしキャッシュが更新されるので、バグ発生箇所の当たりをつけづらく開発効率が低下する 「サーバーからデータ取得→store格納までの処理の記述量が多い」問題は解決できそうですが、新たなデメリットが発生しそうな懸念があったため、Fragment Colocationで解決できると考え、導入しました。 デメリットのカバー 子コンポーネントで欲しいデータをFragmentで定義、親コンポーネントで欲しいデータを子コンポーネントのFragmentを組み合わせて定義、最上位階層の子コンポーネントで親コンポーネントのFragmentを組み合わせてQueryを定義し一括取得する手法になります。詳細は Apollo Clientの公式 を御覧ください。 トップのコンポーネントで取得したデータは、コンポーネントの再利用性を高めるためにprops経由で子コンポーネントに流し込むことにしました。 ディレクトリ構成は下記になります。 lib ├── atoms ├── molecules ├── organisms │ └─ parts/ │ │ └─ {コンポーネント名}.tsx │ │ └─ {コンポーネント名}.stories.mdx │ └─ {モデル名}/ │ └─ {コンポーネント名}/ │ └─ Container.tsx:この中にQuery, Mutation, Fragmentも書く。 │ └─ Presentation.tsx │ └─ Story.stories.mdx └─── pages/ └─ {画面名}/ └─ Container.tsx └─ Presentation.tsx 下記サンプルコードになります。Fragment Colocationについて示したいため、Query部分のみ記載してます。フックや型はgraphql-codegenで生成したものを使用しています。 organisms/Job/JobClient/Container.tsx のFragmentをまとめ上げて取得していることが分かります。 Fragment Colocationとは別ですが、サーバーから取得しないが、コンポーネント間で共有したいデータはuseReducerを利用。 pages/Sample/Container.tsx import React , { useEffect , useReducer } from 'react' import { Presentation } from './Presentation' import { gql } from '@apollo/client' import { ClientTest , JobTest } from '@/ts/objects/organisms/Job/JobClient/Container' import { useIndexQuery } from 'generated/graphql' const INDEX_QUERY = gql ` query index { clients { ...ClientTest } jobs { ...JobTest } } ${ ClientTest } ${ JobTest } ` export interface Store { isInit: boolean selectedClient?: string selectedJob?: string } type ActionType = 'SET_INIT' | 'SET_VALUE' | 'SET_CLIENT' export interface Action { type : ActionType payload?: any } const reducer: React.Reducer < Store , Action > = ( store , action ) => { switch ( action. type) { case 'SET_INIT' : return { isInit: false , selectedClient: String ( action.payload.clients [ 0 ] .id ), selectedJob: String ( action.payload.jobs [ 0 ] .id ), } case 'SET_VALUE' : return { ...store , [ action.payload.name ] : action.payload.values , } case 'SET_CLIENT' : return { ...store , selectedClient: action.payload.values , selectedJob: action.payload.jobs !== 0 ? String ( action.payload.jobs [ 0 ] .id ) : '1' , } default : return store } } export const Container: React.FC = () => { const { data , loading , error } = useIndexQuery () const initialStore: Store = { isInit: true , } const [ store , dispatch ] = useReducer ( reducer , initialStore ) useEffect (() => { if ( ! data || ! store.isInit ) { return } dispatch ( { type : 'SET_INIT' , payload: data , } ) } , [ data ] ) if ( store.isInit && loading ) { // 必要に応じてカスタマイズ return < div > Loading... < /div > } if ( error ) { // 必要に応じてカスタマイズ return < div > Error ... < /div > } return < Presentation data = { data } store = { store } dispatch = { dispatch } / > } pages/Sample/Presentation.tsx import React from 'react' import { JobContainer } from '@/ts/objects/organisms/Job/JobClient/Container' import { Store , Action } from './container' interface Props { data: any store: Store dispatch: React.Dispatch < Action > } export const Presentation: React.FC < Props > = ( { data , store , dispatch } ) => { return ( <> < JobContainer clients = { data.clients } jobs = { data.jobs } selectedClient = { store.selectedClient ?? '1' } selectedJob = { store.selectedJob ?? '1' } dispatch = { dispatch } / > < / > ) } organisms/Job/JobClient/Container.tsx import React from 'react' import { ClientTestFragment , JobTestFragment } from 'generated/graphql' import { gql } from '@apollo/client' import { JobPresentation } from '@/ts/objects/organisms/Job/JobClient/Presentation' export const ClientTest = gql ` fragment ClientTest on Client { id name } ` export const JobTest = gql ` fragment JobTest on Job { id name clientId } ` interface Props { clients: Array < ClientTestFragment > jobs: Array < JobTestFragment > selectedClient: string selectedJob: string dispatch: React.Dispatch < any > } export const JobContainer: React.FC < Props > = ( { clients , jobs , selectedClient , selectedJob , dispatch } ) => { const handleChangeClient: React.ChangeEventHandler < HTMLSelectElement > = e => { const nextClientId = e.target.value ? String ( e.target.value ) : String ( clients [ 0 ] .id ) dispatch ( { type : 'SET_CLIENT' , payload: { name: 'selectedClient' , values: nextClientId , jobs: jobs.filter (( job: JobTestFragment ) => String ( job.clientId ) === nextClientId ), } , } ) } const handleChangeJob: React.ChangeEventHandler < HTMLSelectElement > = e => { dispatch ( { type : 'SET_VALUE' , payload: { name: 'selectedJob' , values: e.target.value ? String ( e.target.value ) : String ( jobs [ 0 ] .id ), } , } ) } return ( < JobPresentation clients = { clients } jobs = { jobs } selectedClient = { selectedClient } selectedJob = { selectedJob } handleChangeClient = { handleChangeClient } handleChangeJob = { handleChangeJob } / > ) } organisms/Job/JobClient/Presentation.tsx import React from 'react' import { ClientTestFragment , JobTestFragment } from 'generated/graphql' interface Props { clients: Array < ClientTestFragment > jobs: Array < JobTestFragment > selectedClient: string selectedJob: string handleChangeClient: React.ChangeEventHandler < HTMLSelectElement > handleChangeJob: React.ChangeEventHandler < HTMLSelectElement > } export const JobPresentation: React.FC < Props > = ( { clients , jobs , selectedClient , selectedJob , handleChangeClient , handleChangeJob } ) => { return ( <> < div > < label > 企業: < select value = { selectedClient } onChange = { handleChangeClient } > { clients.map (( client: ClientTestFragment ) => { return ( < option value = { client.id } key = { client.id } > { client.name } < /option > ) } ) } < /select > < /label > < label > 求人: < select value = { selectedJob } onChange = { handleChangeJob } > { jobs .filter (( job: JobTestFragment ) => String ( job.clientId ) === selectedClient ) .map (( job: JobTestFragment ) => { return ( < option value = { job.id } key = { job.id } > { job.name } < /option > ) } ) } < /select > < /label > < /div > < / > ) } 導入して分かったこと 実装したメンバーの声も含め、よかったこと、困ったことを紹介します。 よかったこと storeに格納する処理がなくなったので、想定通りコード量が減った store設計が不要になったので、設計工数を削減できた 「前提」の章で述べたとおり、非同期処理に関わるデータ、ロード、エラーの状態を宣言的に扱えて書きやすかった graphql-codegen による型生成のおかげで、型安全かつ簡単にFragment Colocationを実現できた(型安全性なフロントエンド開発については こちら ) サーバーから取得しないが、コンポーネント間で共有したいデータはuseReducerで管理した。管理したいデータ数がそんなに多くないこと、objectやarray等の複雑になりうるデータ構造を管理することも少なかったことから、useReducerでも問題なかった 困ったこと react-select の仕様で、コンポーネントに渡すために整形する処理を完全に消すことができなかった クエリやFragmentは元々他ディレクトリに切り出していたが、それぞれpages/{モデル名}配下、organisms/{モデル名}配下のContainer.tsxに定義。 理由は下記の通りです。 クエリやFragment毎にディレクトリを切ると行ったり来たりが多く開発が大変になるため、特に慣れてない初期は大変だと判断した PresentationにUI部分は切り出しており、ContainerにクエリやFragmentを記載してもファイルサイズが長大になりすぎないため Apollo Clientとは関係ないが、データを受け取るコンポーネントの再利用を考えると、Atomic Designには適切な置き場所がないので、propsをバケツリレーで渡していくことになる 最後に Reduxのstore構造の複雑さの軽減、コード量の削減、(Apollo Clientではないですが)GraphQL周辺ツールの充実さによって、開発効率を向上させることができました。 私は新卒2年目ですが、この技術選定を担当させていただきました。レバレジーズでは、経験が浅くても提案が受け入れてもらえる環境があります。このような環境に興味がある方はぜひ一緒に働きましょう。
アバター
はじめに みなさん、こんにちは! メディカルケア事業部の『看護のお仕事』にてシステム担当をしている渡辺と申します。私は2020年に新卒で入社し、同年7月からメディカルケア事業部のエンジニアとして配属されました。職種や経験年数に関わらず自発的な提案を受け入れて任せるレバレジーズの企業文化の下、私も配属後から開発プロセスの改善案を出すうちに、去年の終わり頃から媒体の責任者となり、『看護のお仕事』のサイト品質改善のためにさまざまな施策の実装を行っています。 今回は、『看護のお仕事』で開発プロセス改善を行ったことについてお話したいと思います。媒体責任者として、開発に関わるメンバーがスムーズに作業にあたることができるよう開発プロセスの改善を進めてきました。 この記事が開発プロセスの改善策を模索している方やレバレジーズに興味のある方の一助となれば幸いです。 『看護のお仕事』について 『看護のお仕事』は看護師向けの転職支援サービスです。『看護のお仕事』は無料で使うことができ、自分にあった職場を納得の行くまで探すことをサポートします。本記事をご覧いただいている方で、看護師のお知り合いがいましたら、ぜひ『看護のお仕事』をご紹介ください! 各種ミーティングについて 『看護のお仕事』の開発チームは、スクラム開発を参考にオリジナルのスクラム開発体制でサイト運営を行っています。また、スプリントは2週間単位で回しています。今回お話しする中で、関係のあるミーティングは下記になります。 施策レビュー会 参加者:オウンド責任者、ディレクター、デザイナー兼コーダー、エンジニア 内容:施策について意見を出し、話し合う会です。『看護のお仕事』のサイト品質改善を進めるにあたって施策が有効かなどを話し合うための場になります。 工数見積もりのミーティング 参加者:エンジニア 内容:実装に必要な工数を見積もります。この時に実装方針についても話し合います。見積もりにはプランニングポーカーを採用しています。 振り返り会 参加者:オウンド責任者、ディレクター、デザイナー兼コーダー、エンジニア 内容:KPTを用いてスプリントの振り返りを行います。 実際に何したの? では、実際に行った改善策について3つお話しします。 ①やるべきタスクの精査 『看護のお仕事』開発チームでは、実装が必要なタスクに工数付けを行います。エンジニア個々人のリソースと工数管理の観点から、見積もりは絶対見積もりで行っています。 私がチームへ配属されてから少し経過した頃、ABテストを行う施策に実際の作業時間が5分であるにも関わらず、1時間の見積もりを割り当てていることに気づきました。ABテストの施策は実装方法の改善がされた結果、5分で作業が出来るほどマニュアル化されており、エンジニア以外でも作業が可能なものだとわかりました。そのため、そもそもエンジニアが対応する必要があるかを見直しました。 そこで、作業を同施策の他の対応と合わせてコーダーに巻き取ってもらうことを相談し、そのようになりました。結果として、エンジニアで対応が必要かどうかの見直しができ、1時間の工数がつく施策がスプリント内で3~5施策ほどあることが続いていましたので、最大5時間の工数見積もりの削減になりました。また、施策ごとにエンジニアで対応すべきかを確認することの重要性を再確認でき、やるべきタスクの精査をよりしっかりと行うようになりました。 ②工数付けプロセス改善 工数をつけるために仕様を把握する必要があります。しかし、『看護のお仕事』開発チームでは工数をつけるときもまだ仕様が詰まりきっていないタスクが多々発生しており、そのことを関係者間で問題であるという共通認識を持てていませんでした。同時に、特定のメンバーが工数付けのミーティングまでにディレクターと短い時間で仕様についてヒアリングを行っていたため、該当メンバーに負担がありました。 この状況を改善するにあたって、ディレクターへ以下のお願いをしました。 ディレクターもしくは依頼者が、工数付けのミーティングまでに施策の実装をする上で懸念事項がないか事前に確認すること 施策レビュー会中に話がまとまらない場合には別途ミーティングの場を設けること また、エンジニアからも考慮して欲しい観点について施策レビュー会中に発言をするようにしています。 振り返り会にて相談をしたことで、仕様に抜け漏れが発生していることを共通認識として持つことができました。その結果、既存のミーティングをうまく活用することができ、仕様確認に関するやりとりなどの手間やメンバーへの負担を軽減することができました。 ③工数ズレをなくすために ソースコードレビューでは、レビューを受けてから大幅に実装方針を変更し手戻りすることや、レビュアーと実装者で追加の実装すり合わせが必要になることが多々ありました。これでは予定工数を超過してしまいますし、施策のリリースにも影響します。 その状況の根本的な原因は、レビュアーと実装者との間で実装方針のすり合わせが不十分なことでした。そこで工数付けの時間に実装方針を話し合い、その内容を実装タスクを管理しているものにコメントとして残すようにしました。このようにして、話し合った内容をいつでも確認できるようにすることでエンジニアチーム全体で共通認識をとりました。 後々の工数を減らすと同時に、実際の実装対応に近い工数を見積もることができ、工数付けの精度向上にもなりました。 終わりに これらの開発プロセス改善は私が入社後、配属半年で行ったことになります。自身で考えた改善案とその対応策を上長に提案し、チームメンバーと共に相談しながら進めてきました。このようにできたのも、社会人歴や入社年数に囚われずに話を聞いてくれてそれを受け入れてもらえる環境のおかげです。 組織は変化していくものであると同時に、開発チームという組織も流動的なものです。エンジニアチームも続々とメンバーが加わり、『看護のお仕事』がサービスとして何をすべきかのフェーズも日々進化していきます。柔軟にやり方を模索しながらエンジニア以外の方々とも協力しながら日々サイト運営を行っております。 本記事が開発プロセスの改善を進めたい方や、弊社に興味のある方に意義あるものであると大変嬉しく思います。最後までご覧いただき、ありがとうございました!
アバター
はじめに こんにちは、レバレジーズ株式会社の牛嶋です。 レバレジーズ株式会社で、teratailをはじめとしたサービスの開発に携わっております。現在、teratail管理画面のリニューアルに取り組んでおり、技術選定にあたって、CSSフレームワークとして、tailwindcssを採用しました。 この記事では、tailwindcssを採用した理由や、実際にフロントエンド開発で使ってみて良かった点や困った点を紹介します。弊チームの他のプロジェクトでは、CSS Modulesを採用しているため、それと比較しながら紹介していけたらと思います。 tailwindcssとは tailwindcssとは、CSSフレームワークの一つです。最大の特徴として、ユーティリティファーストなアプローチでデザインを組み立てていく点が挙げられます。 CSS Modulesやstyled-componentでは、デザインをゼロベースで構築することができますが、CSSを一から書かなくてはならず、また、それの運用コストも発生します。 また、BootstrapやMUIなどのCSSフレームワークでは、コンポーネントが事前に用意されているので、効率的にデザイン実装ができる反面、デザインに画一性が生まれてしまいます。 その点、tailwindcssでは事前に提供されている低レベルのユーティリティクラスを用いて、実装速度を担保したまま、独自のデザインを組み立てることができます。 ユーティリティクラスとは tailwindcssでは、m-6, text-whiteなどのクラスをユーティリティクラスと呼んでいます。これら一つ一つのユーティリティクラスには、対応するCSSプロパティが用意されています。 tailwindcssは、MUIなどが提供している事前に用意されたコンポーネントは提供しておらず、プリミティブなCSSプロパティに対応したユーティリティクラスを提供しています。これらをHTMLのclass属性に追加していくことにより、デザインを構築していきます。 実装例 弊チームでは、tailwindcssとCSS Modulesを採用しているので、それらの実装例を見ていきます。 tailwindcssでは、CSSを記述することなく、ユーティリティクラスを用いて、インラインスタイルでデザイン構築を行なっていきます。 一方、CSS Modulesでは、JSXファイルとSCSSファイルをわけて、デザインを構築していきます。 tailwindcssの場合 // 公式サイト(https://tailwindcss.jp/docs/utility-first)から引用 const ChitChat = () => { return ( < div className = "max-w-sm mx-auto flex p-6 bg-white rounded-lg shadow-xl" > < div className = "flex-shrink-0" > < img className = "h-12 w-12" src = "/img/logo.svg" alt = "ChitChat Logo" > < /div > < div className = "ml-6 pt-1" > < h4 className = "text-xl text-gray-900 leading-tight" > ChitChat < /h4 > < p className = "text-base text-gray-600 leading-normal" > You have a new message ! < /p > < /div > < /div > ) } export default ChitChat ; CSS Modulesの場合 // 公式サイト(https://tailwindcss.jp/docs/utility-first)から引用 import styles from “./chitChat.modules.scss” const ChitChat = () => { return ( < div className = { styles.chatNotification } > < div className = { styles.chatNotificationLogoWrapper } > < img className = { styles.chatNotificationLogo } src = "/img/logo.svg" alt = "ChitChat Logo" > < /div > < div className = { styles.chatNotificationContent } > < h4 className = { styles.chatNotificationTitle } > ChitChat < /h4 > < p className = { styles.chatNotificationMessage } > You have a new message ! < /p > < /div > < /div > ) } export default ChitChat ; .chatNotification { display : flex ; max-width : 24rem ; margin : 0 auto ; padding : 1.5rem ; border-radius : 0.5rem ; background-color : #fff ; box-shadow : 0 20px 25px -5px rgba( 0 , 0 , 0 , 0.1 ) , 0 10px 10px -5px rgba( 0 , 0 , 0 , 0.04 ) ; } .chatNotificationLogo-wrapper { flex-shrink : 0 ; } .chatNotificationLogo { height : 3rem ; width : 3rem ; } .chatNotificationContent { margin-left : 1.5rem ; padding-top : 0.25rem ; } .chatNotificationTitle { color : #1a202c ; font-size : 1.25rem ; line-height : 1.25 ; } .chatNotificationMessage { color : #718096 ; font-size : 1rem ; line-height : 1.5 ; } なぜtailwindcssを導入したか tailwindcssは、「チームとして新しい技術にチャレンジしたかった」という理由に加え、下記の効果を期待して導入しました。 実装速度の向上 CSS設計と運用コストの削減 スコープが担保した上でのデザイン CSSファイルサイズの最適化 詳細は以下の章で説明させていただきます。 メリット 実装速度が向上する tailwindcssはユーティリティクラスの他にも、hoverなどの容易に適用することができる疑似クラスバリアントや、レスポンシブデザインを容易に実現できる、レスポンシブユーティリティバリアントを提供しています。 以下は一例です。mdをプレフィックスとしてつけることで、指定されたスタイルに「min-width: 768px」のメディアクエリが適用されます。 // 公式サイト(https://tailwindcss.jp/docs/responsive-design)から引用 < div className= "md:flex" > < div className= "md:flex-shrink-0" > < img className= "rounded-lg md:w-56" src = "https://images.unsplash.com/photo-1556740738-b6a63e27c4df?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=448&q=80" width = "448" height = "299" alt = "Woman paying for a purchase" > </ div > < div className= "mt-4 md:mt-0 md:ml-6" > < div className= "uppercase tracking-wide text-sm text-indigo-600 font-bold" > Marketing </ div > < a href = "#" className= "block mt-1 text-lg leading-tight font-semibold text-gray-900 hover:underline" > Finding customers for your new business </ a > < p className= "mt-2 text-gray-600" > Getting a new business off the ground is a lot of hard work. Here are five ideas you can use to find your first customers. </ p > </ div > </ div > 最初こそ慣れるまで時間がかかると思いますが、ある程度の書き方を覚えてしまえば、CSSを直接で書くよりも速く実装することが可能だと思います。 CSS設計と運用コストの軽減 tailwindcssを用いると、ユーティリティクラスを直接class属性に記述するだけなので、CSSのクラス設計をする必要がなくなります。CSS設計の規則を考える必要もないですし、それらを基準にして、class名を考える必要もなくなります。 また、CSSファイルを作成する必要がなくなるため、ファイルの運用コストもなくなります。そもそもファイルを作成しないので、CSSが肥大化することもありません。 スコープが担保されるので、安全にデザインを変更できる tailwindcssは基本的にclass属性に記述していくので、HTMLファイルに変更が閉じられ、自然とデザインを適用するスコープが担保されます。 そのため、任意の箇所のデザインを変更することになっても、影響範囲を考えることなく、安全にデザインを変更することができます。 CSSファイルのサイズを小さく出来る tailwindcssはPurgeCSSを採用しており、本番用にビルドする場合には、未使用のスタイルを自動的に削除します。 その結果、プロジェクトで使用しているスタイルのみを出力したCSSファイルが生成され、ビルドサイズが小さくなります。 purgecss.com デメリットと対応方法 慣れるまではクラスを探すのが面倒? tailwindcssのユーティリティクラスに慣れるまでには、CSSプロパティに対応するクラスを探すのに苦労する可能性があります。最初のうちは多少時間を取られると思います。 しかし、エディタのプラグインを使えばある程度負担を軽減することができます。 例えば、VS Codeで提供されているプラグインの「Tailwind CSS InteliSense」を利用すれば、クラス名の自動補完等の機能を使うことができ、比較的開発が楽になるかと思います。 marketplace.visualstudio.com メンテナンス性が落ちる? ユーティリティクラスをクラス属性に書いていくと、多くのクラスがクラス属性内に記述されることになります。結果的に、そのクラスに記述された内容が何をしているかわからない状態になることもあると思います。 その点、tailwindcssはapplyというディレクティブを提供しており、既存のユーティリティクラスを独自のカスタムCSSにインライン化することができます。 tailwindcss.com また、別の方法として、必要な単位でコンポーネントを抽出する方法があります。 tailwindcss.com 個人的には、後者の方法の方がメリットを享受できると考えています。具体的な理由として、Reactをはじめとするコンポーネント志向のフレームワークとの相性が良い点、また無用なユーティリティクラスを増やすことがない点などがあげられます。 提供されていないCSSが必要な場合はどうする? tailwindcssのユーティリティクラスで提供されていないデザインを実装したい場合があるかもしれません。基本的にデフォルトで実装するには十分な量のユーティリティクラスが提供されています。 しかし、万が一足りなくなった場合でも容易に拡張することができます。公式サイトで独自のユーティリティクラスを追加するためのベストプラクティスが紹介されています。 tailwindcss.com 今後の展望 上記メリットでは紹介しなかったのですが、tailwindcssには設定ファイル(tailwind.config.js)が存在します。設定ファイル上で、自分たちのデザインシステムに沿って、tailwindcssをカスタマイズすることができます。以下が設定ファイルの例です。 // Example `tailwind.config.js` file // 公式より引用(https://tailwindcss.jp/docs/configuration) module . exports = { important: true , theme: { fontFamily: { display: [ 'Gilroy' , 'sans-serif' ] , body: [ 'Graphik' , 'sans-serif' ] , } , extend: { colors: { cyan: '#9cdbff' , } , margin: { 96 : '24rem' , 128 : '32rem' , } , } , } , variants: { opacity: [ 'responsive' , 'hover' ] , } , } 上記のように、設定ファイルでデザインをカスタマイズすることが可能なので、tailwindcssはデザインシステムと非常に相性がいいと考えています。 弊チームでは、今後サービスの横展開も考えており、その際にデザインシステムの作成とともに、tailwindcssを利用したUIライブラリを実装していけたらと考えています。 最後に CSSフレームワークにtailwindcssを採用したことで、設計面での負担軽減や、実装速度向上等、開発面で多くのメリットを享受することができました。 今後の展望で記載した通り、個人的にtailwindcssはデザインシステムを組み合わせてこそ真価を発揮します。サービスが展開していくことも考え、tailwindcssをベースとしたUIライブラリを構築していけたらと考えています。今後の取り組みなどについては、また別の機会で知見を共有できたらと思います。 私は現在、新卒2年目なのですが、今回の技術選定を担当させてもらいました。弊社には、意思決定に合理性と納得性があれば年次に関係なく、自由に発言や提案ができる環境があります。 「主体性を持って働きたい人」や「新しい技術にチャレンジしたい人」には最適な環境だと思うので、興味のある方はぜひ一緒に働きましょう。 参考サイト tailwindcss.com
アバター
はじめに こんにちは、レバテック開発部の河村です。 私はレバテック各種メディアのリプレイスを担当しており、バックエンドを中心にフルスタック開発を行っています。 今回は管理画面のリリースで採用した、フルスタックフレームワークである frourio について、 frourio を採用した理由や使ってみて良かったこと、困ったことを紹介します。 この記事を通して、frourioのメリット、デメリットだけでなく、レバテック開発部ではどのような背景のもと、技術・アーキテクチャの選定を行っているのか、どれくらいのスピード感で開発を行っているのかをお伝えできればと思います。 なお、この記事では frourio における環境構築や使い方等の説明は割愛させていただきます。 開発背景・経緯 今回、開発する対象となった管理画面は、レバテックの各メディアで運用する記事やセミナー情報、エントリー情報を管理するものになります。 すでに管理画面として、PHP(CakePHP)をベースとした技術で作られているものを利用している状態でしたが、 PHP自体のバージョンも古く、利用しているライブラリなどを含めレガシーな状態になっており、開発速度、品質、運用、保守性が低下している状態でした。 また、 レバテック組織全体でTypeScriptの標準化 を進めており、管理画面のリニューアルが必須の状態でした。 管理画面のため、特別凝ったデザイン等は必要ありませんが、利用するロールやメンバーが多く、複雑な機能が必要となることもあり、管理画面のフレームワーク(ReactAdminやAdminBroなど)を利用してしまうと、機能の実装が困難になる可能性もあり、基本的には 拡張性が高い状態、フルスクラッチで開発できる環境 を構築する必要がありました。 さらに、事業側の方々の要望もあり、約1か月程度での本番リリース、稼働開始が必要だったため、 上記の背景、経緯を踏まえた上で 開発速度 が出せるようなフレームワークを選定する必要がありました。 このような背景、経緯があり、frourioを採用することにしました。 frourioを採用した理由については後ほど詳細を説明させていただきます。 frourioとは frourioは一言で言うと、TypeScript製のフルスタックWebフレームワークで、詳細については、 公式ドキュメント または Github を参照してください。 frourioの特徴 frourioの1番の特徴は、 フロントエンドからバックエンドまですべてTypeScriptでかつ型安全に開発できる ところです。 以下の図が、frourioが内包しているフレームワークやORMになります。 上記の図の通り、frourioはTypeScriptをベースに利用できるフレームワークを内包しています。 フロントエンドだとNext.jsやNuxt.jsを選択できたり、ORMだとTypeORMやPrismaを利用することができます。 また、Node.js系のフレームワークでハイパフォーマンスと言われているfastifyも利用することができ、安定したパフォーマンスを出すことも期待できます。 その他にも以下のような特徴がありますが、本記事では割愛させていただきますので、気になる方はぜひ調べてみてください。 コマンド1発でフロントエンドからバックエンドの環境構築 Vercel社が開発しているReact Hooksライブラリ「SWR」に対応 関数ベースでControllerを実装可能 関数ベースでDI(Dependency Injection)の実現 frourioを採用した理由 frourioを採用した理由として、大きく以下の3つになります。 TypeScriptを利用して型安全に開発できる チームとして利用したいフレームワークが内包されている 自動生成により開発速度を大きく向上させることができる 上記の理由について、以下、詳細に説明させていただきます。 TypeScriptを利用して型安全に開発できる frourioの特徴でもお伝えしましたが、frourioはフロントエンドからバックエンドまですべてTypeScriptでかつ型安全に開発することができます。型安全に開発することで、不正な動作を事前に防げたり、フロントエンドとバックエンドをつなぐ部分のInterfaceを明示的に宣言でき、フロントエンドからバックエンドのAPIを呼び出す際も安全にかつスムーズに呼び出すことができます。 例えば、以下のように、バックエンドのAPIのInterfaceを定義し、 export type Task = { id: number label: string done: boolean } export type Methods = { post: { reqBody: Pick < Task , 'label' > resBody: Task } } フロントエンドからInterfaceを参照しながら、安全にかつスムーズにAPIを呼び出すことができます。 await apiClient.tasks.post ( { body: { label } } ) ここで、バックエンドのAPIのInterfaceを定義しましたが、 frourioのルールにしたがって定義することで、 ApiClient(APIのクライアント)部分を自動生成 してくれます。 こちらの自動生成については、 自動生成により開発速度を大きく向上させることができる の箇所で説明したいと思います。 チームとして利用したいフレームワークが内包されている チームとして、以下のフレームワークを利用して開発工数をできる限り抑えることを目指していました。 フロントエンド Nuxt.js Next.js バックエンド TypeORM まず、フロントエンドですが、frourioではNuxt.js、Next.jsともに利用することができます。 チーム内ではVue.js、Nuxt.jsの方が経験のあるメンバーが多いため、どちらを選定するかで迷いましたが、TypeScriptとの相性、チームメンバーのスキル向上、モチベーションアップを考慮した上で、Next.jsを選定しました。型安全に開発できることをメインに置いていることもあり、TypeScriptとの相性の良いReact.jsをベースとしたNext.jsを選定することが当然であるかもしれませんが、それ以上に、チーム内でReact.js、Next.jsの技術を身につけていきたいというモチベーションが選定において大きな判断材料になったかと思います。 ちらっと、TypeScriptとの相性について述べましたが、こちらの記事が参考になりますので、気になる方は参考にしてみてください。 完全に独断と偏見だけどReact vs Vue してみた また、バックエンド、主にORMになりますが、 TypeORM を採用しました。 現在、レバテック全体で各種機能のマイクロサービス化が進んでおり、TypeORMを利用しているチームやメンバーが多いことから選定しました。個人的にはPrismaを使ったみたいという気持ちもありましたが、開発工数をできる限り抑えるということで、すでに知見が溜まっているTypeORMを選定しました。 これにより、バックエンドの開発は、今までとほぼ変わらず、frourioのお作法を覚えるだけで開発できる環境を整えることができました。 余談:CSSフレームワーク フルスクラッチで開発する以上、UIを作成するために、管理画面のフレームワーク(ReactAdminやAdminBro等)と比べて、UIの実装にある程度の工数がかかってしまいます。なので、そこで、CSSフレームワークでReactと相性の良い Material-UI を採用しています。 さらに、管理画面においては、同じような画面、Componentの使い回しが多くなることから、 Atomic Design の概念を採用して、再利用される可能性のあるComponentはできる限り共通化(atoms, moleculed, organismsを適用)し、UIの実装にかかる工数をできる限り削減しました。 自動生成により開発速度を大きく向上させることができる frourioのお作法にしたがって開発することで、様々な部分を自動生成をしてくれて、開発工数を大きく削減でき、開発速度を向上させることができます。個人的に、特にフロントエンドとバックエンドをつなぐ部分、ApiClient(APIのクライアント)部分の自動生成がとても便利だったので、紹介させていただきます。 ディレクトリを作成するだけで、Controllerを自動作成してくれる APIのInterfaceを定義するだけで、ApiClientを自動生成してくれる ディレクトリを作成するだけで、Controllerを自動作成してくれる frourioの機能を利用することで、バックエンドのController部分に該当する ./server/api ディレクトリに対象のエンドポイント用のディレクトリを作成するだけで、必要なファイルを生成してくれます。 上記の機能を説明する前に、まず、frourioを使って環境構築した後のディレクトリ構成を紹介します。 frourioのディレクトリ構成 frourioのコマンドを使って環境構築を行った際の設定が以下になります。 Next.jsのcreate-next-appのようなコマンドがfrourioにもあるので環境構築は簡単にできます。 frourio環境構築時の設定1 frourio環境構築時の設定2 上記を行った結果、以下のようなディレクトリ構成が出来上がります。 . ├── README.md ├── aspida.config.js ├── components # React用のComponent ├── ecosystem.config.js ├── jest.config.ts ├── next-env.d.ts ├── package.json ├── pages # Next.jsのpagesに該当 ├── public # フロントエンド用の静的ファイル ├── server # バックエンド │ ├── $orm.ts │ ├── $server.ts │ ├── api # バックエンドのController部分に該当 │ ├── entity # TypeORMのEntity │ ├── entrypoints # バックエンドのサーバーを立ち上げるための設定等 │ ├── index.js │ ├── migration # TypeORMによって生成されたMigrationファイル │ ├── ormconfig.ts │ ├── package.json │ ├── service # レイヤードアーキテクチャ的に言うと、アプリケーションサービスに該当 │ ├── static # バックエンド用の静的ファイル │ ├── subscriber # TypeORMのSubscriber │ ├── test # バックエンド用のテスト │ ├── tsconfig.json │ ├── types # バックエンド用の型定義ファイル │ ├── validators # Service等で利用したいValidation用のDTOに該当 │ ├── webpack.config.js │ └── yarn.lock ├── styles # CSS ├── test # フロントエンド用のテスト ├── tsconfig.json ├── utils # フロントエンド用のUtil │ └── apiClient.ts └── yarn.lock 正直、利用しないディレクトリ等も生成されてしまいますが、不要なものは後から取り除く形で問題ないかと思います。 (実際に、 ./styles や ./server/service 、 ./server/subscriber は不要だったので取り除いています) また、1つのディレクトリ内でフロントエンド、バックエンドを管理しているために、一見モノリシックに見えますが、Interface(型定義)で繋がっていること以外は、フロントエンド、バックエンド、それぞれに別のpackage.jsonがあり、別のプロジェクトとして扱うことができます。ライブラリ等が依存し合ったりすることもなく、仮にバックエンドを別のものに差し替えたとしても、フロントエンドのAPIの呼び出し部分を変更するだけで可能になります。 ./server/api ディレクトリにディレクトリを作成してみると 上記のディレクトリ構成の通り、 ./server/api ディレクトリがバックエンドのController部分に該当します。 補足ですが、frourioのコマンドで環境構築をすると、デフォルトでサンプルのコードが作成されます。 また、 $ がつくファイルはfrourio側で生成されるファイルになります。 frourioの自動生成機能1 こちらのディレクトリに、 hogehoge ディレクトリを作成して、バックエンド側をビルドすると以下のようになります。 ホットリロードでも生成されるので、実際にはサーバーを起動したまま作成していくことになるかと思います。 frourioの自動生成機能2 上記のようにファイルが生成されており、実際のファイルの中身は以下のようになります。 メソッド自体はサンプル的なものになっていますが、これだけでも自動生成してくれるだけで恩恵を得られるかと思います。 index.ts export type Methods = { get : { resBody: string } } controller.ts import { defineController } from './$relay' export default defineController (() => ( { get : () => ( { status : 200 , body: 'Hello' } ) } )) $relay.ts /* eslint-disable */ // prettier-ignore import { Injectable , depend } from 'velona' // prettier-ignore import type { FastifyInstance , onRequestHookHandler , preParsingHookHandler , preValidationHookHandler , preHandlerHookHandler } from 'fastify' // prettier-ignore import type { Schema } from 'fast-json-stringify' // prettier-ignore import type { HttpStatusOk } from 'aspida' // prettier-ignore import type { ServerMethods } from '../../$server' // prettier-ignore import type { Methods } from './' // prettier-ignore type Hooks = { onRequest?: onRequestHookHandler | onRequestHookHandler [] preParsing?: preParsingHookHandler | preParsingHookHandler [] preValidation?: preValidationHookHandler | preValidationHookHandler [] preHandler?: preHandlerHookHandler | preHandlerHookHandler [] } // prettier-ignore type ControllerMethods = ServerMethods < Methods > // prettier-ignore export function defineResponseSchema < T extends { [ U in keyof ControllerMethods ] ?: { [ V in HttpStatusOk ] ?: Schema }} >( methods: () => T ) { return methods } // prettier-ignore export function defineHooks < T extends Hooks >( hooks: ( fastify: FastifyInstance ) => T ) : ( fastify: FastifyInstance ) => T // prettier-ignore export function defineHooks < T extends Record < string , any >, U extends Hooks >( deps: T , cb: ( d: T , fastify: FastifyInstance ) => U ) : Injectable < T , [ FastifyInstance ] , U > // prettier-ignore export function defineHooks < T extends Record < string , any >>( hooks: ( fastify: FastifyInstance ) => Hooks | T , cb?: ( deps: T , fastify: FastifyInstance ) => Hooks ) { return cb && typeof hooks !== 'function' ? depend ( hooks , cb ) : hooks } // prettier-ignore export function defineController ( methods: ( fastify: FastifyInstance ) => ControllerMethods ) : ( fastify: FastifyInstance ) => ControllerMethods // prettier-ignore export function defineController < T extends Record < string , any >>( deps: T , cb: ( d: T , fastify: FastifyInstance ) => ControllerMethods ) : Injectable < T , [ FastifyInstance ] , ControllerMethods > // prettier-ignore export function defineController < T extends Record < string , any >>( methods: ( fastify: FastifyInstance ) => ControllerMethods | T , cb?: ( deps: T , fastify: FastifyInstance ) => ControllerMethods ) { return cb && typeof methods !== 'function' ? depend ( methods , cb ) : methods } 追加したControllerの場合でも、開発側は特別な設定を追加することなく、frourio側が自動に設定してくれます。 実際には、 ./server/$server.ts に自動で設定されますが、ファイルがけっこうな行数になってしまうので、ここでは割愛させていただきます。 APIのInterfaceを定義するだけで、ApiClientを自動生成してくれる frourioでは、標準でTypeScript製のREST APIクライアントである aspida を利用して、バックエンドのAPIにアクセスすることができます。そのため、フロントエンド側に ./utils/apiClient.ts が生成され、 aspida の設定が追加されています。 ├── utils # フロントエンド用のUtil │ └── apiClient.ts apiClient.ts import aspida from '@aspida/axios' import api from '~/server/api/$api' export const apiClient = api ( aspida ()) 上記の通り、 ~/server/api/$api.ts を読み込んでいるのですが、 こちらがバックエンド側で定義したAPIのInterfaceを元に、自動生成されたClientの接続周りをラップしたものになります。 例えば、上記の例(hogehoge)の場合だと、 ~/server/api/$api.ts は以下のように生成されます。 /* eslint-disable */ // prettier-ignore import { AspidaClient , BasicHeaders , dataToURLString } from 'aspida' // prettier-ignore import { Methods as Methods0 } from '.' // prettier-ignore import { Methods as Methods1 } from './hogehoge' // prettier-ignore const api = < T >( { baseURL , fetch } : AspidaClient < T >) => { const prefix = ( baseURL === undefined ? 'http://localhost:39809/api' : baseURL ) .replace ( /\/$/ , '' ) const PATH0 = '/hogehoge' const GET = 'GET' const POST = 'POST' const DELETE = 'DELETE' const PATCH = 'PATCH' return { hogehoge: { get : ( option?: { config?: T } ) => fetch < Methods1 [ 'get' ][ 'resBody' ] >( prefix , PATH0 , GET , option ) .text (), $get: ( option?: { config?: T } ) => fetch < Methods1 [ 'get' ][ 'resBody' ] >( prefix , PATH0 , GET , option ) .text () .then ( r => r.body ), $path: () => ` ${ prefix }${ PATH0 } ` } } // prettier-ignore export type ApiInstance = ReturnType <typeof api > // prettier-ignore export default api これらのfrourioの機能を利用することで、フロントエンドは特段、APIを呼び出すためにAPI用のClientを実装する必要がなくなります。バックエンドのAPIを実装した時点で、フロントエンドはAPIを呼び出して開発を進めることができます。 frourioを使って困ったこと 現段階でfrourioを使って困ったことをいくつか紹介させていただきます。 ディレクトリ構成が変更しづらい 不要な設定が思ったより追加されてしまう ディレクトリ構成が変更しづらい frourioのお作法にしたがって開発することで、様々な部分を自動生成をしてくれる分、ある程度、frourioが用意したディレクトリ構成に従わないと動作しない機能がいくつかあります。ただし、ORMに関する設定はよしなに変更できたりします(例えば、TypeORMであれば、 ormconfig.ts の設定を変更したりなど) バックエンドを簡単なレイヤードアーキテクチャの構成(UI層→Application層→Domain層→Presentation層)にしようと思っても、上記で紹介した ./server/api ディレクトリを変更するとControllerを追加できなかったり、 ./server/validators ディレクトリをApplication層のDTOとして利用しようと思っても、 ./server/validators ディレクトリ内に作成しないと class-validator が機能しなかったりします。 これらの原因は明確で、frourioが生成される $ が付いたファイルをよしなに書き換えたりすることができないためです。 例えば、validatorsであれば server/$server.ts に自動で設定が作成され、以下のように設定されています。 $server.ts(一部抜粋) ... // prettier-ignore import * as Validators from './validators' ... // prettier-ignore const createValidateHandler = ( validators: ( req: FastifyRequest ) => ( Promise < void > | null ) [] ) : preValidationHookHandler => ( req , reply ) => Promise . all( validators ( req )) . catch ( err => reply.code ( 400 ) .send ( err )) // prettier-ignore const formatMultipartData = ( arrayTypeKeys: [ string , boolean ][] ) : preValidationHookHandler => ( req , _ , done ) => { const body: any = req.body for ( const [ key ] of arrayTypeKeys ) { if ( body [ key ] === undefined ) body [ key ] = [] else if ( ! Array .isArray ( body [ key ] )) { body [ key ] = [ body [ key ]] } } Object .entries ( body ) .forEach (( [ key , val ] ) => { if ( Array .isArray ( val )) { body [ key ] = ( val as Multipart [] ) .map ( v => v.file ? v : ( v as any ) .value ) } else { body [ key ] = ( val as Multipart ) .file ? val : ( val as any ) .value } } ) for ( const [ key , isOptional ] of arrayTypeKeys ) { if ( ! body [ key ] .length && isOptional ) delete body [ key ] } done () } 上記の通り、frourioの機能を存分に活用するには、frourioのルールにある程度従う必要があります。 ただ、frourioの機能、特に自動生成周りの機能は、開発スピード、コストにおいてとても効果を発揮しますが、バックエンドをきれいなアーキテクチャで作成したいようなニーズとはマッチしないかと思います。特に、今回はある程度簡単な機能が多いということもありfrourioを採用しているので、無理してアーキテクチャを整える必要はなく、特に大きな問題とはなっていません。 必要のない設定が思ったより追加されてしまう frourioの公式に従って環境構築を進めると、たしかにコマンド1発でフロントエンドからバックエンドの環境構築ができますが、その分、様々な設定が追加されている状態になります。不必要な設定があると開発者内で認識齟齬につながる可能性があるので、開発において不必要な設定は取り除いておいたほうがいいかと思います。 例えば、バックエンドのサーバーを立ち上げるための設定をしている server/entrypoints/index.ts では、デフォルトの状態だと以下のようになっています。 index.ts import 'reflect-metadata' import path from 'path' import Fastify from 'fastify' import helmet from 'fastify-helmet' import cors from 'fastify-cors' import fastifyStatic from 'fastify-static' import fastifyJwt from 'fastify-jwt' import { createConnection } from 'typeorm' import server from '$/$server' import ormOptions from '$/$orm' import { API_JWT_SECRET , API_SERVER_PORT , API_BASE_PATH , API_UPLOAD_DIR , TYPEORM_HOST , TYPEORM_USERNAME , TYPEORM_PASSWORD , TYPEORM_DATABASE , TYPEORM_PORT } from '$/service/envValues' const fastify = Fastify () fastify.register ( helmet ) fastify.register ( cors ) fastify.register ( fastifyStatic , { root: path.join ( __dirname , 'static' ), prefix: '/static' } ) if ( API_UPLOAD_DIR ) { fastify.register ( fastifyStatic , { root: path.resolve ( __dirname , API_UPLOAD_DIR ), prefix: '/upload' , decorateReply: false } ) } fastify.register ( fastifyJwt , { secret: API_JWT_SECRET } ) server ( fastify , { basePath: API_BASE_PATH } ) createConnection ( { type : 'mysql' , host: TYPEORM_HOST , username: TYPEORM_USERNAME , password: TYPEORM_PASSWORD , database: TYPEORM_DATABASE , port: Number ( TYPEORM_PORT ), migrationsRun: true , synchronize: false , logging: false , ...ormOptions } ) .then (() => fastify.listen ( API_SERVER_PORT )) また、デフォルトの状態の環境変数は以下のようになります。 .env API_SERVER_PORT=39809 API_BASE_PATH=/api API_ORIGIN=http://localhost:39809 API_JWT_SECRET=supersecret API_USER_ID=id API_USER_PASS=pass API_UPLOAD_DIR=upload TYPEORM_HOST=localhost TYPEORM_USERNAME=root TYPEORM_PASSWORD= TYPEORM_DATABASE=hogehoge TYPEORM_PORT=3306 おそらく、初期段階ではファイルのアップロードの設定や認証・認可の設定は不要になるかと思います。逆に、必要になるCORSやTypeORMの設定がされているので、この部分は工数削減になります。ただ、このように様々な箇所にデフォルトで設定がされているので、不要な設定がないかは確認しておいたほうが良いかと思います(他にも、TypeORMの場合、デフォルトでSubscriberの設定が追加されています) 最後に 管理画面の開発にfrourioを採用し、必要最低限のみの機能にはなりますが、想像以上のスピードでリリースすることができました。 ですが、まだまだfrourioの機能を活かせていない部分があり、特に aspida 、 SWR(stale-while-revalidate) 、validationの機能を拡充したり、機能、非機能要件として以下の機能を実装していく必要があります。 NextAuthを利用した認証・認可 レバテック内部の各マイクロサービスとの連携 メール配信、予約配信 記事投稿のためのリッチテキストエディター ...etc 今回の記事で説明しきれていないfrourioの特徴やfrourio以外で困ったこと(Next.js、TypeORM固有の課題など)、frourioを利用したシステムアーキテクチャ(AWS)、また、今後上記の機能を実装していくなかで得られた知見などをまた別の機会に共有できたらなと思います。 参考記事 憧れのTypeScriptフルスタック環境がコマンド1発で作れる超軽量フレームワーク「frourio」 TS製フルスタックフレームワークfrourioの入門会を主催しました! frourio でフロントエンドとバックエンドを一緒に静的型検査する
アバター
対象読者 マイクロサービス化を検討しており、実際に作る場合の構成を参考にしたい。 ドメイン駆動設計について、基本的な用語の知識がある。 TypeScript を多少触ったことがある。理解がある。 はじめに こんにちは。エンジニアの吉村です。 現在、弊社が運営する teratail というサービスに携わっており、CakePHP で動作しているモノリシックな既存サービスをマイクロサービスに移行するというプロジェクトを進行中です。 この記事では、実務を通して得た知見として、マイクロサービス化によりどんな恩恵があるのか、具体的にどのような構成で実装をしているのかについてご紹介します。 TL;DR マイクロサービスのバックエンドサービスの実装に焦点を絞って、ドメイン駆動設計 + オニオンアーキテクチャをベースに設計をしました。 本記事では、具体的に「ユーザ新規登録処理」の実装をする場合を例にとり、実装手順をなぞりながら詳細を解説しています。 結論としては、従来の MVC ベースのアーキテクチャと比較して、メンテナンス性が高く技術的負債が生じづらい、スケーリングが容易な構成にできたのではないかと思います。その反面、簡単な処理の実装であっても記述量が多くなってしまうため、初期コストは割と高めな構成であるように思いました。 なぜマイクロサービス化を導入したのか モノリシックサービスとマイクロサービスとで、どちらの構成が優れているかについては、実装するサービスの規模や様々な背景などによって左右される部分ではありますが、 今回のケースでは 比較的規模の大きいシステムであること サービスの改善を更に速度を上げて実施すべく、スケールしやすい構成にしておく必要があること 弊社の他マイクロサービスと相互連携をしやすい構成にする必要があったこと などの理由から、マイクロサービスとしてリプレースをしていく方針となりました。 マイクロサービスでシステムを実装をしていくにあたって、初期設計の段階で失敗してしまうとメンテナンスが困難なシステムができあがってしまう恐れがあるため、アーキテクチャの検討は注意深く行っていく必要がありました。 様々な文献で構成を調べる上で、特に気をつけたポイントは、なるべく技術的負債を回避でき、処理の把握が容易でメンテナンス性が高く、ミドルウェア等の環境の変更に強いシステムとなることでした。 それら検討の上で現在どのような構成で実装をしているか、簡単な処理での例を基にご紹介します。 サービスの構成 サービス全体の構成としては、フロントエンドサーバ、BFFサーバ、バックエンドサーバ と分けて実装をしていますが、 今回はバックエンドサーバのサービス構成に焦点を絞ってご紹介していきます。 構成図 サービスの構成は、オニオンアーキテクチャ[1]を参考に、以下の図の構成としています。 サービスの根幹となるドメイン層を中心に置いて、アプリケーション層、アダプタ層、インフラストラクチャ層と順に囲っていき、 外側の層から内側の層への単方向の依存のみ許可しています。※図の右側の各モジュールの詳細については後述します。 マイクロサービス化をするにあたり、ドメイン駆動設計を参考に、一つのアプリケーションを、例えばユーザ のアカウントについて管理する user サービスや、投稿された内容を管理する post サービスなど、それぞれ独立して成立するサービスとして分割していきます。このときに分割されたサービスの粒度を1単位としてサブドメインと呼ぶことにし、サブドメインごとに上図の構成のサービスを実装していきます。 ディレクトリ構成 ディレクトリの基本的な構成は以下の通りです。 以下の例ではサブドメインが user のみの構成となっていますが、サブドメインの数だけここに同様の構成のものが並びます。 src/shared には共通のコアロジックや、ベースクラスなどを配置しています。 src ├── server.ts ├── subdomains │  └── user │      ├── Entities │      │   └── User.ts │      ├── Events │      │   └── UserCreated.ts │      ├── Infrastructures │      │   └── typeorm │      │   └── User.ts │      ├── Mappers │      │   └── UserMap.ts │      ├── Repositories │      │   ├── UserRepository.ts │      │   └── implements │      │      └── TypeOrmUserRepository.ts │      ├── Subscriptions │      │   └── AfterUserCreated.ts │      ├── UseCases │      │   └── CreateUser │      │      ├── CreateUserUseCase.ts │      │      ├── CreateUserDTO.ts │      │      ├── CreateUserErrors.ts │      │      └── CreateUserController.ts │      └── ValueObjects │      └── UserName.ts └── shared ├── core │ ├── AppError.ts │ ├── Guard.ts │ ├── Result.ts │ ├── UseCase.ts │ └── UseCaseError.ts └── domain ├── events │ ├── DomainEvents.ts │ ├── IDomainEvent.ts │ └── IHandle.ts ├── AggregateRoot.ts ├── Entity.ts ├── Identifier.ts ├── UniqueEntityID.ts ├── ValueObject.ts └── WatchedList.ts 実際に「ユーザ登録処理」を作ってみる ドメイン層の定義 まず、ドメイン層として ValueObject と Entity の定義をしていきます。 ドメイン層はサービスの根幹となる部分であり、すべての処理はこのドメイン層で定義されたものを操作する形で実装していきます。 ValueObject ValueObject は、Entityを構成するプロパティとして使用したり、レイヤ間のやり取りをする箇所などで用います。 ValueObject を用いるメリットとしては、対象の値が正しい形であるということが担保できるという点と、その値に対してのビジネスルールを豊かに表現できるという点があります。 オブジェクトを生成する際に必ずバリデーションロジックを通すように作ることができるため、ValueObject としてデータが存在している時点で、形式的に正しい値を持っているということが保証され、様々な箇所で形式チェックをする必要がありません。 また、例えば Email という ValueObject を作成したときに、対象の文字列からドメイン部分のみ抽出するというメソッドを用意したりなど、対象の値に対して何らかの加工をした形で取得をするといったことをまとめて定義しておくことができます。 以下の例では、ユーザの名前を表現する UserName ValueObject を定義しています。 入力された値が 3 文字以上 32 文字以下の場合はエラーを返す、簡単なバリデーションロジックを組み込んでいます。 // src/subdomains/user/ValueObjects/UserName.ts export interface UserNameProps { value: string } export class UserName extends ValueObject < UserNameProps > { get value () : string { return this .props.value } private constructor( props: UserNameProps ) { super( props ) } private static isValidName ( name: string ) { const re = /^.{3,32}$/ return re.test ( name ) } public static create ( name: string ) : Result < UserName > { if ( ! this .isValidName ( name )) { return Result.fail < UserName >( `Invalid Argument - userName:[ ${ name } ]` ) } return Result.ok < UserName >( new UserName ( { value: name } ) ) } } ※ Result Result クラスは、成功形か失敗形かの状態を持たせた結果データを表すものとして実装しています。 成功形の場合は成功した値を取得することができ、失敗形の場合はエラーレスポンスを取得できます。どちらの状態であるかは isSucceed と isFailure のプロパティの値で判別可能です。 上記 ValueObject の生成メソッドでこの戻り値を利用していますが、状態ごとの処理を記述する可能性のあるあらゆる箇所で利用しています。 Entity 用意した ValueObject をプロパティとして設定し、Entity を定義します。 Entity は固有の識別子として id を持っており、 id が一致している Entity は、プロパティが異なっている場合でも同一のものとして扱います。 // src/subdomains/user/Entities/User.ts interface UserProps { name: UserName createdAt: DateTime updatedAt: DateTime deletedAt?: DateTime } export class User extends Entity < UserProps > { get name () : UserName { return this .props.name } get createdAt () : DateTime { return this .props.createdAt } get updatedAt () : DateTime { return this .props.updatedAt } get deletedAt () : DateTime | undefined { return this .props.deletedAt } private constructor( props: UserProps , id?: UniqueEntityID ) { super( props , id ) } public static create ( props: UserProps , id?: UniqueEntityID ) : Result < User > { const user = new User ( { ...props } , id ) return Result.ok < User >( user ) } } アプリケーション層の実装 次に、定義されたドメイン層の部品を操作して、ユーザ作成処理を記述するアプリケーション層の UseCase、 DTO、 RepositoryInterface を実装していきます。 DTO まず、 DTO(Data Transfer Object)を定義します。 DTO は、UseCase の入出力パラメータを表します。 UseCase がリクエストとしてどういう形式を求めているか、レスポンスとしてどんなデータが返るのかを明確に定義しておくことで UseCase の仕様を把握しやすくするなる他、UseCase を実行する際にはこの定義に従って呼ぶようにすればよいため、 通信プロトコルが何なのか、または DomainEvent (後述します)の後続処理として実行されるのかなど、実行元が何であるかなどを意識することなく UseCase を定義することができるようになります。 // src/subdomains/user/UseCases/CreateUser/CreateUserDTO.ts export interface CreateUserRequestDTO { name: string } export interface CreateUserResponseDTO { success: boolean } UseCase UseCase は、ドメイン層を操作してデータの加工をし、Repository にデータを渡す処理や、Repository から取得したデータを返却する処理を記述していきます。 UseCase は、できるだけ単一の操作にのみ責任を持つように作るほうが良いです。例えば、エンティティを新規作成する UseCase を作成する場合は、データ作成の成否のみを(必要であれば作成したエンティティのIDのみあわせて)返却するよう実装し、エンティティ全体のデータを取得する UseCase は別途作成するようにしておきます。 このように、データの操作のみに責任をもつ Command 処理と、データの返却にのみ責任を持つ(データを操作しない) Query 処理に明確に分けて実装しておくと、それぞれの処理の最適化に注力しやすくなり、データの読み出しに関してはデータに副作用を与えないことが担保された Query 処理を使うことで、安全にシステムを利用することができるようになります。※CQRS(コマンドクエリ責務分離)[2] 以下の例では、ユーザデータを作成を実行して、成功か失敗かの bool 値のみを返す Command UseCase を実装しています。 // src/subdomains/user/UesCase/CreateUser/CreateUserUseCase.ts type Response = Either < | CreateUserAlreadyExistsError | CreateUserFailedToCreateUserError | UnexpectedError | BadRequestError , Result < CreateUserResponseDTO > > export class CreateUserUseCase implements UseCase < CreateUserRequestDTO , Promise < Response >> { constructor( private userRepository: UserRepository ) {} async execute ( req: CreateUserRequestDTO ) : Promise < Response > { const nameOrError = UserName.create ( req.name ) if ( nameOrError.isFailure ) { return failed (new BadRequestError ( nameOrError.errorValue () .value )) } const name = nameOrError.getValue () const now = DateTime.utc () try { if (await this .userRepository.isExists ( name )) { return failed (new CreateUserAlreadyExistsError ( name )) } const userOrError = User.create ( { name: name , createdAt: now , updatedAt: now } ) if ( userOrError.isFailure ) { return failed (new CreateUserFailedToCreateUserError ()) } const user = userOrError.getValue () await this .userRepository.save ( user ) return succeed ( Result.ok < UserCreateResponse >( { success: true , } ) ) } catch ( err ) { return failed (new UnexpectedError ( err.message )) } } } ※ Either UseCase のレスポンスは、Either クラスで表現しています。 UseCase の結果値として、複数の Result オブジェクトが返る場合がありますが、それぞれが正常系か異常系かを振り分けて定義しています。 これにより、 UseCase の利用側はレスポンスを判別が成功系か異常系か判別して処理を記述しやすくなるほか、UseCase の定義書のように機能し、どんなパターンのレスポンスが存在するか一目で分かるようになる利点があります。 UseCaseError UseCase の異常系レスポンスとして、UseCaseError を定義します。 // src/subdomains/user/UesCase/CreateUser/CreateUserError.ts export class CreateUserAlreadyExistsError extends Result < UseCaseError > { constructor( name: UserName ) { super( false , { message: `User name already exists. - name: ${ name.value } ` } as UseCaseError ) } } export class CreateUserFailedToCreateUserError extends Result < UseCaseError > { constructor() { super( false , { message: "Failed to create user." } as UseCaseError ) } } RepositoryInterface UseCase 実装時点で、Repository にアクセスする必要がある箇所を記述する場合は、Repository の実装を抽象化した RepositoryInterface を作成し、抽象化された Repository へアクセスする形で実装していきます。※依存性逆転の原則[3] これをする理由は、UseCase は本来、データを永続化する仕組みが何であるか、ORMとして何を使っているのかなどは意識する必要がなく、永続化データへの読み書きさえできれば Repository の具体的な実装を気にしなくても良いためです。 また、Repository を抽象化せずに具体実装に対してアクセスするようにしてしまうと、アプリケーション層がそれより外側のアダプタ層に依存する形での実装となってしまい、内側の層が外側の層に依存してはならないというルールに反してしまうため、これを逆転させる目的があります。 // src/subdomains/user/Repositories/UserRepository.ts export interface UserRepository { isExists ( name: UserName ) : Promise < boolean > save ( user: User ) : Promise < void > } データ永続化処理実装 UseCase 実装の際に定義した RepositoryInterface の定義に基づき、DB 定義とデータ永続化の具体実装をしていきます。 従来型の MVC による実装などに慣れていると、まず DB の定義をしてから UseCase 処理の実装をしていきたくなるかもしれませんが、今回のアーキテクチャではそれとは逆で、ドメイン層とアプリケーション層を実装したあとに DB 定義を行います。 Repository は アプリケーション層・ドメイン層の処理の中で生成・更新された Entity を永続化し、永続化されたデータを Entity として返すことにのみ責任を持つように実装していきます。 こうすることでサービスのコアとなるロジックが DB の定義や ORM などに密接に依存してしまうような作りを回避することができます。 DBスキーマ定義 TypeORM を使用する場合を例に以下のようにモデルを定義します。 // src/subdomains/user/Infrastructures/typeorm/User.ts import { Column , CreateDateColumn , DeleteDateColumn , Entity , PrimaryGeneratedColumn , UpdateDateColumn } from "typeorm" ; @Entity () export class User { @PrimaryGeneratedColumn () id: number ; @Column () name: string ; @CreateDateColumn ( { type : "datetime" , } ) createdAt ! : Date @UpdateDateColumn ( { type : "datetime" , } ) updatedAt ! : Date @DeleteDateColumn ( { type : "datetime" , nullable: true , } ) deletedAt?: Date } Repository 実装 // src/subdomains/user/Repositories/implements/TypeOrmUserRepository.ts import { createConnection , getRepository } from 'typeorm' import { User } from '../../Infrastructures/typeorm/User' import { UserMap } from '../../Mappers/UserMap' export class TypeOrmUserRepository implements UserRepository { async isExists ( name: UserName ) : Promise < boolean > { const connection = await createConnection () const userRepository = getRepository ( User ) const user = await userRepository.findOne ( { name } ) await connection.close () return !! user } async save ( user: User ) : Promise < void > { const connection = await createConnection () const userRepository = getRepository ( User ) await userRepository.save ( UserMap.toPersistent ( user )) await connection.close () } } Mapper 実装 Entity <=> 永続化データ間の変換処理を Mapper として実装します。 // src/subdomains/user/Mappers/UserMap.ts export interface UserRawProps { id: string name: string createdAt: Date updatedAt: Date deletedAt?: Date } export class UserMap implements Mapper < User > { public static toDomain ( raw: UserRawProps ) : User { const userNameOrError = UserName.create (new UniqueEntityID ( raw.name )) if ( userNameOrError.isFailure ) { throw new Error ( userNameOrError.error ) } const userOrError = User.create ( { name: userNameOrError.getValue (), createdAt: DateTime.fromJSDate ( raw.createdAt ), updatedAt: DateTime.fromJSDate ( raw.updatedAt ), deletedAt: raw.deletedAt ? DateTime.fromJSDate ( raw.deletedAt ) : undefined } , new UniqueEntityID ( raw.id ) ) if ( userOrError.isFailure ) { throw new Error ( userOrError.error ) } return userOrError.getValue () } public static toPersistent ( userEntity: User ) : UserCreationParam { return { id: userEntity.id.toString (), name: userEntity.name.value , createdAt: userEntity.createdAt.toJSDate (), updatedAt: userEntity.updatedAt.toJSDate (), deletedAt: userEntity.deletedAt?.toJSDate () } } } 通信インターフェース実装 ここまででユーザ登録処理の UseCase の実装が完了しました。次にこれを呼び出すインターフェースの実装をしていきます。 ここでは gRPC で実装する場合を例に実装していきます。 protocol buffer 定義 syntax = "proto3"; package sample.user.command_user; service CommandUser { rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); } message CreateUserRequest { string name = 1; } message CreateUserResponse{ bool success = 1; } Controller 実装 // src/subdomains/user/UesCase/CreateUser/CreateUserController.ts export class CreateUserController { constructor( private createUserUseCase: CreateUserUseCase ) async createUser ( req: CreateUserRequest , metadata?: Metadata ) : Promise < CreateUserResponse > { const reqDTO = req as CreateUserRequestDTO const res = await this .createUserUseCase.execute ( reqDTO ) if ( res.isFailed ()) { const error = res.value switch ( error. constructor) { case CreateUserAlreadyExistsError: case CreateUserFailedToCreateUserError: case UnexpectedError: case BadRequestError: default : return { success: false } } } return { success: true } } } これでユーザ登録処理の実装が完了しました。 gRPC で対象のインターフェースを呼び出すことで、DB に対象ユーザのデータが登録ができていることを確認することができます。 ユーザ作成直後に処理を実行したい場合 ユーザ登録処理が完了した直後、メールなどの通知を送信したりレポートデータを書き込みたいなどの後続処理を実装する必要が出てくるケースがあります。その場合は、既存の UseCase (今回でいうと CreateUserUseCase )に手を加えて実装はせず、別の UseCase として各処理を実装し、 DomainEvent を利用して処理を実行するのが望ましいです。 ① CreateUserUseCase を実行 ② User エンティティを生成したときに UserCreated イベントを発行。データの永続化が完了した時点でイベントを発火させる。 ③ Subscription がイベントの発火を検知して、後続処理を実行する。 DomainEvent User エンティティが生成される際に、対象の Entity 情報を EntityID でタグ付けした DomainEvent を発行します。 この時点では、 DomainEvent を発行しているだけで、まだ発火はしていません。 // src/subdomains/user/Entities/User.ts - export class User extends Entity<UserProps> { + export class User extends AggregateRoot<UserProps> { // ... public static create(props: UserProps, id?: UniqueEntityID): Result<User> { const user = new User({...props}, id) + // 新規作成時のみイベントを発行するため、 id が付与されていない場合に実行 + if (!id) { + this.addDomainEvent(new UserCreated(user)) + } return Result.ok<User>(user) } } イベントの発火は、データの永続化の完了を検知したタイミングで実行します。 TypeORM の場合を例にとると、 Entity Listener を使って以下のように実装します。 // src/subdomains/user/Infrastructures/typeorm/User.ts @Entity() export class User { // ... + @AfterInsert() + dispatchAggregateEvents() { + const aggregateId = new UniqueEntityID(this.id); + DomainEvents.dispatchEventsForAggregate(aggregateId); + } } Subscription は、 DomainEvent の発火を検知して、DomainEvent 内の Entity 情報を使って後続処理を実行するように実装しておきます。その上で、 Subscription をサーバ起動時に常時監視状態にしておくことで、ユーザ登録完了後に後続処理を実行する実装の完了です。 おわりに マイクロサービスのバックエンドサービスの実装に焦点を絞って、オニオンアーキテクチャをベースに設計をしました。 実際にこの構成を採用してシステムを実装した所感として、従来の MVC アーキテクチャなどの実装と比較して良かった点、気になる点をまとめると以下のとおりです。 良かった点 サービスのコアロジックを独立して実装できるため、ミドルウェアの変更など環境の変化に柔軟に対応可能。 各実装レイヤーごとに明確に責務を割り振ることで、処理の流れを把握しやすい。 一貫したルールでの実装を半ば強制することで、システムの規模が大きくなるにつれて処理が複雑になりすぎるということが起きづらい。 機能の追加・削除が容易で、スケールしやすい。 気になる点 簡単な機能であってもコードの記述量が多くなってしまうため、初期コストが高い。 冒頭で述べたように、運用面でのメリットは多くありますが、初期の実装にかかるコストは高いように思いました。 リリースまでのリソースに余裕があり、長期的に運用をしていく可能性のあるシステムには導入する価値があるように思いますが、スタートアップのような限られたリソースでとにかく早くリリースをしたいという場合は、慎重に採用の判断をしたほうが良い構成であると思います。 各項目の解説については詳細を割愛している部分も多々ありますが、全体像の大まかなイメージだけでも伝われば幸いです。 参考記事 [1] オニオンアーキテクチャとは何か - https://qiita.com/cocoa-maemae/items/e3f2eabbe0877c2af8d0 [2] DDDで設計するならCQRSの利用を検討すべき - https://qiita.com/ledmonster/items/22b00c65208dffeff7e4 [3] よくわかるSOLID原則5: D(依存性逆転の原則) - https://note.com/erukiti/n/n913e571e8207 [4] khalilstemmler.com - https://khalilstemmler.com/
アバター
こんにちは、レバテック開発部の長澤です。 タイトルの通り、今回は私の所属部署でのシステム開発について一部をご紹介します! 執筆の背景 私は現在4ヶ月目の中途入社の社員です。 まだわずかな期間ではありますが、すでにレバレジーズのシステム開発は前職までの経験と大きく違うことを実感しています。 転職前は、作成された仕様書にのっとり「機械的」に「工場」の様に開発することを求められていました。 どちらかといえばトップダウンでシステム仕様を決めることが多く、開発者の意見が採用されることは多くありません。 そのためか 「使われない新機能」 「報告するためだけのドキュメント」 「固定化した開発プロセス」 「負債を抱えたレガシー技術」 「形骸化した会議」 などなど、本質的ではない事象をよく見てきました。 そのような環境から一転、私は今とても充実して開発をしています。 なぜ充実しているのか?それは開発プロセスに秘密があります。 今回は「4つのレバレジーズ開発現場の特徴」を皆様にご紹介します! 1、本質の追求 予定していなかった新技術もプロダクトの価値が向上するなら、開発途中に導入することも珍しくありません。 開発時に発生した課題は対話をベースに議論を交わします。無駄なドキュメントは作りません。 2、フィードバック 開発したサービスは利用者が多く、リリース当日の新機能でも利用者がその日に反応し、開発した達成感が得られます。 マーケティングチームから数値ベースでフィードバックがあり、利用状況の良し悪しから、開発観点からも改善施策の提案も行います。 3、高い専門性 リードエンジニアやDBスペシャリストも在籍しており、高度な技術について議論する事もあります。 DDD/クリーンアーキテクチャ、マイクロサービス、gRPC等、社内標準技術スタックに、モダンでより専門性を必要とする技術を採用しています。 4、戦略共有 開発メンバー会議以外にも、マーケター・セールス・デザイナーを含めた部署横断でのプロジェクト進捗共有を毎週開催し、開発内容や優先度に対する認識をすり合わせています。 仮説と根拠が伴った中長期戦略から、開発の方向性の納得感を得ることができ、自信を持った開発をしています。 最後に いかがでしたでしょうか。 ご紹介した内容は取り組みのほんの一部ですが、プログラミングだけでは無く、様々な部署を通じてプロダクトを成長させていくために、広い視点で開発をしていることを感じていただけましたでしょうか。 システム以外の知識も必要とされる場合もあり、課題にぶつかることもありますが、乗り越えた際は、顧客志向やマーケティング等のシステム以外の観点についても自身の成長を実感しています。 レバテック開発部では、一緒にサービスを作り上げてくれる仲間を募集中です! ご興味のある方は、以下のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
アバター
はじめに  こんにちは。21卒エンジニアの田中、五十嵐、益子です。  エンジニアの新入社員向け研修といえば、開発に関わる研修を中心に受けるのが一般的だと思います。レバレジーズでは、エンジニアもマーケティング職と同じプログラムでマーケティング研修を受けます。約1ヶ月間、マーケティングの基礎の学習から始まり、最終的には顧客理解に基づいた「重視すべきサービスが提供する価値の定義と改善施策」の提案を行いました。  本記事では、実際に研修を受けた体験談を通じて、なぜレバレジーズのエンジニアはマーケティング研修を受けるのか、どのようなことを学ぶのか、配属後の業務にどのように活きているのかを紹介します。 顧客理解×エンジニアリング="いいサービス"  レバレジーズにとってマーケティングとは、「顧客のニーズを満たすこと」であり、顧客に最適な解決策を提供することです。  なぜレバレジーズのエンジニアは、マーケティングを学ぶのでしょうか。  レバレジーズは、セールス・マーケター・デザイナー・エンジニアなど、さまざまな職種が社内にいるオールインハウスの組織です。職種の枠を超えたスピード感のあるコミュニケーションや連携を通じて、様々な事業を展開しています。その中で、顧客理解に基づいた"いいサービス"を作り上げるために、エンジニアはマーケターやデザイナーの考え方を理解した上で、密にコミュニケーションをとり、実践する必要があります。  そのためにエンジニアもマーケティング研修を受けることで、マーケターが業務でどんなことをしているのか、どういった思考のプロセスが求められるのかを学びます。 こんなメンバーがマーケティング研修を受けました 田中 「新規の事業作りをしたい思いが強く、オールインハウス組織で、若いうちから職域を広げた働き方がしたいと考え、レバレジーズへの入社を決めました。マーケティング研修はすごく楽しみでしたが、正直どんなことをやるのか想像つきませんでした。」 五十嵐 「会社として急激に成長しており新規事業に携われる可能性が高く、他職種との関係性が密なため、求められるスキルの幅が広いと考えレバレジーズへの入社を決めました。マーケティング研修を受ける前は、研修が楽しみな気持ちが半分と、内定者インターンをしていたチームから離脱することに対する不安が半分ありました。」 益子 「エンジニアとして技術を大切にしながらも、マーケティングや事業開発まで職能を広げたいと思い、レバレジーズへの入社を決めました。学生時代は受託開発企業で働きつつ、マーケティングのゼミで事業計画書を作り、役員への事業提案もしていました。3人の中でも特にマーケティング研修を楽しみにしていたと思います。」 どんなことをしたのか  次に、マーケティング研修の内容について紹介します。この研修では、レバレジーズのマーケターがどんな考え方や方法で業務に取り組んでいるのかを学びました。実務レベルで実際のレバレジーズの事業部のデータ分析を行い、データを元にペルソナ設計からUX(顧客体験)改善施策立案まで行いました。  具体的には以下のプログラムです。 ロジカルシンキング研修 マーケティング概論 ビジネスモデル研修 UX研修 プロモーション研修 オウンドメディア研修 CRM研修 データ活用研修 プロセス研修  これらの研修を受けた後に、最終アウトプットとして、レバレジーズの既存サービス改善施策提案を行いました。  改善施策提案では、顧客が求めていることや提供している価値から、サービスとしての理想状態を自分たちで定義しました。そして、顧客インタビューの記録や実際のデータ分析を通じて、最適な顧客体験を提供するために何が足りないか、どのターゲットに対して施策を打つべきかを特定しました。  顧客体験を考える際には、研修で学んだペルソナ設計やカスタマージャーニーマップ設計などのアプローチを活用。定義した顧客体験を実現するための施策を立案し、営業現場のヒアリングや期待できる効果検証・工数の見積もりなども行い、顧客の理想や現場の実情に即したアウトプットにこだわりました。 研修を受けて感じたこと 田中  元々、エンジニアでも事業課題の解決やサービス改善施策立案をやりたいと思っていたので、マーケティングの基礎を学ぶ時間があることはすごく貴重な時間でした。研修では、より良いサービスやプロダクトを作るために顧客理解が大事なことを学びました。    現在は、「ハタラクティブ」というメディアの開発を担当していて、チームで顧客インタビューを実施し、顧客理解に基づいた改善施策を実行しています。 エンジニアの立場でも顧客インタビューに積極的に関わらせていただき、職種を問わずチーム全体で顧客のことを考えた改善施策を進めていくことに、レバレジーズの良さが表れていて、僕が目指していた職域を広げた働き方ができています。配属されたばかりですが、さらにマーケティングの思考を生かした施策提案などにも挑戦していきたいです。 五十嵐  研修でマーケターの実務に近い経験をさせていただいたのは、とても貴重な経験で、たくさんのことを勉強させていただきました。僕は現在、新規開発の事業部に所属しています。新しいプロダクトを作る上で、まず顧客に対してどんな体験や価値を提供するかを考え、それを実現するためにどんな機能が必要かを定義する必要があります。  最初にUXをどれだけ深く考えられるかがその後のプロダクトの価値を左右すると考えているので、新規開発でもUXを意識して業務に取り組んでいます。エンジニアリングだけでなく、幅広い知識を身につけて業務に望んでいきたいという、選考時に抱いていたことを実際に経験できています。今後、サービスがリリースされたら、マーケティング戦略が本格的に動き出すため、その際に、研修で学んだことを更に活かし、開発業務の枠を超えたエンジニアになれるように挑戦を続けていきたいです。 益子  自分はエンジニアの枠を超えて、課題定義から戦略・戦術の策定、さらには事業開発まで関わりたいと考えていました。そんな自分にとって、社内のマーケターから社内で取り組むマーケティングを網羅的に学べる機会は、非常に貴重なものでした。 マーティング研修で得た知見は、既に業務にも活きていると感じています。開発業務において、各事業課題が設定された背景に意識が向くようになり、「仮説に対しての検証施策に対し、細かな変更に対応できる記述になっているのか」などの新しい視点を持つようになりました。    開発業務以外では、エンジニアリング以外の職域にも挑戦するために、顧客ニーズ・顧客行動の調査などの積極的な情報収集を始めました。まずは何を目的に、どのようなタスクが動いているのか、業務の現状を理解することから取り組んでいます。顧客ニーズの調査方法や顧客行動の調査方法は企業によって異なりますが、マーケティング研修で実務のプロセスで調査手法を学んだことで、スムーズに必要な情報を理解することができています。エンジニアとしての職域にとらわれず、マーケティングを含め、幅広い面から事業に貢献できるプレーヤーになるため、今後も積極的な取り組みを続けたいと思っています。 最後に  3人とも配属先での業務も異なるため、研修で学んだことの活かし方が異なりますが、顧客体験や施策背景といった様々な視点を持って開発業務に取り組むことで、確実にそれぞれにとってプラスになっています。 今後は、サービスやプロダクトを利用する顧客について理解した上で、「いいサービス」の開発に取り組み、社会に影響を与えられる人材となるために、切磋琢磨して日々努力していきます。  「マーケティングも学びたい、若いうちから職域を広げて将来的に、事業をリードするエンジニアを目指したい」と考えている方は是非レバレジーズで一緒に働きませんか。お待ちしています! https://leverages.jp/recruit/
アバター
はじめに こんにちは。レバレジーズ株式会社の大滝です。 私は、レバレジーズのHRテック事業部に所属し、新規SaaSサービスのフロントエンド開発を行っております。 今回は雑然としがちな新規開発、とりわけフロントエンド開発で避けたかった4つの課題を、技術的な観点から回避していった点を紹介したいと思います。 新規開発で回避したかった問題 私たちの開発は新規開発でしたので、できるだけ技術負債を作らないように、かつスピード感を持って開発を行う必要がありました。 そこでフロントエンド開発を行う上で回避したかったポイントがいくつかあります。 バックエンドとフロントエンド間でAPI仕様確認と管理に時間がかかる 型安全ではない 画面によってコンポーネントのデザインがバラバラ 入力動作が遅い また前提として、開発中のサービス全体がマイクロサービスアーキテクチャを採用しており複数のサービス間がGraphQLで通信されていると言う特徴がありました。 図1 マイクロサービスアーキテクチャ構成図 フロントエンドはBFF(Backends For Frontends)に接続し、BFFではバックエンドのマイクロサービスのAPIの集約を行っています。 問題の回避方法と技術選定 上記した問題をクリアするには適切な技術選定を行う必要がありました。 しかし技術選定の難易度が高かったため、弊社のテックリードや開発メンバーと協力し調査を行いました。 結果的に下記の他のマイクロサービスで使用している技術と近く、かつ社内ナレッジがある程度蓄積されていると言う観点から、Apollo Client(graphql-codegen)/TypeScript/Reactを採用し、フォームライブラリとして、React Hook Formを利用しました。 これらの技術により、1の課題に対して、フロントエンドはBFFのエンドポイントからschema(APIの型定義)を取り込みそこからコードを生成することで回避しました。 また、schemaから生成したコードをもとに静的型付き言語であるTypeScriptを用いて実装を行うことで2の課題を回避しました。 3の課題に対しては、デザインの再利用性を高められるようにAtomic designを採用し、それに相性の良いReactを用いました。 さらに、動作速度向上のためにReact Hook Formという依存関係が少なく、軽量なライブラリを用いることで動作速度を向上させることで4の課題を回避しました。 画面実装までのフロントエンド開発フロー 上記の課題をクリアした実際の開発の様子を紹介します。 実際の開発では下記のようなフローで開発を行っております。 図2 実際の開発の手順 この開発フローに沿って、下記の画像のような簡単なユーザーの住所を変更する画面を実際に作ってみます。 図3 ユーザーの住所を変更する画面 GraphQL schemaの実装 サンプルのGraphQLスキーマを用意しました。 今回取り込むschemaはこちらです type User { firstName : String lastName : String address : String } type Query { user : User } 氏名、住所を持っているUser情報を取得するQuery型に入れます。 今回はサンプルなので1名分のUserを取得する形にします。 フロントエンドでのschemaの取り込み 次に、このスキーマをフロントから取り込みます。 まずはQuery情報を記載するgraphqlファイルを作成します query userSearch { user { firstName lastName address } } これをGraphQL Code Generatorという機能を使用して、上記のgraphqlファイルのスキーマ情報を取り込みます。 GraphQL Code Generatorは codegen.yaml にエンドポイントやgraphqlファイルのディレクトリ等を記載してスキーマ情報を読み込みます。 React Hook Formを用いたFormの実装 取り込んだスキーマを使用できるFormを実装します。 React Hook Formを用いてテキストボックスを実装してみます。 今回はMaterial-UIのMuiTextFieldを使います。 textFields.tsx に下記のようにMuiTextFieldをReact Hook Formでラップします。 仕様としてはFormのデフォルト値、ヘルパーテキスト、エラーメッセージが表示でき、nameをキー、入力値をバリューとしてsubmitできるものとしておきます export type FormTextProps = TextFieldProps & { name: string ; defaultValue?: string ; showError?: boolean ; rules?: Exclude < RegisterOptions , 'valueAsNumber' | 'valueAsDate' | 'setValueAs' >; } ; const TextFields: React.FC < FormTextProps > = ( { name , rules , defaultValue = '' , error , showError = true , ...textFieldProps } ) => { const { control , errors } = useFormContext (); return ( <> < Controller control = { control } name = { name } defaultValue = { defaultValue } rules = { rules } error = { !! (get( errors , name ) || error ) } render = { ( { onChange , onBlur , value } ) => ( < MuiTextField onChange = { onChange } onBlur = { onBlur } value = { value } { ...textFieldProps } / > ) } / > { showError && ( < ErrorMessage errors = { errors } name = { name } render = { ( { message } ) => ( < FormHelperText error = { true } > { message } < /FormHelperText > ) } / > ) } < / > ); } ; export default memo < FormTextProps >( TextFields ); コンポーネントを実装する際のポイントですが、下記の5点を意識しています。 Material-UIのTextFieldPropsの型定義を拡張してReact Hook Formで扱いやすくする。 nameタグはReact Hook Formでsubmitした際のkeyにあたるので必ずpropsとして注入するように必須にする。 rulesはReact Hook FormのRegisterOptionsの型定義から必要なものを集めてくる。 defaultValueは指定していないとwarningになるので空文字を初期値として設定する。 メモ化して無駄なレンダリングを減らす。 Form値に入力した値の表示テスト 最後に新住所を入力して入力値をconsoleで確認できるところまで作ってみます。 ユーザーの氏名を表示して、住所を新しく登録する画面を作成していきます。 見栄えをよくするためにスタイルも当てていきます。 const SamplePage: React.FC < {} > = () => { const methods = useForm < { testTextFields: string } >( { mode: 'onBlur' , } ); const { handleSubmit , getValues } = methods ; const onSubmit = () => { console .log ( 'submit:' , getValues ()); } ; const { loading , data } = useUserSearchQuery (); useEffect (() => { console .log ( data ); } , [ data ] ); return ( < FormProvider { ...methods } > < form onSubmit = { handleSubmit ( onSubmit ) } > { !! loading && <> loading... < / > } { !! data && ( <> < Box display = "flex" justifyContent = "center" mb = { 2 } mt = { 2 } > ユーザーの住所情報を変更してください < /Box > < Grid container alignItems = "center" justify = "center" > < Grid item xs = { 8 } style = {{ backgroundColor: '#668bcd0f' }} > < Box m = { 2 } > < Grid container justify = "center" > < Grid item xs = { 4 } > 姓: { data?.user?.firstName } < /Grid > < Grid item xs = { 4 } > 名: { data?.user?.lastName } < /Grid > < Grid item xs = { 8 } > 現在の住所: { data?.user?.address } < /Grid > < /Grid > < /Box > < /Grid > < Grid item xs = { 6 } > < Box mt = { 4 } > < TextFields name = { 'newAddress' } label = { '新住所' } rules = {{ required: { message: 'この項目は必須です' , value: true , } , }} defaultValue = { data?.user?.address || '' } helperText = { '新しい住所を入力してください' } variant = { 'outlined' } fullWidth = { true } / > < /Box > < /Grid > < /Grid > < Box mt = { 3 } display = "flex" justifyContent = "center" > < Button type= "submit" variant = "contained" color = "primary" > 更新 < /Button > < /Box > < / > ) } < /form > < /FormProvider > ); } ; export default SamplePage ; APIで取得したデータを表示する際は、codegenでgenerateしたファイルからAPIをfetchするuseUserSearchQueryをインポートして使用します。 ここで使用しているqueryのhookはGraphQL Code Generatorにtypescript-react-apolloのpluginを入れて生成されるもので、手間のかかるAPIのエラーのハンドリング部分の実装をせずにhookをimportするだけですぐにAPIを使用することができます。 useUserSearchQueryの実態をgenerateされたファイルで確認してみます export function useUserSearchQuery ( baseOptions?: Apollo.QueryHookOptions < UserSearchQuery , UserSearchQueryVariables >) { const options = { ...defaultOptions , ...baseOptions } return Apollo.useQuery < UserSearchQuery , UserSearchQueryVariables >( UserSearchDocument , options ); } UserSearchQuery型がGenericsで渡されているので戻り値は型安全になっています、また使用する際はスニペットが効くのでかなり開発しやすいです。 空の状態でsubmitした際にはバリデーションがかかり、onBlurでもバリデーションがかかるように実装しています。この時にuseFormにGenericsで渡したFormの型がReact Hook Formに登録されます。 今回はMaterial-UIのBoxとGridを用いて画面を実装しましたが、これによりレスポンシブにも対応できる作りになっています。 まとめ 簡単ではありますが、新規開発等でも型安全にかつスピード感を持って開発できるような開発手法を紹介いたしました。 このように、GraphQLのスキーマから型情報を取得しTypeScriptとReactを用いて型安全な実装ができる上に、React Hook Formを用いることで簡単にFormの値の制御が行うことができるので非常に使い勝手が良いです。 HRテック事業部では一緒にサービスを作ってくれる仲間を募集中です!ご興味を持たれた方は、下記リンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
アバター
はじめに こんにちは。レバレジーズ株式会社エンジニアの原田です。 私は、レバレジーズのシステムマネジメントチームに所属し、社内の業務改善のため、さまざまなWebサービスの導入や社内ツールの開発を行っています。 例えば、SlackとDocBaseのWebサービス同士のグループを同期させるツールを開発しました。いくつか問題が起きたことがあったので、どうやって対策したのかを紹介させていただきます。 DocBaseとは DocBaseは気軽に書き込めるナレッジ共有サービスで、弊社では毎日数百件ナレッジが作られ共有されています。 このナレッジの閲覧権限はグループで管理することができ、ユーザーをグループに参加させることで簡単にアクセス権を管理することができます。 同期ツールとは 同期ツールは、AWSのLambda上で動作し、下記のイベントでグループの作成やリネーム、グループ参加者の管理を自動で行うツールです。 Slackチャンネルが作成された Slackチャンネルがリネームされた Slackチャンネルに誰かが参加した Slackチャンネルから誰かが退出した このイベントをもとに、Slackチャンネルと同名のグループをDocBase上に用意します。その後、Slackチャンネルに参加しているメンバーをグループ参加者として追加する動作を行います。 ただし、稀にSlackからのイベントを取得できないことがあり、「ナレッジを閲覧することができない」お問い合わせが発生することがありました。そのため、定期的にSlackチャンネルの情報をDocbaseに一括して同期するバッチ処理を追加で作ることにしました。 バッチ処理の内部動作 当初、バッチ処理は以下の図のように動作させることを考えていました。 早速バッチ処理用のLambdaを作成し、Slack APIを使って実装を行いました。 動作確認のためテストを行ったところ、次のような問題が発生しました。 一定期間内におけるSlack APIの実行回数上限を上回ってしまう Slack APIの実行回数上限を超えないようウエイト処理を挟むと、Lambdaの実行時間上限を超えてしまい処理が中断される この時上限に達することを想定していなかったため、どのように問題を解決すれば良いかとても困った記憶があります。 なぜ上限に達したのか Slack APIには毎分実行できるAPIの実行回数が設定されており、それを超えると429エラーが返ってくるよう設計されています。 なのでAPIを実行した後に2 ~ 3秒のウエイト処理を実行することでこの実行回数上限は回避できる、という仕様が存在します。 また、Lambdaは15分以上実行させようとするとタイムアウトしてしまい、処理が中断してしまうという仕様が存在します。 今回の追加開発では全Slackチャンネル情報が必要になるため、Slackチャンネル数分APIを実行する必要がありました。 この時、APIの実行が必要な回数は3,000回を上回っており、ウエイト処理を実行させると15分以上処理に時間がかかるため、Lambdaが途中で処理を中断させてしまうのです。 どのように解決したか Slack APIとLambdaの仕様をチームメンバーに伝え、どのようにこの問題を解決するか相談したところ「1度にまとめてやろうとせず、処理を分割して行う」方針で解決する話になりました。 処理を分割すれば、Lambdaの実行時間上限を超えないようSlack APIを実行できるのでSlack APIとLambdaの仕様どちらも解決可能です。 こうして、同期ツールのバッチ処理開発を行うことができ「記事が閲覧できない」というお問い合わせを大きく減少させることができました。 もし、同じように困っている方がいましたら、参考にしていただけますと幸いです。 まとめ 今回の問題に遭遇したことで、予め上限や制約などがないか調べる癖を付けると良いなと実感しました。 レバレジーズでは、業務上の問題や課題は、一人ひとりの問題ではなく、チームメンバー全員の問題や課題として扱うことで自然と知見を共有できるため、すぐに問題解決が行えます。 システムマネージメントチームでは一緒にレバレジーズを支えてくれる仲間を募集しています!ご興味を持たれた方は、下記リンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
アバター
はじめに レバレジーズ株式会社エンジニアのカラバージョ(Caraballo)です。今回は、8言語(*1)で求人情報を提供しているメディアである WeXpats Jobs で実装した多言語検索のコスト最適化についてご紹介します。 (*1) 2021年2月現在。 なぜコストの最適化が必要だったのか? チームの目標として、ユーザーエクスペリエンス(UX)を向上させるために日本語で書いてある求人情報を複数の言語で検索できるようにする必要がありました。 私たちのチームでの最初のアプローチは、Google translate APIを使用して各求人情報を翻訳し、Elasticsearchにインデックスを付ける予定でした。これは簡単なアプローチのようにみえますが、APIの費用が100万文字あたり$20USDであり費用対効果が低いことに気付きました。 月額だと約 $4,000USDの費用がかかる計算です。 この問題にどのように取り組んだのか? まずはじめに、ブレインストーミングを行い問題を根本的なものに集約しました。つまり、「日本語のテキストデータを元にして他の言語での検索を効率的に行う方法は何か」ということです。 たとえば、次のテキストのように 「東京でReactを使用したフロントエンドエンジニアとしての職務」の中から、仕事を探すという文脈で意味を伝えたい重要な部分は [東京、React、使用、フロントエンドエンジニア、職務 ] の名詞であり、[で,を,した,としての]を省略しても他の言語に翻訳したときに元のテキストの意味をほとんど反映できます。 したがって、各求人情報の名詞を抽出して翻訳することで翻訳の必要があるAPI呼び出しの数が減る、さらに記事の間で何度も使われている名詞の翻訳結果をキャッシュすることでさらに翻訳の数を減らすことができました。 その結果、翻訳されるデータが多くなるほど、辞書が増え必要なAPI呼び出しが少なくなっていきます。 計画は次のとおりでした。 日本語のテキストをtokenizeし、名詞のみを抽出する。 抽出した名詞を共通の辞書に保存し、必要に応じて各言語に翻訳を追加する。 翻訳された名詞を準備して、Elasticsearchの各言語のインデックスを作成する。 実装 上記の要件を実装するために、今回はGolangを利用しました。 日本語の原文を取り込む トークナイザーを使用して名詞のみを抽出する 新しい単語がある場合、辞書に保存する 翻訳されていない単語を翻訳する 複数の言語でのテキストBLOBを作成する Logstashを使ってElasticsearchにデータを取り入れてインデックスを作成する 日本語をトークン化するために、次のgolangライブラリを使用しました。 github.com/ikawaha/kagome/tokenizer Kagome Japanese Morphological Analyzer( https://github.com/ikawaha/kagome/tree/master ) 例: package main import ( "fmt" "github.com/ikawaha/kagome/tokenizer" ) func main() { nouns := GetTokens( "東京でReactを使用したフロントエンドエンジニアとしての職務" ) fmt.Printf( "%v" , nouns) } func GetTokens(content string ) [] string { t := tokenizer.New() tokens := t.Analyze(content, tokenizer.Normal) var nouns [] string for _, token := range tokens { if token.Class == tokenizer.DUMMY { continue } feature := token.Features()[ 0 ] switch feature { case "名詞" : nouns = append (nouns, token.Surface) } } return nouns } output [東京 React 使用 フロントエンドエンジニア 職務] まとめ 今回は、多言語検索のためのコスト最適化の例をご紹介しました。 翻訳された名詞の辞書を作成することで、API呼び出しの数を大幅に減らすことができ、将来的には WeXpats Jobs 以外のサービスでも多言語検索サポート機能を使用できるように拡張していく予定です。 レバレジーズ株式会社では現在、サービスを開発し、優れたユーザーエクスペリエンスを作成するための新しい仲間を探しています。興味のある方は、こちらのリンクに応募してください。 https://recruit.jobcan.jp/leverages/
アバター
はじめに こんにちは。レバテック開発部の園山です。私は、レバテック開発部のビジネスサポートグループに所属し、システム開発業務を担当しています。 本記事では、開発効率を向上するためにインターフェイス定義言語 (IDL) ベースの開発に切り替えて、設計書管理を行わず、どのように開発を行っているかについてご紹介します! レバテックにある主な3つの機能として、「営業支援・管理ツール」「ユーザーや取引先企業が使用する登録者向けサービスシステム」「オウンドメディア」があり、現在、それらを1つの統合化したプラットフォームに集約するプロジェクトにおいて業務設計・開発を進めています。 今回のリプレイスにあたり、システム開発をより良いものにするためにはどうしたら良いかメンバー間で日々意見を出し合っており、そこで生まれた案をひとつずつ取り入れています。その中のひとつとして、ドキュメントで設計書を管理する体制を廃止しました! これまでのやり方 これまでは、設計者が設計書をドキュメントに起こし、ドキュメントに書かれたものを実際の開発担当者が読み、開発を行っていました。複数の施策が同時並行で進んでいることが多く、仕様を更新する場合は設計担当者が設計書のコピーを作り、仕様を更新して、差分がわかるように打ち消し線や文字列の色を変える工夫をしていました。 結果、原本のドキュメントのメンテナンスが後回しになってしまったり、特殊仕様の認識合わせにコミュニケーションコストがかかっていました。 インターフェイス定義言語 (IDL) ベースの開発とは インターフェイス定義言語 [IDL: Interface Definition Language](以下、「IDL」という) とは、特定のプログラミング言語とは別にオブジェクトのインターフェイスを指定するために使用される汎用言語のことで、本プロジェクト内では gRPC のプロトコルバッファーから IDL を使用しています。 プロトコルバッファー以外にも、OpenAPI(Swagger)、GraphQLや、Apache ThriftなどがIDLでSchemaを定義する技術になります。 プロトコルバッファーIDL は、オープンな仕様を持つプラットフォームに依存しないカスタム言語で、開発者は、入力/出力共にサービスを記述する .proto ファイルを作成し、API仕様を決めることができます。 これらの .proto ファイルはクライアントとサーバーの言語を生成できるので、TypeScript ⇄ PHP といった複数の異なるプラットフォームで通信でき、開発時にも.protoファイルを共有することで、コードの依存関係を取得することなく他のサービスを使用するためのコードを生成することが可能になっています。 IDLベースの開発は社内でも前例がありませんでしたが、1人が起案したところから始まり、サンプルコードを元に勉強会を実施して、実際に開発を進めながら習得していきました。 複雑な責務を持つマイクロサービスの場合、事前にある程度知識を深めるための資料を用意して、関係者間で認識合わせをしてから着手することもありましたが、基本はIDLベースで問題なく進めることができています。 違いを比較してみると 実際の開発シーン別にその違いを比較してみます。 開発あるある①:仕様が途中で変更になる 既存  設計者が設計書を更新 → 開発者が設計書を見て実装を修正 IDL  設計/開発者がIDLを更新 → 開発者はコード生成を再実行し、コンパイルエラーが発生していたらエラーを解消 開発あるある②:開発を分担していて連携部分が心配 既存  片方の実装が終わるまで動作確認ができず、実装完了後に不具合が見つかったりする IDL  IDLからコードを生成するため、定義通りのスキーマになることが保証され、連携部分の心配がなくなる 開発あるある③:設計書の管理が難しい 既存  施策単位や特殊仕様に応じてドキュメントが増えがちで、設計書をメンテナンスする優先順位が低く管理が行き届きづらい IDL  IDLから設計書を出力する(手動で差分を最新化する手間がなくなりました) 以前よりも変化を迅速にシステムへ反映させていくことができるようになりました! やってみた感想 設計者と開発者の垣根がなくなったことが大きく、開発をしながら改善していく楽しさを実感しています。 デメリットは、慣れるまでの学習コストやある程度の設計スキルが必要なことで、I/Fだけでは見えない仕様パターンの考慮をどのようにメンバー間でコミュニケーションを取りながら進めていくかなど、チームルールを整備する必要がある点も課題に感じています。 まとめ 今回は開発効率を向上するためにインターフェイス定義言語 (IDL) ベースの開発に切り替えたことについてご紹介しました。IDL開発について何か1つでも参考になる点があれば幸いです。社内では、デメリットに挙げた点についても改善などの提案が常に行われており、解決へ向けて積極的に取り組んでいるため、また別の機会にご紹介できればと思います。 レバテック開発部では、一緒にサービスを作り上げてくれる仲間を募集中です! ご興味のある方は、以下のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
アバター
こんにちは。レバレジーズ株式会社のテックリードの竹下です。 2021/1/13に、 KARTE のサービスを運営しているプレイド社とマイクロサービスに関して、合同勉強会を開催しました。 今回は、クローズドかつ、リモートで勉強会を開催したため、リアル開催やリモート一般公開と比べてどのようなメリットがあったかをご紹介していきます。 目次 目次 開催の経緯 勉強会の内容 クローズドで内容が充実する 業務に密接に関係する話ができる 踏み込んだ話ができる 懇親会も濃密 開催者も参加者も楽できる 周知や参加者管理が楽 リモートなので参加者が楽 初めての発表の人も気が楽 まとめ レバレジーズと勉強会しませんか? 開催の経緯 これまでレバレジーズでは、社内勉強会に加え、セミナールームを勉強会に貸し出すことで、社外とも交流を持ち、エンジニアが学べて成長できる環境を作ってきました。しかし、コロナ禍によって勉強会が全てリモート開催になったり、延期になってしまったことで、勉強会の機会が減っていました。 社内での勉強会は、社内の知見共有が中心となってしまうため、世の中の技術トレンドや、他社での取り組みを知る機会は多くありません。自社でリモート勉強会を開催しようと考えましたがレバレジーズは技術的な知名度がまだ低く、集客は難しいと判断しました。(開催したのはいいけど、社員しか参加してくれなかったら悲しいじゃないですか……😥) 一般公開では集客が難しそうなので、TypeScriptでマイクロサービス化という技術スタックをすでに運用しているプレイド社に「2社で勉強会をしませんか」と声をかけたのが開催の発端です。 勉強会の内容 勉強会のタイムスケジュールは、「マイクロサービス」をテーマに15分程度で発表を2社2名づつ行い、その後に懇親会という流れで行いました。 それぞれのタイトルと資料は下の通りです。(敬称略、公開確認取れたもののみ掲載しています) レバレジーズ株式会社 住村 「マイクロサービス五里霧中」 株式会社プレイド 大矢「Tilt.dev を使ったリモート k8s 開発環境」 レバレジーズ株式会社 竹下 「開発効率爆上げを目指したインフラ技術スタック構想」 Slide@Prezi 株式会社プレイド 山内「アンチパターンから学ぶマイクロサービス」 私の発表に関しては詳しい内容は、また後日改めてブログに書きますので、ご期待ください クローズドで内容が充実する 業務に密接に関係する話ができる クローズド開催にすることで、関係者は2社のみとなるため、お互いが興味のあるテーマの勉強会を開催することが可能になります。一般公開の勉強会を行う場合、集客性や、世の中のエンジニアの人に役に立つような内容にすることを考慮する必要があるため、幅広い人に興味を持って貰えるようなテーマ設定になりがちです。 今回プレイド社とは、「TypeScript」と「マイクロサービス」という2つの共通点があり、マイクロサービスに関する勉強会を開催する運びになりました。 踏み込んだ話ができる クローズド開催になったことで、内容に関しても踏み込んだ内容まで発表することができました。一般公開した場合、その分野に詳しくない人も来ることが想定されるため、その人達にもわかるような発表をする必要があり、どうしても本題に入るのが遅くなってしまいます。 しかし、2社間だと前提とする知識が共通してあるため本題の説明に時間を多く割くことができました。(そのため、発表スライドだけを見てもらうと端折られているように感じる部分があるかもしれません。) さらに、発表内容自体も「開発環境をどう作っているか」や「どんな失敗をしたか」「どういう挑戦をしているか」など、一般論に終始しない実務に根ざした内容が盛りだくさんになっていて、普通の勉強会ではなかなか聞くことの出来ない内容になっています。 懇親会も濃密 勉強会では発表も重要ですが、懇親会も発表を補完する機能を持っています。クローズドだとお互い実務に携わっている人が多く参加しているので、発表者の人に話を聞くだけでなく、他のエンジニアに実務の中でのノウハウを聞いたり、あるある話に花を咲かせることも可能です。また、プロジェクトマネージメントや気になっている技術のことなど、普段なら相手のバックグラウンドを探ってからでないと聞きにくいようなことも聞きやすく、勉強会のテーマ以上のことを学ぶことも出来ました。 開催者も参加者も楽できる 周知や参加者管理が楽 私は前職で隔週で Scala勉強会 の会場係を7,8年務めていたり ScalaMatsuri の運営を5,6年手伝っていましたがが、参加者を集めたり内容を企画するのにいろいろ苦労をしてきました。connpass, atendなどのイベント告知サイトにイベント登録をしたり、発表を募ったり、キャパ制限があれば先着順や抽選をしたり、リアル会場なら会場への入場方法を案内したり、懇親会の店をとったりと様々な雑務が必要です。 しかし、クローズド+リモートにすることでそれらの手間がかなり軽減されました。周知はイベントサイトなど作る必要が無く、お互いの会社でSlackやメーリスで流すだけとなり、全員リモート参加なので人数制限や会場への案内も不要で、懇親会のお店の予約や準備もいりませんでした。そのため、勉強会の開催ノウハウや人員がいなくても手軽に開催が可能になります。今回は、初回ということもあり入念に準備しましたが、それでもミーティングが約1時間半くらいと、接続テスト30分程度で準備が出来ました。 勉強会を継続するには、勉強会の運営や管理をしていく人が必要になりますが、これぐらいの労力なら片手間に出来るので、ひとりでも継続して開催することができます。 リモートなので参加者が楽 リモートになったことで参加者も楽になっています。リアル開催の場合、会場へ移動する必要があるためどうしても参加障壁が上がります。今回は、19時開始でしたが、もしプレイド社(銀座本社)で開催した場合、レバレジーズは渋谷に本社を構えているため、遅くとも18時30分には退社し会場に向かう必要があります。懇親会含めると22時30分くらいまでやっていたため、家につくのは23時を過ぎてしまいます。レバレジーズ社とプレイド社ならまだ近いですが、大阪や福岡にある会社の場合は、そもそも参加すらできません。 しかし、リモートだと移動時間が全くなくなるので拘束時間は実際の勉強会と懇親会の時間だけになり、もし急遽業務の割り込みが合ったとしてもすぐに業務に戻ることも可能です。そのため、気軽に参加してもらうことが可能になり双方の参加者を増やすことが出来ます。 初めての発表の人も気が楽 私もはじめはそうでしたが、見ず知らずの人の前で発表をすることは初めての人にとってはハードルが高いものです。しかし、2社間クローズドにすることで半分は自社の人で見知った人も多くいるため、発表になれていない人にとっても発表がしやすいです。また、当日は資料を完全公開にする必要が無いため後で手直しも可能なため、発表慣れしていない人にとっては嬉しいかしれません。 まとめ 今回は、レバレジーズが現在取り組んでいるマイクロサービス化について、すでに運用して1年ほど経つプレイド社の知見を大いに学ぶことが出来ました。プレイド社のハマったポイントや、レバレジーズで現在抱えている問題をどのように解決しようとしているかなど、知りたいと思っていることを学ぶことが出来ました。 私もいろいろな勉強会に参加していますが、通常は入門的な内容が多かったり、今知りたいことと少しずれてたりするので、ここまで密度の高い勉強会はなかなか経験がありませんでした。 これは、クローズドかつリモートという形式を取ったことの効能だと感じました。 また、勉強会の開催の手間も少なく、ハードルも低いため頻繁かつ定期的に開催することも出来ると思います。今後もひとつ一の取組みとしてクローズドかつ、リモート形式での勉強会を継続して開催し、エンジニアの技術力UPの一助にしていきたいと思います。 レバレジーズと勉強会しませんか? 現在レバレジーズでは、マイクロサービス化、TypeScriptの導入、gRPCの採用、DDDやクリーンアーキテクチャの採用、Vue.js/Reactの導入、IaCによるインフラ管理など様々な 技術スタックの刷新 を行っています。もし、同じような技術を持っていたり、導入を考えている方いたら竹下や的場にご連絡ください!是非、一緒に勉強会を開催しましょう。 お問い合わせはこちら にお願いします。
アバター
はじめに こんにちは。プロジェクトマネジャーの丸山です。 最近、プロジェクトマネジメントに関する記事をたくさん見かけますが、 「社内システムのプロジェクトマネジメント」のテーマはそこまで出回ってないように思います。 そこで今回はレバレジーズの社内システムのプロジェクトマネジメントがどのように行われているのかを紹介します。 年商150億円を支える基幹システム! プロジェクトマネジメントについて紹介する前に、対象のシステムについて紹介させて下さい。 営業活動を行う時に利用するシステムを社内の事業部に提供しています。この類を営業支援システム(SFA)と呼び、有名なものではセールスフォース・Hubspotなどがあります。 このシステムは、顧客管理・案件管理・進捗管理・書類管理・金銭管理といった基本的なSFAに付いている機能に加え、 連絡機能(電話・メール・LINEなど)やマーケティング情報の管理機能なども付いているので、営業だけでなく事業の全領域を支援しています。(よって基幹システムと題させて頂きました) このシステムは、1つで4事業を支えています。看護師さんの紹介事業・派遣事業・介護士さんの紹介事業・派遣事業の4つです。 看護業界と介護業界の中身は異なる部分が多いですし、紹介事業と派遣事業のビジネスモデルも全く違いますが、人材業界という大きな枠組みでシステムを設計したことで、4事業の全領域を管理するシステムを作ることが出来ました。 このシステムが支援する4事業の年商は約150億円です。社員による月間アクセス数は250万PVとなります。 基幹システムの全体像 プロジェクトマネジメントをどのように行っているか?? それでは本題に入ります。 まず、全体の流れを簡単に示すと下記のようになります。 プランニング → 要件定義 → 基本・詳細設計 → 実装・テスト → 導入 → リリース → 効果測定 → プランニング このサイクルをおよそ1ヶ月スパンで回しています。関係者が全員社内にいるため、コミュニケーション調整コストがほとんどかからない社内システムだからこそ、この短いスパンを実現出来ています。 それでは、各プロセスについて具体的に説明します。 プランニング 今後どのようなシステム改修をしていくか、4事業の責任者と話し合い、優先順位を付けます。 4事業の売上を最大化するために、事業部の全活動の中でどこを改善するべきか(事業課題)を話し合います。事業課題の中でシステム改修によって解決可能なものがあれば、「このような改修によってこの事業課題がこのくらいのインパクトで改善される」などの提案をします。採用された提案には優先順位を付け、優先度の高い改修のスケジュール調整を行います。 採用された提案の多くがプロジェクトマネジャー発案であり、エンジニアが考えてプロジェクトマネジャーに提供した提案も採用されています。 ほとんどの改修で、自分達がやる価値があると思ったことに取り組んでいるので、開発チーム全体のモチベーションが高く保てています。 要件定義 プランニングで決まったシステム改修案の要件を定義します。 関係者を集め、どのような改修をしようとしているか説明を行い、実現可能性・課題解決の方法・役割分担などについて話し合います。 営業関連の改修なら営業部のリーダー、マーケティング関連の改修ならマーケティング部の担当者と、集める関係者は改修案によって異なります。同じ会社の仲間なので、協力的に動いて下さる社員が多く、様々な領域のプロフェッショナルと意見交換が出来るので視野が広がります。 基本・詳細設計 要件をベースに基本・詳細設計をします。 基本・詳細設計は画面設計・入出力設計・機能設計・DB設計の4つで構成されています。画面設計は、関係者に要望をヒアリングしたりプロトタイプを見せたり意見交換をしながら行います。 要望をヒアリングした際に、4事業の文化の違いやチーム体制の違いにより意見が割れることも多いです。この場合には全ての要望を汲み取るべきか取捨選択するかを意思決定し、対立意見の関係者を説得することで意見を収束させます。 4事業を支えるシステムだからこそ、難しい部分もありますが、この苦労があるからこそ4事業を支えるシステムが成立します。 また、DB設計では4事業間の整合性を担保するために抽象化・正規化が適切に行われているかを細かくチェックします。最初は上手く設計が出来ませんでしたが、経験を経て上手く設計出来るようになりました。DB設計が上手くなっただけでなく、概念的に考える力がかなり成長したと感じます。 実装・テスト 実装・テストの実行はエンジニアが担当します。 プロジェクトマネジャーの担当はスケジューリングとサポートです。 開発チームでは2週間ごとに区切りをつける、スプリント形式で開発しています。スケジューリングとは、プランニングで定めた改修のスケジュールを守れるようにタスクをスプリントに割り振っていくことです。サポートとは、エンジニアに要件や基本・詳細設計を説明をしたり、開発で発生した問題を解決したり、実装・テストを進めるための支援活動のことを指します。 複数のプロジェクトが並行して走ることが多く、スケジューリングやリソース調整は大変になりますが、「スケジュール通りに開発を進める」という、プロジェクトマネジメントで一番基本となる能力の成長に繋がりました。 導入 全てのシステム利用者に改修内容を説明します。 要件定義や基本・詳細設計において全てのシステム利用者を巻き込むことは時間制約上不可能なので、改修に関する情報があまり伝わってないシステム利用者もいます。 その方々に向けて、「どのような事業課題に対してどのような改修をしたのか」を説明します。システムの最大の目的は「4事業の売上の最大化」であるため、それに基づいてシステム利用者が表面上不便に思う改修を行うこともあります。 そのような場合にも改修の理由を事前に説明して理解してもらうことで、システム利用者との信頼関係を保つことが出来ます。 システムへの関心が薄い相手もいる中で、まんべんなく理解を得ることは難しく、このプロセスも初めは苦戦しました。ただ、試行錯誤を重ねる内に分かりやすい説明と共感性の高いメッセージの発信が身について、システム改修について大部分の人に理解してもらえるようになりました。 リリース エンジニアがリリース作業を行っている間にプロジェクトマネジャーがSlackでリリースを告知します。改修の内容が良ければ、告知後すぐにSlackのスタンプや称賛のコメントがたくさん返ってきます。反応が薄いときはもっと頑張らないとなと思いますし、反応があったときには純粋に嬉しい気持ちになります。 効果測定 システムのアクセスログや売上データを分析して、事業課題の改善インパクトを測定します。インパクトがプランニングで提案した時の基準に達していれば、システム改修によって事業課題を解決出来たと評価されます。 事業課題を解決出来た場合の達成感は、リリース時以上のものです。 リリース時の反応は重要であるものの指標の1つでしかなく、チームの最終的な使命は「事業課題の解決」と、それによる「4事業の売上の最大化」だからです。 事業課題を解決出来た際は、チームSlackで成果を共有し、達成の喜びを分かち合います。数多くの困難を共に潜り抜けたチームメンバーと共に達成の喜びを分かち合えたときは、全ての苦労が報われたように感じます。 効果測定が完了すると、その結果を踏まえて更なる事業課題が無いかプランニングで検討します。効果測定からプランニングに繋がることで、プロセスが循環しています。 まとめ レバレジーズの基幹システムのPMとは、一言で表すと「4事業の売上最大化を目的としたシステム改修の仕掛け人」です。この仕事には主に2つの魅力があります。 システムの規模が大きいこと 扱っているシステムは基本的なSFAの枠組みを大きく超え、連絡機能(電話・メール・LINEなど)やマーケティング情報の管理機能などもついているので、事業の全てを支える基幹システムと言えます。そのうえ、ひとつのシステムで4事業、年商約150億円を支えています。 裁量権が大きいこと システム開発において、上流として位置付けられる要件定義だけでなく、事業の責任者と事業課題を話し合うプランニングから参加しているため、システム開発に関する全ての意思決定について関わることが出来ます。 事業を成長させたい思いで自ら考え動かした改修案が、リリースされシステム利用者に喜ばれる、嬉しい経験も味わうことが出来ました。 4事業が関わるシステムなので、難しい意思決定が多いですが、コミュニケーション力や論理的思考力などのスキルの成長に繋がりました。 We are Hiring レバレジーズではプロジェクトマネジャー・エンジニアを募集しています。 興味を持って頂けた方はこちらからご連絡頂けると幸いです。 https://recruit.jobcan.jp/leverages/
アバター
はじめに こんにちは!エンジニアの呉です! 今回は社内で開発している電話アプリについて、Chrome拡張機能からElectronへリプレースした話をご紹介します。 リプレースしたきっかけ ■問題点 社内で開発している電話アプリでは、いくつかの問題が顕在化していました。 コードの見通し 電話という特異的な機能に加えて、Chrome拡張機能独自の お作法 によりコードの見通しが悪くなっていた 手動リリース ウェブストアのダッシュボードから審査の申請をする必要がある リリースコントロールがしにくい Chrome拡張機能の自動更新が最大5hのタイムラグが生じる( chroniumの対象コード ) ウェブストアの審査が介入するため、リリースが手間 ■解決手段 今回これらの問題を解決する手段として、Electron + Vue.jsでリプレースをすることを決めました。 コードの見通し 社内で知見の多いVue.jsを採用し、学習コストを低減 言語はTypeScriptを採用し、型宣言による開発効率、保守性の向上 表示のコンポーネント化、機能のモジュール化を行うことにより、それぞれの責務を明確にし、コード全体の見通しを向上 手動リリース GitHub Actionsを利用し、リリースを自動化 リリースコントロールがしにくい electron-builderの electron-updater パッケージを使い、自動更新タイミングを自分でコントロールできるように解決 AWS S3へのアップロードを行うだけのため、審査介入によるリリースコストダウン 出来上がったもの 今回出来上がったものを簡単なご紹介します。 ■Electronアプリケーション 主なディレクトリ構成は以下の通りです。 src ├── assets ├── background ├── components ├── constants ├── models ├── plugins ├── router ├── services ├── store └── views ディレクトリ名 役割 assets グローバルで利用するCSSやフォント、ロゴなどのリソース background Electronアプリケーションのライフサイクル制御とプロトコル設定、バージョンチェックなど components 表示部品単位でのコンポーネント定義 constants 通話で利用する結果コードの定義や外部イベントの定数を定義 models APIや通話で利用する連絡先などのモデル定義と型定義 plugins API通信で利用するaxiosやGoogle OAuth、Slackなど外部ライブラリのラッパーを定義 router ページのルーティング定義 services 業務ロジックをサービス層として抽出し定義 store モジュール単位での状態管理とアクション定義 views ページ単位単位でのコンポーネント定義 ■リリースフロー リリースフロー これまで他のプロジェクトでは、 CircleCI を使ったリリースの自動化をしていましたが、今回は GitHub Actions を使ったリリース方法を採用しました。 理由としては、 社内でElectronを使ったプロジェクトが多く発足する可能性を考慮し、プロジェクト独自のリリースフローで良いと判断 GitHub Actionsのワークフローを定義のみで設定作業のコストを削減 CircleCIのmacOSビルド環境(executor)を追加で契約する必要があったため、ランニングコストを削減 よかった点 今回電話アプリの開発を実際に担当している身として、前述の問題点に対してどうすべきなのか、どうしたらやりやすくなるのかをエンジニアサイドから考えた上での行動に起こしました。 ■運用保守コスト 結果として、リプレースによる利用者の満足度を向上させるようなダイレクトなインパクトはありませんでしたが、エンジニアサイドの心理的安心感や運用保守コストダウンにより、間接的に利用者に機能提供するまでの開発効率を向上することができました。 ■スキル ElectronやGitHub Actionsなど社内でもあまり導入実績のない技術に対して挑戦することで、個人の成長を実感することができました! ■意思決定 今回のリプレースの提案に対しても「イイじゃん!イイじゃん!」と共感してもらった上で、その場で「じゃあいつまでにできそう?」とスピード感に若干驚きました(笑) ボトムアップの提案に対してもスピーディに対応し、承認までの間隔が短く、提案することに対して億劫にならない環境だなーと私個人としてとても印象に残りました。 大変だった点 …ここまで良いことばかり書いてきましたが、もちろん良いことだけではありませんでした。 ■Twilio 元々前任者がベースを開発していたこともあり、全容を完璧に把握できていたわけではなかったので、動作が変わらないように全体を見渡す時間がとてつもなくかかってしまいました。 ■Appleの公証 macOSを利用している方もいるため、Appleの公証(アプリ署名)を行う必要がありました。 Apple Developer Programからの証明書発行、発行した証明書を用いてビルド・リリースの自動化で苦戦をしました。 最終的には electron-notarize を使うことで解決しました。 まとめ いかがでしたでしょうか? 社内のカイゼン事例として、社内電話アプリのChrome拡張機能からElectronにリプレースした話をご紹介いたしました。 今回エンジニアによるボトムアップからの提案に対してスピーディに実現ができたことが素直にとても嬉しかったです。 みなさんも「やりたい!」と思ったことをまずは声に出してみるところから始めてみてはいかがでしょうか。 We are Hiring レバレジーズでは、一緒に今をより良くしていく仲間を募集しています。 弊社に少しでも興味を持っていただいた方は是非ご連絡いただけると幸いです。 https://recruit.jobcan.jp/leverages/
アバター
はじめに こんにちは!エンジニアの藤野です! 今回は キャリアチケット が運営するキャリアチケットカフェの iOS/Android向けアプリ をFlutterにフルリプレイスした話をご紹介します。 なぜFlutterに移行したのか 元々アプリ開発はiOSはSwift, AndroidはKotlinを使用し、開発を進めていましたが、開発を進めていく上で生産性が上がらない問題が発生していました。 最終的には1人でモバイル開発をしていたため、iOS/Androidのどちらとも実装する時間がなく、iOSのみに実装するといったOSによって機能が違うアプリになっていました。また、1人で仕様検討から実装・リリースまでを行っていたため、開発効率も下がっていました。レビュワー等もいなかったため、プルリクエストを投げた後もセルフマージで対応していました。 以上のような問題を解消するために、Flutterにフルリプレイスすることを決めました。Flutterにした理由は下記2つです。 未経験でも取っ付き易い OS関係なくひとつの言語で統一できる 他のチームメンバーにもFlutterを触ってもらうことで、開発できる人を増やせること、さらに生産性を向上させるメリットがあったため、Flutterへのフルリプレイスへ踏み切りました。 構成 今回使用した状態管理手法やディレクトリ構成を紹介します。 状態管理の手法はChangeNotifier+Providerパターンを採用しています。Providerは Pragmatic State Management in Flutter (Google I/O'19) で公式に推奨されています。 設計当初にはまだ登場していませんでしたが、最近はStateNotifier+Freezed+Providerを使った状態管理や Riverpod の登場などがあります。 ディレクトリ構成 ディレクトリ構成は以下の通りです。どのような役割になっているかも、ひとつずつ簡単に説明しています。 lib ├── config │ └─ route.dart ├── models │ └─ tmp │ ├─ tmp.dart │ ├─ tmp.freezed.dart │ └─ tmp.g.dart ├── resources │ ├─ api │ │ └─ tmp_api_provider.dart │ └─ repositories │ └─ tmp_repository.dart ├── screens │ ├─ common │ └─ tmp │ ├─ widgets │ └─ tmp_screen.dart ├── services ├── utils ├── assets ├── viewmodels │ ├─ common │ └─ tmp │ └─ tmp_view_model.dart └── main.dart config 設定ファイルを配置(ルーティングを設定するファイルなど) models 作成するモデルのディレクトリを作ってその中にdartファイルを配置する freezedパッケージ を使用 resources api APIとのやり取りをするファイルを配置 ファイル名 : xxxx_api_provider.dart repositories apiからデータを取得して各モデルの形にマッピングしたり、データ送ったりする ファイル名 : xxxx_repository.dart screens 各画面ごとにディレクトリを作成してその中にファイルとwidgetsディレクトリを作成する 基本的には1画面1screenを作成し、widgets配下に画面内で使用するWidgetを配置 services 主に外部サービスとの連携周りの処理を書いたものを配置 utils 定数などのファイルを配置 assets 画像などの素材系を配置 viewmodels screenの状態管理をここで全て行う provideパッケージ を使用(Provider & ChangeNotifier) 構成は以上のようになっています。伝わりづらい部分はサンプルとして、 APIからデータを取得して表示するだけの簡単なアプリを作成しながら詳しく説明します。 1. パッケージの導入 providerパッケージやfreezedパッケージを利用するため、pubspec.yamlを以下のようにします。 dependencies: flutter: sdk: flutter - cupertino_icons: ^0.1.3 + provider: + freezed_annotation: + http: dev_dependencies: flutter_test: sdk: flutter + json_serializable: + build_runner: + freezed: 2. モデルの作成 モデルはFreezedパッケージを使用しているため、以下のように書いた後に以下のコマンドを実行します。 flutter pub run build_runner build これによって event.freezed.dart ファイルと event.g.dart が生成されます。 詳しいFreezedパッケージの使い方は ドキュメント を参照してください。 import 'package:freezed_annotation/freezed_annotation.dart' ; import 'package:json_annotation/json_annotation.dart' ; part 'event.freezed.dart' ; part 'event.g.dart' ; @freezed abstract class Event implements _$Event { factory Event ({ int id, String title, @nullable @JsonKey (name : 'created_at' ) String createdAt, }) = _Event ; factory Event . fromJson ( Map < String , dynamic > json) => _$EventFromJson (json); } 3. APIからの取得処理実装 APIから実際にデータを取得する部分をプロバイダーとして作成します。 import 'package:http/http.dart' as http; class EventApiProvider { Future getAll () async { return await http. get ( 'http://localhost:8080/events' ); } } Repositoryでは上記で作成したプロバイダーから取得したデータをモデルの形にマッピング import 'dart:convert' ; import 'package:sample/models/event.dart' ; import 'package:sample/resources/api/event_api_provider.dart' ; class EventRepository { Future < List < Event >> getAll () async { final _response = await EventApiProvider (). getAll (); return json. decode (_response.body) . map < Event > ((json) => Event . fromJson (json)) . toList (); } } 4. ViewModelの作成 ここで先ほど作成したEventRepositoryを利用してデータを取得し、画面側へ結果を伝えます。 import 'package:flutter/material.dart' ; import 'package:sample/models/event.dart' ; import 'package:sample/resources/repositories/event_repository.dart' ; class EventViewModel extends ChangeNotifier { final EventRepository _repository = EventRepository (); List < Event > events = [] ; Future getAll () async { events = await _repository. getAll (); notifyListeners (); } } 5. Screenの作成 ボタンを押すとAPIからデータを取得し、取得結果を表示するような画面を作成します。 ボタン押下時にViewModel側の getAll() を呼び出してデータを取得、 notifyListeners() が呼び出されたタイミングでListViewが再描画されます。 import 'package:flutter/material.dart' ; import 'package:provider/provider.dart' ; import 'package:sample/viewmodels/event_view_models.dart' ; class EventScreen extends StatelessWidget { @override Widget build ( BuildContext context) { EventViewModel _viewModel = Provider . of < EventViewModel > (context); return Scaffold ( appBar : AppBar ( title : Text ( 'Event List' ), ), body : Center ( child : Column ( children : < Widget > [ FlatButton ( onPressed : () async => _viewModel. getAll (), color : Colors .lightBlue, child : const Text ( '取得' ), ), Expanded ( child : ListView . builder ( itemCount : _viewModel.events.length, itemBuilder : ( BuildContext context, int index) { return ListTile ( title : Text ( " ${_viewModel.events[index].id} : ${_viewModel.events[index].title} " ), ); }, ), ), ], ), ), ); } } 6. main.dartの修正 各画面の遷移先は以下のようにルーティングファイルに記載します。 import 'package:flutter/material.dart' ; import 'package:sample/screens/event_screen.dart' ; final Map < String , WidgetBuilder > routes = { '/' : ( BuildContext context) => EventScreen () }; そして MaterialAppのroutesに先ほど作成したルーティングを設定することによってEventScreenが呼び出されるようになります。 import 'package:flutter/material.dart' ; import 'package:provider/provider.dart' ; import 'package:sample/config/route.dart' ; import 'package:sample/viewmodels/event_view_models.dart' ; void main () { runApp ( MyApp ()); } class MyApp extends StatelessWidget { @override Widget build ( BuildContext context) { return MultiProvider ( child : MaterialApp ( routes : routes, ), providers : [ ChangeNotifierProvider (create : (context) => EventViewModel ()), ], ); } } 完成! 取得ボタンを押すと、APIからデータを取得し表示します。 実際にリプレイスしてみて メリット 開発工数の削減 Flutterで開発すると、SwiftとKotlinの別々で書いていたときよりも 体感4割ほど工数が削減できました。ただ、全て共通で同じ実装をできるかと言われるとそうでなく、OSに依存する機能などは個別に実装する必要があるため、その部分に関しては工数がかかってしまいます。 また、Flutterの大きな特徴の一つである ホットリロード機能 がとても便利で、開発中のデバッグにかかる時間がかなり削減されました。 未経験でも開発が容易 今回のリプレイス作業は、自分以外のチームメンバーのほとんどがFlutter未経験者でしたが、全員スムーズに開発をすることができました。公式ドキュメントが充実していて、UI部分に関してもSwiftやKotlinよりも簡単に実装ができるため、Flutter未経験でも簡単にモバイルアプリを開発することができます。 UIの実装が簡単 こちらもFlutterの大きな特徴ですが、UI部分は全てWidgetで構成されており、マテリアルデザインなどといった、様々なデザインが備わっています。そのため、ある程度のデザインであれば公式のライブラリを使用することで、簡単に実装をすることが可能です。 コードレビューが楽 SwfitやAndroidではそれぞれStoryBoardとレイアウトファイル(xmlファイル)で実装していたため、コードレビューの際に変更箇所の差分が分かりにくく、苦戦していました。Flutterに移行したことで、変更箇所が分かりやすく、コードレビューが楽になりました。 デメリット OSに依存する機能などは個別に実装が必要 OS依存の機能を使わない場合に関しては、特に問題ありません。通知機能やAppleサインインなどのOS依存の機能を実装する場合は、その部分に関する知識が必要になってきます。 また、リリース時もiOSとAndroidで証明書が異なるため、別々に管理することが必要となります。 パフォーマンスを考慮した設計 さまざまなWidgetを組み合わせて簡単にUI部分を実装することは可能ですが、何も気にせずに実装を進めていくと、ネイティブよりもパフォーマンスが低下し、アプリ実行時に全体的にもっさりした感じになります。そのため、複雑なUIなどの場合にはパフォーマンスを考慮して設計をしなければいけません。 開発を行ったチームメンバーの感想 モバイルアプリ開発未経験にも関わらず、リプレイス作業に携わってもらったチームメンバーから、以下のような感想を貰いました。 環境構築がそこまで複雑ではないのですぐに開発をすることができた UI部分はWidgetがHTMLみたいで直感的で書きやすい UIパーツを組み上げる感覚で、思ったほど苦労せずに実装できた Flutterの進歩が早い 開発がスムーズ進められた(デバッグツールが使いやすい、ホットリロード機能最強!など) メリットでもあるUI部分に関しては、直感的で未経験者でも開発がしやすいとのことです。 Flutter移行中に起きた問題とその対応 Flutterに移行する際にさまざまな問題等に直面したので、その内容と解決方法、注意点などをいくつかご紹介します。 UI設計をあらかじめした方が良い 今回は画面単位で担当者を割り振ったため、コンポーネント化出来るWidgetがコンポーネント化されていなかったりと煩雑になってしまいました。他にもContainer Widgetを使う実装があったので、パフォーマンスなども考慮して適切なWidgetを使うことをおすすめします。 iOSのバージョンアップデートには事前に対応しておくべき 今回iOS側には Universal Links 機能を実装していたのですが、アプリのリリース前にiOSのバージョンがiOS 14に上がったことで Associated Domains 周りの仕様が若干変更されました。そのため、サーバサイド側の設定変更を余儀なくされました。 iOSの次バージョンがリリースされる3ヶ月ほど前の6,7月頃にはBeta版がリリースされるので、事前にBeta版を使ってあらかじめ対応するべきでした。 審査に落ちてしまった iOS側の話が続きますが、いざリリース申請に出したところ、 App Store Reviewガイドラインの5.1.1(xi) に引っ掛かっている理由のため、リジェクトにされました。 ガイドラインに引っ掛かった原因は、アプリ内に下記のようなRSSから記事を取得して表示する機能を実装していたものの、その中に「新型コロナウイルス」に関する記事が表示されていたため、リジェクトされていました。 そのため、サーバサイド側でRSSから記事を取得する際、タイトルに「新型コロナウイルス」に関する単語が入っている場合は表示しないよう、一時的に対応しました。その後、もう一度審査に出したところ、何の問題もなく審査を通過しました。 外部からの取得したデータを表示する場合などは、念のため気をつけましょう。 リリース周りが大変 iOSとAndroidではデメリットのひとつ目にも記載していますが、リリースまでの手順が異なるため、OSごとに対応が必要となってきます。ビルドからデプロイまで全て手動で行うのは大変でしたが、現在ではFlutterなどモバイルに特化したCI/CDツールである Codemagic の導入を検討しています。 終わりに 今回、SwiftとKotlinで実装されたアプリをFlutterに移行した話をご紹介しました。チームメンバーのほとんどがFlutter未経験にもかかわらず、スムーズに移行を進めることができました。 この移行プロジェクトは自分からチームリーダーに提案をしたものですが、快く承諾していただきました。メンバーが提案しやすい環境があること、感謝しています。 未経験者でも、簡単にモバイルアプリ開発をすることができます。Flutterを触ったことのない方は是非、Flutterを触ってモバイルアプリ開発をしてみてください。
アバター