こんにちは。PR TIMES の開発本部でフロントエンドエンジニアをしている鈴木雄大(@szkyudi)です。2022年2月、企業ページにプレスキット機能を追加するリリースしたので、そのお話をしようと思います。
企業ページおよびプレスキットについては下記の PR TIMES MAGAZINE の記事をご覧ください
新機能「プレスキット」とは
「プレスキット(または、メディアキット)」とは、メディア関係者向けに作成する、企業や事業に関するプロモーション用の資料や画像・動画素材などをまとめたもの です。
まずプレスキットとは何かというと冒頭で紹介した記事にもあるように、上記のようなものになります。これを機能として提供するにあたって、ユーザーが閲覧できる公開画面と、企業が設定するための管理画面を実装しました。
公開画面については、実際に PR TIMES の企業ページにプレスキットが設定されているので下記URLからご覧いただけます。
ただ管理画面については、企業としてログインしているユーザーしか閲覧することができないので、弊社の設定画面を例として紹介していきます。
どのあたりがリッチなのか
プレスキットの管理画面には従来の PR TIMES の管理画面には存在しないようなリッチなUIがいくつか存在しています。
従来のPR TIMES の編集画面は同期的なページが多いのですが、プレスキットの編集画面に関しては編集から公開まで、全て非同期で完了します。そのため、ユーザーがシームレスに編集することができます。
一連の挙動は以下のようになっています。
新機能を実装するにあたって扱った環境
この機能の開発はゼロイチの側面が大きいプロジェクトでした。そこで、今回プレスキットを開発するにあたって扱った技術や構成を紹介したいと思います。
Reactでリプレイスされたフロントエンド環境
PR TIMESのフロントエンドの大部分は jQuery や JavaScript で書かれています。そのため、それらのコードを基盤としてリッチなUIを作るのには工数が大幅にかかってしまう状況でした。
そこで、プレスキットの機能を開発する前に、保守性と拡張性を高める目的でフロントエンドを React へリプレイスするプロジェクトを行いました。
(Developers: 鈴木雄大/柳龍哉/Thai Tepy)

画像の配信をクラウドストレージとCDNを利用して行う
画像の配信を S3 と Fastly Image Optimizer を利用して行なう環境を構築しました。これは、プレスリリース画像の画質を改善する目的で構築された構成になりますが、プレスキットでも活躍する構成となりました。
結果として、S3 によってオンプレのストレージサーバーの容量の枯渇を防ぎ、 Fastly によって画像のダウンロード機能もスピーディーに実装することができました。
(Developer: 柳龍哉)

画像のアップロードにも新たな構成を導入
PR TIMESはフロントエンドだけではなく、バックエンドもレガシーなPHPがほとんどで、従来の実装に合わせて実装すると技術的負債が溜まるという懸念がありました。
そこで、前述の S3 と Fastly Image Optimizer を使うことで、レガシーコードへの変更を減らし負債を溜めない構成になっています。
(Developer: 江間洋平)

生じた障壁とその解決策
従来に比べてモダンな構成になったとはいえ、それによる障壁がいくつかありました。
POSTするデータの肥大化によるハンドリングの複雑化
プレスキットの特性上、企業が慎重に公開することが考えられるので、アップロード後に即時公開される仕様にはできないということで、 /api/logos
や /api/images
のようなエンドポイントではなく、 /api/press_kit
というエンドポイントを用意して、一括で情報を更新するAPIの設計と取りました。
これによって POST する JSON が肥大化し、データの整形やエラーハンドリング、バリデーションも特殊な構成になりました。
ただ、 JSON やエラーが複雑化しても大きな工数なくハンドリングすることができたのは React や Recoil をはじめとしたモダンフロントエンドがあってのことだと実感しました。
下記に Recoil でスッキリ管理できた POST のデータを紹介します。
export const pressKitPostState = selector<PressKitState>({
key: 'pressKitPostState',
get: ({ get }) => {
const status = get(statusState);
const logos = get(editableLogosState);
const images = get(editableImagesState);
const guidelineFiles = get(editableGuidelineFilesState);
const guidelineText = get(editableGuidelineTextState);
return { status, logos, images, guidelineFiles, guidelineText };
},
});
エラーやローディングによる状態管理の複雑化
全ての機能を非同期で処理することによって、アプリケーションの状態が複雑化しました。
従来の管理画面は、編集画面と確認画面といった具合に手続き的な導線になっていましたが、非同期で処理することによって、様々なタイミングでエラーやロードが発生するようになりました。
今回は Recoil を使うことで下記のように状態に応じたハンドリングをすることができました。
export const LogoDetail = ({ id }: Props) => {
const loadable = useRecoilValueLoadable(logoDetailState(id));
switch (loadable.state) {
case 'hasError':
return <ErrorDetail />;
case 'loading':
return <LoadingDetail />;
case 'hasValue':
return <>ロゴのコンテンツ(省略)</>
}
};
ただ、どこでエラーやロードを発生させるかは Recoil とコンポーネントの実装に依存するので、意図したところでエラーやロードを発生させるには、仕様を決めるの段階から考慮しておくべきだと思います。
実際にこのプロジェクトでも考慮しきなかった部分があったので、いくつかデザインを追加していただきました。
Global State と 型の整合性を保つ
公開画面と編集画面のデザインはほとんど同一ですが、特殊な API の構成を取っていることから、公開画面と編集画面では別の API かつレスポンスとPOSTのデータの型が若干違う状態になっています。
ただし、同一のデザインを流用するためには、必要なプロパティを満たした同じ型を使ってコンポーネントに流し込みたいです。
そこで、細かく Global State を切り分けて管理することで比較的シンプルに整合性を保つことができました。
export const logosState = atom<
PressKitLogoListByCompanyIdResults[] | PressKitLogoEditable[]
>({
key: 'logosState',
default: selector({
key: 'logosState/default',
get: ({ get }) => {
const viewMode = get(pressKitViewModeState);
switch (viewMode) {
case 'VIEW':
return get(logosQuery).data.results;
case 'EDIT':
return get(editableLogosState);
}
},
}),
});
残された課題
状態遷移を考慮したコンポーネントおよび Global State の設計
現状はロジックと密になっているコンポーネントも存在しています。そのため、ロジックの部分でローディングやエラーが発生したときに、それらがコンポーネントに伝搬されてしまうという懸念点があります。
必要なコンポーネント内部で Global State を呼び出せるのは便利ですが、ローディングやエラーを発生させたいコンポーネントを意図的に選択して呼び出す必要があるので、今後はそのあたりも考慮して初期設計やデザインのすり合わせをしていきたいと思いました。
Global State の複雑化
リプレイスしたときは Global で管理すべき State が少なかったので、複雑になることはありませんでした。
しかし、今回は複数の箇所で Global State を活用しているので、コード量としても増えてきてしまいました。また、コードの重複も増えて共通化できる部分も一旦無視して進めている部分もあります。
そのため、今後の機能追加に備えて共通化できるところは積極的に活用して共通化していきたいなと思っています。
今後について
今後も企業ページおよびプレスキットに機能を追加していく予定です。その過程でより良いコードにしていけるよう模索してきたいと思っています。
良い意味での発展途上の環境なので、これからコードを良くしていくことでサービスの質も向上していけるよう努めたいと思います。
また、現在 PR TIMES ではフロントエンドエンジニアを積極的に募集しているので、興味がある方はぜひ下記URLより詳細をご覧ください。