こんにちは。開発本部で主にバックエンドの開発をしている ueeda です。
PR TIMES Web クリッピングにおいて、新機能「クリッププレビュー」を実装しました。この実装を通じて、初めてフロントエンド開発に取り組んだため、そこで得た学びを紹介したいと思います。
フロントエンド開発に挑戦したきっかけは、プライベートで React などを触っているうちに、プロダクト開発でもフロントエンドに関わることに興味を持ったためです。
PR TIMES Webクリッピング(以降Webクリッピング)とは様々なサイトから記事をクロールし、その記事にユーザーが設定したキーワードが含まれていればクリップしたりなど、メディア露出の調査・分析などが可能なWebアプリケーションです。

何をやったのか
今回、クリッピングの新機能として「クリッププレビュー」機能を実装しました。この機能について簡単にご紹介します。

上記画面はクリップ作成画面です。画面の最下部に「クリッププレビュー」ボタンがあり、クリップ名とキーワードを最低1つ入力した後、このボタンを押すと

モーダルが開き、入力されたキーワードを含む記事が表示されます。この機能は、お客様がクリップを作成する際に、適切なキーワードを選定するのに役立つことを想定しています。
フロントエンド開発では主に以下の実装を行いました。
- クリッププレビューボタンの実装
- モーダルの実装
- モーダル内に表示するコンポーネントの実装
- モーダル内に表示するデータ取得するロジックの実装
- Playwright を用いた VRT
などです。
チャレンジと学び
初期の学習コストの高さ
弊社では、Vitest、Playwright、OpenAPI、Storybook などの開発ツールを使用しています。
私はプライベートでいくつか Web アプリを実装し、その中で React + TypeScript を扱った経験はありましたが、弊社で使用しているようなツールは扱ったことがありませんでした。
実務でのフロントエンド開発では、多くのツールを使いこなす必要があり、それぞれに慣れるのは容易ではありませんでした。また、各ツールの使い方に合わせて、コンポーネントの適切な分割方法も学びました。
以下は、最初に実装したモーダルコンポーネントのコードです。
export function ClipPreview({
setIsOpen,
clipName,
keyword,
ignoreKeyword,
}: Props) {
return (
<Window
closeModal={() => {
setIsOpen(false);
}}
>
{errorMessage === '' || data === undefined || data.length === 0 || (
<Header setIsOpen={setIsOpen} title={clipName} />
)}
<modal.body>
{errorMessage ? (
<ErrorModalContent title={errorMessage} setIsOpen={setIsOpen} />
) : data === undefined ? (
<ErrorModalContent title='通信中です' setIsOpen={setIsOpen} />
) : data.length === 0 ? (
<ErrorModalContent
body='クリップ作成の条件を変えて再度お試しください。'
title='条件に当てはまる記事が見つかりませんでした。'
setIsOpen={setIsOpen}
/>
) : (
<PreviewArticleTable data={data} />
)}
</modal.body>
</Window>
)
}上記のコードでは、コンポーネントの分割が不十分で、コードが煩雑になっていました。
そこで、以下のように改善を行いました。まず、モーダル部分を新たに modal ディレクトリを作成し、その中に index.tsx としてモーダルコンポーネントを独立させました。
export function Modal({closeModal, title, children}: Props) {
return (
<Window closeModal={closeModal}>
<Header closeModal={closeModal} title={title} />
{children}
</Window>
);
}これにより、モーダルを使用する際は、Modal の子要素として表示したい要素を渡すだけで済むようになり、コードがシンプルになりました。
export function ClipPreview({
setIsOpen,
clipName,
keyword,
ignoreKeyword,
}: Props) {
return (
<Modal
closeModal={() => {
setIsOpen(false);
}}
title={clipName}
>
{data === undefined ? (
<ErrorModalContent setIsOpen={setIsOpen}>
{contentMessage}
</ErrorModalContent>
) : (
<PreviewArticleTable data={data} />
)}
</Modal>
}このような改善は、最初はフロントエンドが得意なメンバーとペアプログラミングを行いながら進めました。サポートしてくれたメンバーには感謝しています。
実装のスピード感が重要
フロントエンド開発では、デザインが大きく変更されることがあり、それまでの作業が全て振り出しに戻る場合があります。これがスケジュールの遅延に繋がる可能性があり、迅速な実装力が求められます。この点で、エンジニアの腕が試されると感じました。
今回は幸いそのような事態は発生しませんでしたが、実装中には「このデザインはこうした方が良いのではないか」といった提案をデザイナーやプロダクトマネージャーに行いながら開発を進めていました。
バックエンドとフロントエンドの視点の違い
今回、バックエンドと比較してフロントエンドの面白いポイントは、ユニットテストのアプローチにあると感じました。
フロントエンドでは、ユニットテストを書いているつもりでも、実際には結合テストになっていることが多いです。例えば、「Aボタンを押したときにモーダルが開く」というテストは、ボタンの挙動だけでなく、モーダルの動作もテストしているため、結合テストといえます。
今回の新機能実装では、ボタンを押してからモーダルが開き、内容が表示される一連の流れを確認したかったため、ユニットテストではなく、Playwright での VRT を実装しました。
コードは以下の通りです。
test('クリッププレビューボタンをクリックするとモーダルが表示されること', async ({
page,
}) => {
await page.route(clipPreviewRoute.url, async (route, request) => {
await route.fulfill({
json: clipPreviewRoute.methods[request.method()].body,
});
});
// クリップ作成画面に遷移する
await page.getByRole('link', {name: '有料クリップを作成する'}).click();
// 項目を入力する
await page.getByRole('textbox', {name: 'クリップ名'}).type('PR TIMES');
await page
.getByRole('textbox', {name: '自動収集キーワード'})
.type('PRTIMES');
await page.keyboard.press('Enter');
const getClipPreviewResp = page.waitForResponse(clipPreviewRoute.url);
// クリッププレビューボタンを押下する
await page.getByRole('button', {name: 'クリッププレビュー'}).click();
const awaitedGetClipPreviewResp = await getClipPreviewResp;
expect(awaitedGetClipPreviewResp.status()).toBe(200);
// この時点でモーダルが表示されているはずなので、画像と比較する
await expect(page).toHaveScreenshot(
'クリッププレビュー_有料クリップ作成ページ.png',
);
// モーダルが開いていることを確認
expect(page.getByRole('dialog')).toBeDefined();
// モーダル内に記事リストが表示されていることを確認
expect(page.getByRole('table')).toBeDefined();
});このコードでは、Playwright を使用して以下のポイントを確認しています。
- 必須項目の入力
- ユーザーが必要なデータを正しく入力できること
- クリッププレビューボタンの動作
- ボタンを押すと正しくモーダルが開き、内容が表示されること
今回のテストにより、フロントエンドのUIが適切に機能すること、そしてユーザー体験が保たれることを担保しました。
フロントエンド開発を通じて得た知見
フロントエンドの経験を通じて、バックエンドでのAPI設計にも良い影響を与えると感じました。フロントエンド側でAPIがどのように使用されるのか、またユーザーがその機能をどう利用するのかを想像しやすくなり、「ユーザー体験を向上させるために」という視点を持ってレスポンスやエラーハンドリングを設計できそうだと感じました。
また、自分の対応できる範囲が広がることで、プロダクト開発においてできることが増えるのは有意義だと感じました。
まとめ
今回は、バックエンドエンジニアが初めてフロントエンド開発に挑戦した際の学びについてご紹介しました。フロントエンド特有のツールや開発手法に戸惑うこともありましたが、新たな視点やスキルを身につけることができました。
フロントエンドはユーザーが最初に目にする部分であるため、自分が実装した機能がユーザーに直接届くという点も、フロントエンド開発の大きな魅力だと感じました。
これで終わりにせず、今後もフロントエンド開発に積極的に挑戦し、さらにスキルを向上させていきたいと考えています。

