PR TIMES のフロントエンドを支える技術 2023

PR TIMES のフロントエンドを支える技術 2023 開発者ブログ。背景に使用している各種ツールのロゴ画像が表示されている。上から順に Playwright、Vite、Next.js、Vitest、OpenAPI、pnpm、GitHub Actions、XO、Renovate、Radix UI、Emotion、Storybook、Lost Pixel
  • URLをコピーしました!

こんにちは。PR TIMES でエンジニアをしている岩元 (@yoiwamoto) です!

プレスリリース配信サイト PR TIMES のフロントエンドは、一昨年ごろまでほぼ全てのページが Smarty + jQuery on PHP で実装されており、直近1、2年は機能追加・改修に合わせてこれらを順次 React 実装にリプレイスを進めています。

このような取り組みをどのような技術構成で行っているか、2023年の振り返りの意味も込めてざっくりと紹介します!

目次

リポジトリ構成

React 実装は、これまでメインのバックエンドサーバーとのモノリスで構成していたリポジトリとは分けて、prtimes-frontend というリポジトリを使用しています。

PR TIMES STORY というサービスも自社で開発が行われていますが、実装は別リポジトリであり技術構成も異なるため、このエントリでは詳細に触れません。

prtimes-frontend は pnpm の Workspace を使用した薄い monorepo であり、ざっくりと以下のような構成になっています。

.
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── apps
│   ├── clipping
│   ├── prtimes
│   └── prtimes-public
├── common
│   ├── press-release-ui
│   ├── prtimes-company-admin-components
│   └── prtimes-public-components
├── packages
│   ├── eslint-config-prtimes-frontend
│   ├── msw
│   ├── openapi
│   ├── prtimes-basis
│   └── tsconfig
└── tests
    ├── clipping
    ├── editor-e2e-vrt
    ├── prtimes
    └── prtimes-public
