DMM.comの、一番深くておもしろいトコロ。

DMMブックスのフロントエンドチームで改善文化を作った話

DMMブックスのフロントエンドチームで改善文化を作った話

  • このエントリーをはてなブックマークに追加

この記事は、DMMグループAdvent Calendar 2021の11日目の記事です。

みなさんはじめまして。DMM.com の中瀬古 渉(@wn_seko)と申します。
電子書籍事業部GrowthWebチームのメンバーとして主にDMMブックスのフロントエンドの開発を担当しています。

本記事では、私が入社してから現在に至るまでの1年間にDMMブックスのフロントエンドでやってきた改善について紹介していきます。

ここに書いてあることは何一つとして目新しいことはやっていません。当たり前のことを明文化して、当たり前にやっていく文化を構築することが最上位の目的となっています。メンバーとして限られた権限の中でできることをやったつもりなので、記事を通してみなさんの改善の手がかりになれば幸いです。

目次

課題に感じていたこと

チームの開発フローとして、プロダクトオーナーなどから上がってきた売上に直結する施策を、優先度順に開発して順次リリースをしています。ただ、施策の優先度が高い関係で、開発効率やユーザー体験を向上させるリファクタリングや環境整備があまりできていない状態でした。
仮にこれらの改善を実施したい場合は、案件内で発生した余剰時間を利用して、改善した内容を案件と共にリリースする必要があります。また、各々がやりたい改善提案そのものや方向性がチーム内で共有・管理されておらず、細かい改善はできても一つの大きなゴールに向けて一貫した改善をできないという問題がありました。

これらの課題を解決するために大きく分けて 2 つの軸で改善を進めました。
1 つ目は現状の問題点を可視化して改善フローを回していくための「文化的改善」、2 つめは具体的に開発効率やユーザー体験を向上させる「技術的改善」です。

文化的改善

文化的改善では改善フローを回すための時間と方法を確保し、改善する文化を定着させます。
改善文化が定着することで問題が発生時やルールの曖昧さを発見した時に素早く方針修正を行うことができます。これは改善時だけでなく、日頃の案件でも行われるようになりました。

必要な時間の確保

改善を行う決意をしても時間が確保できなければ実行できません。
具体的に必要な時間としては「改善タスクをこなす時間」と「改善自体を精査する時間」の 2 つです。前者は、やることが決まったタスクを実際にこなすことで抱えている課題を解決していきます。後者は、提案された改善内容が目的に沿っているかの検査や、行った改善が効果を出しているかの確認を会議体で行います。

改善の時間を確保するために、まず改善の目的を定めてやること・やらないことを定義してチームの合意をとりました。その上で、案件が手すきになったタイミングでのタスクをこなす時間、1 週間に 1 時間の改善を精査する時間のそれぞれの確保をプロダクトオーナーと交渉して合意をとりました。

改善の目的の定義

闇雲にやりたいリファクタリングを行うのは楽しいですが効率が悪くなりますし、人によってやりたいことの方向性が異なるので収束させることが困難になってきます。また、目的のない改善は投資効果が薄いためプロダクトオーナーとの合意を取りづらかったりします。改善の目的を定めることでチームとしての改善の方向性を定めてやること・やらないことの区別をします。

私達は「プロダクトをグロースするためのPDCAサイクルを回す」ことを大目的とし「開発効率を改善する」「チームをスケールする」「ユーザー体験を向上する」の 3 つの小目的を定めました。
新規の改善案が提示されると、これらの小目的のどれに当てはまるかをラベル付けし、ここから大きく外れるものに関してはやらないという合意をしました。

改善のための優先順位としては、開発効率を改善する > チームをスケールする >>> ユーザー体験を向上する に定めました。プロダクトをグロースする上での最重要課題はあくまで施策案件を効率よく回すことになるため、効率改善やマンパワーを増やすことに重きをおいています。

開発効率を改善する

開発効率を改善するためにやることは「スピードと質の向上」です。開発しやすい体制を整えることでリリースサイクルを早める「スピードの向上」とバグを未然に防いで安定性を高める「質の向上」を行っていきます。
バグを未然に防ぐことは一見すると効率の改善につながらないように見えますが、バグを防ぐための保守性・可読性の高いコードや体制は既存機能の改修の際に役立ちますし、バグ修正対応を減らすことでメインの開発に力をいれることができます。

効果測定方法として、バグの件数に関しては定量的な判断ができます。リリースサイクルに関しては案件サイズがまちまちなので判断が難しいですが、過去の似た案件と比較して実際の開発時間がどのくらい変わったのか、過去の環境と現在の環境の両方で見積もりの開発時間がどのくらい変わったのかを比較します。

チームをスケールする

