こんにちは。プロダクト開発部の川根(@flat_42)です。
先日、あるプロジェクトの開発が落ち着き、新たなプロジェクトにアサインされることになりました。このプロジェクトのフロントエンドは React で構築されていますが、開発の歴史的経緯がありいくつかの問題に直面していました。
問題
- Global State への過度な依存
- URL や query string、HTTP クライアントで管理すべき状態が、ほぼすべて Recoil を使って global state で管理されていました。この結果、コンポーネントを描画するための global state が膨大になり、高レベルのコンポーネントのレンダリングコストが異常に高くなっていました。
- 自動テストの異常な遅さ
- 高いレンダリングコストを持つ高レベルのコンポーネントにテストが集中していたため、CI での Jest のテストに 30 分ほどの時間がかかっていました。
- レイヤーごとの責務が不明確
- feature based pattern がうまく機能せず、実装の重複が発生していました。また、container コンポーネントと presentational コンポーネントの区分が適切にされておらず、データフェッチや global state を取り扱うレイヤーが散らばってしまい、末端のコンポーネントがそれらに依存してしまっていました。結果として、エンジニアが把握しづらく、機能追加や改修を進めづらいコードベースになってしまっていました。
解決策
これらの問題を解決するために、以下の方法でリファクタリングを行うことにしました。
ディレクトリ構成とレイヤーごとの責務の再考
まず、全体のディレクトリ構成と各レイヤーの役割を見直しました。その上で、どのようなコンポーネントがどこに配置されるべきか、どのレイヤーがどのような責務を持つべきかを明確にしました。
Hygen を用いたテンプレート生成
再考した構成通りにリファクタリングを進めるため、Hygen を使用して、必要なコンポーネントのテンプレートを自動生成する仕組みを導入しました。 (Hygenの使い方は公式ドキュメントに記載があるので割愛します)
例えば、汎用的な UI コンポーネントのテンプレートを生成する場合、npm scripts に登録しているcodegen:fc:general
という script を CLI で実行します(package manager は yarn を使用しています)。
yarn codegen:fc:general
コンポーネント名を質問されるので、これに答えます。
? component名を入力してください › TestComponent
すると、コンポーネントと Storybook、テストファイルのテンプレートが生成されます。
Loaded templates: _templates added: src/components/TestComponent/index.tsx added: src/components/TestComponent/index.stories.tsx added: src/components/TestComponent/index.test.tsx
src/components/TestComponent/index.tsx
type Props = { propsName: string }; export const TestComponent = ({ propsName }: Props) => { return ( <div> <div>{propsName}</div> </div> ); };
src/components/TestComponent/index.stories.tsx
import type { Meta, StoryObj } from '@storybook/react' import { TestComponent } from '.' const meta: Meta<typeof TestComponent> = { component: TestComponent, } export default meta type Story = StoryObj<typeof TestComponent> export const Default: Story = { args: { propsName: 'propsValue', }, }
src/components/TestComponent/index.test.tsx
import { render, screen } from '@testing-library/react' import { setup } from '@/test/userEvent' import { TestComponent } from '.' describe('TestComponent', () => { beforeEach(() => { render(<TestComponent propsName="propsValue" />) }) test('テストケース名', async () => { // Arrange const { user } = setup() // Act await user.click(screen.getByRole('role', { name: 'name' })) // Assert expect(screen.getByRole('role')).toBeInTheDocument() }) })
そのほかにも、レイヤーごとにcodegen:fc:feature
やcodegen:fc:page
のような npm script を登録しておきます。
特にfeature 配下の organism 相当のコンポーネントは、API リクエストや状態を取り扱うため多くのレイヤーが必要になります。ファイル作成の手間がかさみますし、コンポーネントごとの責務の境界が乱れやすい箇所です。こういったレイヤーほど自動生成にメリットがあると考えています。
例えば以下の npm script を実行すると、
yarn codegen:fc:feature
このように質問されるのでそれぞれ答えます。
? feature名を入力してください › auth ? component名を入力してください › LoginForm
すると、以下のようにテンプレートが生成されます。
Loaded templates: _templates added: src/features/auth/components/LoginForm/Container.test.tsx added: src/features/auth/components/LoginForm/Container.tsx added: src/features/auth/components/LoginForm/Component.tsx added: src/features/auth/components/LoginForm/index.stories.tsx added: src/features/auth/components/LoginForm/index.ts added: src/features/auth/hooks/LoginForm.ts
このように生成されたそれぞれのテンプレートに、それぞれのレイヤーでよく使用するモジュールをあらかじめimportしておきます。これをベースに開発を進めることで、認識のすり合わせが面倒な設計を開発メンバー間で標準化することができます。また、テンプレートの自動生成により一貫性が保たれ、コードベースの認知コストを削減することができます。
この取り組みにより、開発メンバーが新しいディレクトリ構成やレイヤーの責務に素早く慣れることを期待しています。
最後に
JMDC では、医療・ヘルスケア領域の課題解決に一緒に取り組んでいただけるエンジニアを募集中です! hrmos.co
まずはカジュアルにJMDCメンバーと話してみたい/経験が活かせそうなポジションの話を聞いてみたい方は、下記よりエントリーいただけますと幸いです。 hrmos.co
★最新記事のお知らせはX(Twitter)をご覧ください