PHPのレガシーシステムをTypeScriptで刷新! フロントエンドとバックエンドの職能の壁は壊せるのか?

こんにちは!「ぐるなびウエディング」開発チームの滝口(@ytakiguche)です。普段はサーバーサイド開発を担当しています。

私たちのチームは現在、オンプレミスで長年稼働してきた PHP のシステムをAWSクラウドへ移行し、同時に TypeScript で全面的に書き換えるという、大きな挑戦の真っ只中にいます。

この記事では「フロントエンドとバックエンドの言語統一」をテーマに、その過程で明確になった技術的課題と、それらに対する我々のアプローチについてお話しします。

目次

プロジェクトの背景: なぜ、PHP から TypeScript へ移行したのか

「ぐるなびウエディング」は、結婚式場探しから二次会、記念日までをサポートする、歴史あるサービスです。その裏側では、長年の改修を重ねた PHP システムが複雑化し、いくつかの大きな課題に直面していました。

  • 型活用が不十分なコード基盤:   - PHP 7/8 には型宣言や静的解析がありますが、既存コードが多く、型の恩恵を十分に受けられていませんでした。その結果、改修時の影響範囲の特定やデバッグに時間がかかっていました。

  • 属人化と技術的負債:   - 複数の言語が混在し、ドキュメントが整理されておらず、有識者の異動や退職のたびに知識が失われ、まさに「割れ窓理論」のように小さな問題の積み重ねがシステム全体の健全性を損なっていました。

  • 開発リソースの分断と非効率:   - 限られたメンバーで開発・運用を行う中で、フロントエンド(以下FE)とバックエンド(以下BE)の専門性が分断されてしまったため、タスクに応じた柔軟な人員配置が難しく、開発リソースを効率的に活用できていませんでした。

これらの問題を解決するため「PHP のコード基盤を活かすよりも、TypeScript で新たに書き直す」という全面的な再構築を選択しました。

TypeScript 対応とモノレポ化

FE と BE で使用する言語を TypeScript に統一し、モノレポ構成で管理する方針としました。 FE 開発で TypeScript が標準的になっている現状を踏まえ、言語統一の選択肢として TypeScript が最適だと判断しました。

「プログラム言語を統一すれば FE と BE の担当領域を限定せず、チーム全体の生産性向上が見込めるはずだ」

この理想を実現するため、技術スタックを刷新し、モノレポ構成を採用しました。

バックエンドのフレームワークには NestJS 、モノレポ管理ツールには Turborepo を採用しました。いずれも社内での採用実績があり、安定した開発基盤を構築できると判断し、採用を決定しました。

技術スタックとディレクトリ構成

.
├── apps      # 各アプリケーション
│   ├── api     # APIサーバー (NestJS)
│   ├── bat     # バッチ処理 (commander.js)
│   └── web     # Webフロントエンド (Next.js)
├── infra     # インフラ定義 (AWS CDK in TypeScript)
├── packages  # 共通ライブラリ・モジュール
└── docs      # ドキュメント一式 (markdown)

packages ディレクトリに型定義や共通関数を配置することで FE/BE 間でのコード共有を促進し、開発の効率化と安全性の向上を目指しました。

プロジェクトで直面した課題

プロジェクトを進行する中で、プログラム言語統一だけでは解決できない、主に2つの課題が明らかになりました。

課題1: 既存データの不整合

最初に直面した課題は、既存 DB のデータ不整合です。特に、nullable なカラムの扱いが問題となりました。

管理画面の仕様上は必須入力となっているフィールドでも DB 定義では NULL が許容されているケースが多数存在しました。 TypeScript の厳格な型に合わせる上で、どこまで null を許容するか(null assertion / 型ガード / 必須化)という設計判断は API のレスポンス仕様にも影響を及ぼす難しい問題となりました。

課題2: FE と BE のスキルセットと設計思想の差異

プログラム言語を統一しても、FE/BE それぞれに求められる専門知識と設計思想(デザインパターン)は大きく異なります。例えば、FE ではコンポーネントベースの設計や状態管理が重要視される一方、BE ではデータベース設計や API 設計、インフラ構築が中心となります。

実際に、お互いの領域に踏み込むには、以下のようなスキルギャップがありました。

  • BEエンジニアに求められたスキル:   - FE特有のコンポーネント設計やスタイリング、PHPがメインだったエンジニアにとって TypeScript の言語仕様やエコシステムへの習熟など。   - (例:Container/Presentational、Hooks/Composition といったFE特有の設計パターンの理解など)
  • FEエンジニアに求められたスキル:   - BEで採用したオニオンアーキテクチャの理解、ドキュメントが不十分な既存DBの構造を読み解くスキル、CDK(Composite パターン)による IaC など。

BE で採用した オニオンアーキテクチャは FE には馴染みが薄く、逆もまた然り。互いの「当たり前」が通用しない場面が多くありました。

課題へのアプローチ

上記の課題に対し、チーム内で議論を重ね、以下のような対応を取りました。

1. データ不整合への段階的対応

nullable 問題に対し、当初は型安全性を重視して厳格な型の絞り込み(narrowing)を適用しました。しかし、データ不整合の件数が想定以上に多く、開発の進行を妨げる要因となったため、方針を変更しました。

現在は、一律で warning ログを出力して処理を継続させ、データ自体の修正はサービス運用フェーズで優先度を付けて段階的に対応していく計画です。

2. スキルギャップと文化差への対応

スキルと設計思想の差異に対しては、効率性とチーム内での知識共有のバランスを考慮したアプローチを取りました。

  • 学習とスキルトランスファー:   - プロジェクト初期には、BE エンジニアが Udemy などを活用して TypeScript のキャッチアップを行いました。また FE エンジニアが簡単な API ã‚„ IaC を実装するとことでバックエンドへの理解を深めました。
  • 設計の標準化:   - BE のディレクトリ構成は package-by-feature を基本としつつ、データアクセス層を分離する「リポジトリパターン」を導入することで、両者の設計思想の利点を組み合わせた構成としました。
  • 柔軟なタスクアサイン:   - 大規模な機能開発は、効率を優先して従来の職能(FE/BE)に基づき担当者を決定しました。一方で、テスト工程での軽微な修正などは、意図的に担当領域をまたぐ形でアサインし、相互のコード理解を促進する機会を設けています。

FE/BE で分かれたチームではなく、あくまで一つのチームです。だからこそ、すべてを混ぜ合わせるのではなく、お互いの強みを活かしながら、現実的な方法で協力体制を築いていくことを目指しました。

まとめと今後の展望

「TypeScriptに言語を統一すれば、FEとBEの担当領域の壁はなくなるのか?」という問いに対し、現時点での結論は「なくならないが、協業はより円滑になる」です。

言語統一は、全ての問題を解決する「銀の弾丸」ではありませんでした。しかし、チーム内のコミュニケーションを活性化させ、コードの相互レビューを容易にするなど、プロダクト開発の品質と速度を向上させる上で有効な手段であることは間違いありません。

プロジェクト完了後は、リポジトリが一つに集約されることで、影響調査の工数が大幅に削減され、新メンバーの学習コストも低減されることが期待されます。これにより、より迅速な機能開発とリリースが可能になると考えています。


滝口
趣味はバスケとスノボ。英会話とピアノを習いたいと思ってかれこれ6年以上経過。
好きな言葉は「Done is better than perfect.完璧を目指すよりまず終わらせろ。」
現在『キングダム』からチームビルディングを勉強中。
'; relatedArticle += ''; createLinkList.push(art.link); if (createLinkList.length == 5) { return false; } }); return [relatedArticle, createLinkList] } var showRecommend = function(items1, items2) { var createLinkList = []; var relatedArticle = '
おすすめ記事
'; } else { relatedArticle += articleList1[0] + '
'; } $('footer.entry-footer').before(relatedArticle); } var feedUrl = "/feed"; var myCategory = $("div.entry-categories a:first-child").text(); if (myCategory != "") { feedUrl = feedUrl + "/category/" + myCategory; } getFeed(feedUrl) .then(function(data) { first_items = parseFeed(data); return first_items; }) .then(function(first_items){ var second_items = []; if (first_items.length < 5 && feedUrl != "/feed") { getFeed("/feed").then(function(data) { second_items = parseFeed(data); return [first_items, second_items]; }) .done(function(items) { showRecommend(items[0], items[1]); }) } else { showRecommend(first_items, second_items); } })
'; //挿入するタイトルのHTML for (var i = 0; i < num; i++ ){ var entry = result.feed.entries[i]; var entryImg = ""; var imgCheck = entry.content.match(/(src="http:)[\S]+((\.jpg)|(\.JPG)|(\.jpeg)|(\.JPEG)|(\.gif)|(\.GIF)|(\.png)|(\.PNG))/); //画像のチェック entryImg += ''; if(entry.link != presentUrl){ container.innerHTML += '
' + entryImg + '
' + entry.title + '
' + '
' //挿入する関連記事のHTML }else{ num ++ //今のリンクのときは、表示せずもう1つ記事を取り出す } } } } } google.setOnLoadCallback(initialize);