💭

外注で初期開発したシステムを内製化するためにやったこと

2023/12/27に公開

この記事は FastDOCTOR After Advent Calendar 27日の記事です。

はじめに

ファストドクター株式会社でテックリードをしている shirauix と申します。

弊社では、ある Next.js アプリケーションを別会社のパートナーさんに外注することによって初期開発を行いました。ある時点からこのシステムを内製化することになったのですが、それにあたって多くの課題を解決する必要がありました。

この記事では、外注と内製のそれぞれのメリット・デメリットや、内製に切り替える際にどんな苦労があったのかについての赤裸々な事例をご紹介します。

対象となる読者

  • 外注で初期開発したシステムを内製に切り替えてメンテナンスしようとしているエンジニアの方
  • 新しくシステムを開発したいが、外注と内製のどちらを選択すべきか悩んでいる方

外注と内製の違い

外注するか内製するかはあくまで手段の話であり、どちらにもメリット・デメリットが存在するため、状況に応じて選択する必要があります。

あくまで私見ですが、それぞれのメリット・デメリットには以下のようなものがあります。

  • 外注の特徴
    • メリット
      • 柔軟に開発人員を確保できる(必要になったら増員し、不要になったら削減できる)
      • 特定の領域に強い専門人材を投入できる
      • 組織として開発の成果責任を担保してもらえる
    • デメリット
      • 十分なスキルを持った開発会社を探すのが難しい
      • 社内にノウハウが蓄積しない
      • セキュリティ・パフォーマンス・保守性の高いソースコードなどの非機能要件を求めることが難しい
      • 細かい指示を伝えることが難しい
  • 内製の特徴
    • メリット
      • 適切なスキルを持ったメンバーをアサインできる
      • 社内にノウハウが蓄積できる
      • 非機能要件も考慮した開発を行うことができる
      • コミュニケーションが容易
      • 中長期的にプロダクトを一緒に良くしていくぞというマインドが持てる
    • デメリット
      • 柔軟に開発人員を確保できない

外注によってスピード感をもってシステムを立ち上げ、内製で中長期的に価値を生み出し続ける、といった体制は多くのスタートアップで見られるスタイルかと思います。
その一方で、それぞれの特徴を把握した上で使い分けないと、後々の開発に悪影響を及ぼす場合があります。
実際、今回の事例においては、 「フタを開けてみたら我々が期待するコード品質レベルには達していなかった」 という点に苦労させられました。

しかし、これは開発した企業に責任はなく、あくまで発注側の責任と考えます。
一般に、限られた期間の中での開発においては機能要件を満たすことが優先されるため、コード品質等の非機能要件にまで手が回らないのが現実かと思います。
また、本当にそれが必要なのであれば、発注側が要件として明示すべきです。 今回の開発ではそのあたりを要件に含められていなかったことが大きな反省点となっています。

以降は、この状態のソースコードを受け取ったエンジニアチームがどのように改善を行なっていったか、具体的な実施事項について書いていきます。

課題の分析

内製化が決まってから、まず最初に行ったのはコードの分析です。コードを読み込み、現状の課題を把握した上で、これからどういった方針で改善を行なっていくかを決めていきました。

エンジニアとしてはとにかく手を動かしてリファクタリングしたくなるのですが、そこはグッとこらえて 「課題の可視化」「重要度の明確化」 に時間を割きました。
課題がわかっていないのに、解決方法を考えても効果的な打ち手が打てないためです。

以下が抽出した課題の例です。

  • コンポーネントが適切に分割されていない
    • 1つのファイルに複数のReactコンポーネントやフックが書かれているなど、非常に見通しが悪い状態になっていました
    • また、1つのReactコンポーネントに大量のロジックが書かれているため、数千行におよぶファイルや引数が40個もあるコンポーネントも存在していました。
  • 自動化されたテストがない
    • ユニットテストがありませんでした
    • また、さまざまなロジックが密結合していたため、そのままではテストを書くのが困難な状態になっていました
  • カプセル化が十分ではない
    • ある機能を実現するためのロジックがさまざまな場所に点在しており、把握が困難になっていました
  • 状態変数が非常に多い
    • 機能自体はそれほど多くないアプリケーションなのですが、状態変数が非常に多くなっていました
    • また、 useContext() 等の仕組みや Recoil 等の状態管理ライブラリが使われておらず、大量の状態変数がバケツリレーで引きまわされている状態になっていました。
  • アドホックなスタイリング
    • ある場所では Tailwind CSS が使われている一方、それ以外の場所では直接 style 属性が指定されていたりと、バラバラな実装になっていました。
    • また、レスポンシブウェブデザインを実現するにあたり、CSSにてメディアクエリを使えば良いところ、TypeScriptで画面幅に応じて動的に適用するスタイルを切り替える、といったロジックが多用されていました。
  • ユーザビリティやアクセシビリティの考慮不足
    • 機能要件自体は満たしていたものの、提示したデザインが十分に再現されていなかったり、挙動に不自然な点が多く見受けられました。
  • 不要なライブラリの使用
    • Array.prototype.filter() といったJavaScript標準で実現可能な操作に対して lodash ライブラリが使われているなど、不要なライブラリが使われているケースが散見されました。
  • パフォーマンスの考慮不足
    • 大量のAPIが同期的に呼ばれるためユーザーの待ち時間が長いなど、パフォーマンスに対する考慮が漏れていると思われる実装になっていました。

繰り返しになりますが、 発注時にこれらの要件を伝えていなければ発注側の責任です。 ただ、内製していくにはこれらの課題をクリアしていかなければ今後の機能追加は厳しいと判断せざるを得ませんでした。

また、これらの分析結果は役員やマネージャー陣にも共有を行いました。
ソースコードの品質はエンジニアにしか見えないため、 単に「リファクタしたい」といったところでなかなかその重要性は伝わらず、そこに貴重な工数(つまり高い人件費)を投入して良いか判断しかねる からです。
そのため、課題を赤裸々に共有し、この状態で今後改修していくのは厳しいことや、開発に時間がかかることを前もって伝えました。
幸い、弊社の役員やマネージャー陣はエンジニア経験を持つ方も多いため、すぐに課題を理解していただくことができました。

改善のために取り組んだこと

次に、本格的な内製化に向けての取り組みを開始しました。

大きく分けて、以下の取り組みを行なっています。

  • コードの内部品質の改善
    • 前述の課題を解消するためのリファクタリングを行い、スピーディに改修を行うための基盤を整えました。
    • 特に、ディレクトリ構成やCSS設計など方針が曖昧だった領域については明確な方針を立て、それに従って改善が行えるようにしました。
  • チーム開発を行うための仕組み化
    • フォーマッターやCI基盤などを整備し、繰り返し作業をなるべく自動化することで、作業を効率化しました。
    • また、レビュー時のルールを決めるなど、コミュニケーションコストを下げ、質の高いレビューを行える環境を整えました。

以降では、これらの具体的な取り組みについてご紹介していきます。

コードの内部品質の改善

ディレクトリ構成の改善

最初に取り組んだのは、巨大なコンポーネントの解体とディレクトリ構成のリファクタリングです。

まずは目指すべきディレクトリ構成のガイドラインを策定しました。これについて書くとそれだけでひとつの記事になってしまうため詳細は割愛しますが、以下のような方針を立てました。

  • ディレクトリはなるべく細かく切り、1つのファイルやディレクトリが1つの責務しか持たないようにする
  • features ディレクトリを切り、フィーチャー単位でモジュール化することにより密結合を避ける
  • 各モジュールには index.ts ファイルを置いて最低限のシンボルのみを export し、モジュール内をリファクタリングしやすくする
  • プロダクトコードとテストファイルを同ディレクトリ内に配置することでテストが書かれていないことを明示的にし、テストを書きたくなるようにする
    • 例: useDialog.tsuseDialog.test.ts を同ディレクトリに配置する

自動化されたテストが整備されていない中で巨大なコンポーネントを分割したり、ファイルを移動したりするのはなかなか怖い作業でしたが、幸いTypeScriptを採用していたため、型チェックによってすぐに import 漏れなどの問題に気付くことができました。
使っててよかったTypeScript。

ユニットテストの追加とテストカバレッジの可視化

Jest を導入し、機能を追加する際には基本的にユニットテストを書くようにしました。現時点でのカバレッジはようやく20%を超えたところですが、長期的に改善していくつもりです。

なお、テストは闇雲に増やせば良いわけではありません。そのあたりの考え方は下記の書籍が非常に参考になったので、未読の方には強くおすすめします。

単体テストの考え方/使い方

CSSのリファクタリング

CSSについての実装方針がなかったため、下記の方針を立てました。

  • CSS Modulesに一本化する
  • レスポンシブウェブデザインはCSSのみで実現する (TypeScriptで頑張らない)

特に前者についてはさまざまなCSS in JSライブラリが利用可能なため、アプリケーションやチームによって最適な選択肢は異なるかと思います。今回は以下のようなロジックで判断しました。

  • Tailwind CSSは学習コストが高く、 class 属性を多用するため、可読性が低くなる (好みはあると思います)
  • CSS Modulesであれば生のCSSが書けるため、学習コストが低い。また、 class 属性の変換によってグローバルスコープ汚染を防いでくれるという必須の機能を備えている

CSS Modulesについては型サポートがない不便さやメンテナンス体制に対する不安もありますが、将来的に他のCSS in JSに移行するコストが相対的に低そうであると判断しました。

余談ですが、今回は採用を見送ったものの、Tailwind CSSの Utility-First という思想には一見の価値があります。これについて作者のAdam Wathan氏がブログにまとめている記事が非常に秀逸ですのでおすすめです。(検索すると日本語訳も出てきます)

CSS Utility Classes and "Separation of Concerns"

ライブラリの継続的なアップデート

利用しているライブラリは継続的にアップデートしていく必要があります。 Node.js や Next.js は開発開始時のだいぶ古いバージョンが使われていたため、アップデートを行いました。

今後は Renovate 等のツールを導入し、容易にアップデート可能な仕組みを整備していく予定です。

チーム開発を行うための仕組み化

フォーマッターやリンターの整備

以下のツールを導入し、エンジニア間でコードのスタイルが統一されるようにしました。

  • EditorConfig
    • 改行コードやインデントの数などを設定した内容に自動的に統一してくれるツール
  • Prettier
    • 「行末にセミコロンを付けるか」「シングルクォートとダブルクォートのどちらを使うか」などのスタイルを設定に応じて統一してくれるツール
  • ESLint
    • JavaScript/TypeScriptについて、さまざまなルールをチェックし、安全なコードを書けるようにしてくれるツール

地味な話ではありますが、「ここに , が抜けてますよ」といった瑣末な内容をレビュー時にコメントするのは、レビューする側にとってもされる側にとっても時間の無駄であるため、自動化することを強くおすすめします。

また、自動的にスペルチェックをしてくれる Code Spell Checker のようなIDE拡張もおすすめです。「ButtonBotton になってますよ」といったスペルミスの指摘を行うのはお互いにとても辛いので。。

プルリクエストのテンプレートの整備とレビューの必須化

エンジニア2名体制で開発を行うにあたり、下記のようなプルリクエストのテンプレートを作成しました。

効果的なレビューを行うためには、実装時に考えたことをきちんとレビュアーに伝える必要があると考えます。そのため、レビュアーに伝えてほしいことを網羅できるようなテンプレートにしました。

## やったこと
(このPRでやったことの概要を書く。チケットがあればリンクを貼る)

## なぜやったか
(変更の目的や達成したかったことを書く)

## どうやったか

### 方針
(変更の方針について書く。変更内容が大きい場合はADRを作成して議論したのち、ここにそのリンクを貼る)

### 詳細
(変更の詳細について書く)

## 動作検証

### 確認したこと
(どんな環境で何を確認したのか書く。必要に応じてスクリーンショットも貼る)

- [ ] aaaaが動作することを確認した
- [ ] bbbbが動作することを確認した
- [ ] ccccが動作することを確認した

## その他、レビュアーに伝えたいこと

### 特に確認してほしいこと・気になるポイント
(あれば書く)

### 参考情報
(実装の参考にした記事や関連チケットなどがあれば書く)

また、GitHubのリポジトリの設定にて、レビューされていないPRはマージできないようにしました。

CIの整備

GitHub Actionsを整備し、以下の項目が自動的に実行されるようにしました。

  • TypeScriptの型チェック
  • ユニットテスト
  • ESLint
  • Next.jsのビルド

基本ではありますが、自動化することによりチェック漏れを防ぐことができます。

pre-commit フックの導入

ソフトウェア開発においては、フィードバックの速さは最重要の概念のひとつです。

何か問題があった際に最速でわかるよう、 git commit 時に以下が自動的に実行されるようにしました。

  • TypeScriptの型チェック
  • ESLint
  • Prettier

プルリクエストをプッシュするより早く問題に気付くことができるため、非常に便利です。

なお、この仕組みの実現には以下のツールを利用しています。

また、将来的には以下のようなツールによるチェックも導入していこうと考えています。

特に後者のMarkuplintは正しいHTMLを書くための大きな助けになってくれるツールですので、個人的に非常におすすめです。

エラー監視体制の整備

安定したサービス運用のためには、障害やエラーに対して即時に反応できる体制を作っていくことが必須です。

弊社では Datadog を導入しているため、以下のようなアラートを設定しました。

  • 外形監視によるサーバダウンの検知
  • ブラウザエラーの検知
  • サーバエラーの検知
  • など

アラートはSlackに通知され、担当エンジニアがすぐに反応できるようにしています。

ブランチ単位でのレビュー環境の整備

Heroku の Review Apps 機能を利用し、ブランチ単位で動作確認を行うことができる環境を構築しました。

これによって開発中の要件について容易に挙動を確認してもらうことができるようになりました。

今後やりたいこと

これまでは致命的なバグの修正や上記で紹介したような内部品質の向上で手一杯だったのですが、今後は新機能を追加しつつ、以下のようなこともやっていきたいと考えています。

  • ユーザビリティやアクセシビリティの改善
  • パフォーマンスの向上
  • 状態管理ロジックの洗練
  • など

まとめ

この記事では、外注によって初期開発を行なったシステムを内製に切り替えるためにやってきた具体的な実施事項についてご紹介しました。

細かい実施事項をいろいろと紹介しましたが、念頭においたのは下記の点です。

  • 方針を決める。 一貫した判断ができるようにする。
  • 自動化できるものは自動化する。 人間の貴重な時間はより重要なことに集中させる。
  • 優先度をつける。 限られた時間の中で何に取り組むか明確にする。
  • 課題を共有し、周囲の理解を得る。

冒頭にも書いたとおり、外注と内製にはそれぞれのメリット・デメリットがあります。
それぞれの特徴を理解し、外注する場合も、のちのち必要になる要件を明確に伝えることで、よりスムーズに内製に切り替えることが可能になるかと思います。

本記事で紹介した一つ一つの実施事項はかなり基礎的な内容ではありますが、少しでも参考になれば幸いです。

Discussion