あわせて読みたい
Workspace | pnpm pnpm has built-in support for monorepositories (AKA multi-package repositories,

pnpm-workspace.yaml の内容は以下の通りで、apps、common、packages、tests 直下のディレクトリをそれぞれ package としています。

packages:
  - apps/*
  - common/*
  - packages/*
  - tests/*

区分は以下の通りです。

apps独立してデプロイを行うアプリケーションの単位
commonpackages に依存するが、apps を跨いで依存する共通モジュール
packagesapps を跨いで依存する、primitive な共通モジュール
testsPlaywright でのブラウザテスト

common が要らなそう?(package で良さそう)と思われそうですが、一応、packages のモジュール(例えばデザインシステム準拠のコンポーネント等)に依存するかどうかで明確に線引きをしています。理由としては、packages 内で相互依存している状態だと、後々循環依存が発生するなどして管理がややこしくなりそうなので、それを未然に防ぐためのレイヤとして設けたということになります。

そもそも apps 間でモジュールを共有する必要のある状況が発生していることが問題とも考えられますが、apps の切り方はデプロイの単位であり、扱うドメインというよりは SEO やパフォーマンスなどの非機能要件が考慮されています。これにより、Next.js アプリケーションと、SSR 無しの SPA で同じものを扱うということが生じるので、必然的にこのようなレイヤが生まれています。

apps について「独立してデプロイを行うアプリケーションの単位」と書きましたが、Vite でビルドする SPA 実装である apps/prtimes では、主にページ単位でエントリファイルは分割されており、複数アプリケーションを管理しているとも言えます。

フレームワーク・バンドラ

Vite

PR TIMES のフロントエンドでは、SEO やパフォーマンスが極端に重要というわけではない管理画面などのページで、SSR 無しの React SPA を採用しています。

以前までバンドラは Webpack でしたが、今年の2月に Vite に移行しました。(めちゃくちゃ速いです)

あわせて読みたい
Webpack から Vite に段階的に移行しました こんにちは。PR TIMES フロントエンドエンジニアの岩元 (@yoiwamoto) です。 PR TIMES ではいくつかのページが React で実装されており、Webpack でビルドを行っていま...

Next.js

一方で今年から、要件上 SSR を行うことが妥当だと思われる一部ページでは、Next.js が採用されています。

あわせて読みたい
BFCacheを利用してNext.jsで実装した無限スクロールのUX改善をした話 こんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。 先日、【月間9000万PV】プレスリリース掲載ページの Next.js 移行でやったこと、という記事が公...

選定について詳細は以下のエントリなどに書いていますが、現時点では Pages Router を使用しており、動作環境は ECS、前段に CDN の Fastly を置いて、キャッシュは全て Fastly で管理、という形式をとっています。

あわせて読みたい
【月間9000万PVのPR TIMES】プレスリリース掲載ページの Next.js 移行でやったこと こんにちは!PR TIMES 開発本部フロントエンドエンジニアの岩元 (@yoiwamoto) です。 先日、月間9000万 PV のプレスリリース配信サイト PR TIMES で、もっともアクセス...

静的解析

TypeScript の型チェック

単純に tsc --noEmit で型検査を行っています。

XO

ESLint をラップした linter で、well-maintained なルールセットでもある XO を使用しています。

JavaScript/TypeScript linter (ESLint wrapper) with great defaults

GitHub
GitHub - xojs/xo: ❤️ JavaScript/TypeScript linter (ESLint wrapper) with great defaults ❤️ JavaScript/TypeScript linter (ESLint wrapper) with great defaults - xojs/xo

ファイル名は全て camel-case.ext にする必要があったり、request を req と略記することを許容しないなどなかなか opinionated なルールが多いのでハードルは少し高いですが、細かい議論の発生する余地を無くすという点でかなり有用だと思っています。

また、XO は Prettier も内蔵しているので、prtimes-frontend では直接は prettier を使用しておらず、xo --fix でフォーマットも行っています。

導入事例としてこちらの記事がよくまとまっていました。

https://blog.howtelevision.co.jp/entry/2023/12/02/153945

テスト

Vitest

ユニットテストは Vitest で行っています。

あわせて読みたい
Vitest Next generation testing framework powered by Vite

先に書いたように、今年の初め頃にバンドラを Webpack → Vite に移行したので、併せて導入した形になります。

実はフロントエンドリプレイスが始まった最初期はユニットテストを書く文化があまりなかったのですが、最近は自然にテストが書かれるようになっており、特に Storybook の play function を活用してテストも書かれるなどしています。

あわせて読みたい
Storybookを用いてテストの可視化を進めた話 こんにちは、「PR TIMES Webクリッピング」の開発リーダーをしている小張です。 Storybookをユニットテストで活用している取り組みについて、紹介したいと思います。 【...

既存実装にテストを書いていくタスクはなかなか進めにくいのですが、新しい package などではカバレッジもそれなりです。ただ、ブランチカバレッジがかなり低かったりと、まだまだ正確なテストを書けていないところはあるので、今後の課題となっています。

カバレッジレポートのサマリのスクリーンショット。All files, 95.39% Statements: 13664/14323, 51.65% Branches: 857/1659, 85.71% Functions: 312/364, 95.39% Lines: 13664/14323
apps/prtimes-public のカバレッジレポート(istanbul)
カバレッジレポートのサマリのスクリーンショット。All files, 97.77% Statements: 6095/6234, 85.71% Branche: 228/266, 88.96% Functions: 137/154, 97.77% Lines: 6095/6234
common/press-release-ui のカバレッジレポート(istanbul)

Playwright

ブラウザテスト・VRT に Playwright を使用しています。

あわせて読みたい
Fast and reliable end-to-end testing for modern web apps | Playwright Web automation and testing for apps, scripts, and AI agents

最初は Cypress を使用していたのですが、今年 Playwright に移行しています。

あわせて読みたい
CypressからPlaywrightに移行しました こんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。 先日、フロントエンドのIntegration Testで使用されていたCypressをPlaywrightに移行したので、...

また、少し紛らわしいんですが、Playwright で行っているブラウザテストは基本的に、API レスポンス等を全てモックしてフロントエンドで完結するテストになっています。(そのため、社内ではこれをインテグレーションテストと呼んでいます)

このようなテスト手法は意外とスタンダードというわけではないようなんですが、ブラウザレベルでの挙動や VRT を、バックエンドの環境や DB セットアップなどを考慮せずに気軽に書いていけるので、弊社ではかなりワークしています。

あわせて読みたい
E2EテストのAPIリクエストを全てモックした話 こんにちは。開発本部でインターンをしている桐澤(@kiririLee)です。今回、「PR TIMES Webクリッピングβ版」 というプロジェクトのフロントエンドで実装されていたE2E...

Lost Pixel

コンポーネントには基本的に Storybook の story が書かれていて、それぞれの story について Lost Pixel で VRT を回す仕組みがあります。

あわせて読みたい
Lost Pixel - holistic Visual Regression Testing cloud Easy to integrate and reliable visual regression testing cloud. Sleep better at night while shipping features.

story も多く若干時間がかかるので、影響範囲が大きいプルリクエストなどで特定の Label を貼ることで発火するようになっています。

GitHub の Pull Request サイドバーのスクリーンショット。Assignee に you-5805、Labels に prtimes-storybook-vrt が指定されている。
VRT をトリガーするラベルを貼ったプルリクエスト
GitHub の Pull Request 上のコメントのスクリーンショット。
github-actions (bot) によるコメントで、Oct 19 にコメントされている。
内容は、❌ Storybook VRT failed (prtimes)
download the result here. {url} となっていて、URL にはモザイクがかかっている。
VRT が失敗した際のプルリクエストへのコメント

失敗した際には diff などの情報が生成されるので、それは S3 にアップロードして、ダウンロードするための URL をコメントするようにもしています。

その他の依存

TanStack Query

server state (API から GET で取得するデータ)の管理。

あわせて読みたい
TanStack Query Powerful asynchronous state management, server-state utilities and data fetching. Fetch, cache, update, and wrangle all forms of async data in your TS/JS, React...

Recoil

client state の管理。 一部のアプリケーションでは server state を async selector で管理。
最近は、global state を持たなくても実装が可能なシンプルなページでは、できるだけ使用しない方向性になっています。

あわせて読みたい
Recoil A state management library for React.

Radix UI

ダイアログやコンボボックスなど、簡単にはアクセシブルな実装ができないコンポーネントでベースに使用しています。
一方で、本来は自前実装で要件を満たせるべきなので、どこまで依存してよいかという議論も一部あります。

あわせて読みたい
Radix UI Components, icons, and colors for building high‑quality, accessible UI. Free and open-source.

Emotion

ほとんどのスタイル記述に使用されています。
runtime-js CSS in JS で、エコシステムの観点で言うと将来性には若干の陰りがありますが、
実際のアプリケーションで課題が生じているような段階ではないので引き続き使用しています。

あわせて読みたい

openapi-generator-cli

OpenAPI spec を yaml に記載していて、API クライアントを生成しています。

CI

package によりますが、各変更に対して、基本的に以下のような検査が、GitHub Actions で回ります。

  • TypeScript の型検査
  • XO のリント
  • Vitest のユニットテスト
  • アプリケーションのビルド
  • Playwright のブラウザテスト・VRT
  • Storybook のビルド・テスト

また、以下のような工夫があります。

1. キャッシュの活用

node_modules や Playwright のバイナリを Actions Cache にキャッシュして利用しています。

冒頭で紹介した pnpm-workspace.yaml で test/* も package として使用していましたが、このような設定だと CI でビルド等の前に、アプリケーションのビルドには不要なテスト用の依存もインストールしてしまうことになります。

workspace から除外すると、不要なインストールが発生せず局所的には速くなり得るのですが、prtimes-frontend では monorepo 全体の依存を含む node_modules を、root の pnpm-lock.yaml の内容を key としてキャッシュするようにしているので、これがほぼ常に HIT するようにした方が結果的に速いと判断し、まとめて扱っています。

2. 実行時間 or コスト最適化

アプリケーションも大きくなってきたり、ユニットテストも増えてきたりで、CI 実行時間はどうしても長くなっていってしまいますが、デプロイ等のいくつかのワークフローではできる限り速く終わって欲しいです。

そういったケースでは以下のように step が並列化されたり、ユニットテスト実行がシャーディングされたりしており、高速に終了するようになっています。

GitHub Actions の workflow 実行結果sまり。実行されたファイルは deploy-clipping.yaml で、on:push イベント。Install Dependencies が 12s で終了した後、並列で3つの step が回る。一つは Unit Test で、4 job が sharding されて実行されている。もう一つは TypeCheck で 1分34秒で終了、最後の一つは Build で 1分39秒で終了。それらの完了を待って Deploy step が実行され、18sで完了している。
デプロイワークフローの実行結果サマリ

逆に、こういった実行時間の最適化はコスト面でネガティブに働くこともあります。

以下のエントリのように、並列化が有効なデプロイのようなワークフローや、その他極端に遅いものを除いて、直列実行を有効に使うことでコスト最適化も行っています。

あわせて読みたい
並列で実行していたGitHub ActionsのJobをまとめ、Billable timeを削減した話 こんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。 GitHub ActionsのBillable timeの削減のために、複数に分けて実行していたJobを、ある程度の粒...

3. Storybook がデプロイ・コメントされる

prtimes-frontend では、変更の影響範囲のアプリケーションで、ブランチごとに分離した環境に Storybook がデプロイされ、URL がプルリクエストにコメントされます。

GitHub の Pull Request 上のコメントのスクリーンショット。
コメントしたのは github-actions (bot) で、3時間前のコメント。edited。
内容は、📕 Storybook Preview
テーブル: 行は Application、URL
各行の Application は press-release-ui、prtimes-public、prtimes で、URL には全てモザイクがかかっている。
Storybook の確認 URL のコメント

これによって、UI 影響のある変更について、レビュワーも気軽に story を見て動作を確認することができるようになっています。

4. Renovate

Renovate を入れているので、設定に合わせたスケジュール・対象パッケージ・分割単位で、依存のアップデートのプルリクエストが立つようになっています。

GitHub
GitHub - renovatebot/renovate: Home of the Renovate CLI: Cross-platform Dependency Automation by Men... Home of the Renovate CLI: Cross-platform Dependency Automation by Mend.io - renovatebot/renovate

例えば Package Grouping を使用して依存パッケージ数の多い Storybook 系、ESLint 系などをグルーピングしてプルリクエストをまとめるようにする、などがあります。

制限しているとは言っても大量のプルリクエストが立つので、CI 実行量が過剰にならないよう、renovate の切ったブランチでは必要最低限のワークフローが回るように制御されており、全てのテストが実行されるのは、リリース前の全ての updates をまとめたブランチになります。

今後の課題

まだまだ色々な課題がありますが、大きなものでテストの実行時間削減などがあります。

テストの量は時間が経つとどうしても増えていってしまいますが、全体のテスト量に各アプリケーションの CI 実行時間ができるだけ引きずられていかないよう、テスト実行対象を変更の影響範囲に閉じるようにするなど、一定賢い判定・分岐を導入する必要がありそうです。

また、Recoil 等のライブラリのインターフェースに深く依存しすぎた実装について、移行が必要になった時にアプリケーションになるべく影響を与えずに剥がせるように設計方針をある程度固めることや、Storybook にテストなどをどこまで依存するかなど、決める必要のあることも多く残っています。

来年も引き続きこの辺りの課題に取り組んでいけたらと思います。

We are hiring!

フロントエンドエンジニアはもちろん、各種ポジションで採用を行っています!

あわせて読みたい
株式会社PR TIMES
02.開発部 の求人一覧 - 株式会社PR TIMES 株式会社PR TIMESが公開している、02.開発部 の求人一覧です
  • URLをコピーしました!

この記事を書いた人

2022新卒で PR TIMES に入社し、開発チームでフロントエンドエンジニアをしています。
https://x.com/yoiwamoto

目次