続・EventHubにおけるリリースフローの紹介

はじめに

こんにちは。EventHubの西内です。

前回はEventHubにおけるリリースフローの紹介というタイトルで記事を書かせていただきました。

tech.eventhub.jp

その際に「リリース後の話」や「開発着手前や着手中の細かい話」を話さないこととして挙げていたので、今回はそのあたりについて触れていければと思います。

また、EventHubではフルサイクルエンジニアを前提とした働き方を推進しており、いわゆる設計や実装およびテストといった一般的な開発業務以外で実際にどのようなことを行っているのかについてもお伝えしていければと思います。

リリース後の話

さて、リリース後の話についてですが、エンジニア採用資料のスライドから引用した図を元にすると”O&M”と”Support”が該当します。

O&M ( Operation & Maintenance )

いわゆる運用保守と呼ばれる活動を指します。組織によっては新規開発とは全く別の管轄になっているところもあるかも知れません。

EventHubだと以下のような活動例が含まれます。

  • 障害対応
  • インフラの監視や運用
  • 各種ライブラリ等のバージョンアップ

障害対応や監視を除けば、これらの対応を行うことが決定したら普段の開発スケジュールに乗ってくるので、特に難しく考える必要はないかと思います。

フルサイクルの文脈で言えば、自分たちで開発したプロダクトに責任を持ち運用することを踏まえて開発する必要がある、ということになります。「責任を持つ」と表現すると、プレッシャーを感じることになるかも知れませんが、その分だけ自由さを手に入れられることにもなると考えています。

たとえば、Aという設計や仕様だと運用が難しくなると思うなら、「A’という設計や仕様にしたい」と提案することができますし、EventHubはそういった提案への理解や尊重をしてくれる組織です。

Support

顧客や社内メンバー向けに、プロダクトに関してサポートする活動のことを指します。

O&Mと同様に活動例を載せると、以下になります。

  • 技術的な質問回答や調査
  • 不具合報告の一次受け

エンジニアがあらゆるサポートを行うかというとそういうわけではなく、EventHubにはCustomer Support(通称Cサポ)というポジションの方が数名在籍しており、社内外からの問い合わせに対して最初はCサポの方々に回答をしていただいています。そこからさらに技術的な調査や機能に関する詳細な振る舞いについての回答が必要な場合は開発組織に連携が行われます。

そうして連携された内容に関して、週替りの2名体制で対応する仕組みとなっています。

“技術的な質問回答や調査”では、先述の通りCサポだけでは回答が難しい技術的な質問やプロダクトの機能のエッジケースなどについて調査・確認を行い、結果をお伝えしています。

“不具合報告の一次受け”では、報告された内容を精査し不具合の重篤度を判断しています。このことをEventHubでは「バグレベル判定」と呼んでおり、判定のための目安になる定義も存在しています。また、同時に緊急度も判断し、必要に応じてエスカレーションを行うことがあります。

ちなみにこれらのやり取りはそれぞれ専用のSlackチャンネルがあり、Slackのワークフローを使って問い合わせや報告をテンプレートに沿って投稿できるようになっています。

問い合わせイメージ

その他

サイクルの図にはないですが他の活動で言えば、リリースした機能がどれぐらい利用されているかを不定期でチェックすることがあります。

このあたりはプロダクトマネジメントの文脈に当たるかとは思うので詳細は省きますが、こういった数値を調べることが顧客理解に繋がることがあると個人的には感じています。

開発着手前や着手中の細かい話

では次の話題に移ります。

前回、自分が記事を投稿したのは2024年の8月末頃でした。つまり、約8ヶ月ほど経過しています。

その間に、いくつかの記事がアドベントカレンダーやテックブログにて投稿されており、それらでも開発着手前や着手中の話について触れられているので、このタイミングで紹介しておきます。

もし良ければ以下の素敵な記事にも目を通していただければと思います!

note.com

note.com

tech.eventhub.jp

 

さて、まずは話の前提を揃えておければと思います。

  • ここで扱う”開発”とは?
    • お客様の要望に基づくスモールな開発(以下、スモール開発)
    • 今後のプロダクト戦略に沿った中・長期規模の開発(以下、エピック開発)
  • 開発フローの概要は?
    1. 要件定義
    2. 調査(Spike)&設計
    3. 実装&テスト
    4. リリース

EventHubではスクラムによるアジャイル開発を用いており、そしてどちらの“開発”についてもほぼ同じ開発フローで進め、規模感や要件に応じて適宜調整を行うイメージです。

スモール開発であれば、事前の調査や設計というフェーズを省いて実装しながら考えるということもありますし、事前にすり合わせてから実装に臨むこともあります。いずれにせよ、スケジュールは短めです。

エピック開発であれば、調査や設計に一定の時間を使うことが多いです。これは実現可能性の調査や後戻りできない(しづらい)設計にならないようにするためとなります。 また、実装とテストを進めていくうちに要件や仕様の調整や提案をエンジニアから持ちかけ、期日/品質/コスト/運用などのバランスを取りつつ、開発を進めていっています。

ここまでの話を聞くと「うちと大体一緒だな」と感じた方もいると思います。そしてその感覚はおそらく正しいと思います。大枠としては、他社と比較して特別な工夫をしているわけではないのかな、と感じています。

そのため、ここからは自分が感じたEventHubにおける開発組織の特徴を挙げる形で「開発着手前や着手中」の様子をお伝えできればと思います。

エンジニアからPdM/デザイナーに提案することがよくある

1つ目はこちら。

タイミングとしては随時発生する可能性があるものなのですが、実装に着手し始めてからが多い気がします。開発が進むにつれて解像度が上がってきたり、UIを実装して動くものが出来てきたりするからかなと思っています。

提案する際の観点としては、より良い機能にするためであったり、実装コストを抑えて素早くリリースするためであったりします。他の観点もあるとは思いますが、最たる例はこの2つだと感じます。

具体例としては、仕様に関して「なぜAという仕様ではなくBなのか」みたいな疑問を感じて質問や相談をすることがあるかと思います。そういった場合にはAやBの仕様のメリットとデメリットを考えたうえで、提案しつつ質問や相談することをEventHubでは推奨しています。

可能であれば提案ベースで話すことを推奨しているので「提案することがよくある」のは至極当然の話ではあるのですが、自分がこれまで在籍した3社と比較しても多いほうなんじゃないかと感じています。

いまやる必要があるのか?の判断軸での議論がよくある

2つ目はこちら。

たとえばPull Requestの内容として、本来のタスクでやりたかったことに加えてリファクタを含んでいたとします。そのリファクタについて「いまやる必要があるかどうか」の軸でレビュイーが意義を説いたりレビュアーが意見したりする、そういった場面を目にすることがあります。

言わずもがなですが、リファクタ自体を否定しているわけではなく、むしろ推奨しているぐらいではあります。しかし、EventHubではそれ以上にタイミングを大事にしているということかなと個人的には感じています。

「リファクタリングをしていてリリースが遅れた」となれば、顧客への価値提供がその分遅れることになります。反対に「今後の開発速度に影響を与えかねないから、いまリファクタリングをする」という判断をすることもあります。

だからこそ”いまやる必要があるのか?”を問う必要があるのだと思っています。

さいごに

けっこう具体例を盛り込んでみたのですが、いかがだったでしょうか。前回同様、開発組織の雰囲気が少しでも伝わっていれば良いなと思います。

あと、リリースフローの紹介というタイトルでしたが「EventHubにおけるフルサイクルエンジニアはどういったことをしているのか?」みたいなタイトルでもあんまり違和感がない話になったな、と書いた内容を振り返ってみて感じました。

…というわけで、ここまで読んでくださりありがとうございました!
いつもの採用情報などを記載しておいて本記事を締めさせていただきます。

jobs.eventhub.co.jp

note.com

初めてのプロジェクトマネジメントで学んだ3つのこと

