メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Capybaraとreg-cliを使ってお手軽にビジュアルリグレッションテストを行える環境を整備しました📸

こんにちは、MedPeerの開発を担当している森田です。 今回は私が開発に参画しているMedPeerに元々E2Eテストで利用していたCapybaraと、reg-cliを利用してビジュアルリグレッションテスト(以下VRT)を行える環境を整備したので、それについてご紹介させていただきます。

なぜ、VRTを導入するのか?

MedPeerでは元々System Specを活用したE2Eテストを利用してフロントエンドを含めて品質を担保しておりましたが、デザイン崩れの影響を検知するのは難しく、規模の大きい変更を行う際には手動での画面確認を行っておりました。

しかし、手動での画面確認は検証コストも高く、開発上のボトルネックになりがちであったのと、手動での検証は本来であれば検出されてほしい影響を検知できずにリリースされてしまうこともあり、検証精度を担保しつつスピード感を持った開発を実現する上で課題となっておりました。

そんな中で、VRTを既存のCIに組み込み、デザイン崩れを検知できれば、前述の課題の解決に寄与できるのでは?と思ったのが導入の背景です。

VRTの要件と技術選定

上述の通り、検証精度を担保しつつスピード感を持った開発に寄与するためにVRTを導入するにあたって、必要な要件を以下と考えました。

  • コストを掛けずにVRTを記述・組み込めるようにすること
    • 幅広くVRTを記述する必要があるので、記述のハードルをなるべく下げるために既存の仕組みの延長線上で実現する
  • メインブランチへのマージ前にデザイン崩れの発生に気づき修正できること
    • デザインへの影響がPRのstatusで判断でき、CIを落とすことでマージ前に気づいて対応できるようにする

この要件に合わせて、MedPeerではCapybarareg-cliを使って構築することにしました。

github.com

github.com

すでにSystem Specを使って行なっているE2Eテストで利用しているCapybaraの機能であるCapybara::Session#save_screenshotを使って任意のタイミングでスクリーンショットを取得し、ローカルでも実行できるreg-cliを使って取得したスクリーンショットの差分を検知することで、既存の仕組みを活かしコストを掛けずにCIでデザイン崩れをマージ前に検知できるのではないかと考えました。

実際に構築したVRT基盤の概要

構築したVRT基盤の概要が以下の通りです。

VRT基盤の概要フロー図

まず事前作業としてspec/systems/visual_regression/screenshots/masterに正となる現時点でのスクリーンショットを取得するSystem Specを作成し、メインブランチに配置しておきます。

そして実際にPRが作成された際に、PRのブランチにて追加したSystem Specを実行し、CI上のPRのブランチで取得したスクリーンショットをspec/systems/visual_regression/screenshots/compareに配置します。

そして、reg-cliを使って、それらのディレクトリに配置された同一パス・名称のファイルの差分をチェックし、差分があればCIを失敗させるようにしています。

成功時

CI status(成功時)

失敗時

CI status(失敗時)

VRT基盤の具体的な話

System Spec内でスクリーンショットを取得する

System Spec内でCapybaraを使ってVRT用のスクリーンショットを取得できるように以下のHelperを用意しました。

module VrtScreenshotHelper
  VRT_SCREENSHOT_BASE_PATH = 'spec/system/visual_regression/screenshots'

  def vrt_screenshot(page, path:, full: true)
    return unless screenshot_enabled?

    target = update_master? ? 'master' : 'compare'
    base_path = screenshot_base_path(target: target)
    if full
      save_full_size_screenshot(page, base_path.join(path))
    else
      page.save_screenshot(base_path.join(path))
    end
  end

  private

  def save_full_size_screenshot(page, path)
    original_size = Capybara.current_session.driver.browser.manage.window.size
    resize_window_to_fit_page
    page.save_screenshot(path)
    reset_window_size(original_size.width, original_size.height)
  end

  def screenshot_base_path(target:)
    Rails.root.join(VRT_SCREENSHOT_BASE_PATH, target)
  end

  def screenshot_enabled?
    ENV["VRT_SCREENSHOT_ENABLE"] != "false"
  end

  def update_master?
    ENV["VRT_SCREENSHOT_UPDATE_MASTER"] == "true"
  end

  # NOTE: フルサイズのスクリーンショットを取得するためにウィンドウサイズをページに合わせる
  def resize_window_to_fit_page
    width = Capybara.page.execute_script(<<~JS)
      return window.outerWidth
    JS

    height = Capybara.page.execute_script(<<~JS)
      return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
    JS

    reset_window_size(width, height)
  end

  def reset_window_size(width, height)
    Capybara.current_session.driver.browser.manage.window.resize_to(width, height)
  end
end

Helper内に実装しているvrt_screenshotを使うことで利用者側で以下の設定を行いVRT用のスクリーンショットを取得できるようにしています。

  • スクリーンショットを配置するディレクトリ
    • reg-cliで差分チェックを行うディレクトリspec/system/visual_regression/screenshotsを自動設定
    • 正となる画像を更新する場合には自動的にmasterに配置する
  • フルサイズでのスクリーンショットの取得
    • スクリーンショット取得時に画面サイズをフルサイズに変更してからページ全体のスクリーンショットを取得する

このヘルパーを使って利用する側は以下のような形でフルサイズのスクリーンショットを差分チェックするディレクトリ(spec/system/visual_regression/screenshots/compare/service_name/root_page.png or spec/system/visual_regression/screenshots/master/service_name/root_page.png)に自動配置できるようにしました 📸

require 'support/vrt_screenshot_helper'

RSpec.describe 'Service name', :js do
  include VrtScreenshotHelper

  it 'sample vrt' do
    visit root_path
    expect(page).to have_css '.sample-selector' # NOTE: ページが一定表示されるのを待つ
    vrt_screenshot(page, path: "service_name/root_page.png")
  end
end

reg-cliでスクリーンショットの差分をチェックする

reg-cliを使った以下のスクリプトをpackage.jsonに設定し事前作業で取得していた正となる画像とCI(またはローカルでも)上で取得した画像を比較して5%以上の差分があった場合にエラーにするようにしています🕵️

