株式会社メドレーのエンジニアの笹塚です。 私が開発を担当しているジョブメドレーで、先月 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 のレールにのっかり、型安全な開発環境を手に入れることができました。 我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。 さいごに メドレーではエンジニア・デザイナーを積極募集しています。 「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。 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
こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。 今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。 リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。 また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、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
初めまして。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
初めまして。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
初めまして。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