チームのスケールにはスキルトランスファーによって属人性を下げつつ各人のできることを増やす「スケールアップ」と、暗黙知を明文化することで新しい人を受け入れやすくする「スケールアウト」に主眼をおいています。

スケールアップでは、スキルトランスファーを行うことで各人の得意なドメインの交換を行います。
これを行ったきっかけは、ある基盤等を実装した人がそのまま案件で使用する状態が長く続いていたため、他の人が仕様について全く理解できない状態が続いていました。この状態が続くと属人化が発生して、例えばその人が退職した際にオーパーツ化する恐れがあります。そこでドメインについて詳しくなるために、タスク交換を行います。不明点等をその場でヒアリングする、暗黙知をドキュメント化することで属人性を下げる効果が期待されます。また、これにより各人が自分のドメイン領域を広げて、より広い範囲のタスクにあたることができます。

スケールアウトでは、暗黙知の明文化を行うことで新しく入ったメンバーが迷わずに実装できる環境を整えます。

前述したドメイン知識の明文化に加えて、過去にはlintルールに曖昧な部分があったり、ディレクトリに対するファイルの配置ルールをメンバーが説明できずに迷うケースがありました。迷ったときはプルリクエストなどで軽く議論を行い、決定した内容をwikiに反映することで次回実装するときや新規メンバーが参入したときに迷わずに実装できるようになります。

現在、これらの取り組みは進行中で振り返りが行えていないので効果については不明ですが、タスク交換や明文化によって仕様把握が捗ることを期待してます。

ユーザー体験の向上

ユーザー体験を向上することでDMMブックスのファンを増やし定着することでプロダクトのグロースにつなげます。

現在のDMMブックスは PHP が配信するHTMLにjQueryで動きをつけているレガシーな環境とAPIとMPAが分離しているモダンな環境が混在しています。レガシー環境とモダン環境ではページの読み込み速度に大幅な差があるため、まずはすべてのページでレガシー環境を脱却することを目標としています。
また、読み込み速度の向上は SEO にも有利に働くため、サービスの知名度向上にも繋がります。

効果測定方法として、ページの読み込み速度とバンドルサイズの定期的な計測と振り返りを行います。

改善ボードの設置

改善案は日々の業務の中で多く生まれます。これらを任意のタイミングで気軽に提案しやすくすることで改善が行われやすい文化をつくります。蓄積された改善案は妥当性を判断した上で優先度がつけられます。妥当性の判断は前述のとおり目的ラベルを付けることができるか、優先度の判断はラベルの種別とグロースへの影響の大きさから判断します。

チーム内ではこれらの改善案の管理に ZenHub を使用しています。
すべての改善案は例外なく ISSUE テンプレートに従って「背景」「目的」「受け入れ条件」「タスク」の定義がされます。提案段階でこれらが記載されていることで、目的に沿った改善案であるかの判断をすることができたり、時間が経ってしまって不要になったタスクを捨てることができるようになります。また、背景や目的によっては実装ではなく運用などでカバーすることも重要です。やることを極力減らすことで少ないリソースを効率よく改善に回すことができます。

会議体の設置

会議体は「週次定例」と「月次定例」の 2 つを設置しています。
「週次定例」では主に改善案の精査や優先度付けを行います。新規に提案されたものはここでやる・やらないの判断をした上でチームメンバー全員がタスクを取れる状態にします。「月次定例」では主に効果測定と振り返りを行います。定期的に会議体を開く理由には、進捗や効果の確認を行うことで改善文化が廃れることを防ぐ目的もあります。

週次定例

週次定例では新規に提案されたISSUEの確認、終了したISSUEのクローズ、進捗の確認を行います。
新規に提案されたISSUEはチームメンバー全員で「背景」「目的」「受け入れ条件」「タスク」の確認を行い、不明点を洗い出した上でラベル付けを行います。ラベルをつけられないものがあれば、やらないと判断されてそのままクローズされます。ラベル付けをされたISSUEは優先度を考慮してBacklogかIceBoxに追加されます。終了したISSUEは「受け入れ条件」を確認して問題がなければクローズされます。

何らかの理由で定例中にISSUEを追加しなければならなくなった場合は、その場では書かずにISSUEを書くTODOを議事録に残して終了します。ISSUEを短時間で書こうとすると内容に不足が生まれる可能性が高まりますし、その場で書くことによって貴重な定例の時間が少なくなるためです。 これらのTODOは次週の定例までの宿題となり、週次定例の最初に確認します。

月次定例

月次定例では各種指標の確認と振り返りを行います。指標は以下のとおりです。

  • テストカバレッジ
    • ユニットテストカバレッジ
  • パフォーマンス
    • Page Speed Insight
    • バンドルサイズ
  • エラーログ
    • Sentry

各指標に対して前月との比較を行い、問題があれば ISSUE を立てて対応します。
ここで作成されたISSUEはMonthly Backlogという専用のレーンに置かれて翌月の月次定例までの宿題になります。

また、月次定例ではNodeやnode_modulesのアップデートの確認も行います。アップデートを怠るとセキュリティーの問題が発生した際に大掛かりな改修が必要になるため、こまめな確認とアップデートは重要になります。
メジャーバージョンアップなど、アップデート内容によってはやる・やらないの判断を行う場合もあります。ここでアップデートが必要になった場合は、アップデート用のISSUE作成をTODOに残して月次定例を終了します。

バックエンドメンバーへの波及

フロントエンドメンバーの改善文化定着を受けて、バックエンドメンバーにも改善フローの提供を行いました。
こちらは先日レクチャーを終えたばかりなので効果測定はできていませんが、フロントエンド・バックエンドの双方が改善を行うことで、より広い範囲でのタスクにあたれることが期待されます。

技術的改善

技術的改善では、現在の状態を理想の状態に近づけるために、ロードマップを意識しながら改善タスクをこなしていきます。DMMブックスでは開発効率の改善を優先するため、既存コードの構造整理から始めて共通コンポーネントの括り出しまでを行います。
コンポーネントの再利用性向上や確認フローを整理したことで、新規案件での実装や既存コードの修正を容易にできるようになりました。

迷わず実装するための改善

方針が決まっていないことによって実装時に迷ってしまうケースを防ぐためにルールを整備します。
これは悩む時間をへらすことで効率改善を行うだけでなく、チームメンバーとルールを共有することで誰もが気軽にルールを修正の提案をできる環境を作ります。

ディレクトリ構造変更

既存のディレクトリ構造はチームメンバーでも「どちらに入れればよいか」「ディレクトリの役割はなにか」を明確に答えられない状態にありました。そこで、ディレクトリの役割とインポート方向のルールを明確に割り振りチーム内で合意をとった上でドキュメント化を行いました。

ディレクトリの構造が決定したあとは実際にコードに反映してきます。
ディレクトリの構造変更時に注意した点として、ファイルの場所とインポート先以外を書き換えをしないようにしました。Github上でレビューを行う関係でファイルの場所とコードを同時に編集してしまうと差分が追いづらくなります。コードの分離に関しては後続のタスクにまわして、ここではディレクトリ構造の定義とそれに伴うファイルの場所の変更のみにとどめました。

Lint 整備

詳細は省略しますが、大きな変更として前述のディレクトリのインポートルールをLintに追加することで、インポートの方向が一方向になるようにしました。これによってインポート違反を検出して無秩序にアーキテクチャが破壊されることを防ぎます。

既存のLint環境はtsconfigを設定した上でeslintをpre-commitで実行していましたが、全ファイルが検査対象になっていたのでコミットごとに 26 秒程度かかっていましたが、検査対象を差分に絞ることで 12 秒程度まで短縮できました。
また、stylelintを導入することでCSSに関してもlintレベルのルール統一を行いました。理想であれば型チェックも行うべきですが、コミット時間が大幅に伸びてしまうのでCIで確認するようにしています。

開発を効率化するための改善

ルール整備が完了した段階で実際のコードに手を入れていきます。ここでは堅牢性を担保してデグレによるバグを防ぐためのTypeScript化やコンポーネントの再利用性向上、作成環境の構築を行いました。

JavaScript の TypeScript 化

既存コードでは新規に書かれた一部のみがTypeScriptで書かれていて、残るほとんどのコードがJavaScriptで書かれていました。幸いにもtsconfigはstrict: true等の厳しめのルールが設定されていたためTypeScriptで書かれている部分には一旦手を加える必要がなさそうでした。

TypeScript化をするときに注意した点としては、レイヤーの低い層から修正を加えたこととページ単位で修正したことの 2 点があげられます。
レイヤーが高い層から修正するとインポート元がJavaScriptのままになるので型があっているのかを検査しづらくなります。インポートの必要がないUIコンポーネントやドメイン層から実装を行うことでこれを防ぎます。
また、ページ単位で修正することで影響範囲を限定しました。DMMブックスでは(よほど小さな修正でない限り)リリースのたびに影響のあるページに対してデグレチェックや新機能チェックのための検証項目書を作成して検証を実施しています。一度に大きな修正を行ってしまうと検証箇所が増えてコストがかかる上にチェック漏れが増える可能性があります。

コードの修正は以下の順序で行いました。

  1. tsconfigにルールを追加
  2. ドメインモデルの型を作成
  3. UI コンポーネントのTypeScript化
  4. その他のコンポーネントのTypeScript化
  5. リクエスト層のTypeScript化
  6. ページのTypeScript化