{
  "scripts": {
    "test:vrt": "reg-cli spec/system/visual_regression/screenshots/compare spec/system/visual_regression/screenshots/master spec/system/visual_regression/screenshots/diff -R spec/system/visual_regression/screenshots/diff/report.html -J spec/system/visual_regression/screenshots/diff/reg.json -T 0.05",

実行しているスクリプトの詳細は以下の通りです。指定できるオプションの詳細は公式のREADMEをご確認いただければと思います。

$ yarn run reg-cli \
  spec/system/visual_regression/screenshots/compare \ # チェック対象のスクリーンショットの配置先
  spec/system/visual_regression/screenshots/master \  # 正とするスクリーンショットの配置先
  spec/system/visual_regression/screenshots/diff \    # 差分を表す画像の出力先
  -R spec/system/visual_regression/screenshots/diff/report.html \ # 差分レポートの出力先
  -J spec/system/visual_regression/screenshots/diff/reg.json \    # 差分レポート(JSON)の出力先
  -T 0.05 # 許容する差分の閾値(%)

実際の実行結果は以下のように確認することができ、差分があった際にはexit code 1.となりCIが失敗します🍎

yarn run v1.22.22
$ reg-cli spec/system/visual_regression/screenshots/compare spec/system/visual_regression/screenshots/master spec/system/visual_regression/screenshots/diff -R spec/system/visual_regression/screenshots/diff/report.html -J spec/system/visual_regression/screenshots/diff/reg.json -T 0.05
✔ pass    spec/system/visual_regression/screenshots/compare/service_name/root_page.png
✘ change  spec/system/visual_regression/screenshots/compare/service_name/sub_page.png

✘ 1 file(s) changed.
✔ 1 file(s) passed.

Inspect your code changes, re-run with `-U` to update them. 
error Command failed with exit code 1.

分かりやすいコマンドでVRTを実行できるようにする

前述までの手順にて、CIでSystem Specを実行しスクリーンショットを取得後にreg-cliでの差分チェックのスクリプトを実行すれば、一定VRTとして機能するようになったかと思います😀

しかし、VRT実行のために複数のスクリプトを手動で実行するのは手間に感じたので、以下のようなRakeタスクを用意してbin/rails visual_regression:runでSystem Specによるスクリーンショットの取得、reg-cliによる画像比較を実行するようにしました。

require 'optparse'

namespace :visual_regression do
  desc 'Run visual regression tests'
  task run: :environment do
    options = {}
    option_parser = OptionParser.new do |parser|
      parser.banner = 'Usage: rake visual_regression:run [options]'

      parser.on('-t', '--target TARGET',
                'The directory to run the tests (default: spec/system/visual_regression)') do |v|
        options[:target] = v
      end

      parser.on('-u', '--update', 'Update the master screenshots (default: false)') do |_v|
        options[:update] = true
      end

      parser.on('-h', '--help', 'Show Help') do |v|
        options[:help] = v
        puts option_parser.help
        exit
      end
    end

    # NOTE: OptionParser#order! は optionに存在しない値があるとパースを中断してしまうので、
    # rake taskで利用する場合に指定するコマンド名とオプションのセパレーター`--`を削除する
    # https://docs.ruby-lang.org/ja/latest/class/OptionParser.html#I_PARSE--21
    option_parser.parse(ARGV - ["visual_regression:run", "--"])
    options[:target] ||= 'spec/system/visual_regression'
    options[:update] ||= 'false'
    env = {
      'VRT_SCREENSHOT_ENABLE' => 'true',
      'VRT_SCREENSHOT_UPDATE_MASTER' => options[:update].to_s,
    }

    rspec_success = system(env, 'bin/rspec', options[:target]) # System Specによるスクリーンショットの取得
    raise "Get ScreenShot command failed with exit code #{$CHILD_STATUS.exitstatus}" unless rspec_success
    
    vrt_success = system('yarn', 'run', 'test:vrt') # reg-cliによるスクリーンショットの差分比較
    raise "Check Image diff Command failed with exit code #{$CHILD_STATUS.exitstatus}" unless vrt_success
  end
end

特定のVRTの正となる画像ファイルを更新する際にも以下のコマンドで更新できるようにしました。

$ bin/rails visual_regression:run -- -u -t spec/visual_regression/your_test_spec.rb

CIで差分をチェックする

MedPeerではCircleCIを利用しているので先ほどのRakeタスクを実行し、結果をアーティファクトにアップロードするようなstepを設定することでCI上でVRTを実行するようにしています。

  visual_regression:
    steps:
      - run:
          name: run visual regression
          command: bin/rails visual_regression:run
      - store_artifacts:
          path: spec/system/visual_regression/screenshots

CircleCIのアーティファクトはブラウザ上で閲覧できるため、reg-cliで作成したhtmlレポートを以下のように、そのままブラウザで表示して差分の詳細を確認することができて非常に便利でした 👍

reg-cliによる画像差分のhtmlレポートのサンプル(一覧画面)
reg-cliによる画像差分のhtmlレポートのサンプル(詳細画面)
https://github.com/reg-viz/reg-cli/tree/main?tab=readme-ov-file#html-report

OS間での利用フォントによる違いを吸収する

当時MedPeerではfont-familyの指定が以下のようなユーザーのOSフォントを尊重するようなフォント指定になっておりました。

font-family: system-ui, sans-serif;

開発環境はDebian系のOSイメージを利用していますが、CIではUbuntu系のイメージを使用しており、実行環境によって適用されるフォントが変わってしまうことで、ローカルで事前に取得した正となるスクリーンショットとCIで取得したスクリーンショットを比較する現状の方式では、OSによって適用されるフォントが異なるため差分が発生してしまいました。

これが原因でVRTが失敗してしまうことが多かったので、以下のようにVRT実行時にのみtrueとなるカスタムコンフィグを設定して、

Rails.application.configure do
  # NOTE: VRTのスクリーンショット取得を判別するためのカスタム設定
  screenshot_enable = ENV["VRT_SCREENSHOT_ENABLE"] == "true"
  config.x.visual_regression.screenshot_enabled = Rails.env.test? && screenshot_enable
end

以下のようなVRT用のWebフォントを適用するCSSを用意し、

@import "https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap";

body {
  font-family: "Noto Sans JP", sans-serif !important;
}

VRT実行時だけ読み込むことで OS 間のフォント差分を無視できるようにしました。

<% if Rails.configuration.x.visual_regression.screenshot_enabled %>
  <%= stylesheet_pack_tag 'visual_regression/override' %>
<% end %>

おわりに

まだ拡充途中のため具体的な効果までは検証できていませんが、MedPeerにVRT基盤を構築したことによって、手動テストの削減やCSSリファクタリングを安全に行うための環境を整備できるようになりました🎉

MedPeerは医療を扱うサービスのため、こういった仕組みを利用して安定的なサービス提供を実現しつつ、スピード感も維持していきたいです💪

最後まで読んでいただきありがとうございました✨

参考にさせて頂いた資料

tech.speee.jp

engineering.linecorp.com


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Ruby 3.3(+YJIT)へのアップデートによるパフォーマンス変化の計測

こんにちは。サーバーサイドエンジニアの @atolix_です。

今回はメドピアで本番運用をしているアプリケーションの1つであるやくばと for Clinicにて、Ruby 3.2からRuby 3.3にアップデートを行った際のパフォーマンスの変化を計測しました。

Ruby 3.3ではYJITの大幅な改善が含まれているので、これによるアプリケーションへの影響を確認していきます。

www.ruby-lang.org

gihyo.jp

前提

本記事に記載されたデータは以下の条件で計測をしています。

  • Rails: 7.1.3.4
  • YJIT有効化時のオプションは特に付与していない状態(Dockerfileから環境変数を与えて有効化)
  • 3.2.4(+YJIT)から3.3.1(+YJIT)へのアップデート
  • 有効化前後の1週間を比較

パフォーマンスの変化

やくばと for Clinicではモノレポのアプリケーション内で、クリニックAPI・患者APIといった括りでエンドポイントおよびサーバーを分割しているので、それぞれのAPI Latency/CPU/Memoryの変化を確認していきます。

API Latency(低い方が性能が良い)

クリニックAPI

パーセンタイル before(avg) after(avg)
p50 49ms 45ms
p90 344ms 310ms
p95 518ms 470ms
p99 644ms 580ms

約9~10%の短縮がされているので、かなり改善されていることが分かります。

患者API

パーセンタイル before(avg) after(avg)
p50 132ms 104ms
p90 742ms 523ms
p95 888ms 619ms
p99 937ms 645ms

患者API側はより大きく効果が出ており、全体で30%弱の短縮が実現できています。

CPU使用率(低い方が性能が良い)

クリニックAPI

before(avg) after(avg)
0.42% 0.34%

0.08%減で改善されているようです。

患者API

before(avg) after(avg)
0.73% 0.51%

0.22%減でクリニックAPIよりも大きく効果が出ていそうです。

メモリ使用率(低い方が性能が良い)

クリニックAPI

before(avg) after(avg)
441MiB 455MiB

メモリ使用率は3%ほど増加しています。

患者API

before(avg) after(avg)
517MiB 533MiB

こちらもメモリ使用率は3%ほど増加しています。

RubyVM::YJIT.enableでの有効化

Ruby 3.3からのYJITの変更点として、RubyVM::YJIT.enableの実装も挙げられます。

これまでのRubyでは環境変数RUBY_YJIT_ENABLE=1の設定やコマンドラインで--yjitの付与をしないとYJITを有効化することができませんでしたが、Ruby 3.3からはコード内でRubyVM::YJIT.enableを呼び出すことでYJITの有効化が行えるようになりました。

開発中のRails7.2ではデフォルトでRubyVM::YJIT.enableを呼び出すinitializerが実装される予定です。

また、YJITの起動方法をRubyVM::YJIT.enableに切り替えることでメモリ消費量の点でもメリットがあります。

k0kubun.hatenablog.com

もう一つの利点は、YJITの起動を遅延させることで、 アプリ初期化後は使われないコードのコンパイルを避けメモリ消費量を削減できる点である。 Railsのイニシャライザでも効果はあるが、理想的にはUnicornのafter_forkやPumaのafter_worker_forkから呼び出すと良い。 これにより、起動するワーカーの半分だけYJITを有効化し、インタプリタと性能を比較する基盤として利用することもできる。

という訳でYJITの有効化方法をDockerfile上での環境変数からRubyVM::YJIT.enableに切り替えて、上記のメモリ消費量から更にどのような変化が現れるか1週間分の計測をします。

Rails7.2の実装と同じようにinitializer配下にRubyVM::YJIT.enableを呼び出すコードを配置します。

if defined? RubyVM::YJIT.enable
  Rails.application.config.after_initialize do
    RubyVM::YJIT.enable
  end
end

クリニックAPI

Ruby 3.2.4(avg) Ruby 3.3.1 環境変数によるYJIT有効化時(avg) Ruby 3.3.1 RubyVM::YJIT.enableによるYJIT有効化時(avg)
441MiB 455MiB 424MiB

455Mib -> 424MiBなので7%ほどの減少で効果が出ていそうです。

患者API

Ruby 3.2.4(avg) Ruby 3.3.1 環境変数によるYJIT有効化時(avg) Ruby 3.3.1 RubyVM::YJIT.enableによるYJIT有効化時(avg)
517MiB 533MiB 503MiB

533MiB -> 503MiBで6%ほど消費量が抑えられています。

まとめ

既にYJITを有効化しているRuby 3.2からのアップデートでも大幅なAPIレイテンシ/CPU使用率改善が確認できました。 また、RubyVM::YJIT.enableによってメモリ消費量の削減も確認できたので、まだ有効化方法を切り替えていないプロジェクトでは積極的に導入するのが良さそうです。 一方でプロジェクトによってはメモリの使用量が増加する場合も有り得るのでアップデート前後で継続的な監視が必要そうです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Matzさんを交えて、RubyKaigi 2024の社内共有会を実施しました!

こんにちは!メドピアの伏見(@fussy113)です!

メドピアでは、Rubyの父でありメドピアの技術アドバイザリーを務めていただいているMatzさん(@yukihiro_matz)をお招きして、オンライン会議を開催しています。

そのオンライン会議にて、5月に開催されたRubyKaigi 2024に参加したエンジニアからの共有会を実施しました。

この記事ではその共有会のレポートをお届けします!

RubyKaigi 2024 と メドピアのスポンサーの運用

1人目はVPoTの平川さん(@arihh)より、スポンサーブース運営や、RubyKaigi 2024全体についての振り返りのLTでした!

メドピアはブースでアンケートを行っており、その内容の共有であったり、他社さんの取り組みで良かったものなど、RubyKaigi 2024全体の雰囲気が伝わるような内容でした。

今後のスポンサーブース運営の際に、また見直したい内容のLTでした。

(ちなみにMatzさんからは、メドピアのスポンサーブースで配っていたデカバッグについて、とても良かったという感想をいただきました!)

ブースの振り返りは、MedPeer Styleでも記事を書いているので、ご覧ください。

style.medpeer.co.jp

RubyKaigiのプロポーザルを通したい。

2人目は榎本さん(@toshimaru_e)より、RubyKaigi の特殊性、熱量を伝える内容のLTでした!

RubyKaigi はRubyを"使って"いる人ではなく、Rubyを"作って"いる人が来る、"最高のTech カンファレンスを作りたい"という運営の強い思いを持って作り上げられていること。

そんな最高のTech カンファレンスにプロポーザルを通すには、どうすれば良いのかを過去の発表内容をもとに考えようといった内容でした。

RubyKaigi の特殊性や唯一無二性を聞き、そこに登壇しているエンジニアは本当にかっこいいなと素直に感じました。

自分も自慢できるコードを日々書き続けて、RubyKaigi にプロポーザルを出したいと刺激をいただきました。

発表された資料は、公開もされているので、ぜひご覧ください。

speakerdeck.com

ruby.wasm 最前線 2024 - wasmでMockServerをつくる

折り返し、3人目は草分さん(@lni_T)より、ruby.wasm についてのLTです!

RubyKaigi 2024で話されたruby.wasm に関連するセッションの共有と、その内容を活用してサーバー起動が不要なモックサーバーをruby.wasm × Service Worker で作ったというデモが実施されました。

bundler がruby.wasm に対応して、build の手順がどんどん楽になったり、Pure RubyなGemはruby.wasmに組み込むことが出来るようになっていたりと、開発者がどんどん触りやすくなっていることに驚きでした。

またMatzさんから、他のプログラミング言語でのwasm の試みの動向について聞くことができたのも興味深かったです。

発表された資料は、公開もされているので、ぜひご覧ください。

speakerdeck.com

Improved REXML XML parsing performance using StringScanner

最後は、RubyKaigi 2024のLT に登壇した内藤さん(@naitoh)のリバイバル講演でした!

REXML Gem内の実装をRegexpからStringScanner に書き換えることによって、処理速度を本番でも利用を検討できるレベルに改善されたという内容でした。

書き換えにあたってベンチマークを用意して計測を行い、ボトルネックを確認してから実装を進めるなど、ライブラリの速度改善についての進め方として参考にしたいなという印象を持ちました。

MatzさんからもLTではなく、30分のセッションでも良い内容と嬉しい言葉をいただきました!

発表された資料は、公開もされているので、ぜひご覧ください。

speakerdeck.com

終わりに

Meetで集まって、コメントや、リアクションで盛り上がりつつ、ワイワイと共有会を進めることができたので、とても楽しむ会ができたと思っています!

開催後アンケートより参加したエンジニアからも、"ダイジェスト的に内容を知ることができた"、"ruby.wasmなど、さらに進化していることを感じることができた"とポジティブな感想をいただき、よかったです。

今後もワイワイと社内を盛り上げていけたらと思います!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Nuxt 3 への移行に Nuxt Bridge を使うのはいかが?

こんにちは!フロントエンドエンジニアの土屋 (@tutti2612) です。

いよいよ Nuxt 2 の EOL が迫ってきましたね。

nuxt.com

先日、弊社でもとあるプロダクトの Nuxt 3 への移行を完了させました。

メドピアでは既に複数のプロダクトで Nuxt 3 への移行を行ってきましたが、今回の移行では今までとは違ったアプローチを取りましたので、その詳細をお伝えしたいと思います。

これから Nuxt 3 への移行を考えている方にとって、少しでもお役に立てれば幸いです。

今回の移行の特徴

今回の移行の特徴として、以下の2点が挙げられます。

  1. Nuxt Bridge の使用
  2. 積極的な新機能開発を行いながらのビッグバンリリース

それぞれ解説していきます。

Nuxt Bridge の使用

今回の Nuxt 3 移行における一番の特徴は、Nuxt Bridge を使用したことです。

Nuxt Bridge とは、Nuxt 2 で Nuxt 3 の機能の一部が使えるようになるライブラリです。これを使うことで、Nuxt 3 への段階的な移行が可能になります。 nuxt.config.ts でフィーチャーフラグを切り替えることで、Nuxt Bridge が提供する機能を簡単に有効/無効化することができます。 これにより、移行作業中に必要な機能を段階的に有効化し、Nuxt 3 の機能を試しながら安定した環境を維持することができます。

nuxt.config.ts

Nuxt Bridge を使った移行手順については、Nuxt Bridge のコアコントリビュータである wattanx さんのスライドや、Nuxt 公式のマイグレーションガイドで詳しく紹介されています。これらのリソースを参考にすることで、スムーズな移行を実現することができます。

nuxt.com

積極的な新機能開発を行いながらのビッグバンリリース

もう一つの特徴として、今回の移行では積極的な新機能開発を行いながらのビッグバンリリースを実施しました。

どのくらいのビッグバンリリースかというと、このくらいの規模になります。

リポジトリのほぼ全てのファイルに変更を加えています。

なぜこのようなビッグバンリリースになったかというと、このプロダクトは自動テストをあまり書いておらず、動作確認は手動で行う必要があったためです。Nuxt Bridge を使用しても、Nuxt 3 の破壊的変更を適宜リリースするには手動での動作確認に多大な工数がかかると判断しました。移行の前に自動テストを拡充することも考えましたが、それもまた多大な工数が必要で、EOL までに移行を完了するスケジュールが立てられませんでした。

さらに、成長途中のプロダクトであるため、新機能開発を止めることができませんでした。新機能の動作確認にも多くの工数が必要だったため、Nuxt 3 移行の細かなリリースによる動作確認に十分な工数を割くことができませんでした。

これらの点を考慮し、ビッグバンリリースを選択しました。

具体的には、マイグレーションブランチを用意し、Nuxt 3 移行はそのブランチ上で行いました。新機能開発での変更箇所は適宜マイグレーションブランチにマージしていくことで、develop ブランチとの乖離を防ぎました。

成功の要因

積極的な新機能開発を行いながらのビッグバンリリースというリスクのある移行方法でしたが、無障害で移行を完了することができました。

成功の要因としては、以下の点が挙げられます。

  1. Nuxt Bridge の使用
  2. 週 1 回の定例ミーティング
  3. エンジニア以外にも協力してもらった手厚い手動テスト
  4. 巨人の肩に乗ることができた

これらもそれぞれ解説していきます。

Nuxt Bridge の使用

成功の要因の一つとして、今回の移行の特徴でもある Nuxt Bridge の使用が挙げられます。

ビッグバンリリースを行ったため、段階的に移行ができる Nuxt Bridge のメリットが無いのでは?と思われるかもしれませんが、実際にはそうではありません。Nuxt Bridge を使用して段階的に移行することで、Pull Request の粒度を細かくすることができました。これにより、コードレビューのしやすさが格段に向上しました。

粒度の細かいPRの例

また、各破壊的変更の動作確認を Nuxt 2 が動いている状態で行えることも大きなメリットです。Nuxt Bridge を使わない場合、すべての破壊的変更に対応してからでないと動作確認ができませんが、Nuxt Bridge を使用することで、各変更の影響を個別に確認しながら進めることができました。

このように、Nuxt Bridge を活用することで、段階的な移行が可能となり、結果として無障害での移行に大きく貢献しました。

週 1 回の定例ミーティング

移行作業はのべ 3 人のエンジニアで行いましたが、新機能開発と同時進行だったため、3 人が常に移行作業に専念できるわけではありませんでした。

しばらく移行作業から離れていたメンバーが状況を把握できるように、移行関係者で週1回の定例ミーティングを実施しました。このミーティングは「Nuxt 3 作戦会議」と題し、移行作業の進捗状況や課題を共有しました。タスクの管理には GitHub Projects を用い、進捗状況を可視化していました。

GitHub Projects のロードマップ

このミーティングのおかげで、各エンジニアのタスクの進捗と次のステップが明確になり、効率的な作業を促進しました。

エンジニア以外の協力を得た手厚い手動テスト

無障害での Nuxt 3 移行に成功した要因のひとつに、エンジニア以外の協力を得た手厚い手動テストの実施があります。

先に述べた通り、このプロダクトは自動テストの量が少なく、手動テストに頼らざるを得ませんでした。エンジニア視点のテストだけだと、抜け漏れが発生する可能性を拭いきれませんでした。そこで、PdM やカスタマーサクセスなど、エンジニア以外のメンバーにも手動テストに協力してもらいました。

これにより、複数の視点から手動テストを実施することができ、無障害での移行に大きく貢献したと考えています。

巨人の肩に乗ることができた

最後に、この移行を成功させた要因として欠かせなかったのは、社内に既に複数の Nuxt 3 移行を成功させていたエキスパートたちがいたことです。彼らの経験と知見が社内に蓄積されていたおかげで、スムーズな移行が可能となりました。

移行の注意点を事前に彼らから聞くことができ、さらに彼らが残してくれたドキュメントを参照することで、予想される落とし穴を回避することができました。

これらの貴重なドキュメントの一部は、弊社ブログや Speaker Deck で公開しています。

tech.medpeer.co.jp

tech.medpeer.co.jp

tech.medpeer.co.jp

それぞれ異なるアプローチを紹介しているため、これから Nuxt 3 移行を考えている方はぜひ一読することをおすすめします。あなたのプロダクトに最適な手法が見つかることでしょう。

こうすれば良かった

無事に Nuxt 3 への移行が成功しましたが、振り返ってみると「こうすれば良かった」と思う点もあります。

今回の移行では、Nuxt のバージョンが 2.14 の状態から Nuxt 3 に移行したのですが、少なくとも 2.17 にアップデートしてからビッグバンリリースを行うべきだったと感じています。

マイグレーションブランチに develop ブランチをマージした際、コンフリクトの解消に非常に苦労しました。Nuxt 2.17 にアップデートしていれば、Nuxt で使用している Vue のバージョンが 2.7 に上がり、CompositionAPI が Vue 本体から提供されるため、コンフリクトの発生数を減らせたと考えています。

おわりに

メドピアの Vue 3 / Nuxt 3 移行の集大成として臨んだこのプロジェクト。 なんとか EOL までに Nuxt 3 に移行することができたと思ったのも束の間、Nuxt 4 のリリースが間近に迫っています。

nuxt.com

Nuxt 4 への移行においても、今回の Nuxt 3 移行で得た知見は大いに役立つはずです。これまでの経験を活かし、スムーズな移行を目指していきましょう。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

開発生産性の改善から1年経過したチームで考えていること

こんにちは。エンジニアの保立(@purunkaoru) です。

僕のチームでは、開発生産性の改善に取り組んでから1年経過しました。 開発生産性の改善系の記事やノウハウは世間によく出ていますが、1年経過した今、開発生産性に対してEMの立場で何を考えているかを言語化します。

チームメンバーの構成は、執筆時で以下の通りです。

  • フロントエンド: 5名
  • サーバーサイド: 9名
  • モバイルアプリ: 3名
  • EM(保立): 1名

弊社では、Findy Team+ を導入し、開発生産性を見えるようにしています。 まずはFindy Team+の画面を見ながら、改善結果を見ていきます。

  • 直近1年間

  • 直近2年間

直近2年で見ると、後半1年で生産性が改善されており、その改善が一定維持できていることがわかります。

ちなみに、このサイクルタイム分析について、数値的な目標を今まで一度も掲げてきませんでした。 どうしても指標に対する数値目標を掲げてしまうと、それに対するハックが進んでしまい、本質的な改善にならないと感じていたからです。チーム内では、開発に時間がかかる・レビューに時間がかかると誰かが感じた時に、Findy Team+の定量的な数値を見てチームに改善を促すコミュニケーションを取るケースが多かったです。

また、Four Keysの分析(Findy Team+でいうDevOps分析)は、botでリリースPRを作っているせいか、うまく集計できないので分析対象から外しています。 社内には、Four Keysの分析をOKRに組み込んでいるチームもあります。

それでは、ここから本題に入り、開発生産性の改善が進んでから1年経過して、何をチェックしているか記載していきます。

1. チーム別のアウトプット量の推移や負荷の確認

エンジニアチームとして、アウトプットの量の推移を確認することは不可欠です。アウトカムとしてのKPI(サブKPI)への貢献もチーム全体で把握していますが、開発に関わるアウトプット量の変化にも注目しています。

アウトプットの量は「1人あたりのプルリクエスト数」と「1プルリクエストあたりの平均変更行数」で測っています。以下の画像の棒グラフ(薄い色の方)で示されている「1人あたりのプルリクエスト数」は、昨年に比べて約20%増加していることが分かります。

一方で、「1プルリクエストあたりの平均変更行数」に関しては、別の画面で確認する必要がありますが、月ごとの推移を1画面で見る方法を見つけられなかったため、ここでは画像の提供は省略します。僕のチームの平均変更行数は、月ごとにばらつきはありますが、過去と比べて増加または減少した傾向は見られませんでした。

ここから読み取れることは2つあります。

1つ目は、チーム全体のアウトプット量が増加していること。これは、要件定義や設計のコミュニケーションが整備され、スムーズになったこと、またエンジニア自身の技術力やレビュー能力の向上が背景にあります。

2つ目は、12月にプルリクエスト数が急増しており、稼働時間が多い状況になったこと。主な要因は体制変更にありました。新しいチーム体制での見積もり精度が落ち、稼働が厳しくなってしまいました。11月末にはプルリクエスト数が増加する傾向にありましたが、対応が12月初旬まで遅れたことは、データチェックの不足や閾値設定の不備が原因です。以降、プルリクエストの数の増減にはこまめに目を光らせるようになりました。

このように、定量的なデータを活用して、チームの生産性と負荷のバランスを定量的な指標をもとに管理しています。

2. サイクルタイム分析による開発サイクルの確認

開発サイクルの改善を行ってから1年が経過し、サイクルは成熟しており、サイクルタイムの増減は限定的です。 サイクルタイムに大きな変動が生じる主な要因には、大規模な機能のリリース、メンバーの交代、チーム体制の変更、施策の追加や削除などがあります。これらの要因は、開発サイクルが改善されるか、逆に悪化するかを事前に予測できます。

たとえば、2024年4月から新しいメンバーが加わり、全エンジニアにメンターを配置する制度も導入しました。これにより、教育やMTGに費やす時間が増えました。このような新しい施策を実施したにもかかわらず、サイクルタイムや1人あたりのプルリクエスト数に変化が見られない場合、新たに増えた教育や会議の時間を考慮せずに見積もりを行い、チームメンバーが無理してタスクを完了させている可能性が考えられます。

そのため、新しい施策を導入する際には、いつごろから数値が改善するか、あるいは悪化するかを予測し、実際にその通りになっているかを後から確認するようにしています。

3. 十分な質のアウトプットが出せる量を超えてアウトプットをしていないかの確認

多くのサービスや機能を一人で担当することは効率が悪くなるとされています。エンジニアの世界でも、「チームトポロジー」などの書籍を通じて、シンプルで効率的な組織構造の重要性が説かれています。

同様に、一人が多数のプルリクエストやチケットを抱えると、対応を忘れてしまったり、ひとつひとつの作業に対する注意力が散漫になることがあります。チームによっては、レビュー待ち状態やテスト待ち状態のプルリクが放置されてしまう事象があると思います。

例えばメンバークラスのエンジニアが、タスク管理と開発の両方を行う役割に移行した場合を考えてみます。当然、タスク管理の責任が増えるため、割り当てられるチケット数やプルリクの数は自然と減少すべきです。しかし、変更前と同じ量を割り当ててしまうケースがよくあります。また、チームメンバーが増えた場合も、タスク管理の工数は増えますが、チームの増減を考慮せずにチケットを割り当てがちです。そのため、各人の役割に応じたタスク量を適切に考慮し、負荷を調整することが必要です。

現在、負荷量は「1日あたりのプルリク数」×「コミットからマージまでの日数」という計算式で把握しています。単純な「1日あたりのプルリク数」や「チケット消化数」では、プルリクやチケットの内容による重さを正確に反映できません。そのため、軽いプルリクはコミットからマージまでの時間が短く、重いプルリクは時間がかかると見込んで、この計算式を採用しました。

この計算式で算出される負荷の量を、各メンバーが理想的に対応できる数値と比較し、適切かどうかをチェックしています。自分自身の場合、集中的にプレイヤーとして活動できる時期は「18」が上限ですが、10名程度を管理していた際は「6」が限界でした。ただし、個人の生活リズムやその他の業務負荷により、「コミットからマージまでの日数」は変わるため、他人と比べるよりも過去の自分自身と比較して閾値を設定しています。

上記の例だと、 「1日あたりのプルリク数」は、52PR / 20営業日 = 2.6PR/営業日 となります。 「コミットからマージまでの日数」は、(2.5時間 + 8.5時間) / 8時間 = 1.4時間弱となります。 (「8時間」は、1日あたりの稼働時間です) 負荷量(「1日あたりのプルリク数」 * 「コミットからマージまでの日数」)は、2.6 * 1.4 = 約3.6 となります。 安全ですね。

この指標は改善が必要かなと思いつつ、ひとりひとりの負荷をアクティビティベースに定量的に確認する術が無く、今はこの方法で管理しています。

4. 新しいメンバーが機能しているか・無理していないかの確認

オンボーディング施策の一環として、新たにチームに参画したメンバーが機能しているか・無理していないかも確認しています。これは、主に業務時間で見ていますが、徐々にやれることが増えているかという点で、プルリク数・プルリクに対するコメント数・レビュー数などでも確認しています。

特に多いのが、急激に頑張ろうとしてプルリク数とレビュー数が急激に伸びているケースです。このケースでは、1プルリクあたりに対するレビューコメント数も多くなる傾向があり、レビュワー・レビュイーともに疲弊するケースがあります。こういった場合は、目標を落として、その分ペアプロをしたり、レビューコメント数が少なくなるための施策を一緒に考えます。

5. サービスの障害に関する確認

最後に、Findy Team+から離れた場で開発生産性に関して確認している数値について説明します。

サービスの障害を未然に防ぐため、自動テストだけでなく、エンジニアによる手動テストやプロダクトマネージャー、QA、CS担当者の手動テストも行っています。これらのテストにかかる期間や発見されたバグの数を記録し、各々の閾値を設定しています。もちろん障害の発生件数も確認していますが、障害発生前の手戻りやバグ検出に伴う工数も管理し、減少させる努力を続けています。これらの数値は品質管理の文脈で語られることが多いですが、結合テストや受入テストの工程の工数が減ると開発生産性は上がるので、今回の記事にも取り入れました。

この取り組みは、今期から始めたばかりですが、既に一定の成果が見られ、良い施策であったと感じています。

まとめ

数値化による生産性の管理は、チームのパフォーマンスや成長を明確に追跡することができ、またチームの負荷も把握することができます。これにより、客観的なデータに基づいて意思決定や評価を行うことができます。一方で、一度数値化してしまうと、数値を良くすることに目を向けすぎて、本質的な改善にならないケースもあります。そのため、前提を疑いながらウォッチしていくことが大切だと考えています。

今回記載した開発生産性の改善から1年経過したチームで考えていること・見ていることについて、参考になれば嬉しいです。メドピアの開発者ブログでは、技術的なことやチームマネジメントなど、参考になりそうなことを発信しているので、是非ブックマークをしていただけると嬉しいです。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

#RubyKaigi 2024 セッションレポート

サーバーサイドエンジニアの内藤(@naitoh) です。

RubyKaigi 2024に参加されていた皆さん、お疲れ様でした。 RubyKaigi のセッションの中で印象に残った発表をご紹介します。

RubyKaigi 2024 セッションレポート

タイムテーブル

タイムテーブルは以下から確認できます。

rubykaigi.org

Namespace, What and Why

今回のRubyKaigi で非常に気になっていたセッションの一つです。

  • アプリケーション、ライブラリをある空間の中でライブラリを読み込み、他の空間から隠す。
  • 空間の中で定義されたメソッドを別空間から呼び出すこと
  • 別空間から呼び出されたメソッドは、元の空間内で動作すること

という感じで複数のバージョンのライブラリに依存した場合のコンフリクト発生を解決するのが Namespace とのことで、内容が整理されておりわかりやすかったです。

Namespace "on read" ということで、 既存の gem をそのまま使いたい が特徴で、"on write" だと、Namespace を使うときに宣言して使うアプローチ(例: 別言語, python)になり、これだと既存の gem の対応が必要になるので、なるほどと思いました。

Namespace があると 1つの Ruby プロセス上で、複数のアプリケーションサーバーを動かすことが可能になり、コンテナレスで開発環境を構築するのが容易になりそうとのことで、柔軟性高いなーと感じました。

speakerdeck.com

It's about time to pack Ruby and Ruby scripts in one binary

実行ファイル一つで実行できるOne Binary 形式の話です。 Ruby の One Binary 用途として、Rubyプログラムを配布したいときが想定されており、 通常の Ruby スクリプトをそのまま配布した場合、配布先のユーザーの Ruby 環境が古いなど、期待しないバージョンの環境の場合だと動作しない問題を解決できるとの事。 既存の one binary ツールには下記の課題があるため、kompo gem を作ったとの事です。

  • Ruby 3.0 未満のサポート
  • Ruby にパッチが必要
  • Windows のみサポート
  • 一時ファイル書き込みがある

クロスコンパイル未対応の課題はありますが、native(C拡張 gem?) にも対応してるそうなので、使い勝手が良さそうですね。

speakerdeck.com

Ruby Committers and the World

毎年楽しみにしている、Ruby Committers and the World です。

今年は下記の内容が議論されていました。

  • #frozen_string_literal: true をデフォルトにする話
    • "" をバッファに使うケースは多いので、+"" にすれば良いのはわかるが手間が増える。
    • Quine で 1文字増えるのは、それだけで厳しい。
    • YJIT には効果ありそう
    • Ractor は Frozen したい
    • 型は他言語の静的方向の流れとは逆張りしたのに、#frozen_string_literal: true は他言語と同じ immutable の方向。

というような議論になっていました。 Ruby 3.4.0 preview1 はデフォルト有効になっているけど、引き続き議論中という流れでした。 個人的には性能UPに繋がりそうなのでデフォルト有効は賛成ですが、3.4 での変更ではなく 4.0 でのメジャーアップデート時の変更だとわかりやすくていいなと思いました。

  • Embed RBS

YARDにも型があるぞということで、整合性をとるために YARD → RBS 変換できるといいねという話がありました。

個人的には別ファイルだと型は書かないだろうなという感じですが、メソッドのすぐ上に書けるなら型を書くのもありかなという感じです。

  • GNU autotools を cmake に変更したい
  • (Python のように)GVL を取りたい
  • async/await の usability の話
    • async/await をいっぱい書くのは、書きにくいのでは
    • 最初から、対象のメソッドに async/await が必要ってわかっていて書けているのか?
    • 後から、必要になって async/await を書くのは bad 体験
  • Golang の defer が欲しい話
    • ネストを深くなるのを回避できる
    • いい文法があれば
    • ensure は最後に書くことになるのが問題 (リソースを使用した直後に書きたい)

などの議論がありました。 Ruby開発者の温度感がわかって楽しかったです。

YJIT Makes Rails 1.7x Faster

YJIT みなさん使ってますか?

YJIT Makes Rails 1.7x faster / RubyKaigi 2024 - Speaker Deck で 紹介されたように MedPeer でも YJIT を有効にして速くなっています。 感謝!

講演では Ruby 3.3 で高速化された YJIT の高速化された手法の紹介がありました。

  • Method Call Fallback の仕組みでJIT できない場合、Ruby インタプリタに処理を戻していたケースのいくつかで、JIT 可能になり高速化。
  • スタック変数をメモリ上で処理していたのをレジスタ上で処理するようになった。
  • Active Support の NilClass#blank? がインライン化され 1命令になるのは圧巻でした。

という感じで、素晴らしい改善ですね。 Ruby 3.4 でも改善が進んでいるみたいなので超期待です。

speakerdeck.com

Speeding up Instance Variables with Red-Black Trees

毎回、わかりやすく説明頂ける @tenderlove さんの講演です。 ObjectShape キャッシュミス時の検索に 赤黒木 を使うことで、計算量を O(n) から O(log(n)) に減らしたという内容でした。

ObjectShape の各オブジェクトもツリー構造に配置されるので、赤黒木のアルゴリズムが使えるんですね。 該当の PR は https://github.com/ruby/ruby/pull/8744 になると思います。

ただ、私は 赤黒ツリー を理解していなかったため内容にあまりついていけず、非常に残念な思いをしました。 観たいセッションの講演概要に知らないキーワードがある場合は、事前勉強が重要ですね!

コンピュータサイエンスの知識があると、最適なアルゴリズムを選択できるため知識は重要です。動画や資料が公開されたら再度確認したいです。

Lightning Talks

手前味噌ですが、LTで発表させて頂きました。 詳細は、下記を参照頂ければと思いますが、LT発表すると他の方のLT内容を聞けないのが残念ですよね。 こちらも動画が公開されたら確認したいです。

おわりに

3日間に渡るRubyKaigi 2024が終了しました。 非常に魅力的な公演が目白押しでしたが、3トラックなので観れるセッションが限られていたのが辛いところです。

次回のRubyKaigiは 2025年4月16日から4月18日、場所は愛媛県松山市です。

今年もAfter RubyKaigi を開催します!

昨年に続き今年もZOZOさん、FindyさんといっしょにAfter RubyKaigiを開催します!!!

medpeer.connpass.com

  • イベントタイトル: 『After RubyKaigi 2024〜メドピア、ZOZO、Findy〜』
  • 開催日時: 2024/05/28(火) 19:00 〜 21:30
  • 会場: ファインディ株式会社
    • 東京都品川区大崎1-2-2(アートヴィレッジ大崎セントラルタワー 5階)
    • ※ オンライン参加枠有り

メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

メドピアはRubyKaigi 2024にPlatinumスポンサーとして協賛します!

はいさい、メドピアの榎本です!

メドピアは2024年5月15日〜17日に沖縄・那覇文化芸術劇場なはーとで開催される RubyKaigi 2024 にPlatinumスポンサーとして協賛します!

今年もメドピアは会場でブースを出展します!

目次

ブース企画

今回メドピアブースでは大きく2つの企画をご用意しました。

  1. アンケートに答えてガチャガチャに挑戦!
  2. VPoTのXをフォローでデカバッグをプレゼント!

アンケートに答えてガチャガチャに挑戦!

簡単なアンケートに答えるだけでガチャガチャに挑戦でき、メドピアオリジナルのアクリルスタンドが獲得できます。

VPoTのXをフォローでデカバッグをプレゼント!

さらに、弊社のVPoT・ありひーこと平川 弘通のXアカウント(@arihh)をフォローで、メドピアオリジナルのデカバッグをプレゼントいたします。

ノベルティ紹介

アクリルスタンド

アンケートに答えて挑戦できるガチャガチャでは、アクリルスタンドが獲得できます。

オリジナルアクリルスタンド

メドピアのオリジナルキャラクター・メドベアがメッセージとともに登場します。RubyKaigi 限定のメドベアも登場するかも...?

ぜひ写真に収めてSNSに投稿してください!

デカバッグ

弊社のVPoT・ありひーのXアカウント(@arihh)をフォローすることで、昨年好評いただきましたメドピアオリジナルのデカバッグを獲得できます。

オリジナルデカバッグ(with 弊社VPoTのありひー)

荷物が嵩張っても大体のものが入る大きめサイズとなっておりますので、ぜひこちらもあわせてゲットしてください!

この場所でお待ちしています

メドピアは下記でブース出展しております。

RubyKaigi 2024 メドピアブース位置

ブース付近では、メドピアカラーの濃い緑色のTシャツを着たメンバーが立っておりますので、お気軽に声をお掛けください。

当日皆様にお会いできるのを楽しみにしております!

メドピアから内藤がLT登壇します

2日目の夕方(2024年5月16日 17:20-18:20)には弊社から内藤がLTに登壇いたします。

内藤から発表内容に関するコメントも下記のように頂いております。

REXMLは Ruby で実装された XML パーサーで、Ruby の標準添付ライブラリ(Bundled Gem)です。 Pure Ruby なのでインストールが容易ですが、処理速度が遅いという課題があります。 このREXMLのパース処理をStringScannerで書き換えて高速化した話をします。

今年もAfterイベントを開催します!

昨年に続き今年もZOZOさん、FindyさんといっしょにAfterイベントを開催します!!!

findy.connpass.com

  • イベントタイトル: 『After RubyKaigi 2024〜メドピア、ZOZO、Findy〜』
  • 開催日時: 2024/05/28(火) 19:00 〜 21:30
  • 会場: ファインディ株式会社
    • 東京都品川区大崎1-2-2(アートヴィレッジ大崎セントラルタワー 5階)
    • ※ オンライン参加枠有り

ぜひ奮ってご参加ください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp