2020年に立ち上げたWebフロントエンド構成の振り返り
こんにちは、よしこです。
株式会社ナレッジワーク というスタートアップで、2020年4月の創業時から一人目のフロントエンドエンジニアをしています。
初期に考えて組み上げたスタックで1年半ほど開発・運用してみて、なかなか快適に日々開発ができているので
- 新規開発のプロダクト立ち上げ時にどのようにフロントエンドを構築したのか?
- 立ち上げから1年以上開発・運用を続けてきた今、それらの選択はどうだったのか?
を記事にして振り返り、公開したいなと思いました。
(プロダクトの内容はステルスで進めていてあまり対外的な発信ができないので、かわりに技術的なところはどんどんオープンにしていきたいなという気持ちがあります)
いろいろな項目ごとに振り返りたいので、この記事は各項目を横断するindexとして項目ごとの概要を簡単に説明し、深堀りは項目ごとに追って詳細な記事を書いていく予定です!
前提
プロダクトとしての要件
- ツールとしての側面が強いBtoB SaaS
- アプリケーション部分に関してはログインが必要で、SEOやページキャッシュの必要性はない
- スマートフォン対応は視野には入れておきたいがメインで想定する端末はPC
- IEはサポート外
フロントエンドチームの規模
- 現在フルタイムのフロントエンドエンジニアは自分一人
- フロントエンドの開発に携わってくれているメンバーは他に4人ほど(副業の方や別職種の社員など)
ベースの技術
- TypeScript、React、Next.jsを採用
- TypeScriptは型が欲しかったので必須
- Reactは、正直メジャーなFWなら大抵どれでも要件を満たしたSPAは作れば作れると思っているので、好みと慣れの側面が一番強い
- Next.jsはルーティングとビルドやパフォーマンス面の支援が欲しかったのであわせて採用。要件にSSRの必要性が薄いので、今後静的なホスティングもやろうと思えばできるようにSSR関連の機能やAPI機能は使っていない
SPA アーキテクチャ
Stateのアーキテクチャ
- アプリケーションに存在するStateを以下の3種類に分類
-
Server Data Cache
,Global State
,Local State
-
- Server Data Cacheはデータ取得ライブラリのSWRを通して読み書き
- Global StateはRecoilを通して読み書き
- single stateではなく役割ごとに独立したstateを作る
- Local StateはReactのuseStateのこと
詳細記事:
Applicationのアーキテクチャ
- 存在する主なレイヤーはComponent, Usecase, Repository, Store
- 必要最小限のかなりシンプルなものにしており、特定のアーキテクチャに基づいたものではない。fluxでもない。
- データフローが単方向になるようにはしている。Component -> Usecase -> Store -> Component というかなり単純な形
- RepositoryはStoreに対するRepositoryではなくAPI serverに対するRepositoryなのでデータフローには関わらない。Usecase内で使われるだけ
- hooks APIと純粋関数の境界をどこにどうやって置くか?を色々考えて工夫した
- UsecaseとRepositoryはテストしやすいよう引数で依存を注入する純粋な関数にしておき、アプリケーション内で使うときにはhooksでラップしたものを使っている(hooksラッパー内で他のhooksから依存を集めてきて注入できる)
詳細記事:
Componentのディレクトリ構成
- アプリケーションに存在するComponentを以下の4種類に分類してディレクトリを分ける
src/components/page
src/components/model
src/components/ui
src/components/functional
- pageは1つのページを表すComponent
- modelは何らかのmodel(user等)に関心を持つComponent
- modelの種類ごとにディレクトリを切って、同じmodelに関心のあるComponentをその下に並列に並べている
src/components/model/user/UserAvatar
-
src/components/model/user/UserPicker
みたいな
- modelの種類ごとにディレクトリを切って、同じmodelに関心のあるComponentをその下に並列に並べている
- uiはmodelに関心のないUI Component
- checkboxとかbuttonとか。layout関連もUIではないけど今はここに入れている
- functionalはmodelに関心がなく、かつviewを持たず振る舞いだけを持つComponent
- hooks APIではなくComponentの形式で提供したいものはここ
- ErrorBoundaryとか、refを伴うhooksをラップしたものとか
詳細記事:
APIとの繋ぎ込み
- Protocol Buffersを使用したスキーマ駆動開発
- protoファイルからGo/TypeScriptの型定義ファイルを生成してそれぞれ使うことで、通信部分も型安全
- 型定義ファイルの生成はCIで自動化
- 事前にスキーマが決まっていることで、APIが未実装でもfrontend内でAPIの型に沿ったmockを作って実装を進めることができる
- 実際の通信はfrontendとserverの間にExtensible Service Proxy(ESP)が立っており、frontend <-> ESP間はREST、ESP <-> server間はgRPC
詳細記事: Coming soon
スタイリング
スタイリング
- Pure CSSに一番近い.cssファイルを運用できるCSS Modulesを採用
- 読み込み順序の問題に対しては詳細度の工夫で対応
- 変数はネイティブのCustom Propertiesを使用
- 基本的にstyleはUI Componentに集約し、pageやmodelのComponentではUIを組み合わせることでデザインを実装できるのが理想形
- styleを伴うUIフレームワークやライブラリは基本的に使わない
詳細記事: Coming soon
デザインとの協調
- デザインはFigmaでおこなわれている
- src/components/uiに存在するComponentとFigmaに存在するデザインコンポーネントをなるべく1:1に対応させていく
- 1:1対応推進のため、frontendの実装済みUI Componentの一覧は常に最新の状態のstorybookを社内にホスティングしてデザインチームがいつでも見られるようにしておく
- 逆にFigmaのデザインコンポーネントもエンジニアがいつでも見られるようにしておく
- TypographyのバリエーションもComponentとして管理
詳細記事: Coming soon
開発補助
テスト
- 今回しているテストは
Unit Test
,Visual Regression Test
,E2E Test
の3つ - Unit Testはjest。対象はUsecase、Repository、entityごとの関数群など。PRごとにCIで回す
- VRTはreg-suit。対象はComponentをスタイルごと。PRごとにCIで回す
- E2E Testはplaywright。対象はdev環境のE2E用テナントで、1日1回実行される。stg環境に対しての実行もできる。大半のmodelのCRUDをチェックしている
詳細記事: Coming soon
linter/formatter
- eslint, stylelint, prettierを設定
- eslintは社内でディレクトリ間のimportルールを決められるカスタムルールを実装・運用しており、好ましくない方向の依存に対して自動でエラーを出せる
- たとえばcomponents/uiがmodelに依存するのはNGとか
-
.vscode
もリポジトリで共有して推奨設定はある程度効くようにしてある- ここは好みも強そうなので、チームメンバーの意見を尊重。今の所皆快適そう
-
editor.codeActionsOnSave
でsource.fixAll.eslint
とsource.addMissingImports
をかけるのがとても快適。 - 特に自動importとimport順序ルールのautofixを組み合わせたことでファイルの上部をいじりに行くことがなくなった
詳細記事:
mockの活用
- model(User, Category...等)ごとに必ずモックデータを手動で定義するようにしている
- すべてのmodelにモックデータがあるので、それをAPIのスキーマ定義通りに返すオブジェクトを作ることでAPI Serverのモックが作れる
- それを利用して、API通信先モックに置き換えたdebug mode(API通信を伴わない起動モード)を実現している。serverの実装前はこちらを使うことでfrontendに閉じて機能開発ができる
- モックデータはユニットテストやstorybookでも活用している
詳細記事: Coming soon
scaffolderの活用
- model, page, component作成用にそれぞれ scaffdog を使ったscaffolderを用意している
- それぞれの作成時に必ずscaffolderを利用することで、フォーマットを啓蒙しなくてもチーム全体で自然と統一されたフォーマットを運用することができる
詳細記事: Coming soon
依存ライブラリの継続的アップデート
- 月1でrenovateを使って依存ライブラリをアップデートしている
- 1年ぐらいは週1だったが、頻繁すぎてリリース直後のバグをひいてしまうことがたびたびあったので月1にしてみた。が、それはそれで溜まりすぎる感じもあるので頻度はまだ試行錯誤中
- dependenciesとdevDependenciesでPRを分け、devDependenciesはCIが通ればauto-mergeする設定
詳細記事: Coming soon
その他
ディレクトリ構成
src以下をざっくり紹介
-
components
: Componentアーキテクチャの項目で紹介済み -
usecases
repositories
: Applicationアーキテクチャの項目で紹介済み -
globalStates
: Stateアーキテクチャの項目で紹介済み -
mocks
: mockの活用の項目で紹介済み -
generated
: スキーマ定義から自動生成されるファイル -
libs
: node_modulesのライブラリを直接使わずにラップしたい場合ここに -
models
: modelごとの型定義や関数郡など。modelは振る舞いを持たないデータ構造体にしているので、model methodにあたる振る舞いは別途参照透過な関数群として用意している -
pages
: Next.jsのルーティング用。ここにあるファイルには実装は書かず、対応するcomponents/page以下のComponentをimportしてexportするのみ -
styles
: reset.cssやbase.css、variables.cssなど -
hooks
: 共通で使いたいhook
詳細記事: Coming soon
ホスティング・デプロイ
- 立ち上げ時にインフラメンバーがいなかったこともあり、デプロイまわりを自分一人でも構築できそうだったVercelを利用
- toBなのでトラフィックの増加は事前に予想がつきやすく、Serverless Functionsも使わないのでProプランの上限に達するにはだいぶ猶予がありそうと判断(実際まだ余裕)
- monorepoだとGitHub連携に色々と障壁があり、現在は連携はせずにCI内でVercel CLIを叩いている
詳細記事: Coming soon
以上!
Coming soonになってる各項目の詳細記事は今後こつこつ書いてリンクしていきたいと思います。
(上から順に書くと決めてるわけでもないので、もし「これ特に知りたい」のような反応があればそれを先に書くかもしれません)
あと技術的なところ以外だと、たとえば採用面ではスキル選考をする立場としての練度を上げるために以下のような募集をしてみたりと、色々チャレンジ中です!
この記事は目次なので各項目の課題感にはあまり触れませんでしたが、まだまだ足りてないところや手が回ってないところもあるので、もっとチーム拡大してやれること増やしていきたいなーと思っています。
なので最後にちょっとだけ宣伝しますが、この記事の内容や、またはステルスで進めているプロダクトってどんな内容なの?などなどどんなことでも興味を持ってくださった方がいたらお気軽に Twitter DM ください!(フォローしなくても送れます)
Discussion
とてもためになりました!
ディレクトリ構成やComponentアーキテクチャの部分を更に知りたいなと思いました!
ありがとうございます!
2つとも書いて公開できましたのでよろしければぜひ!
とても参考になりましたので、バッジを贈らせていただきました!
いろいろなコンテクストの上で、試行錯誤された感じが伝わってきて、とても読み応えがありました。
ありがとうございます!バッジもコメントも嬉しいです🙌
とてもわかりやすく参考になりました。スタイリングについてもう少し知りたいです。具体的には、CSSのフォルダ構成についてお聞きしたいです。また、なぜUIフレームワークやライブラリを使用しないのでしょうか。自前のライブラリを作っているのですか。
コメントありがとうございます!
UI Componentは自前で揃えています!
UI系のライブラリは使っているものもありますが、CSSを伴わないHeadlessなものだけ使うというポリシーにしています!
そのあたりも含め、スタイリングまわりの記事検討してみますね!🙆♀️