こんにちは、EventHubの井上です。2024年12月から2025年3月の約3ヶ月に渡り、エピック(リリースまで数ヶ月を要する中規模以上の機能開発)のプロジェクトマネジメントを担いました。これまでマネジメント経験がない中での挑戦だったため、いくつもの壁にぶち当たりました。本記事では、プロジェクトを通して学んだ3つのことをご紹介します。

開発体制について

本題に入る前に、開発体制についてご紹介します。今回のエピックは以下のメンバー構成でスクラムによる開発を行いました。

  • プロダクトオーナー(PO)2名:メインPOとサブPO
  • デザイナー1名
  • エンジニア4名
    • チームリーダー1名
    • メンバー3名(私を含む)

上記エンジニア4名は普段から固定のチームで動いています。プロジェクトマネジメントは「エピック担当」という名称で、チームリーダーを含む人員の中から一人がエピックごとに交代制で役割を担います。今回は私がエピック担当として選出されました。

1. プロジェクトの完了に責任を持つ

マネジメントという言葉から、なんとなく「管理する」のが仕事だと思っていましたが、プロジェクトの完了に責任を持つのが仕事だと教わりました。メンバーの協力を得たり、自分自身でも手を動かしたりして、あらゆる手段を使って完了させる意識で取り組みました。

特に意識していたことは以下2点です。

  • 自身のコミットしたタスクは必ず期限内に終わらせること
  • 全てのPRに目を通し、In Reviewになった当日にレビューすること

EventHubでは1週間スプリントのスクラム開発を行っていて、JIRA上のスプリントに乗せた全てのチケットをDONEにするために、とにかく実装とレビューを頑張りました。メンバーからの協力を得るために、自分は本気でこのプロジェクトを完了させたいんだという姿勢を示すようにしました。

また、レビューサイクルを早めることでチーム全体の実装スピードが上がると感じたため、認知負荷はかかりますが、GitHubやSlackの通知がきたらすぐにレビューするようにしました。

少し根性論のようなやり方かもしれませんが、マネジメントの引き出しのない自分が小手先であれこれやるよりも、自分が誰よりもプロジェクトにコミットするのが、シンプルで効果的だと考えました。

2. 見積もりに時間をかけすぎない

今回のエピックは機能スコープが広く、設計やチケットの洗い出しに苦労しました。普段のエピックでは技術スパイクという形でプロトタイプ的なものを作り、それを元に精度の高い見積もりを行うようにしていますが、全てをカバーしたプロトタイプを作るのは現実的ではありませんでした。

そこで、機能全体に関わる部分だけスパイクを行い、その他は解像度の粗い状態でチケットを切りました。その後のチームでの見積もりも当然ざっくりしたものになりますが、各チケットの担当者が実装する中で精度を高めていった方が、結果的に早くリリースできると考えました。

副作用として、8ptと見積もっていたものが実際は21ptぐらいのサイズだったことが判明したりと、当初よりポイントが膨らんでしまうものもありましたが、不確実性の高いものから取り組むようにしたことで、大幅なスケジュール遅延は避けられました。この点については後述します。

粗い見積もりでプロジェクトをスタートすることにはリスクが伴いますが、実装してみないと分からないことも多いため、早めに実装フェーズに移ることで、結果的にリリースタイミングを早めることができたと思います。例えば、1ヶ月かけて精緻に見積もりし、2ヶ月半のスケジュールを引いてぴったり完了したらトータル3ヶ月半です。一方で、2週間で粗い見積もりをし、2ヶ月のスケジュールを引いて2週間の遅れで完了したら3ヶ月でリリースできたことになります。

ただ、計画より遅延すると今後の開発計画やユーザーコミュニケーションに問題が生じる可能性があります。そのリスクを抑えるために、序盤は仮スケジュールと周知しておき、エピックの中盤でスケジュールを確定するような形にするといいのかなと思います。実際、今回のエピックも「見積もりが粗いので仮スケジュールです」と関係者にお伝えしてから開始しました。

3. 不確実性の高いものから取り組む

見積もりの話と関連しますが、工数やスケジュールの確度を上げるためには、不確実性の高いタスクから取り組むと良いことを学びました。個々のチケットの不確実性の総和が、リリーススケジュールの不確実性だとすると、当然、不確実性の高いものから順に取り組めば、後に残るのは工数が読めているものばかりになるため、スケジュールが予測しやすくなります。何より心理的な面で安心です。

ただし、不確実性の高さよりも優先すべきは、チケット単体でユーザーに価値提供できるものです。結果的に同じ工数なら、ユーザーへの提供は早いに越したことはないからです。また、DBマイグレーションなど先に終わらせておくことで後続のタスクがスムーズに進むものは、並列でタスク消化できるように優先すべきです。

以上を踏まえ、今回のエピックでは以下の優先順位でチケットを配置しました。

  1. ユーザーに価値提供できるストーリー
  2. 複数のブロッキングになるタスク
  3. 不確実性の高いタスク
  4. その他のタスク

不確実性の高いタスクは実装方針が不明瞭なことも多く、難易度が高いです。そのため、着手することに不安を覚えます。「どのチケットやりたいですか?」という希望性で取っていくと、たいてい不確実性の高いタスクは敬遠されて後ろ倒しになってしまいます。なので、不確実性の高いことから取り組む重要性をチームで共有し、勇気を持って先に着手していくことが大事だと思います。私個人としては、少なくとも選り好みはしないように気をつけました。

余談ですが、今回のエピックでは、序盤に不確実ではないと判断していたものの、後半になって考慮漏れがあったことに気づき、大幅な作業量増になってしまったチケットがありました。この問題の原因や防止策はチームで改めて振り返る予定ですが、目の前の実装に集中するあまり、チケットの再整理に時間を割けていなかったのが個人的な反省です。このあたり、自身の技術力やマネジメントスキルの不足であると実感します。

さいごに

今回のエピックは3月末で一旦終了となる予定ですが、来月すぐに後続のエピックが控えていて、そのマネジメントも引き続き担当します。きっと最適なプロジェクトマネジメントの仕方があるのだと思いますが、たった数ヶ月で熟練することはできなさそうです。今回と同じく個人としてはフルコミットでやりつつ、周囲の方の助けをお借りして、完了責任を果たそうと思います。

初めて指揮役をやってみて気づいたのは、チームの中にプロジェクトにフルコミットしてくれる方がいるととても心強いということです。自分自身もメンバーとして入る際は、今まで以上に前のめりな姿勢でやろうと思っています。プロジェクトマネジメントはなかなかタフな仕事ではありますが、個人の成長という意味でもすごく価値のある経験でした。3ヶ月間、未熟なマネジメントで課題だらけの中、一緒に走りきってくれた皆さんに心から感謝しています。

現在、EventHubではエンジニアを募集しています。本記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談からご連絡ください!

jobs.eventhub.co.jp

note.com

EventHubのバリデーションを支えるclass-validatorについて

はじめに

こんにちは!! EventHub の Web エンジニアの須田です 🐱

今回は、EventHub で採用している技術の 1 つである class-validator についてと、その活用事例についてご紹介していきます 🐱

class-validator とは?

class-validator は、TypeScript および JavaScript 向けのデコレーターベースの検証ライブラリです。

主にクラスのプロパティにデコレーターを追加することで、データのバリデーションを簡単に実装できます。

ちなみに、TypeScript におけるデコレーターは、クラスやそのメンバー(メソッド、プロパティ、アクセサ、パラメータ)に特別な機能を追加するための構文(修飾子)で、@記号で始まります。

例えば、次のようなコードでは、@Contains@IsEmailなどのデコレーターを使用して、インスタンスのプロパティに対してバリデーションを実行しています。

import { Contains, IsEmail } from "class-validator";

export class PostReq {
  @Contains("EventHub") // textに'EventHub'という文字列が含まれているかをチェックする。
  text: string;

  @IsEmail() // emailが正しいメールアドレス形式かをチェックする。
  email: string;
}

github.com

class-validator のデコレーターについて

class-validator に標準搭載されているバリデーション・デコレーターの種類は、README.md に整理されているとおり、8 種類に分けられます 📝

これらの検証デコレーターを組み合わせることで、フォーム入力や API リクエストなどに対して強固なバリデーションを実装し、入力データの安全性や整合性を確保することができます。

詳細は、README.md の次のセクションをご覧ください。 https://github.com/typestack/class-validator?tab=readme-ov-file#validation-decorators

class-validator の特徴・どんな場面で利用するのか?

class-validator の特徴は、クラスの検証したいプロパティにデコレーターを付与して、バリデーションルールを定義するスタイルにあるかと思います。

既存の Class のプロパティに対して、バリデーション機能を追加できるため、Class を使ったフォーム入力や API リクエストなどのバリデーションに適しています。

EventHub で採用しているフレームワークである NestJS では、公式にも class-validator を利用していることが記載されています。

(Class ベースのプロジェクトと、class-validator は相性がいいため、NestJS では class-validator を利用しているのかと思われます。)

docs.nestjs.com

class-validator と class-transformer の関係性

class-transformer は、class-validator とセットで使用されることが多いので、その関係性について整理しておきます。

class-transformer は、プレーンオブジェクトをクラスインスタンスに変換したり、その逆を行ったりするためのライブラリです。

class-validator でバリデーションをするには、Class である必要があります。

そこで、プレーンオブジェクトをクラスインスタンスに変換するツールである class-transformer をセットで利用することが多いです。

また、変換先の Class にバリデーションデコレーターを付与していれば、class-transformer で Class 変換する際にバリデーションも合わせて実施するようにすることもできます。

上記のような組み合わせて使用できるため、EventHub では class-transformer と class-validator をセットで利用しており、 Form のプレーンオブジェクトを Request のクラスに変換する際や、API のレスポンスを Response のクラスに変換する際などに使用しています。

ちなみに NestJS 公式でも、class-validator と class-transformer はセットで紹介されています。

github.com

EventHub での class-validator の使用ルールについて 📝

次の引用にもある通り、class-validator は、ブラウザと node.js どちらの環境でも動作するので、フロントエンド でも バックエンド でも共通して使用することができます 👍

Allows use of decorator and non-decorator based validation. Internally uses validator.js to perform validation. Class-validator works on both browser and node.js platforms. 引用元: GitHub class-validator

フロントエンド でも バックエンド でも共通して使用することで、フロントエンドとバックエンドの間でデータの型を一致させることができるため、データの整合性を保ちやすいです。

そして、EventHub では、フロントエンドとバックエンド両方で TypeScript を採用しているため、どちらでも class-validator の恩恵を受けることができます。

また、EventHub のコーディング規約には、次のような記載があります。

バリデーター

  • バリデーションのロジックが class-validator に用意されているもので不十分な場合は、カスタムバリデーターとして切り出す。
  • 基本的にバリデーションはサーバー・フロントエンドの両方で実行する。何か特別な理由がある場合は都度判断してサーバーのみは許容する。フロントエンドのみでのバリデーションは基本はなしにする。(サーバーに関連がなくフロントエンドのみで完結する場合はフロントエンドのみで問題ない)

バリデーション(値の検証)はフロントエンド/バックエンドの両方で実行して、受け渡すデータの保証をしていく必要があるので、

そういう意味でも、フロントエンド でも バックエンド でも動作して共通利用できる class-validator はありがたい存在なわけです。

また、class-validator の標準デコレーターで対応できないバリデーションに関しては、独自のバリデーションルールも実装しています。

カスタム・バリデーターに関しては、後述します 📝

EventHub での class-validator の活用ポイント 📝

EventHub での class-validator の主な活用場面は、リクエストのバリデーションです。 (他にも活用場面はありますが、割愛させていただきます 🙏)

例えば、以下のようなリクエストの定義ファイルがあったとします。

import { IsNotEmpty, IsString, ValidateIf } from "class-validator";

import DataValidationError from "$shared/constants/DataValidationError";
import Spec from "$shared/constants/Spec";

// アイテム作成のリクエストクラス
export class CreateItemReq {
  @ValidateIf((_, v) => v != null)
  @IsString()
  @IsNotEmpty({ message: DataValidationError.DATA_IS_EMPTY })
  readonly nameJa: string | null;

  @ValidateIf((_, v) => v != null)
  @IsString()
  @IsNotEmpty({ message: DataValidationError.DATA_IS_EMPTY })
  readonly nameEn: string | null;

  constructor(arg: { nameJa: string | null; nameEn: string | null }) {
    arg = arg || {};

    this.nameJa = arg.nameJa;
    this.nameEn = arg.nameEn;
  }
}

上記のリクエスト(Request Class)をフロントエンドでの API Request 前にバリデーションする場合は、次のようなコードになります。 SampleCode は、一部省略していたり、命名を変更しています。

import { ValidationError } from "class-validator";
import { ItemApi } from "$cms/api/ItemApi";
import { CreateItemReq } from "$shared/types/req/cms/item/CreateItemReq";
import ClassUtils from "$shared/utils/ClassUtils";
import { isValidationErrors } from "$shared-front/utils/TypeGuardUtils";
// ・・・ 省略・・・

export const useCreateItemModal = () => {
  // ・・・ 省略・・・
  const [errors, setErrors] = useState<ValidationError[]>([]);

  const createItem = async () => {
    setLoading(true);
    try {
      // ・・・ 省略・・・

      const [nameJa, setNameJa] = useState<string>("");
      const [nameEn, setNameEn] = useState<string>("");
      const [errors, setErrors] = useState<ValidationError[]>([]);

      // サポートされている言語ではない場合は、nullを設定する
      const req: CreateItemReq = {
        nameJa: isSupportedJa ? nameJa : null,
        nameEn: isSupportedEn ? nameEn : null,
      };

      // リクエストクラスに変換する処理。
      // convertToClass()の内部では、"class-transformer"のplainToClass()や、
      // "class-validator"のvalidate()を実行している。
      const request = await ClassUtils.convertToClass(CreateItemReq, req);

      // API リクエストを実行する処理。
      // class-validator の バリデーションを通過したら、API リクエストを実行する。
      await ItemApi.create(eventKey, request);
      // ・・・ 省略・・・
    } catch (err) {
      // エラーハンドリングの処理。
      // エラーが、class-validator のバリデーションエラーの場合は、エラーをセットする。
      if (isValidationErrors(err)) {
        setErrors(err);
      } else {
        ErrorUtils.handleError(err);
        throw err;
      }
    } finally {
      setLoading(false);
    }
  };

  // ・・・ 省略・・・
};

上記の Code 内のコメントの通り、ClassUtils.convertToClass()という EventHub のユーティリティ関数で、次のような 2 つの処理を内部的に行なっています。

  1. データをクラスに変換する。("class-transformer"のplainToClass()を使っている。)
  2. バリデーションを実行する。("class-validator"のvalidate()を使っている。)

そして、このClassUtils.convertToClass()は、主にフロントエンド/バックエンドのリクエスト/レスポンス のデータをやり取りする際に使用されています。

そして、class-validator の バリデーションを通過したら、API リクエストを実行する。

もし、バリデーションでエラーが発生した場合は、catch内のエラーをセットして、UI にバリデーション結果を通知するような仕組みになります。

また、このリクエストクラスは、フロントエンドだけでなく、バックエンド(NestJS)で受け取る型としても利用されています。

import { Body, Controller } from "@nestjs/common";
// ・・・ 省略・・・

@Controller("/api/cms/item")
export class CmsItemController {
  // ・・・ 省略・・・
  @Post(`/:${ParameterKey.eventKey}`)
  async create(@EventBody() event: EHEvent, @Body() req: CreateItemReq) {
    await this.dataSource.transaction(async (em) =>
      this.service.create(em, event, req)
    );
    return {};
  }
}

このようにリクエストを検証することでデータの安全性を担保することができます。

さらに、型を共有することでフロントエンドとバックエンドの間でデータ型を統一できるため、データの整合性も保ちやすくなります。

EventHub での class-validator の活用 Tips 📝

ここからは、EventHub での class-validator の活用 Tips📝 をご紹介していきます。

条件付き検証: @ValidateIf

@ValidateIf を使うと「ある条件が成立する場合だけ」特定のバリデーションを実行することができます。

条件の判定結果がfalseの場合は、バリデーション自体がスキップされるため、エラーも発生しません。

フォーム入力や設定項目など、「条件次第で必須かどうかが変わる」ケースなどで、柔軟に使えるデコレーターになります!

条件付き検証は、ドメインロジックに依存するバリデーションを実装するのに適しています。

EventHub では、ドメインロジックに応じたバリデーション・ルールの作成に@ValidateIfはよく使われています。

例えば、多言語対応で、バリデーションメッセージを言語ごとにカスタマイズする必要があるため、次のようなバリデーションを実装しています。

  • @ValidateIfを使って、日本語イベントの時だけバリデーション・ルールを付与する。
  • @ValidateIfを使って、英語イベントの時だけバリデーション・ルールを付与する。
import { Type } from "class-transformer";
import { IsNotEmpty, IsString, ValidateIf, MaxLength } from "class-validator";

import DataValidationError from "$shared/constants/DataValidationError";
import Spec from "$shared/constants/Spec";

export default class VideoBaseReq {
  // only for ValidateIf
  @IsNotEmpty() isSupportedJa: boolean;
  @IsNotEmpty() isSupportedEn: boolean;

  // 日本語イベントの時だけ、displayNameを検証する。
  @ValidateIf((o: VideoBaseReq) => o.isSupportedJa, { always: true })
  @IsNotEmpty({
    message: DataValidationError.DATA_IS_EMPTY,
  })
  @IsString({
    message: DataValidationError.DATA_IS_INVALID,
  })
  @MaxLength(Spec.maxLength.video.displayName, {
    message: DataValidationError.DATA_IS_TOO_LONG,
    context: { constraint1: Spec.maxLength.video.displayName },
  })
  displayName: string;

  // 英語イベントの時だけ、displayNameEnを検証する。
  @ValidateIf((o: VideoBaseReq) => o.isSupportedEn, { always: true })
  @IsNotEmpty({
    message: DataValidationError.DATA_IS_EMPTY,
  })
  @IsString({
    message: DataValidationError.DATA_IS_INVALID,
  })
  @MaxLength(Spec.maxLength.video.displayName, {
    message: DataValidationError.DATA_IS_TOO_LONG,
    context: { constraint1: Spec.maxLength.video.displayName },
  })
  displayNameEn: string;
  // ・・・ 省略・・・

  constructor(arg: {
    isSupportJa: boolean;
    isSupportEn: boolean;
    displayName: string;
    displayNameEn: string;
    // ・・・ 省略・・・
  }) {
    arg = arg || {};

    this.isSupportedJa = arg.isSupportJa;
    this.isSupportedEn = arg.isSupportEn;
    this.displayName = arg.displayName;
    this.displayNameEn = arg.displayNameEn;
    // ・・・ 省略・・・
  }
}

デコレーターの重ね合わせ

デコレーターは重ね合わせて、1 つのプロパティに対して複数のバリデーションデコレーターを併用することができます。 (複数のバリデーションを実行することができます)

例えば、次のようなメールアドレスのバリデーションを考えてみます。

  • 未入力でないこと
  • 256 文字以内であること
  • ASCII 文字であること
  • メールアドレス形式であること

上記の条件をすべて満たしている場合は、メールアドレスとして有効であると判定している処理になります。

import { IsAscii, IsEmail, IsNotEmpty, MaxLength } from "class-validator";

export class MailCheckReq {
  @IsNotEmpty({
    message: "未入力です",
  })
  @MaxLength(256, {
    message: "{{constraint1}}文字までです",
    context: { constraint1: 256 },
  })
  @IsAscii({
    message: "不正な値です",
  })
  @IsEmail(
    {},
    {
      message: "不正な値です",
    }
  )
  email!: string;

  constructor(email: string) {
    this.email = email;
  }
}

class-validator のカスタムバリデーター (Custom validation classes/Custom validation decorators)

class-validator の提供する標準的なデコレーターで対応できないバリデーション・パターンの場合は、カスタムバリデーターを作成することができます。

カスタム検証には大きく分けて以下の 2 パターンがあります。

  • Custom validation classes を作成して、@Validateデコレーターから呼び出す。
  • Custom validation decorators を作成して、デコレーターとして使う。

Custom validation classes

Custom validation classes は、@ValidatorConstraintデコレーターとValidatorConstraintInterfaceのインターフェースを使用して作成します。

EventHub では、Custom validation classes を使って、様々なカスタム・バリデーションを実装しています。

次のようなVideoKeyValidatorという Custom validation classes では、videoTypeによって受け取ったvideoKeyの検証を違う方法で行っています。

import {
  isAscii,
  IsIn,
  IsNotEmpty,
  IsString,
  IsUrl,
  Validate,
  ValidationArguments,
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from "class-validator";

import DataValidationError from "$shared/constants/DataValidationError";

enum VideoType {
  videoType1 = "videoType1",
  videoType2 = "videoType2",
}

/**
 * videoTypeによってvideoKeyをvalidateする。
 */
@ValidatorConstraint({ async: false })
class VideoKeyValidator implements ValidatorConstraintInterface {
  validate(videoKey: string, args: ValidationArguments) {
    const req = args.object;
    switch (req.videoType) {
      case VideoReq.videoType1: // VideoType1の場合は、URLであることを検証
        return IsUrl(videoKey);
      case VideoReq.videoType2: // VideoType2の場合は、URLではないことを検証
        return !IsUrl(videoKey) && isAscii(videoKey);
      default:
        // videoTypeが不正な場合は、バリデーションエラーとする。
        return false;
    }
  }

  defaultMessage(args: ValidationArguments) {
    const req = args.object;
    switch (req.videoType) {
      case VideoReq.videoType1:
        return DataValidationError.VIDEO_URL_IS_INVALID;
      case VideoReq.videoType2:
        return DataValidationError.VIDEO_ID_IS_INVALID;
      default:
        // videoTypeが不正な場合は、不正なデータとしてメッセージを返す。
        return DataValidationError.DATA_IS_INVALID;
    }
  }
}

Custom validation classes は、@Validateデコレーターを使って呼び出すことができます。

Custom validation decorators

Custom validation decorators は、registerDecoratorメソッドを使用して作成します。

EventHub では、Custom validation decorators を使って、様々なカスタム・バリデーションを実装しています。

例えば、ValidatePasswordという Custom validation decorators では、「password の値が必要な文字を 2 種類以上含んでいる」ことを検証するようにしています。

import { registerDecorator, ValidationOptions } from "class-validator";

import DataValidationError from "$shared/constants/DataValidationError";

/**
 * passwordの値が必要な文字を含んでいるかをvalidateする。
 * password文字列は次の4種類の内、2種類以上を含む必要がある。
 *
 * - 英小文字
 * - 英大文字
 * - 数字
 * - 記号
 */
export const ValidatePassword = (validationOptions?: ValidationOptions) => {
  return function (object: Record<string, any>, propertyName: string) {
    registerDecorator({
      name: "validatePassword",
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      validator: {
        validate(value: string): boolean {
          if (typeof value !== "string") {
            return false;
          }

          const containsLowercase = /[a-z]/.test(value);
          const containsUppercase = /[A-Z]/.test(value);
          const containsNumber = /[0-9]/.test(value);
          // ASCIIコードの中からsymbolを順に書いた。
          const containsSymbol = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(
            value
          );

          // 上のboolean変数4つのうち、trueの数を計算する。
          const numOfCharTypes = [
            containsLowercase,
            containsUppercase,
            containsNumber,
            containsSymbol,
          ].filter((e) => e).length;
          return numOfCharTypes >= 2;
        },
        defaultMessage() {
          return DataValidationError.PASSWORD_NOT_CONTAIN_EXPECTED_CHARS;
        },
      },
    });
  };
};

さいごに

今回は、EventHub で採用している技術の 1 つである class-validator についてと、その活用事例についてご紹介していきました 🐱

class-validator でフロントエンド, バックエンドの値を検証する仕組みは、プロダクトの信頼性を高めるので、これからも活用していきたいです 💪

もしこれを見て EventHub に興味をお持ちいただけたら、ぜひ以下のリンクから詳細をご覧ください!

jobs.eventhub.co.jp

note.com

参考・引用

class-validator GitHub

https://github.com/typestack/class-validator

class-validator npm

https://www.npmjs.com/package/class-validator

TypeScript Decorators

https://www.typescriptlang.org/docs/handbook/decorators.html#introduction

NestJS Validation

https://docs.nestjs.com/techniques/validation

class-transformer GitHub

https://github.com/typestack/class-transformer

class-transformer npm

https://www.npmjs.com/package/class-transformer

マネージャへの挑戦の壁をなくすために

はじめに

こんにちは!EventHub CTOの井関です。

これは EventHub Advent Calendar 2024 の24日目の記事です。昨日はCorpIT/RevOpsマネージャの横山さんの 2024年は、私の筋トレ元年 でした。そちらもぜひご覧ください!

adventar.org

今年は、普段人前に出るのが苦手な私が人生初のピッチイベント(CTO of the year 2024)に出場することになりました。これは、人事の磯さんのサポートのおかげです。イベント当日の内容については、ログミーさんが記事にまとめてくれていますので、興味のある方はぜひご覧ください!

logmi.jp

ピッチは6分間という限られた時間内で伝えたいことを絞り込む必要があり、内容を省略する作業は非常に難しかったです。今回は、その中でも特に省略した「マネジメント体制」について詳しくまとめたいと思います。

現在EventHubでは14名のエンジニアで4つのチームを組成しています。

各チームのマネージャにはプロダクト・テクノロジー・プロジェクト・プロダクトマネジメントの4つのマネジメント領域全てを任せる構成をとっています。

なぜ、このような体制をとったのか事業面・組織面に関しては伝えたことがありますが思いの部分をあまりまとめたことがなかったので、その点を今回はまとめます。

マネージャの役割

ピープルマネジメントの壁

創業初期は代表の山本と二人で創業したため、肩書はCTOでありながら1人で設計から実装までの全てをこなすような体制でした。

最初は資金が少なく、正社員を雇う余裕もなかったため、業務委託を中心にエンジニアを少しずつ増やしていきました。そして、次第にプロダクトマネジメントやプロジェクトマネジメントなどの領域も私が担当するようになりました。 事業が成長するにつれて、エンジニアも増え、ピープルマネジメントの必要性を感じ始めました。しかし、この「人をマネジメントする」ことは予想以上に難しく苦労しました。

この壁を乗り越えようと、知り合いや紹介してもらったCTO・EMの方々10人ほど話を聞いたのですが、それぞれの意見がまったく異なっていて、逆に悩みが深くなりました。

階層構造を作らずに50人ぐらいまではCTO一人で組織を見た方が良いという意見があると思ったら、早い段階で構造化し組織を分割すべきという意見もありました。CTOはコードを書かずに組織設計に回るべきという意見もあれば、CTOが率先してコードを書き方針を示すべきという意見もありました。

どの方も現職で成果を出していたのでどれも正解なのは間違いないとは思いますが、事業・組織、そしてCTOの思考によって全く逆の方針をとっていることもあり驚きました。ただ、全員が一貫していたのは自身で考え抜いて経験を重ね今のスタイルを構築したことが分かり、単に数時間ヒアリングしただけの自分が見よう見まねで取り入れても成果につながるような単純なものではないことも思い知らされました。

壁の乗り越え方

組織を成長させるためには、私自身がピープルマネジメントのスキルを身につけることが必要でした。しかし、スキルの向上と組織の成長のペースが一致せず、スキルが追いつかないという現実に直面しました。

そのギャップを埋めるために、代表のりえさんにサポートを頼んだり、チームメンバーに支えてもらいカバーしてもらうこともたくさんありました。ピープルマネジメントを一人でこなすのは難しかったですが、周りの力を借りて自分の得意領域で成果を出しながら進めることができました。

周りに支えてもらい苦労しながらの挑戦でしたが、少しずつスキルを身に着けたことで見えた景色は大きく変わりました。ピープルマネジメントのスキルが身についたことでプロダクト・プロジェクトマネジメントにまつわる業務での動きに影響があったりと、各スキルが他の領域で生きる場面が多いことにも気づきました。

チームとしての成果を最大化するために

チームマネージャに求めている役割は端的にまとめてしまうとチームの成果の最大化です。

それを実現する方法はチームマネージャ自身の強みや志向性も影響しますし、メンバーの強み・志向性にも大きく影響します。そのため役割を限定してしまい、渡す権限が不十分になってしまうとその人にあった方法を模索することができず思うような成果を出せなくなってしまいます。

私がピープルマネジメントの壁にぶつかった時にテクノロジー・プロジェクト・プロダクトの領域から離れてピープルマネジメントだけに集中しなければならないという制約が課されていたら、相当しんどい状況になっていたと思いますし、どこかで挫折してしまったと思います。自分の得意領域を活かしつつ、周りの協力を得ながら足りないスキルを補填していける環境は創業者としての権限や信頼があったからこそ実現ができたものだと思います。

このような環境を創業者でなくても作れる要素は何かと考えてとった方法が4つのマネジメント領域を全て任せる形です。

マネージャという役割に関して

組織が成長してくためには、マネージャーの数を増やすことが重要です。しかし、これは単にマネージャの肩書を持った人を増やすことを意味するわけではありません。本質的には、チームや組織の成果に責任を持ち、そのために行動する人を増やすことが求められます。

メンバーとマネージャの間には根本的には壁があります。任せられる領域・責任が広がることでどうしても求められるスキルなどは変わってきます。そのため、メンバーからマネージャになることは非連続で急な変化を求められてしまうものになります。

ただ、ここの段差が大きいとよりメンバーとマネージャの意識にも差が出てきてしまいもっとマネジメントに挑戦しづらい環境になります。誰しもがチームの成果に向き合い行動ができる環境を作り出すためには、メンバーからマネージャへの挑戦をよりなめらかで連続的にする必要があります。

メンバーとマネージャの段差をなめらかにすることで、少しずつマネージャに挑戦できる環境を作ることができ意欲がある方には少しずつひとつ上の業務に挑戦できる環境を作れます。また、挑戦したいメンバーが増えればチーム成果に責任を持とうと行動する方が増えてよりマネージャへの挑戦のハードルを下げることができ挑戦しやすい環境になります。

この状態を作るために4つのマネジメント領域全ての権限を渡せる環境を作りました。

一見すると4つのマネジメント全てを求めることは何でもできる万能人間しかマネージャになれないというようにハードルを上げてしまっているように見えますが、駆使できる能力の幅を広げることでむしろ挑戦するハードルを下げることができます。

最後に

メンバーとマネジメントの段差を少なくするための施策はまだまだ途中で現時点でも多くの段差がある状態です。ただ、今ではマネジメント未経験でも挑戦してくれた方が増えてその知見を蓄積していくことで少しずつその段差をなめらかにしていくことができています。

ぜひ、そんな挑戦をしてみたいという方は連絡をお待ちしてます!!

jobs.eventhub.co.jp

次の25日目の記事は、弊社代表の山本です!! お楽しみに!

チーム開発でボーイスカウト・ルールを実践する

こんにちは、EventHubの井上です。今回は『プログラマが知るべき97のこと』で紹介されている「ボーイスカウト・ルール」について、チーム開発でどのように実践しているかをお話しします。このルールを適用することで、コードの品質向上や技術的負債の削減につながることを実感しており、具体的な手法を共有します。

ボーイスカウト・ルールとは何か

ボーイスカウト・ルールとは、既存のコードに手を加える際に、そのコードに関連する箇所をより良い状態にリファクタリングすることです。簡単にいうと「来た時よりも美しく」です。例えば、以下のようなJavaScriptコードがあったとします。

const add = (a, b) => {
return a + b;
}

これを以下のように修正します。

// 整数を足し合わせる
const add = (num1, num2) => {
  return num1 + num2
}

説明コメントを追加し、変数名を変更し、インデントを整えました。挙動に変更はありませんが、変更前と比べるとコード・リーディング時に生じる疑問を減らすことができました。このように、実装時に関連処理をできる範囲できれいにするのが、ボーイスカウト・ルールです。

ボーイスカウト・ルールを実践しないとどうなるか

日々の開発で軽微な修正を重ねていくことをしない場合、処理の複雑性が増したり、サポート終了が近づいているライブラリを使い続けるなど、技術的負債が溜まっていきます。それを返済するには、どこかのタイミングで時間を確保してリファクタリングする必要が出てきます。競合のプロダクトが日々進化していく中で、その工数を確保する意思決定は簡単ではありません。実装者だけでなく、QAを含めたレビュワーの工数も必要になります。

また、リファクタリングの規模が大きくなるほど、その過程で新たなバグを生み出してしまう(デグレ)リスクが高まります。家の掃除に例えると、ゴミ屋敷になってしまってからではもう手遅れなので、日々の小さな工夫で、少しずつ綺麗にしていけばいいのではないでしょうか。

ボーイスカウト・ルールを実践する上での注意点

ボーイスカウト・ルールを実践する際には注意すべき点があります。「デグレのリスク増加」と「デリバリースピードの低下」です。平たく言うと、余計なことをしてバグを発生させたり、リファクタリングに時間をかけすぎることでリリースが遅くなってしまうリスクです。こういった負の側面から、触らぬ神に祟りなしということで、よろしくないコードに目を瞑りながら開発されている方も多いと思います。では、これらのリスクを最小化しつつ実践するにはどうすればよいのでしょうか。

デグレのリスクを最小化する

デグレのリスクを下げる上で、以下2点が重要だと考えています。

  1. 基本的に軽微な修正に留める
  2. テストコードがある状態で実施する

軽微な修正とは、コメント追加、lint修正などです。これらの修正は挙動に影響を与える可能性が低いため、安全に実施することができます。

ただ、処理の共通化やライブラリの刷新など、軽微ではない修正を実施することもあります。その際に、テストコードがない状態で修正を加えてしまうと、壊れていることに気づけないままリリースしてしまうかもしれません。なので、挙動に影響のある変更は、テストコードがあることを前提にすべきだと思います。

デリバリースピードの低下を抑える

実装工数が増えることは避けられないため、レビュー負荷削減の観点で、以下2点を意識するようにしています。

  1. コーディング規約に沿った形で改修する
  2. 差分が大きくなる場合はPR(Pull Request)を分ける

「来た時よりも美しく」の美しいの定義は人によって異なります。実装者はこれが美しい状態だと思っていても、レビュワーがそうではないと感じれば、PRの議論が白熱し、なかなかマージできなくなってしまいます。一方、コーディング規約に沿った変更であれば、意思決定がスムーズにいきやすく、レビューのリードタイムを削減することができます。

また、修正範囲が広がるとレビュワーの負荷が高くなるため、状況に応じてリファクタリング用のPRとメイン実装のPRを分けることで、レビューがスムーズにいきやすくなります。

ボーイスカウト・ルールの実践例

おさらいになりますが、個人としてボーイスカウト・ルールを実践する際に意識しているポイントは以下の4つです。

  1. 基本的に軽微な修正に留める
  2. テストコードがある状態で実施する
  3. コーディング規約に沿った形で改修する
  4. 差分が大きくなる場合はPRを分ける

ここからは、私が直近でどのようにボーイスカウト・ルールを実践したか非常に簡単な例を一つご紹介します。

コメントで@deprecatedアノテーションを追加する

EventHubではクラウドインフラとしてAWSを利用しています。AWS関連の関数はAWSUtilsというファイルにまとめられており、今回はある関数に改修を加えました。

この関数ではAWS SDK for JavaScript v2が使用されていましたが、v2は2025年9月にサポートを終了するというアナウンスがされていることを知りました。

aws.amazon.com

これを受けて、この関数については、SDK v3を使った書き方に変更しました。

この変更を行うだけでは、通常の実装と何ら変わりません。ここからがボーイスカウト・ルール実践の話になります。

他にもSDK v2を使用した関数がありましたが、他の関数に手を加えるのはデグレのリスクや工数の観点で断念しました。そこで、以下のようなJSDoc形式の関数コメントを追加することにしました。

/**
* AWS S3にファイルをアップロードする
*
* @deprecated
* sdk v2は2025年9月にサポートが終了するため、v3での実装を推奨します。
* 新たに処理を追加する場合は、v3を使用して再定義してください。
* @see https://aws.amazon.com/jp/blogs/developer/announcing-end-of-support-for-aws-sdk-for-javascript-v2/
*/
public static async uploadToS3(

このように@deprecatedアノテーションを付けることで、ソースコード上では以下のように打ち消し線が引かれ、非推奨の旨が表示がされるようになります。(VSCode、TypeScriptの場合)

@deprecatedアノテーションを付けた時の表示

この変更によって、後続の開発者はdeprecatedの関数を避け、v3対応を検討しやすくなります。結果として、将来のコード修正コストを削減し、技術的負債の再生産を未然に防ぐことができます。また、当然ですが、コメント追加のみのためデグレの心配はありません。

このように、小さな変更でも積み重ねることで、ソースコードの品質向上に繋がります。

tipsとして、直したい気持ちはすごくあるけど時間がないという時には、とりあえずコメントだけ残しておくのをおすすめしたいです。

直すだけではなく負債を増やさない心がけも大事

有名な話で、ソースコードの割れ窓理論というのがあります。建物の窓を割れた状態で放置すると、他の窓を割る心理的ハードルが下がりどんどん割られてしまう現象のことです。

ソースコードも同じで、汚いコードを残すと再生産されていってしまう傾向にあります。一度レビューを経たものだから、真似しておけば合意を得られるはずと考えるためです。ボーイスカウト・ルールで少しずつ綺麗にしていくのと同様に、日々の実装やレビューでそのようなコードを生まないようにする心がけが大事だと思っています。

まとめ

ボーイスカウト・ルールは、日々の小さな改善を積み重ねることで、プロダクトの健全性を保つための強力な指針です。チームの誰か一人がこのルールを実践し始めると、その姿勢が自然と周囲に伝わり、広がっていきます。私自身、今回ご紹介したボーイスカウト・ルールは、他のメンバーに影響を受けて実践し始めました。ぜひ、皆さんの開発現場でも取り入れてみていただければと思います。

EventHubにはコード品質を良くしていこうという文化があり、私はそれがとても気に入っています。テックブログ以外にも会社全体の雰囲気を知るためのブログやXもありますので、もし興味を持っていただけたらそちらもぜひ見てみてください!(エンジニア絶賛採用中です!)

jobs.eventhub.co.jp

note.com

x.com

 

Reactで動画プレイヤーを作成するならreact-playerがおすすめ!

はじめに

こんにちは!! EventHubのWebエンジニアの須田です!

今回、React で新しい動画プレイヤーを実装するための技術調査をしたので、その内容についてと、 React で動画プレイヤーを作成するなら react-player が簡単に実装できて、おすすめなのでご紹介していきます🐱

React 動画ライブラリ比較結果

2024年9月時点で、React の動画プレイヤー・ライブラリで調べて出てきたものを比較してみました👀

候補は「React 動画ライブラリ」で調べてでてきた次の 5 つです 🙏

  1. react-player
  2. mux-player-react
  3. react-video-js-player
  4. video-react
  5. video.js

npmtrends.com

結論:react-player がよさそう

判断基準は、次のとおりです👀✨

  1. 要件を満たす機能を備えていること。
  2. React 対応のライブラリであること。
  3. 正式リリースがされており、安定版であること。
  4. 定期的にメンテナンスがされていること。
  5. npm trends や GitHub Star などを参照して、使用率や人気がある状態であることが望ましい。

調べた感じ「React ✖️  動画ライブラリ」の選択肢が、あまりない状況に見えました👀

react-player とは?

react-player は、React アプリケーションで動画や音声などのメディアを再生するためのオープンソースの React コンポーネントです。

YouTube、Vimeo、Mux、Twitch、SoundCloud、Facebook など、さまざまなプラットフォームからのコンテンツをサポートしています。

また、シンプルな API と豊富なプロパティを提供しており、メディアプレーヤーのカスタマイズや制御が簡単です 💪🥺🔥

npm や GitHub は、次のとおりです 👀✨

npm: react-player

www.npmjs.com

GitHub: react-player

公式 Doc がない代わりに、GitHub の README.md にて設定できる Option に関する説明が細かく掲載されています👀✨

github.com

React Player デモ環境

react-player のデモで、いろいろな動画タイプや、パラメーターを試すことができます 🙏

cookpete.github.io

ちなみに、react-player のメンテナンスは、Mux チームが引き継ぐらしいです 🙌

(Mux は、アメリカの同名のソフトウェア会社が運営する、API ベースの動画配信サービスです)

参照:https://www.npmjs.com/package/react-player#-the-future-of-reactplayer

react-player で動画プレイヤーを作成する

それでは、実際に react-player で動画プレイヤーを作成していきます。

react-player をインストール

npm install react-player

yarn add react-player

シンプルな Video Player を実装してみる

試しに、シンプルな Video Player を実装してみると、 これぐらいのCode量で、簡単な Video Playerが実装できます👀✨

import ReactPlayer from "react-player";

interface SimpleVideoPlayerProps {
  videoUrl: string;
  isLoop?: boolean;
  isAutoPlay?: boolean;
  isControls?: boolean;
}

export const SimpleVideoPlayer = ({
  videoUrl,
  isLoop,
  isAutoPlay,
  isControls,
}: SimpleVideoPlayerProps) => {
  return (
    <ReactPlayer
      url={videoUrl}
      width={"100%"}
      height={"100%"}
      playing={isLoop} // 自動再生
      loop={isAutoPlay} // ループ再生
      controls={isControls} // 動画の操作が可能かどうか
    />
  );
};

React Player の Option について

react-player の README には、渡せる Props や Callback 系などの設定値の一覧がまとまっているので、これを見れば何ができるのかは、把握できます🙆‍♂️

また、Youtube, Vimeo, Mux などの各動画配信サービスの設定プロパティにも対応しているのも、Goodなポイントです🙌

Props

主な Props は、次のとおりです👀✨

Prop Description Default
url 再生するビデオまたは曲のURL。array または MediaStream オブジェクトを指定可能。
playing true または false を設定してメディアの再生や一時停止を行う。 false
loop true または false を設定してメディアをループ再生する。 false
controls ネイティブプレーヤーコントロールを表示するかどうかを設定。Vimeoビデオの場合、コントロールを非表示にするにはビデオ所有者が有効にする必要がある。 false
light true に設定するとビデオのサムネイルのみが表示され、クリックでフルプレーヤーが読み込まれる。プレビュー画像をオーバーライドするには画像URLを渡す。 false
volume プレーヤーの音量を 0 から 1 まで設定。null の場合、全プレーヤーでデフォルトの音量が使用される。 null
muted プレーヤーをミュートにする。volume が設定されている場合のみ機能。 false
playbackRate プレーヤーの再生速度を設定。YouTube、Wistia、およびファイルパスでのみサポートされている。 1
width プレーヤーの幅を設定。 640px
height プレーヤーの高さを設定。 360px
style ルート要素に inline styles を追加。 {}
progressInterval onProgress コールバックの間隔をミリ秒単位で設定。 1000
playsinline 対応している場合に playsinline 属性を適用。 false
pip true または false を設定してピクチャー・イン・ピクチャーモードを有効化または無効化する。ファイルURLで再生している場合のみ、特定のブラウザでサポートされている。 false
stopOnUnmount pip を使用している場合、stopOnUnmount={false} を使用してReactPlayerがアンマウントされてもピクチャー・イン・ピクチャーモードで再生を続けるようにする。 true
fallback 遅延読み込みを使用している場合に使用するフォールバック要素またはコンポーネント。 null
wrapper コンテナ要素として使用する要素またはコンポーネント。 div
playIcon ライトモードで再生アイコンとして使用する要素またはコンポーネント。
previewTabIndex ライトモードで使用するタブインデックスを設定。 0
config 各プレーヤーの設定を上書きするオプションを指定。config propを参照。

参照・引用: https://github.com/CookPete/react-player?tab=readme-ov-file#props

Callback Props

主な Callback Props は、次のとおりです👀✨

Prop Description
onReady メディアがロードされ再生の準備が完了した際に呼び出される。playingtrue に設定されている場合、メディアは即座に再生される。
onStart メディアの再生が開始されたときに呼び出される。
onPlay メディアが再生または一時停止後やバッファリング後に再開されたときに呼び出される。
onProgress playedloaded の進捗状況を割合で提供するコールバック。秒単位で playedSecondsloadedSeconds も提供される。例: { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 }
onDuration メディアの長さ(秒単位)を含むコールバック。
onPause メディアが一時停止されたときに呼び出される。
onBuffer メディアがバッファリングを開始したときに呼び出される。
onBufferEnd メディアのバッファリングが完了したときに呼び出される。ファイル、YouTube、Facebookで動作する。
onSeek seconds パラメータでメディアがシークされたときに呼び出される。
onPlaybackRateChange プレーヤーの再生速度が変更されたときに呼び出される。YouTube、Vimeo、Wistia、およびファイルパスでサポートされている。
onPlaybackQualityChange プレーヤーの再生品質が変更されたときに呼び出される。YouTubeでのみサポートされている(有効にした場合)。
onEnded メディアの再生が終了したときに呼び出される。looptrue に設定されている場合は発火しない。
onError メディアの再生中にエラーが発生したときに呼び出される。
onClickPreview ユーザーが light モードのプレビューをクリックしたときに呼び出される。
onEnablePIP ピクチャー・イン・ピクチャーモードが有効になったときに呼び出される。

参照・引用: https://github.com/CookPete/react-player?tab=readme-ov-file#callback-props

Youtube Video Player を実装する

続いて、Callback Props を活用した Youtube Video Player を実装してみます。

Callback Props の挙動を確認するために、再生中かどうかを確認するようなFlag判定を持たせています。

また、onProgressに関しては、動画の再生状況のデータの内容を確認できるようにlog出力を仕込んでみます。

実際に実装した画面は、次のような感じです👀✨

import { Fragment, useState } from "react";
import ReactPlayer from "react-player";
import type { OnProgressProps } from "react-player/base";

interface YoutubeVideoPlayerProps {
  videoKey: string; // youtube video key
  autoPlay?: boolean; // 自動再生フラグ
  originUrl?: string; // ルートURL
}

// TODO: 実際に実装する際は Logic は Custom Hook に切り出す
export const YoutubeVideoPlayer = ({
  videoKey,
  autoPlay,
  originUrl,
}: YoutubeVideoPlayerProps) => {
  // 再生中フラグ
  const [isPlaying, setIsPlaying] = useState(false);

  const handlePlayOn = () => {
    setIsPlaying(true);
  };

  const handlePlayOff = () => {
    setIsPlaying(false);
  };

  // 再生中に定期実行されるコールバック
  const handleProgress = (progress: OnProgressProps) => {
    console.log("onProgress Called", progress);

    const {
      played, // 動画の再生済み部分を全体の割合で示した値 (0〜1)
      playedSeconds, // 動画の再生済み時間を秒単位で示した値 (秒)
      loaded, // 動画の読み込み(バッファリング)済み部分を全体の割合で示した値 (0〜1)
      loadedSeconds, // 動画の読み込み済み時間を秒単位で示した値 (秒)
    } = progress;

    // 再生済みのパーセンテージを表示
    console.log(`再生位置(%): ${(played * 100).toFixed(2)}%`);
    // 再生済みの時間を表示
    console.log(`再生位置(秒): ${playedSeconds.toFixed(2)}秒`);

    // 読み込み済みのパーセンテージを表示
    console.log(`読み込み済み(%): ${(loaded * 100).toFixed(2)}%`);
    // 読み込み済みの時間を表示
    console.log(`読み込み時間(秒): ${loadedSeconds.toFixed(2)}秒`);
  };

  return (
    <Fragment>
      <ReactPlayer
        url={`https://www.youtube.com/watch?v=${videoKey}`}
        width={"100%"}
        height={"100%"}
        playing={autoPlay} // 自動再生
        loop={false} // ループ再生
        controls={true} // 動画の操作が可能かどうか
        onStart={handlePlayOn} // 再生開始時 Callback Func
        onPlay={handlePlayOn} // 再開する時 Callback Func
        onPause={handlePlayOff} // 一時停止時 Callback Func
        onEnded={handlePlayOff} // 再生が終了した時 Callback Func
        progressInterval={1000} // onProgressの実行間隔(秒) Default: 1000(1秒)
        onProgress={handleProgress} // 再生中に定期実行される Callback Func
        // 各プレーヤーごとの独自設定
        config={{
          /**
           * YouTubeのプレーヤーのパラメータ設定 Docs
           * https://developers.google.com/youtube/player_parameters?playerVersion=HTML5&hl=ja
           */
          youtube: {
            playerVars: {
              autoplay: autoPlay ? 1 : 0, // 自動再生
              playsinline: 1, //  iOS 上の HTML5 プレーヤーで動画をインライン再生する
              origin: originUrl, // ルートURL指定
              rel: 0, // パラメータの値が 0 に設定されている場合、関連動画は表示されません。
            },
          },
        }}
      />
      <p className="flex items-center justify-center mt-5 text-lg">
        再生中: {isPlaying ? "Yes" : "No"}
      </p>
    </Fragment>
  );
};

さいごに

今回は、React で新しい動画プレイヤーを実装するための技術調査として、react-playerを使って動画プレイヤーを試しに実装してみました。

react-player は、動画プレイヤーを簡単に実装できオプションも豊富なので、おすすめです。

もし、これをみてEventHubに興味をお持ちいただけたら、ぜひ以下のリンクから詳細をご覧ください!

jobs.eventhub.co.jp

note.com

参考・引用

zenn.dev

www.npmjs.com

github.com

EventHubにおけるリリースフローの紹介

はじめに

こんにちは。EventHubの西内です。

今回はEventHubで開発した機能がどのようなフローで本番環境にリリースされるかについて、ご紹介できればと思います。

採用面接の際、リリースフローに関する質問をいただくことがありますし、質問はせずともCI/CDがどんな感じなのか知っておきたい方もいるかと思いますので、ぜひ参考にしていただければなと考えています。

記事のスコープ

話すこと

  • 開発サイクル
  • ブランチの運用フロー
  • CI/CDの概要

話さないこと

  • リリース後の話
  • 開発着手前や着手中の細かい話

開発サイクル

EventHubでは週に1度リリースを行うサイクルで開発を行っています。

そのサイクルでリリースを行えるようにするために日々テストコードをメンテナンスしており、何らかの機能開発や修正を行ってもデグレードが起きていないことをできる限り確認できるようにしています。

さらに本番環境(production)と同等の構成で検証可能なQA環境(qa)を用意しており、そこでテストを行ったり開発以外の部署に方に触ってもらったりもしています。

開発サイクルの話題を冒頭に持ってきた理由としては、どのぐらいの速度感でプロダクトをデプロイしていきたいかがブランチの運用やリリースフローといった設計の妥当性に関わると考えているからです。

ブランチの運用とリリースフロー

先程の開発サイクルは、以下の図のようなブランチ運用によって成り立っています。

なお、ローカルでのブランチ運用に関してはエンジニアによって異なるので、一例として捉えていただければと思います。

通常のブランチ運用

  1. 開発時にはmainブランチから開発用にローカルブランチを作成
    • 開発が済んだらPull Requestを作成してレビューを行い、2件以上の承認を得たらmainにマージ(Squash)します
    • ちなみにEventHubではtopicブランチごとに動作確認が可能な環境がAWS上で自動的に構築されるので、その環境を使って都度検証を行います
  2. 週に1度、mainブランチでリリースバージョンのタグを作成し、それを元にしてqaブランチ向けにPRを作成
    • productionブランチ向けのPRは前回リリース後に、その時点のqaブランチを元にして作成してあります
    • その後、qaとproduction向けのPRをマージしておき、リリースに備えます
  3. 翌日のリリース予定時刻になったら、qaとproductionのCIを開始させて自動デプロイ

このフローは私が入社(2023年6月)するよりも前から存在しており、ブランチ運用やリリースフローのドキュメントの履歴を見ると2021年9月には既に出来上がっていたようで、そこからほぼ変わっていないみたいです。

さて、ここまで紹介してきたEventHubでのブランチ運用やリリースフロー、および開発サイクルですが、以下のような良さがあるのではないかと考えています。

  • 機能追加や修正内容に応じてヘルプページの作成/編集などを行う猶予がある
    • 週に一度Sprint Reviewと称して、QA環境や本番環境にリリースされる内容を社内に共有しています。小さい追加修正などはそこで初めて知ることがあるものの、実際にリリースされるまでの時間があるのでヘルプページの作成/編集や特定の顧客への連絡などが必要であっても、うまく回せていると感じています
  • 別の機能を開発中にQA環境でも発生する不具合が見つかったら、本番環境にリリースされるまでの間に修正ができる
    • だからといって、普段の検証をおろそかにしてもいいわけではないのですが、リカバリー可能であることで多少は心に余裕ができますね
  • いわゆるトランクベースな開発のメリットを一部享受できる
    • わかりやすい例としてはコンフリクトが起きづらくなりますし、PullRequestも小さくなるのでレビューしやすくなることでしょうか

mainブランチにマージしてから約2週間後に本番環境へリリースされるわけですが、もしかすると人によっては遅いと感じられたりするかも知れません。

しかし、以上のような事柄を加味すると、ちょうど良いサイクルなのではないかなと私は感じています。

CI/CDの概要

EventHubではCI/CDのツールとして、Circle CIを採用しています。

CI/CDにはQA環境や本番環境へのデプロイ以外に、日々の開発を支えるパイプラインも用意しています。開発ブランチ用のパイプラインでは、主に「自動テスト」と「ブランチごとの検証環境構築」のJOBが実行されるようになっています。

EventHubの自動テストの種類の割合は、バックエンドのコードに対してのテストがざっくり90%を占めているのではないかと思います(カバレッジ等の指標は追っていないため個人の感覚です)。

もちろん、フロントエンドでも必要であればコンポーネントのテストを書いたり、独立したfunctionのテストを書いたりすることもあります。また、ヘッドレスブラウザを使ったEnd-to-Endの自動テストを書くこともあります。

Cicle CIはSlackと連携させているので、失敗するとすぐにわかるようになっています

また、EventHubのリポジトリはモノレポの構成を採用しているため、フロントエンドとバックエンドのソースコードが1つのリポジトリで管理されています。このモノレポの良さの1つとして挙げられそうなのは、CI/CDの構築のしやすさがあるのではないかなと感じています。

先程も少し触れましたが、topicブランチごとに動作確認が可能な環境がAWS上で自動構築されるようになっています。

こういった仕組みを異なるリポジトリを組み合わせて行おうとすると、なかなか複雑なものになると思いますし、後から参画した人にとっても触れづらい領域になってしまいかねないと思うので、EventHubぐらいの規模感であればモノレポで開発できることはメリットだと捉えています。

さいごに

「EventHubの開発やリリースってこんな感じでやってるんだな」というのが良くも悪くも伝わっていれば幸いです。

そして、テックブログ以外にも会社全体の雰囲気を知るためのブログやXもありますので、もし興味を持っていただけたらそちらも是非見てみてください!

jobs.eventhub.co.jp

note.com

x.com

TSKaigi 2024では配信プラットフォームとしてEventHubが採用され、弊社CTOの井関がスポンサーLTで登壇したときの動画がありますので、こちらも是非。

www.youtube.com