TECH PLAY

株式会社メドレー

株式会社メドレー の技術ブログ

1363

こんにちは、第一開発グループの矢野です。ジョブメドレー開発エンジニアとして、主にバックエンドを担当しています。 直近では、ジョブメドレーが先月リリースした 「動画選考」 機能の開発プロジェクトに携わっており、動画ファイルのアップロード/配信環境の設計・実装を行っていました。 今回のブログでは、この「動画選考」機能の開発に利用した AWS Elemental MediaConvert サービスと、 hls.js という OSS ライブラリについて紹介したいと思います。 ジョブメドレーの「動画選考」機能 はじめに、今回リリースした「動画選考」機能について概要を紹介します。 新型コロナウイルス感染拡大によって、対面での面接に不安を感じたり、公共交通機関の利用が難しくなったりすることにより、満足な転職活動ができなくなっている方もいらっしゃるかと思います。 このような課題を解決するために、ジョブメドレーではリアルタイムにオンラインで面接を行う「WEB 面接」と、事業者があらかじめ設定した質問に対して応募者が動画で回答を送る「動画選考」の 2 つの機能を提供開始いたしました。 ref. WEB 面接・動画選考機能のリリースのお知らせ 動画選考(動画面接)は、近年増加傾向にあるオンライン選考の一種です。一般的に、求職者 / 就活生が PC ・スマートフォン等のカメラで、予め用意された設問に応じて動画を撮影し、企業に送ることで選考を行います。 ref. WEB 面接・動画選考とは? 実施の流れ、使用ツール、マナー、注意点などを徹底解説! 私たちジョブメドレーの動画選考では、事業所があらかじめ設定した質問に対して、求職者が回答動画を提出することができます。事業所も求職者も、動画で質問・回答を送ることで、書類だけでは伝わらない雰囲気や強みを相手に伝えることができます。 WEB 面接・動画選考機能のリリースのお知らせ 動画配信サービスの設計ポイント Web アプリでこのような動画配信サービスを開発する場合、「ユーザによる動画アップロード環境」と「ユーザへの動画の配信・再生環境」を提供する必要があります。 ジョブメドレーで扱う動画は一般公開されるものではなく、公開条件も複雑です。 よって今回は、この「動画アップロード/配信環境」を自サービス内に構築する方針をとり、以下のような動画まわりの設計ポイントについて検討・技術選定を行うことにしました。 (もちろん、要件によっては YouTube や、法人向け動画配信プラットフォームを契約した方が手軽な場合もあるかと思います) 動画の録画・撮影 サポートしたい動画ファイルのフォーマットをどうするか Web アプリ内に録画機能を設けるか 動画のアップロード(ストレージ) 動画ファイルのバリデーションで「動画ファイルの解析」を行うか 動画ファイルのアップロード先(ストレージ)をどこにするか 動画のエンコード 動画ファイルのエンコード形式(H.264、HLS 等)をどうするか 非同期エンコードの場合、ステータス検知・エラーハンドリングをどうするか 動画の配信(ダウンロード) 配信形式(ダウンロード/ストリーミング)をどうするか 暗号化をする場合、復号をどのように行うか 動画ファイルの公開方法(アクセス制限)をどうするか 動画の再生 Web ページ上で再生させるのか、その場合の表示・再生制御をどうするか ブラウザサポートをどこまでにするか、非対応・エラー時の制御をどうするか 今回は、上記の太字で記載した 「動画のエンコード」に MediaConvert を、 「動画の再生」に hls.js をそれぞれ採用しています。 各項の詳細は省きますが、全体を通して大まかに、以下のフローで「動画アップロード → エンコード(変換)→ 配信・再生」を実現することにしました。 ブラウザから Ajax で動画を S3 へアップロードする MediaConvert が動画を HLS 形式にエンコード(変換)する ブラウザで hls.js を使い動画を CloudFront からストリーミング形式で受信、再生する 今回はこの「動画アップロード → エンコード(変換)→ 配信・再生」に焦点を絞り、MediaConvert と hls.js をどのように使ったのかを紹介します。 MediaConvert による HLS エンコード AWS Elemental MediaConvert は、S3 との親和性が高いファイルベースの動画変換サービスです。自前で ffmpeg などを使って動画エンコードサーバを構築・管理することなく、スケーラブルな動画変換処理を手軽にシステムに組み込むことができます。 ref. AWS Elemental MediaConvert 料金は出力する動画の再生時間に応じた従量課金です。AWS コンソールから GUI ベースでエンコード設定を作成したり、ジョブ(エンコード処理)を登録することができます。 また、他 AWS サービス同様に API が提供されており、AWS CLI や各言語の SDK を使ってプログラムからエンコード処理を登録することができ、システム連携も容易です。 # CLI でエンコードジョブを登録する例 $ aws --endpoint-url https://abcd1234.mediaconvert.region-name-1.amazonaws.com --region region-name-1 mediaconvert create-job --cli-input-json file://~/job.json 上記 CLI コマンドで下のようなエンコード設定を記載した JSON を使いジョブを作成すると、S3 上の動画ファイルをサクッとエンコードしてくれます。ジョブはキューイングされ、内部で並列処理されるため、大量のエンコード要求にも簡単に応じることができます。 { ... "Settings" : { "Inputs" : [ { # 入力元の S3 バケット上の動画ファイル key を指定 "FileInput" : "s3://testcontent/720/example_input_720p.mov" } ], "OutputGroups" : [ { "OutputGroupSettings" : { "FileGroupSettings" : { # 出力先の S3 バケット key を指定 "Destination" : "s3://testbucket/output" } }, # 動画・音声のエンコード設定を指定 # ここで品質レベル毎に振り分けた複数のファイルを出力したり # サムネイル jpg を作成したりすることも可能 "Outputs" : [ { "VideoDescription" : { … }, "AudioDescriptions" : { … } } ] } ] } } ref. AWSCLI を使用した AWSElemental MediaConvertCreateJob の例 エンコードが完了したジョブは、cron + SDK などで API を介して定期チェックする他に、CloudWatch Events によるイベント監視 → Lambda で処理するようなこともできます。 ref. AWS Elemental MediaConvert による CloudWatch イベント の使用 なぜ動画を再エンコードするのか 通常、ユーザからアップロードされる動画ファイルは、既に何らかのコーデックで圧縮され .mp4 や .mov などのコンテナフォーマットに変換されていることが殆どです。 しかし Web ページで <video> タグを使いこれら動画ファイルを再生しようとした場合、 「動画フォーマットにブラウザが非対応だと再生できない」 という環境依存問題があります。 ブラウザと動画フォーマットのサポート表 ref. HTML5 video > Browser support この問題に対応するため、多くの動画配信サービスでは、ユーザの動画を多くの環境で再生可能な MP4 コンテナフォーマット(H.264 + AAC コーデック)などの形式へ「再エンコード」しています。 ジョブメドレーの動画選考では上記目的に加えて、動画閲覧時の回線・端末負荷を抑える 「HTTP ストリーミング形式」 で動画を配信するために、アップロードされた動画を全て HLS 形式 にエンコードしています。 HLS - HTTP Live Streaming 形式 HLS は HTTP Live Streaming の略で、Apple 社の開発した規格です。HTTP ベースのストリーミング通信プロトコルで、細切れにした MP4 動画ファイルを分割ダウンロードさせることで動画のストリーミング配信を実現しています。 HLS 形式にエンコードされた動画は .ts という分割されたメディアファイル群と、 .m3u8 という、メディアファイルの取得先や秒数などを記載したテキストファイルで構成されます。 .m3u8 ファイルの例(マニフェストファイル、プレイリストファイルとも) #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-VERSION:3 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:9.97663, media-0.ts #EXTINF:9.97663, media-1.ts #EXTINF:7.10710, media-2.ts #EXT-X-ENDLIST ref. RFC 8216: HTTP Live Streaming HLS は他のストリーミング形式と比較して、ライブ配信 / VOD どちらにも対応可能なこと、対応ブラウザが多いこと、専用の配信サーバを使わずに配信可能なことなどから、近年の動画配信サービスで広く利用されています。 Web エンジニアの視点から見ても、 HTTP ベースなためキャッシュや HTTPS 暗号化など、既存 Web 技術と掛け合わせることが想像しやすく、扱いやすい印象でした。 MediaConvert の HLS エンコードジョブ設定 実際にプログラムから API 経由で HLS エンコードジョブを登録する際の設定 JSON は、以下のように GUI でジョブテンプレートを作成して確認することができます。 この「 JSON を表示」で、前述した CLI コマンド mediaconvert create-job --cli-input-json に渡せる JSON が表示されます。実装の際にはこちらを参考にしながら、 ユーザーガイド を参照して利用したい機能にあわせた設定を追加していくことをおすすめします。 注意点・つまづいたポイント 利用前に IAM で MediaConvert 用ロールの設定が必要です ステップ 3. IAM 権限の設定 AWS コンソールの Service Quotas > AWS サービス > AWS Elemental MediaConvert から確認できますが、エンコード並行処理の同時実行数上限は 20 になっています AWS ルートアカウント 1 つにつき 1 サービスが割当てられるので、これを増やしたい場合は申請が必要です エンコードジョブをキューイングする「キュー」を作成して、ジョブの登録時に選べるのですが、上記した「並行処理の同時実行数上限」はこの「キュー」毎に均等に振り分けられます 例えば「本番キュー」と「検証キュー」の 2 つのキューを作成した場合、それぞれの並行処理の同時実行数上限は 10 ずつになるので注意してください マニフェスト期間形式(Manifest duration format)に整数(INTEGER)を指定していると、iOS Safari で「動画の実際の再生時間と、再生プレイヤーのシークバーに表示される合計時間にズレが生じる」問題がありました 浮動小数点(FLOATING POINT)に変更することで対応しました、マニフェストファイルに出力される各 .ts ファイルの長さが、浮動小数点 → 整数に変換され切り上げられることでズレが生じているようでした hls.js による HLS 動画の再生制御 MediaConvert により HLS 形式にエンコードされた動画を、Web ブラウザで再生するために必要なのが、hls.js です。 ref. video-dev/hls.js 実は HLS によるストリーミング配信は、現状 Safari など限られたブラウザでしかネイティブでサポートされていません。 ref. https://caniuse.com/http-live-streaming この HLS 動画を Safari 以外の Google Chrome や IE11 などの主要ブラウザで再生可能にするため、hls.js が利用されています。内部的には、非対応ブラウザ環境において、ブラウザの MediaSource 拡張 を使って HLS 動画を再生する仕様になっています。 Video.js との比較 似たようなライブラリに Video.js というものもあり、導入を迷ったのですが … Video.js は UI もセットになった「 HLS に対応した再生プレイヤー」ライブラリ HLS 対応以外にも、字幕や章分けなど機能が豊富 hls.js はブラウザ標準の <video> タグで HLS に対応することだけを目的にした「 HLS クライアント」ライブラリ UI などはなく、動画再生プレイヤーはブラウザ標準のまま …と、上記のように hls.js の方がシンプルにやりたいことを実現できるため、今回は hls.js を採用しました。 GitHub のスター数は先発の Video.js の方が多いのですが、hls.js も開発は活発で、日本では グノシー さん、世界的には TED や Twitter でも採用されており、十分実績があるかと思います。 hls.js による実装 基本的には README の Getting Started の通りで実装できます。一部 README のサンプルコードから抜粋して解説すると… var video = document . getElementById ( "video" ); var videoSrc = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" ; if ( Hls . isSupported ()) { var hls = new Hls (); hls . loadSource ( videoSrc ); hls . attachMedia ( video ); hls . on ( Hls . Events . MANIFEST_PARSED , function () { video . play (); }); } 上記 Hls.isSupported() の分岐で、HLS をネイティブサポートしていないブラウザの処理を実装しています。 本来 <video> の src 属性にセットするべき .m3u8 ファイルの URL へ hls.loadSource() でアクセスさせ、クライアントから XHR リクエストを飛ばします。その後 hls.attachMedia() でインスタンスを DOM 上の <video> タグに紐づけています。 else if ( video . canPlayType ( 'application/vnd.apple.mpegurl' )) { video . src = videoSrc ; video . addEventListener ( 'loadedmetadata' , function () { video . play (); }); } 上記の分岐が iOS Safari など、HLS 動画をネイティブサポートしているブラウザ向けの処理です。単純に .m3u8 への URL を <video> タグの src へ付与しているだけですね。 (サンプルコードでは、マニフェストファイルのロード後に自動再生させるようになっているようです) 注意点・つまづいたポイント hls.js クライアントが取得する HLS 動画ファイル群は、CORS ヘッダで GET リクエストを許可された環境に設置する必要があります .m3u8 マニフェストファイルをアプリの API などから返却する場合、Content-Type を application/x-mpegURL にして渡す必要があります iOS Safari などの hls.js 非対応ブラウザ向けの実装を意識する必要があります hls.js による制御が複雑になるケースでは、同じような制御を hls.js 非対応ブラウザ向けに実装できるか?をイメージできないと手戻りが発生しそうです この他、フロントエンドでは <video> タグのブラウザ毎の挙動や、表示の違いに時間がかかりました。(ある程度予想はしていましたが、やはりメディアの取り扱いは難しい…) hls.js 自体は導入も手軽で、サクッと HLS 動画のマルチブラウザ対応が実現でき、とても使いやすかったです。@types も存在するので、TypeScript 環境でも難なく実装できました。 SSR や HLS + AES-128 の再生にも対応しているので、興味のある方は一度 公式ドキュメント を確認してみてください。 おわりに 従来、動画配信サービスを構築する場合、ffmpeg を載せたエンコードサーバや、ストリーミング配信サーバを別建てして、負荷に応じてスケールさせて…のような設計が必要だったかと思います。 今回、MediaConvert をはじめとした AWS サービスと hls.js を利用することで、手軽に、スケーラブルな動画エンコード/HTTP ストリーミング配信環境を構築することができました。 ジョブメドレーの動画選考はまだリリースしたばかりですので、今後反響を見ながら、さらなる改善を重ねていけたらと思います。最後までお読みいただきありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 10 月 23 日に WEB 面接・動画選考をリリースしました。 job-medley.com WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。 リリースは 2 つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。 WEB 面接概要 WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。 専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。 サービス選定 開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。 SkyWay とは WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。 2020 年 11 月時点で、JavaScript SDK、iOS SDK、Android SDK が提供されています。 シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です 使用上限つきの無料プランもあります NTT コミュニケーションズが開発しています webrtc.ecl.ntt.com WEB 面接の対応ブラウザバージョン(2020 年 11 月時点) プラットフォーム 対応バージョン PC: Google Chrome バージョン 84 以上 PC: Microsoft Edge バージョン 84 以上 iOS: Safari iOS12 以上 Android: Google Chrome バージョン 85 以上 Andoid9 以上 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。 webrtc.ecl.ntt.com SkyWay の接続モデル SkyWay でビデオ通話を実装する場合、2 種類の接続モデルから選びます。 SkyWay 電話モデル 電話のように 1 対 1 でのビデオ通話を想定したモデルです。 Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。 接続するためには、相手の PeerID が必要になります。 SkyWay ルームモデル 同一ルーム内の全ての Peer でビデオ通話するモデルです。 ルーム名を使用して参加します。相手の PeerID を知る必要はありません。 ルーム名は API キー毎に独立しています。 ルームの接続タイプはフルメッシュか SFU の 2 種類から選べます。 参加者全員へのチャットなどのデータ送信もできます。 ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。 ルームの通信タイプ ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。 フルメッシュ 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。 SFU SFU の場合、上りの接続は 1 本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。 画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90 通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。 peer . joinRoom ( roomName , { mode: "mesh か sfu を指定" , stream: mediaStream }); webrtc.ecl.ntt.com ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。 実装イメージ const peer = new Peer ( peer_id , { key: api_key , }); peer . once ( "open" , () => { room . once ( "open" , () => { // ルーム参加後に発生するイベント }); room . on ( "peerJoin" , ( peerId : string ) => { // ルームに誰か参加した場合に発生するイベント }); room . on ( "stream" , ( stream : RoomStream ) => { // stream を受けた場合に発生するイベント }); room . on ( "data" , ({ src , data }) => { // data を受けた場合に発生するイベント }); }); peer . on ( "error" , ( error : Error ) => { // エラー発生時に発生するイベント }); peer . joinRoom ( roomName , { mode: "mesh" , stream: mediaStream }); room . close (); peer . disconnect (); Peer を作成し peer.joinRoom() でルームに参加 room.stream イベントで他の参加者の stream を受け取る room.data() でチャットなど、データ送信もできる room.close() でルームから退出 が SkyWay の JavaScript SDK を使用した基本的な実装になります。 この実装に navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す steam イベントで受け取った stream を video で再生する を追加すれば、オンラインでのビデオ通話が可能になります。 スマートフォン対応 PC と同様のコードでほぼ動作しましたが、iOS や Android 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。 タブを移動すると映像が映らない 発生した問題 iOS12、iOS13 などで 複数のタブをひらいた場合に、映像の取得ができなくなる スクリーンロックからの復帰時に、映像が取得できなくなる という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。 対応 この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い 取得済の stream の track を stop する stream を取り直す SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える 以上の実装により、対応しました。 __ document . addEventListener ( "visibilitychange" , async () => { if ( document . visibilityState !== "visible" ) { return ; } localMedia . getTracks (). forEach (( track : MediaStreamTrack ) => { track . stop (); }); const replaceStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); room . replaceStream ( replaceStream ); }); イヤホンの操作で映像が止まる 発生した問題 iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。 対応 それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが video : mute にして映像を再生 audio : 音声を再生 と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。 制約設定 getUserMedia で取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。 設定例: navigator . mediaDevices . getUserMedia ({ audio: true , video: true }); ↓ navigator . mediaDevices . getUserMedia ({ audio: true , video: { frameRate: 15 } }); 制約が設定可能かどうかの確認: getSupportedConstraints() で、対応している制約名を取得することができます。 const supportedConstraints = navigator . mediaDevices . getSupportedConstraints (); 使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。 例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを 20 に抑え、320x320 の解像度に制限することができます。 const options = { facingMode: "user" , frameRate: 20 , width: 320 , height: 320 }; 指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。 動作検証中に、機種依存と思われる挙動をした例を紹介します。 オプション 挙動 frameRate 指定すると一部 Android 端末で以下の挙動をした。 1. Android 端末で面接に参加する 2. iOS Safari で参加する 3. Android 側の映像と音声が Safari に送られない Safari で先に参加する場合には問題がない。 解像度指定 例: width: 320, height: 320 frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。 解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。 制約設定については、現在も調整中です。 参考情報 SkyWay Conference conf.webrtc.ecl.ntt.com github.com SkyWay のルーム機能を使用したデモ環境です。 GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。 開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。 まとめ SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。 スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。 メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう! www.medley.jp
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 https://www.medley.jp/jobs/
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、React を使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。 リニューアルの背景 社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。 前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。 そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。 検証期間も経て、今回のリニューアルにあわせて新規に作成する API は、GraphQL によって実装することを決めました。 型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていれば API の仕様を簡単に把握できる、等がメリットとして感じられました。 特に、GraphQL が持つ型システムが、TypeScript、Apollo、GraphQL Code Generator のライブラリを組み合わせることで、API に渡すパラメータや、レスポンスにも型が適用され、GraphQL スキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。 フロントエンドの技術的なリニューアル内容 今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Client を用いた状態管理、テストコード実装における Tips 等をそれぞれ部分的にご紹介します。 採用したフレームワークと主要ライブラリ 採用ライブラリ 説明 Next.js React 用のフレームワーク(ボイラープレート) TypeScript JavaScript のスーパーセットで、静的型付け言語 React UI を構築するためのライブラリ(バージョン 16.8.0 でリリースされた hooks を全面的に使用) Apollo Client GraphQL API のクライアントで、アプリケーション全体の状態管理を実施 GraphQL Code Generator GraphQL スキーマから定義ファイル(型、カスタム hooks 等)を生成 emotion + Styled System CSS in JS として利用 formik + yup フォームのビルダー + バリデーター Jest + React Testing Library テストコード実装用のツール群 ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング TypeScript 今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。 そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCode をはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮して TypeScript の採用を決めました。また、他のプロジェクトでも既に TypeScript は部分的に利用し始めていた事情もあり、逆に TypeScript を採用しない、という選択肢はあまり考えられませんでした。 React UI を構築するためのライブラリ/フレームワークは React を採用しました。こちらも、弊社では別プロジェクトで React を既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしても TypeScript と GraphQL との相性の良さで、React が優勢でした。 特に、React の場合は、GraphQL スキーマをベースに、GraphQL Code Generator によって型定義ファイルだけではなく、GraphQL API とのやり取りに使えるカスタム hooks も生成して利用できるという点が、大きな利点として考えられました。 Next.js フロントエンド開発環境を素早く構築するため、ボイラープレートとして Next.js を採用しました。 Next.js の具体的な採用ポイントとしては、主に次の3点です。 webpack における、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる 必要があれば、next.config.js で設定を拡張できる CRA(Create React App)とは異なり、拡張性に優れている pages 配下に置く React Component のディレクトリ構成が、自動的にルーティングとして定義される ルーティングに関する設計作業が不要になる 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる React Component の分類 component は大きく2つに分類し、 src/components/app/ と src/components/ui/ それぞれのディレクトリに component を置いています。分類は以下の基準で行ないました。 app : 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的な component ui : 本アプリケーション外でも使用可能な、再利用性が高く、抽象的な component 社内向けシステムではあるものの、Material-UI や Ant Design 等をはじめとする、外部の UI ライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。 app 配下と ui 配下、どちらの component も基本的には コロケーション の考え方でファイルを構成しています。 一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。 この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。 src/ components/ app/ partials/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ... screens/ ${ component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx ${子 component 名} / apollo.cache.ts apollo.query.graphql index.tsx index.test.tsx validation.ts src/components/app ディレクトリ配下でさらに、 partials と screens のディレクトリで component を分けています。 screens には、Next.js で route として扱われる src/pages 配下の component から import される component が配置されています。 画面のバリエーションが増える度に、この screens にファイルが追加されていきます。 partials には、app 配下で複数の component から利用される component(画面をまたいで共有されるもの等)を配置しています。 screens と partials それぞれ直下の component で、必要であれば適宜、component を分割して子 component を持つ構成にしています。 apollo.cache.ts と apollo.query.graphql については後述の状態管理の話でご紹介します。 状態管理 アプリケーションの状態管理については、グローバルにアクセスできる状態の管理には Apollo Client の InMemoryCache による cache 機構で行い、特定の component 内に閉じている局所的な状態の管理には useState 等の React Hooks を使って行っています。 状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべく useState 等の hooks を用いた local state だけで済ませられないかどうかを検討します。 例えば、クリックするとドロップダウンリストが表示されるセレクトボックスの component で、ドロップダウンリストの表示状態をその component 内だけで扱いたいのであれば useState を用いた local state で十分であると考えられます。 親子関係ではない component 同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Client の cache を利用します。 Apollo Client Apollo に関連するファイルの構成については以下の通りです。 src/ apollo/ cache.ts client.ts types.ts withApollo.ts cache.ts : Apollo における local state の initialState と resolver を全画面分このファイルでまとめて、最終的に Next.js の src/pages/_app.tsx に渡るようにする component 固有の local state に関する initialState および state の updater となる resolver は component 毎の apollo.cache.ts にて、別途定義 client.ts : Apollo Client のインスタンスを生成するファイル types.ts : Apollo 関連の型定義ファイル withApollo.ts : Apllo Client の <ApolloProvider /> でラップして返す Higher-Order Compoents(HOC) 実装については割愛しますが、client.ts と withApollo.ts に関しては、Next.js の example( with-apollo )等を参考にしました。 画面固有の Apollo の状態管理に関わるファイルは src/components/**/${component 名}/ 配下に置いています。 こちらもコロケーションの考え方で、component に関わる状態管理は該当の component と同じ場所に置くことを意識しています。 src/ components/ app/ ${ component 名} / apollo.cache.ts apollo.query.graphql apollo.schema.graphql apollo.cache.ts : component 固有の Apollo における local state の initialState および resolver を定義するファイル apollo.query.graphql : クエリを定義するファイル apollo.schema.graphql : local state の GraphQL スキーマを定義ファイル ファイルの命名について、 ディレクトリ階層をできるだけ深くしたくない ので、 apollo 等によるディレクトリは設けていませんが、Apollo 関連のファイル群として認識できるよう、ファイル名に apollo. のプレフィックスをつけて命名しています。 Query と Mutation の実行について GraphQL Code Generator のプラグイン TypeScript React Apollo をインストールして、hooks を生成する設定にした上で、component 毎にそれぞれ GraphQL のスキーマとクエリが記述された .graphql ファイルをもとに、GraphQL Code Generator が生成するカスタム hooks を利用します。 こちらのカスタム hooks を React Component で利用することで、Apollo Client 経由で GraphQL API とローカルの Apollo cache に接続して、データのやり取りを行うことができます。 Query Query の hooks は2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。 API 実行タイミング useQuery Component が render されたらクエリ実行 useLazyQuery 任意のイベントをトリガーにしてクエリ実行 use***Query 通常であれば useQuery でクエリの結果を render しますが、GraphQL Code Generator を利用する場合は、それぞれのクエリをラップしたカスタム hooks が生成されるので、 useQuery , useLazyQuery をそのまま使うことはありません。 query AllPosts { allPosts { id title rating } } ↑ のようなクエリを用意すると src/__generated__/graphql.tsx に対して、次のようなカスタム hooks が型と一緒に生成される設定にしています。 // Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0 の場合の例 export function useAllPostsQuery ( baseOptions ?: ApolloReactHooks . QueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } export function useAllPostsLazyQuery ( baseOptions ?: ApolloReactHooks . LazyQueryHookOptions & lt ; AllPostsQuery , AllPostsQueryVariables >) { return ApolloReactHooks . useLazyQuery & lt ; AllPostsQuery , AllPostsQueryVariables >( AllPostsDocument , baseOptions ); } React Component では生成されたカスタム hooks を次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。 const { data , loading , error } = useAllPostsQuery (); Mutation データの書き込みは useMutation で行います。 Query 同様、 GraphQL Code Generator によって生成されたカスタム hooks use***Mutation を使っています。 cache の更新 Mutation が複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Client の cache は自動更新されず、Mutation の結果が自動的に render されません。 このような場合でも、 useMutation の update option を使えば、 cache オブジェクトを引数に取れる関数を設定できるので、この関数内で直接 cache を更新できます。 また、 update の代わりに refetchQueries の option を使って、任意の Query を実行して、シンプルに cache を更新することもできます。 但し、この方法だと Network 通信によるオーバーヘッドが発生します。 このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したい Query があるような場合には、この refetchQueries が有効です。 local state の管理 ここからは特定の component の状態管理を local state を使ってどのように管理しているかを、ご説明していきます。 @client を使った Query Next.js のプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type Home { currentPostId: Int! } extend type Query { home: Home } クエリ: # src / components / app / Home / apollo . query . graphql query HomeCurrentPostId { home @ client { currentPostId } } キャッシュの初期値: // src/components/app/Home/apollo.cache.ts export const cache = { __typename: 'Home' , currentPostId: 0 , ..., }; // src/apollo/cache.ts const caches = { ..., home: home . cache , }; export { ..., caches , }; // src/pages/_app.tsx export const cache = new InMemoryCache (); ... const client = new ApolloClient ({ link , cache: cache . restore ( initialState || {}), resolvers , connectToDevTools: true , }); cache . writeData ({ data: caches }); GraphQL クエリとスキーマが定義されていれば GraphQL Code Generator が use***Query のコードを生成する設定にしています。 ローカルデータの場合、クエリで @client ディレクティブをつけてローカルデータであることを明示します。 @client を使った Mutation local state の更新を GraphQL の Mutation として行う場合の例としては、次の通りです。 スキーマ: # src / components / app / Home / apollo . schema . graphql type UpdateCurrentPostId { currentPostId: Int! } extend type Mutation { updateCurrentPostId(id: Int!): UpdateCurrentPostId } クエリ: # src / components / app / Home / apollo . query . graphql mutation UpdateCurrentPostId ( $id : Int !) { updateCurrentPostId ( id : $id ) @ client { currentPostId @ client } } resolver: // src/components/app/Home/apollo.cache.ts const updateCurrentPostId : MutationResolvers [ "updateCurrentPostId" ] = ( _ , args , { cache } ) => { cache . writeData ({ data: { home: { __typename: "Home" , currentPostId: args . id , }, }, }); return null ; }; export const Mutation = { updateCurrentPostId , }; Query 同様に @client ディレクティブをつけてローカルデータであることを明示します。 実際の Mutation の処理自体は resolver の中に cache.writeData() を使って記述します。 Mutation の命名は、 動詞+名詞の形式で可能な限り意味のある具体的な名前をつける ことを意識しています。 Apollo を使った開発を便利にしてくれるツール Apollo Client を使って開発する際は、ローカルの Apollo cache の状態や、クエリを試しに実行するためのツールとして、Google Chrome の拡張機能 Apollo Client Developer Tools が非常に便利です。 こちらの拡張機能を Chrome にインストールすると、Apollo Client を使って GraphQL API にアクセスするサイトに遷移した状態で Chrome Dev Tools を開くと Apollo のタブが表示されます。そこでクエリの実行や、API 仕様の確認、ローカルの Apollo cache の確認等を行うことができます。 GraphQL 関連のテストコードについて Apollo Client を使った React Component の開発で、Query および Mutation 実行のテストを実施するには、テストフレームワークの Jest、react-testing-library とあわせて、Apollo 公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。 クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProvider の代わりに MockedProvider でテスト対象の component をラップすることで、API サーバーや Network 環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。 その仕組みと react-testing-library を使って、component で render される UI 上の操作をトリガーにして実行される、クエリのテストを行うことができます。 Query だけではなく Mutation もモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。 実際にアプリケーションを動かして、テスト対象の component を render し、Query に渡される variables やレスポンスの値を Console に出力し、ブラウザの Dev Tools 上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。 AutoMockedProvider の作成 そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQL スキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれる AutoMockedProvider を、 こちらの記事 を参考にして作成しました。 MockedProvider の代わりに、AutoMockedProvider を用いてテスト対象の Component をラップすることで、MockedProvider を使ってテストしていた内容と同じテストが実施できます。 MockedProvider を使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。 (紹介先の記事では、 graphql-tools の makeExecutableSchema() に渡す schemaSDL が json ファイルで定義されていますが、 graphql-tag のライブラリを併用すれば、graphql ファイルでも同様に schemaSDL として適用することも可能です) リニューアルを振り返って 今回のリニューアルでは、GraphQL、TypeScript、React をセットで採用したことにより、フロント側では GraphQL Code Generator を使って、あらかじめ用意しておいた GraphQL スキーマから、TypeScript の型だけではなく、React の Hooks 関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。 GraphQL API のクライアントで、アプリケーション全体の状態管理を行う Apollo Client の cache 機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScript と GraphQL の型システムの恩恵をフルに受け、Next.js のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
初めまして。CLINICS の開発を担当しているエンジニアの平山です。 (同姓ですが CTO ではございません) CLINICS の開発は「スモールチーム制」をとっておりまして、現在そのうちの1つをチームリードしています。 前職は長らく SIer に勤めていました。去年の 12 月にメドレーに JOIN して、間も無く1年経とうとしている。。と思うと、あっという間だったなぁという印象です。 さて、本日はメドレーで隔週開催している社内勉強会(TechLunch)において発表した内容についてご紹介させて頂ければと思います。 はじめに 「技術を使うための技術」というテーマとなりますが、プロダクト開発をしていく上では欠かせない素養と考えています。メドレーに所属しているエンジニアの1人として、どのように日々課題と向き合っているのか。当テーマを通してお伝えできればと思います。 また、この考え方自体は「医療というテーマ」や「事業の背景(ベンチャー・ SIer)」を問わず必要とされる場面があるかもしれません。(自身も前職では様々な場面でお世話になりました…) 即効性のあるものではありませんが、じわじわ効いてくる内容ではないかと思います。よろしければお付き合いください。 本題 「技術を使うための技術」 みなさんはこの言葉から、何を思い浮かべるでしょうか。筆者が試しに Google で検索した際上位にヒットしたのは AI エンジニア IoT といった結果でした。なるほど。少し雑に解釈すると「技術(アルゴリズム等)を使うための技術(機械学習、家電等)」といった感じなのでしょうか。(この結果を拾ってきたというのも、いい意味で Google すごいなって感じました) 筆者が今回のテーマとして指しているのは、下記となります。 ロジカルシンキング 推論 これらの 思考を整理する「手段」 とエンジニアの武器である 技術という「手段」 をかけ合わせることで、大きなテーマである「医療」に向き合っています。 手段を目的にしてはならない 先に挙げたとおり「思考の整理」も「技術」も 「手段」 に過ぎません。これらを用いて、適切な一手を指していく為には「目的」に対する解像度を高く描く必要があります。 筆者の発表から抜粋した「技術を使うための技術」の要素をイメージした図 整理力 目的を達成する為に必要な「情報」を取捨選択するための要素。 SaaS ✖️ toB の世界においては「プロダクトが解決すべき課題か否かを業務の本質を踏まえて取捨選択する」と言い換えられるかもしれません。 業務知識 目的の「解像度」を高めるための要素。 CLINICS においては、医療情報を扱う上での規制(3 省 2 ガイドライン)や、医療機関(医師・医事・診療科の特性)の業務、診療報酬についての知識、法改正、レセコン(ORCA)… と様々あります。 技術知識 目的を達成する為に必要な「指し手」を選択するための要素。 (エンジニアにとっては説明するまでもない内容であると思いますが) ここが欠けてしまうと絵に描いた餅で終わってしまいます。メドレーのエンジニアにおいても日々研鑽し、プロダクトに対してコミットを続けています。 行動力 目的を達成するための「推進力」を高めるための要素。 各種知識と整理した情報を推進力に変えていく為には、その時の状況に応じた動きをする必要があります。ステークホルダーとの調整は当然ですが、組織内連携といった「横の動き」も必要です。 想像力 これまで挙げたそれぞれの手段を適切に利用していくための要素としての土台が「想像力」であると考えています。 課題(Issue)への取り組み例 番号 概要 ① Issue に取り組む際に「本当に解決すべきこと」についての想像を働かせます。Issue に記載されていることが 本当にプロダクトとして解決すべきことなのか を含めて考えます。これまでの運用が必ずしも正しいわけではない。という点がポイントです。 ② ① について「想像のまま」で終わらせてはいけないので、業務知識と照らし合わせて確度を高めていきます。常勤医師やカスタマーサポートにヒアリングしながら、 医療業務としてのあるべき形の解像度を上げていく プロセスです。 ③ ① 及び ② で高めた解像度は言葉の延長線上なので、関係者間の認識のギャップが発生しやすいです。プロトタイプを作成して、視覚・触感レベルでギャップを埋めていくことで、あるべき形に向けて洗練させていきます。 ④ ① 〜 ③ のタイミングを問わず、必要に応じて関係者と相談しながら進めていきます。エンジニアが立てた仮説をデザイナーの目線で評価・ UI/UX 最適化をして頂いたり、大きめの機能については、医療機関にパイロット運用のご協力を仰いだりすることもあります。 ① 及び ② の項に作業のウェイトが偏っているように見えるかと思います。実際、課題を解決する為の半分以上をここに割いています。 理由は「1度作って公開した機能」は、その後1人歩きをして作成者の意図しない方向で利用をされることがあるからです。 そして、利用者がその運用を定着させてしまうと 誤った機能においても「削ぎ落とすことが困難」 です。これは「使われていない機能」よりも直接的な負債といった形でボディブローのように効いてきます。 「どのような使われ方をするか」について想像すること その使われ方が、プロダクトの目指す世界と合っていること エンジニアは技術を形にする上で、常に想像力を働かせて取り組む必要がある。 というのが筆者の持論です。 さいごに 執筆の締めにあたって CTO ブログを見返してみると、大事なことはここに詰まっていました。筆者は前職の SIer 時代に読んだのですが、この記事にすごく共感したのを覚えています。 https://toppa.medley.jp/02.html メドレーでは 医療ヘルスケアの未来を作る という大きな目標、そしてその未来を作る為に解決すべき課題に向かって、今回ご紹介したプロセスや考え方も含め試行錯誤しながら、事業部一丸でプロダクト開発を推進しています。 エンジニアの総合力を発揮して医療ヘルスケアの未来を一緒に作り上げていきたい!という方にお会い出来ることを楽しみにしております。 https://www.medley.jp/jobs/
初めまして。CLINICS の開発を担当しているエンジニアの平山です。 (同姓ですが CTO ではございません) CLINICS の開発は「スモールチーム制」をとっておりまして、現在そのうちの1つをチームリードしています。 前職は長らく SIer に勤めていました。去年の 12 月にメドレーに JOIN して、間も無く1年経とうとしている。。と思うと、あっという間だったなぁという印象です。 さて、本日はメドレーで隔週開催している社内勉強会(TechLunch)において発表した内容についてご紹介させて頂ければと思います。 はじめに 「技術を使うための技術」というテーマとなりますが、プロダクト開発をしていく上では欠かせない素養と考えています。メドレーに所属しているエンジニアの1人として、どのように日々課題と向き合っているのか。当テーマを通してお伝えできればと思います。 また、この考え方自体は「医療というテーマ」や「事業の背景(ベンチャー・ SIer)」を問わず必要とされる場面があるかもしれません。(自身も前職では様々な場面でお世話になりました…) 即効性のあるものではありませんが、じわじわ効いてくる内容ではないかと思います。よろしければお付き合いください。 本題 「技術を使うための技術」 みなさんはこの言葉から、何を思い浮かべるでしょうか。筆者が試しに Google で検索した際上位にヒットしたのは AI エンジニア IoT といった結果でした。なるほど。少し雑に解釈すると「技術(アルゴリズム等)を使うための技術(機械学習、家電等)」といった感じなのでしょうか。(この結果を拾ってきたというのも、いい意味で Google すごいなって感じました) 筆者が今回のテーマとして指しているのは、下記となります。 ロジカルシンキング 推論 これらの 思考を整理する「手段」 とエンジニアの武器である 技術という「手段」 をかけ合わせることで、大きなテーマである「医療」に向き合っています。 手段を目的にしてはならない 先に挙げたとおり「思考の整理」も「技術」も 「手段」 に過ぎません。これらを用いて、適切な一手を指していく為には「目的」に対する解像度を高く描く必要があります。 筆者の発表から抜粋した「技術を使うための技術」の要素をイメージした図 整理力 目的を達成する為に必要な「情報」を取捨選択するための要素。 SaaS ✖️ toB の世界においては「プロダクトが解決すべき課題か否かを業務の本質を踏まえて取捨選択する」と言い換えられるかもしれません。 業務知識 目的の「解像度」を高めるための要素。 CLINICS においては、医療情報を扱う上での規制(3 省 2 ガイドライン)や、医療機関(医師・医事・診療科の特性)の業務、診療報酬についての知識、法改正、レセコン(ORCA)… と様々あります。 技術知識 目的を達成する為に必要な「指し手」を選択するための要素。 (エンジニアにとっては説明するまでもない内容であると思いますが) ここが欠けてしまうと絵に描いた餅で終わってしまいます。メドレーのエンジニアにおいても日々研鑽し、プロダクトに対してコミットを続けています。 行動力 目的を達成するための「推進力」を高めるための要素。 各種知識と整理した情報を推進力に変えていく為には、その時の状況に応じた動きをする必要があります。ステークホルダーとの調整は当然ですが、組織内連携といった「横の動き」も必要です。 想像力 これまで挙げたそれぞれの手段を適切に利用していくための要素としての土台が「想像力」であると考えています。 課題(Issue)への取り組み例 番号 概要 ① Issue に取り組む際に「本当に解決すべきこと」についての想像を働かせます。Issue に記載されていることが 本当にプロダクトとして解決すべきことなのか を含めて考えます。これまでの運用が必ずしも正しいわけではない。という点がポイントです。 ② ① について「想像のまま」で終わらせてはいけないので、業務知識と照らし合わせて確度を高めていきます。常勤医師やカスタマーサポートにヒアリングしながら、 医療業務としてのあるべき形の解像度を上げていく プロセスです。 ③ ① 及び ② で高めた解像度は言葉の延長線上なので、関係者間の認識のギャップが発生しやすいです。プロトタイプを作成して、視覚・触感レベルでギャップを埋めていくことで、あるべき形に向けて洗練させていきます。 ④ ① 〜 ③ のタイミングを問わず、必要に応じて関係者と相談しながら進めていきます。エンジニアが立てた仮説をデザイナーの目線で評価・ UI/UX 最適化をして頂いたり、大きめの機能については、医療機関にパイロット運用のご協力を仰いだりすることもあります。 ① 及び ② の項に作業のウェイトが偏っているように見えるかと思います。実際、課題を解決する為の半分以上をここに割いています。 理由は「1度作って公開した機能」は、その後1人歩きをして作成者の意図しない方向で利用をされることがあるからです。 そして、利用者がその運用を定着させてしまうと 誤った機能においても「削ぎ落とすことが困難」 です。これは「使われていない機能」よりも直接的な負債といった形でボディブローのように効いてきます。 「どのような使われ方をするか」について想像すること その使われ方が、プロダクトの目指す世界と合っていること エンジニアは技術を形にする上で、常に想像力を働かせて取り組む必要がある。 というのが筆者の持論です。 さいごに 執筆の締めにあたって CTO ブログを見返してみると、大事なことはここに詰まっていました。筆者は前職の SIer 時代に読んだのですが、この記事にすごく共感したのを覚えています。 toppa.medley.jp toppa.medley.jp メドレーでは 医療ヘルスケアの未来を作る という大きな目標、そしてその未来を作る為に解決すべき課題に向かって、今回ご紹介したプロセスや考え方も含め試行錯誤しながら、事業部一丸でプロダクト開発を推進しています。 エンジニアの総合力を発揮して医療ヘルスケアの未来を一緒に作り上げていきたい!という方にお会い出来ることを楽しみにしております。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
初めまして。CLINICS の開発を担当しているエンジニアの平山です。 (同姓ですが CTO ではございません) CLINICS の開発は「スモールチーム制」をとっておりまして、現在そのうちの1つをチームリードしています。 前職は長らく SIer に勤めていました。去年の 12 月にメドレーに JOIN して、間も無く1年経とうとしている。。と思うと、あっという間だったなぁという印象です。 さて、本日はメドレーで隔週開催している社内勉強会(TechLunch)において発表した内容についてご紹介させて頂ければと思います。 はじめに 「技術を使うための技術」というテーマとなりますが、プロダクト開発をしていく上では欠かせない素養と考えています。メドレーに所属しているエンジニアの1人として、どのように日々課題と向き合っているのか。当テーマを通してお伝えできればと思います。 また、この考え方自体は「医療というテーマ」や「事業の背景(ベンチャー・ SIer)」を問わず必要とされる場面があるかもしれません。(自身も前職では様々な場面でお世話になりました…) 即効性のあるものではありませんが、じわじわ効いてくる内容ではないかと思います。よろしければお付き合いください。 本題 「技術を使うための技術」 みなさんはこの言葉から、何を思い浮かべるでしょうか。筆者が試しに Google で検索した際上位にヒットしたのは AI エンジニア IoT といった結果でした。なるほど。少し雑に解釈すると「技術(アルゴリズム等)を使うための技術(機械学習、家電等)」といった感じなのでしょうか。(この結果を拾ってきたというのも、いい意味で Google すごいなって感じました) 筆者が今回のテーマとして指しているのは、下記となります。 ロジカルシンキング 推論 これらの 思考を整理する「手段」 とエンジニアの武器である 技術という「手段」 をかけ合わせることで、大きなテーマである「医療」に向き合っています。 手段を目的にしてはならない 先に挙げたとおり「思考の整理」も「技術」も 「手段」 に過ぎません。これらを用いて、適切な一手を指していく為には「目的」に対する解像度を高く描く必要があります。 筆者の発表から抜粋した「技術を使うための技術」の要素をイメージした図 整理力 目的を達成する為に必要な「情報」を取捨選択するための要素。 SaaS ✖️ toB の世界においては「プロダクトが解決すべき課題か否かを業務の本質を踏まえて取捨選択する」と言い換えられるかもしれません。 業務知識 目的の「解像度」を高めるための要素。 CLINICS においては、医療情報を扱う上での規制(3 省 2 ガイドライン)や、医療機関(医師・医事・診療科の特性)の業務、診療報酬についての知識、法改正、レセコン(ORCA)… と様々あります。 技術知識 目的を達成する為に必要な「指し手」を選択するための要素。 (エンジニアにとっては説明するまでもない内容であると思いますが) ここが欠けてしまうと絵に描いた餅で終わってしまいます。メドレーのエンジニアにおいても日々研鑽し、プロダクトに対してコミットを続けています。 行動力 目的を達成するための「推進力」を高めるための要素。 各種知識と整理した情報を推進力に変えていく為には、その時の状況に応じた動きをする必要があります。ステークホルダーとの調整は当然ですが、組織内連携といった「横の動き」も必要です。 想像力 これまで挙げたそれぞれの手段を適切に利用していくための要素としての土台が「想像力」であると考えています。 課題(Issue)への取り組み例 番号 概要 ① Issue に取り組む際に「本当に解決すべきこと」についての想像を働かせます。Issue に記載されていることが 本当にプロダクトとして解決すべきことなのか を含めて考えます。これまでの運用が必ずしも正しいわけではない。という点がポイントです。 ② ① について「想像のまま」で終わらせてはいけないので、業務知識と照らし合わせて確度を高めていきます。常勤医師やカスタマーサポートにヒアリングしながら、 医療業務としてのあるべき形の解像度を上げていく プロセスです。 ③ ① 及び ② で高めた解像度は言葉の延長線上なので、関係者間の認識のギャップが発生しやすいです。プロトタイプを作成して、視覚・触感レベルでギャップを埋めていくことで、あるべき形に向けて洗練させていきます。 ④ ① 〜 ③ のタイミングを問わず、必要に応じて関係者と相談しながら進めていきます。エンジニアが立てた仮説をデザイナーの目線で評価・ UI/UX 最適化をして頂いたり、大きめの機能については、医療機関にパイロット運用のご協力を仰いだりすることもあります。 ① 及び ② の項に作業のウェイトが偏っているように見えるかと思います。実際、課題を解決する為の半分以上をここに割いています。 理由は「1度作って公開した機能」は、その後1人歩きをして作成者の意図しない方向で利用をされることがあるからです。 そして、利用者がその運用を定着させてしまうと 誤った機能においても「削ぎ落とすことが困難」 です。これは「使われていない機能」よりも直接的な負債といった形でボディブローのように効いてきます。 「どのような使われ方をするか」について想像すること その使われ方が、プロダクトの目指す世界と合っていること エンジニアは技術を形にする上で、常に想像力を働かせて取り組む必要がある。 というのが筆者の持論です。 さいごに 執筆の締めにあたって CTO ブログを見返してみると、大事なことはここに詰まっていました。筆者は前職の SIer 時代に読んだのですが、この記事にすごく共感したのを覚えています。 toppa.medley.jp toppa.medley.jp メドレーでは 医療ヘルスケアの未来を作る という大きな目標、そしてその未来を作る為に解決すべき課題に向かって、今回ご紹介したプロセスや考え方も含め試行錯誤しながら、事業部一丸でプロダクト開発を推進しています。 エンジニアの総合力を発揮して医療ヘルスケアの未来を一緒に作り上げていきたい!という方にお会い出来ることを楽しみにしております。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp