
はじめに
プラットフォーム開発部CSプラットフォームグループ、フロントエンドエンジニアの金粕です。
私は2022〜2023年中頃にかけて、ヘルプ・問い合わせサイトとその管理システム(現:ヘルプセンターシステム)のリプレイスプロジェクトに携わりました。
本プロジェクトの概要に関しては以下の記事をお読みください。
https://inside.dmm.com/articles/new-helpcentersystem/
このプロジェクトに途中参入した経験を通して、元々はフロントエンドエンジニアではなかった私が感じたことや、直面した苦労についてお話しします。
異動の転機
現チーム配属前の経歴
以前いたチームは、ライブチャットサービスの運用に携わるチームでした。
そこでは販促用・集客用の新規施策に向けた開発やバグ改修、データ集計による施策提案がメインのタスクでした。
新規施策に向けた開発では、細かな仕様を決定する要件定義、デザイナーさんから受け取った画像・HTML・CSSを用いた画面制作、データベースのテーブル設計やPHPでのコーディング、JavaScriptを利用した簡単なUI作成までを行いながら、サービス運営全般に携わっていました。
使用していたのはPHPがメインで、フレームワークはLaravelやCakePHPです。
MVCモデルに則ってフロントエンドからバックエンドまで幅広く、UI作成をしたりデータベースの設計・管理を行ったりと多岐にわたるタスクを担当していました。
そのため、複数人での開発を行う場合もフロントエンド/バックエンドで分担するというよりも、必要な処理で分けるような体制を取っていました。
ただ、そのチームでは保守・運用のタスクがメインで、大規模な開発やリプレイスはありませんでした。
大規模開発やリプレイスを経験したいと思い、リプレイス途中だったこのチームに参加しました。
また、業務外ではC言語やGo言語、Pythonにも個人的なプロジェクトで触れていました。
CSプラットフォームグループへの参画
ヘルプセンターリプレイスプロジェクトのチーム構成は、プロダクトオーナー1名、フロントエンドエンジニアとバックエンドエンジニアがそれぞれ5名ずつの合計11人で開発を進めていました。プロジェクトは最初のリリースに向けて残り半分程の進捗でした。
プロジェクトに参画した際に、私はフロントエンドの開発チームに配属されました。フロントエンドの開発では、言語としてTypeScriptを使い、Reactにサーバーサイドの機能が付いたNext.jsというフロントエンドフレームワークを使っていました。
さらに、状態管理にRecoil、テストはJest、コンポーネントカタログにはStorybookを採用してAWS ECS上で動作させていました。
フロントエンドの開発環境ではモノレポを採用しており、その管理を専門チームが行うことで、私たちは言語やフレームワークのバージョンアップを心配することなくコーディングに集中できるという利点がありました。
ちなみにモノレポとは、複数アプリケーションを単一のリポジトリで管理する手法で、各ライブラリのバージョン管理や独自ライブラリの仕様を容易にするなどのメリットがあります。
こちらの記事で詳細に説明されているので、よかったら読んでみてください!
https://inside.dmm.com/articles/dmm-frontend-ecosystem/
フロントエンド開発への挑戦
フロントエンド開発のための学習
TypeScriptやNext.jsを学ぶために業務中に時間を取ることはしなかったので、主に3つの手段で学びながら業務を進めました。
まず、用意された資料による学習です。この資料はフロントエンドのモノレポ基盤を提供しているチームによる学習資料で、リポジトリに導入しているライブラリの扱い方が記載されていました。
そして、実装済みのコードからもさまざまな知識を得ました。リリースに向けて中盤に差し掛かったプロジェクトということもあり、実装も充実し始めていました。ライブラリを使っている部分も多くあり、使い方のヒントになりました。
最後に、チーム内でのコードレビュー会による指摘です。これにより、誤った知識の訂正や疑問の解消ができました。自分の書いたコードはもちろん、他の人のコードもその場でレビューするため、チームとしての実装の軌道修正やそれまでと違う考え方・知見など新しい発見がありました。
また、コードレビュー会では基盤を提供しているチームのメンバーに相談する場も設けてもらい、自分たちとはまた違った観点での指摘や意見を伺うこともしていました。
TypeScriptやReactでの苦労と学び
これまで扱っていたフロントエンド開発はHTMLベタ書きやPHPを使用し、条件分岐や変数の出力、フレームワークやライブラリを用いずに書かれたJavaScriptでの簡単なUI作成程度だったため、コンポーネントごとのUI設計やSPAのルーティング、状態管理は未経験でした。JavaScriptからTypeScriptへ静的型付け言語に変わったことへの抵抗は特になかったのですが、ライブラリが絡んだ場合をはじめ、いくつか苦労点がありました。それについては次に紹介します。
TypeScriptの分割代入、スプレッド構文
私が扱っていたバージョンのPHPには存在しなかった概念が、TypeScriptの「分割代入」と「スプレッド構文」です。
この概念に初めて触れた私としては、当時はこの新たな構文に違和感しかありませんでした。
分割代入とは、配列やオブジェクトを分解し、それぞれの要素を新たな変数に代入することを指します。一方、スプレッド構文は、配列式や文字列などの反復可能オブジェクトを展開するための構文です。
それらを使用した短いサンプルコードを以下に示します。
type FruitBasket = {
apple: number;
banana: number;
orange: number;
};
const getTotalPrice = ({apple, banana, orange}: FruitBasket) => {
return apple + banana + orange;
};
const sample: FruitBasket = {
apple: 100,
banana: 50,
orange: 80,
};
console.log(getTotalPrice({...sample, banana: 100}));
ここでは、分割代入によりFruitBasket型のsampleオブジェクトから各フルーツの値を個別に抽出しています。そして、スプレッド構文を使ってsampleオブジェクトの中のbananaの値を変更し、それをgetTotalPrice関数に渡しています。
スプレッド構文によってsample以外に新たな変数を宣言することなく、別の要素を持ったオブジェクトをgetTotalPrice()に渡すことができました。
React hooks
現チーム配属前にもReactを扱ったことはあったのですが、久しぶりに触れると新機能が追加されていました。
useState()やuseMemo(), useCallback()など、新しい概念で当初は苦労しました。
stateを使うことで値が変更されることでコンポーネントを再描画させることができたり、反対に値や関数をmemo化することで余計な再描画を抑えたりできます。
const [name, setName] = useState('');
const message = useMemo(() => {
return '名前は' + name + 'ですか?';
}, [name]);
const handleChangeName = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
},
[]
);
return (
<>
<input
type="text"
onChange={handleChangeName}
value={name}
/>
<p>{message}</p>
</>
);
上記は入力された名前を元にメッセージを出力する処理です。
このように、useStateを使用してstateを作成し、useMemoで無駄な計算を防ぐことができます。さらに、useCallbackを用いてイベントハンドラの再作成を防ぐことで、パフォーマンスを改善することができます。
ライブラリの利用
下記はreact-hook-formというライブラリを使った実装の一部です。
ライブラリは引数に独自の型を使用していたり、使い方が特殊だったりするため、初見ではなにがなんだかわかりません。
ドキュメントを読んで、その使い方を理解するまでに時間がかかったというのが正直な感想ですが、一度それが理解できれば非常に便利なツールであることに改めて気付きました。
// 1. yupでバリデーションスキーマを作成
const schema = yup.object().shape({
name: yup.string().required(),
email: yup.string().required().email(),
});
export const Form = () => {
// 2. useFormを使ってフォームを作成
const { register, handleSubmit, errors } = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* 3. register関数を使って各フォームフィールドを登録 */}
<input name="name" ref={register} />
{errors.name && <p>This field is required</p>}
<input name="email" ref={register} />
{errors.email && <p>This field is required</p>}
<input type="submit" />
</form>
);
};
react-hook-formはReactでフォームを簡単に扱うためのライブラリです。
本格的な利用にはドキュメントの熟読が必要になりますが、フォーム関連の複雑な状態管理を一手に引き受けてくれるため、生産性の向上に大いに貢献します。サンプルコードでは、短いコードでyupというバリデーションのためのライブラリと組み合わせて入力フォームを実現しています。私達のプロダクトでは、Googleフォームの編集画面のような「フォームを作るためのフォーム」を実現するために導入しました。

フロントエンドエンジニアとしての開発
開発体制の変化
これまでの開発では、APIの入出力は自分で決めて自分で実装をしていました。
しかし、今回の開発ではフロントエンドチームとバックエンドチームで入出力の合意を取り、同時に開発を進めていきました。
フロントエンドとバックエンドを分けた開発をしてこなかった私にとっては、これがなかなか苦労しました。
結合テストのタイミングでフロントエンドとバックエンドのアプリケーションをデプロイしたところ、なぜか動かない。
フロントエンドでの処理やバックエンドからのレスポンスを追ってみると、レスポンスの型が仕様書に書いてあった型と違う。もしくは、nullになると書いてあった部分でフィールドごとなくなり、TypeScript上でundefinedと認識されている…など、当時はAPIの入出力に関してどう合意を取るかが課題でした。
例えば、シンプルそうな文字列のフィールドがあったとします。「値なし」を表現するにあたって、 「''」 (空文字)で表現するか、nullで表現するか、フィールドごと消してundefined扱いにするのか、この3択のうちどれが最適解なのかでとても迷いました。
バックエンドの扱う言語がGo言語だったため、null, 空文字, undefinedの区別がつかなかったことがこの問題の原因でもありました。
自分ひとりで好きなタイミングで開発方針を変更していた開発体制とは違い、APIの仕様書レベルで変更して合意を取り直す必要がありました。
フロントエンド/バックエンド間の連携と問題解決
最初のリリースを終えた後も、新機能のために第二弾、第三弾と開発を続けてきました。その過程で、フロントエンドとバックエンド間の連携については試行錯誤を重ねていました。
当初のフロントエンドチームでは並行開発のしやすさから独自の優先度を設定し、開発を進めました。これにより、各開発フェーズの最終段階でバックエンドのAPIと連携し、機能を完成させていきました。この方法を採用した理由は、並行開発が容易になり、フロントエンドエンジニアのリソースを有効活用できるからです。
しかし、この方法のデメリットとして、修正に時間が費やされたことが挙げられます。バックエンドが長い期間モック状態にあったため、実際に結合してみると正常に動かないことが多くありました。フロントエンドとバックエンドの開発が完全には連携していないため、開発の流れの中で設計の不備や変更点の共有ができず、結合時に問題が露呈したのです。
これを解決するために、各機能の開発優先度を揃え、APIのモック状態での開発時間を最小限に抑えるように改善しました。また、開発にあたってフロントエンドとバックエンドの連携を強化しました。仕様の変更や調整については、両チームが密にコミュニケーションをとりながら、適切に機能するシステムを確実に構築する方向へ方針転換しました。
例えば、APIの入出力に関する定義をバックエンドで作成後にフロントエンドでも精査するなどです。
これにより、フロントエンドとバックエンドの結合時の手戻りが大幅に削減でき、開発全体の工数削減につながりました。
まとめ
エンジニアとして新しいことに挑戦するのは好きですが、少し役割が変わっただけでこんなに苦戦するとは思っていませんでした。
コーディングももちろんですが、ポジションやチーム構成によって開発の進め方が変わり、それまでの経験とは違った開発をする必要がでてきました。
なにか障害にぶつかっても、解決策におそらく正解はなくてそのチームならではの最適解を見つけるのがベストだと思います。
私も最適解を見つけられるよう、知識の引き出しを増やす努力をしていこうと思います。