ディレクトリ構造変更の際にインポートの方向が一方向になっているため比較的容易に修正できたと思います。
コンポーネントの中にはhooksが分離されていなかったり複雑なロジックを持っていたりと分解したいポイントはいくつもありましたが、ここではインターフェースの定義以外の修正は行いません。型がついていない時点でコンポーネントや関数を分離する等のインターフェースを増やすことをしてしまうとデグレが発生する可能性が高まります。

今回はやりませんでしたが、コンポーネントが複数のページで共有されている場合はJSXとTSXの並行稼動を行うことも考えていました。修正対象以外のページで使用されている JSX を残しておくことで、一時的にビルドサイズは増えますがリリース時に影響範囲から完全に切り離すことができます。

モダン環境におけるTypeScriptの割合は、修正前で 17.27% でしたが、修正後には 97.53% まで上昇しました。
また、レガシー環境にもTypeScriptのビルド環境を用意したので、今後新規の開発を行う際にはこちらもTypeScriptで書くことができるようになりました。

コンポーネント分割

既存のコードでは各ページごとにに大きなコンポーネントが定義されており、内部で複雑な出し分けやループ処理が行われている状態でした。
各ページに散らばっていたコンポーネントの中でドメインモデルを共有するコンポーネントや再利用可能なUIコンポーネントを切り出して共通化を図ります。これらをまずドメインモデル単位で分割して共通コンポーネントとして括り出した後、その中で使用されているコンポーネントを汎用的なUIコンポーネントとして切り出しました。

コンポーネントを共通化したことで新規のページやパーツの開発工数を大幅に短縮できます。直近行った案件では新規の開発に対して工数を半分以下に減らすことができました。

hooksに関してはルールが完全に定まっていないので、既存コードには手を入れず、暫定ルールに則って新規開発や追加開発の場合のみ分離する方針で仮置きしています。

StoryBook の導入

DMMブックスの開発ではデザイナーとフロントエンドエンジニアの役割が明確に分けられており、デザイナーが作成したfigmaを元にエンジニアがコンポーネントを実装します。ここでデザインシステムが存在せずにページ単位でデザインが作成されるため、コンポーネントの再利用がしづらい点が問題になります。
StoryBookの役割はデザイナーと作成したUIコンポーネントを共有することでデザインを再利用してもらうことにあります。また、レガシー環境の脱却のときにページを作成せずにUIコンポーネントを作成してデザイナーにレビューしてもらうことができるという副次的な目的もありました。

モック改修

DMMブックスはレガシー環境とモダン環境が混在しているため、ルーティングをPHPで行ってReactを含むJavaScriptをページ単位で配信するMPAになっています。そのため、ローカルでの開発はDockerを起動する必要がありますが、起動やリロードの時間が非常に長いという問題をもっていました。
そこで、ページ単位でモックを作成することで起動やリロードを素早く行うためにモック環境があります。

しかし、既存のモック環境はいくつかの問題を抱えていました。webpack.configがプロダクション用とモック用で共有されているためページやパーツを追加するたびにプロダクションとモックの双方の設定を更新する必要があります。通信方式もaxiosをインターセプトしてモックデータに流していたため、プロダクションコードにモックのための条件分岐が存在していました。
また、インターセプト後の条件分岐も同一のAPIに対してGETとPOSTを同時に設定できないなどの制約が存在します。

これらの問題を解消するために以下の改修を行いました。

  • webpack.config をプロダクション用のものとモック用のものに分離
  • Mock Service Worker(MSW) を導入
  • jQuery で書かれていたデバッグツールを React に変更

webpack.configを分離したことで追加のページやパーツを作成したときにモック用のコードを修正しない選択を取ることができるようになりました。またMSWを導入したことでaxiosのインターセプトのためのコードをプロダクションコードに書く必要がなくなりました。

まとめ

改善文化が定着した大きな要因は主に以下の 3 つをどの粒度でも意識したことだと思います。

  • フローとルールを整備した
    • 文化的な改善フローを整備した
    • 技術的な改善の方向性を整備した
    • ルールを整備して合意形成を取った
    • ルールをlintレベルで縛った
  • 可視化して共有した
    • 課題を可視化して共有する場所・時間を作った
    • ドキュメントを共有する場所・文化を作った
  • 小さな単位からシンプルに解決した
    • フロントエンドの改善からバックエンドの改善、そして一貫した改善へ
    • 文化的な改善から技術的な改善へ
    • 小さなタスク粒度による小さなリリース単位

分解してみるとどれも当たり前のことですが、これらを積み重ねていくことが改善文化を定着につながるのではないかと思います。

まだまだ改善すべき点やその先にあるユーザー体験の向上、やりたい案件はたくさんあります。DMMブックスではこれらの開発を一緒にやってくれるエンジニアを募集しています。この記事で興味を持ってくれた方がいれば、是非一緒にDMMブックスを盛り上げていきましょう!