Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Next.js でリアーキテクトした話 / story-of-re-architect-wit...

Takepepe
November 27, 2021

Next.js でリアーキテクトした話 / story-of-re-architect-with-nextjs

Takepepe

November 27, 2021
Tweet

More Decks by Takepepe

Other Decks in Technology

Transcript

  1. フロントエンド技術スタック ▪ Next.js ▪ CSS Modules ▪ SWR ▪ OpenAPI

    Generator ▪ MSW ▪ Storybook ▪ reg-suit ▪ jest ▪ testing-library ▪ Cypress
  2. API 検討 ▪ BFF として機能する API サーバー ▪ BFF 開発はバックエンドチームが担当

    ▪ FE 観点から、適した API を提案 ▪ Component 境界 = API 境界 = タスク境界 画面単位(Component単位)がタスク境界としても適切な粒度だった
  3. リリース済み内容に沿った、マークアップの再現について。 再現するプロジェクトの CSS は BEM で書かれていました。 (全体的に綺麗に設計されている良い設計) BEM の Block

    は Component 単位として扱うことができるため、 CSS 定義から遡り、Component へ書き換えを進めました。 React Component への書き換え 【課題1】リリース済み内容をリグレッションさせないこと
  4. <ul class="breadcrumb"> <li class="breadcrumb__item"></li> <li class="breadcrumb__item"></li> <li class="breadcrumb__item breadcrumb--mb0"></li> </ul>

    書き換え前のマークアップ .breadcrumb { display: flex; &__item { padding-left: 24px; } &--mb0 { margin-bottom: 0; } } 【課題1】リリース済み内容をリグレッションさせないこと
  5. 書き換え後の React Component // Block はアッパーキャメル変換し、そのまま Component名称とする function Breadcrumb(props) {

    return <ul {...props} className={styles.breadcrumb} /> } // Element は Block 接頭辞をもった Component名称とする function BreadcrumbItem(props) { return <li {...props} className={styles.item} /> } function MyComponent() { return ( <Breadcrumb> <BreadcrumbItem></BreadcrumbItem> <BreadcrumbItem></BreadcrumbItem> <BreadcrumbItem data-mb0></BreadcrumbItem> </Breadcrumb> ) } .breadcrumb { display: flex; } .item { padding-left: 24px; &[data-mb0] { margin-bottom: 0; } } 【課題1】リリース済み内容をリグレッションさせないこと
  6. 書き換えのマッピングコスト 元の CSS 設計が整備されていたこともあり、 スムーズに書き換えを行うことができました。 ▪ CSS Modules に BEM

    定義を移植 ▪ 命名は変更せず、Case 変換し Component 名称とした ▪ Modifier を data 属性に変換した ▪ class 名から Component (Atom) の特定が容易 【課題1】リリース済み内容をリグレッションさせないこと
  7. 元の CSS 設計が整備されていたこともあり、 スムーズに書き換えを行うことができました。 ▪ CSS Modules に BEM 定義を移植

    ▪ 命名は変更せず、Case 変換し Component 名称とした ▪ Modifier を data 属性に変換した ▪ class 名から Component (Atom) の特定が容易 書き換えのマッピングコスト 探しやすく、マッピングコストが最小限。再現性の高い書き換えに 【課題1】リリース済み内容をリグレッションさせないこと
  8. CSS 定義の削除 .alert-text { color: $col-accent; } .annotation { font-size:

    1rem; } .nodata { display: block; text-align: center; } 最後まで残った、 使われているか不明な Global 定義。 【課題1】リリース済み内容をリグレッションさせないこと
  9. CSS 定義の削除 最後まで残った、 使われているか不明な Global 定義。 この定義削除にあたり、 Visual Regression Testing

    を活用。 【課題1】リリース済み内容をリグレッションさせないこと .alert-text { color: $col-accent; } .annotation { font-size: 1rem; } .nodata { display: block; text-align: center; }
  10. ▪ 現在、CSS ソリューションはかつてないほど乱立 ▪ 異なる CSS ソリューション間で移植しやすい定義かどうか ▪ 特定の CSS

    ソリューションでしか実現できない内容ではないか 次世代のリアーキテクトに向けて 【課題1】リリース済み内容をリグレッションさせないこと
  11. ▪ 現在、CSS ソリューションはかつてないほど乱立 ▪ 異なる CSS ソリューション間で移植しやすい定義かどうか ▪ 特定の CSS

    ソリューションでしか実現できない内容ではないか 次世代のリアーキテクトに向けて CSSソリューションは、次世代のマッピングコストも配慮して 【課題1】リリース済み内容をリグレッションさせないこと
  12. 初学者でも参画しやすいコードベースを目指して まだ React は「誰でも書ける」まで至っていません。 初学者であっても、参画しやすいコードベースを目指しました。 ▪ 見様見真似でも、ある程度実装できる ▪ 学習しながらでも、開発に参画できる ▪

    つまづきやすいポイント・そうではないポイントが分離されている コードベースの理解しやすさは、学習曲線に影響する 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと
  13. ▪ 【易】Component 構築 → マークアップの延長線 ▪ 【易】型定義で縛って、Props を渡す ▪ 【難】込み入った

    hooks の記述 ▪ 【難】React Context Provider の設計 React 初学者でも習得しやすい点 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと
  14. ▪ 【易】Component 構築 → マークアップの延長線 ▪ 【易】型定義で縛って、Props を渡す ▪ 【難】込み入った

    hooks の記述 ▪ 【難】React Context Provider の設計 React 初学者が習得し辛い点 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと
  15. ▪ 【易】Component 構築 → マークアップの延長線 ▪ 【易】型定義で縛って、Props を渡す ▪ 【難】込み入った

    hooks の記述 ▪ 【難】React Context Provider の設計 React 初学者が習得し辛い点 Custom Hooks の整備に着手 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと
  16. Custom Hooks でコードベースを標準化 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと export function useMyComponent()

    { const { openModal } = useModal(); const handleClick = () => { openModal({ contentsNode: <div></div> }); }; } モーダルを表示する Custom Hooks
  17. Custom Hooks でコードベースを標準化 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと export function useMyComponent()

    { const { openAlertDialog } = useAlertDialog(); const handleClick = () => { openAlertDialog({ text: "エラーが発生しました ", buttonLabel: "OK", }); } } ダイアログを表示する Custom Hooks
  18. Custom Hooks でコードベースを標準化 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと export function useMyComponent()

    { const { showNotification } = useNotification(); const handleClick = () => { showNotification("削除しました"); } } ノティフィケーションを表示する Custom Hooks
  19. Custom Hooks でコードベースを標準化 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと export function useMyComponent()

    { const { errorMessages, handleErrorWithErrorMessages, } = useHandleErrorWithErrorMessage(); const { execFetch } = useFetcher(async () => { try { ... } catch (err) { handleErrorWithErrorMessages(err); } }); } エラーハンドリングを集約した Custom Hooks
  20. Custom Hooks でコードベースを標準化 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと export function useMyComponent()

    { const { validationErrors, handleValidationErrorsWithAlertDialog, } = useHandleValidationErrorsWithAlertDialog(); const { execFetch } = useFetcher(async () => { try { ... } catch (err) { handleValidationErrorsWithAlertDialog(err); } }); } エラーハンドリング・Component 表示を兼ねた Custom Hooks
  21. ▪ 多くの画面で処理を共通化できた ▪ 見様見真似でも Component 作成が可能に ▪ 画面毎の考察ポイントは、画面固有のロジック実装のみ Custom Hooks

    でコードベースを標準化 【課題2】 React 開発に慣れたメンバーが アサインできるとは限らないこと Custom Hooks でコードベースを標準化することができた
  22. OpenAPI でバックエンドとフロントエンドの齟齬をなくす ▪ BFF として機能する API サーバー ▪ Component を起点として

    API を考える ▪ フロントエンドエンジニアが OpenAPI を書く スキーマ駆動開発 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある
  23. OpenAPI でバックエンドとフロントエンドの齟齬をなくす ▪ BFF として機能する API サーバー ▪ Component を起点として

    API を考える ▪ フロントエンドエンジニアが OpenAPI を書く スキーマ駆動開発 API 定義が予め決まっている通常の開発フローとは「逆」 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある
  24. OpenAPI でバックエンドとフロントエンドの齟齬をなくす ▪ BFF として機能する API サーバー ▪ Component を起点として

    API を考える ▪ フロントエンドエンジニアが OpenAPI を書く スキーマ駆動開発 OpenAPI 定義に慣れている開発メンバーは少数。定義のしやすさが課題に 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある
  25. API スキーマの考察 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある export type ServerProps =

    { id: string; firstName?: string; lastName?: string; }; Component を組み上げ、API に求めるデータを考える export const ModalEditUser: React.VFC<ServerProps> = (props) => { return ( <form> <input type="hidden" name="id" defaultValue={props.id} /> <input type="text" name="firstName" defaultValue={props.firstName} /> <input type="text" name="lastName" defaultValue={props.lastName} /> <button>送信</button> </form> ); };
  26. API スキーマの考察 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある Component を組み上げ、API に求めるデータを考える export

    type ServerProps = { id: string; firstName?: string; lastName?: string; }; export const ModalEditUser: React.VFC<ServerProps> = (props) => { return ( <form> <input type="hidden" name="id" defaultValue={props.id} /> <input type="text" name="firstName" defaultValue={props.firstName} /> <input type="text" name="lastName" defaultValue={props.lastName} /> <button>送信</button> </form> ); };
  27. API スキーマの考察 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある MSW でモックを書き、Component が成立するか検討 export

    const handlers = [ rest.get("/path/to/api", (_, res, ctx) => { return res( ctx.json({ id: "xxxx", firstName: "taro", lastName: "yamada", })); }), ];
  28. API スキーマの考察 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある 更新系のメソッドも、モックが記述可能 export const handlers

    = [ rest.get("/path/to/api", (_, res, ctx) => { return res( ctx.json({ id: "xxxx", firstName: "taro", lastName: "yamada", })); }), ]; export const handlers = [ rest.put("/path/to/api", (req, res, ctx) => { return res(ctx.json({ statusCode: 200 })); }), ];
  29. API スキーマの考察 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある 要件定義とすりあわせ、考慮漏れの洗い出し export const handlers

    = [ rest.get("/path/to/api", (_, res, ctx) => { return res( ctx.json({ id: "xxxx", firstName: "taro", lastName: "yamada", })); }), ]; export const handlers = [ rest.put<Partial<{ id: string }>>("/path/to/api", (req, res, ctx) => { return res(ctx.json({ statusCode: 200 })); }), ];
  30. API スキーマの考察 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある 要件定義とすりあわせ、考慮漏れの洗い出し export const handlers

    = [ rest.get("/path/to/api", (_, res, ctx) => { return res( ctx.json({ id: "xxxx", firstName: "taro", lastName: "yamada", })); }), ]; export const handlers = [ rest.put<Partial<{ id: string }>>("/path/to/api", (req, res, ctx) => { if (req.body.id) { return res(ctx.json({ statusCode: 200 })); } return res( ctx.status(400), ctx.json({ statusCode: 400, errorMessages: ["不正なリクエストです"] }) ); }), ];
  31. API スキーマの考察 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある API スキーマ考察の軸となった export const

    handlers = [ rest.get("/path/to/api", (_, res, ctx) => { return res( ctx.json({ id: "xxxx", firstName: "taro", lastName: "yamada", })); }), ]; export const handlers = [ rest.put<Partial<{ id: string }>>("/path/to/api", (req, res, ctx) => { if (req.body.id) { return res(ctx.json({ statusCode: 200 })); } return res( ctx.status(400), ctx.json({ statusCode: 400, errorMessages: ["不正なリクエストです"] }) ); }), ];
  32. Stoplight Studio を使う Open API 定義にあたり、 チーム内標準ツールとして Stoplight Studio を使用。

    MSW のレスポンスから、 サンプル JSON を取得。 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある
  33. Stoplight Studio を使う Stoplight Studio には、 JSON から OpenAPI 定義を

    Generateできる機能がある。 この機能を使い、 OpenAPI 定義の工数を削減。 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある
  34. スキーマ駆動開発 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある $ npm run openapi:gen 書き上がった

    OpenAPI から、 fetch client を生成 ######################################### # Thanks for using OpenAPI Generator. # # Please consider donation to help us maintain this project 🙏 # # https://opencollective.com/openapi_generator/donate # #########################################
  35. スキーマ駆動開発 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある fetch client を、 Component にバインド

    export const ModalEditUserBase: React.VFC<ServerProps> = (props) => { return ( <form> <input type="hidden" name="id" defaultValue={props.id} /> <input type="text" name="firstName" defaultValue={props.firstName} /> <input type="text" name="lastName" defaultValue={props.lastName} /> <button>送信</button> </form> ); }; export const ModalEditUser: React.VFC = () => { const { data, error } = useSWR( "/path/to/api", get<ServerProps>("path/to/api") ); if (error) return <ErrorFallback error={error} />; if (!data) return <>loading...</>; return <ModalEditUserBase {...data} />; };
  36. スキーマ駆動開発 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある export const ModalEditUserBase: React.VFC<ServerProps> =

    (props) => { return ( <form> <input type="hidden" name="id" defaultValue={props.id} /> <input type="text" name="firstName" defaultValue={props.firstName} /> <input type="text" name="lastName" defaultValue={props.lastName} /> <button>送信</button> </form> ); }; export const ModalEditUser: React.VFC = () => { const { data, error } = useSWR( "/path/to/api", organismsApi.organismsModalEditUserGet() ); if (error) return <ErrorFallback error={error} />; if (!data) return <>loading...</>; return <ModalEditUserBase {...data} />; }; fetch client を、 Component にバインド
  37. スキーマ駆動開発 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある この段階で期待値と、OpenAPI 定義に齟齬があれば気付く(型推論あわせ) export const ModalEditUserBase:

    React.VFC<ServerProps> = (props) => { return ( <form> <input type="hidden" name="id" defaultValue={props.id} /> <input type="text" name="firstName" defaultValue={props.firstName} /> <input type="text" name="lastName" defaultValue={props.lastName} /> <button>送信</button> </form> ); }; export const ModalEditUser: React.VFC = () => { const { data, error } = useSWR( "/path/to/api", organismsApi.organismsModalEditUserGet() ); if (error) return <ErrorFallback error={error} />; if (!data) return <>loading...</>; return <ModalEditUserBase {...data} />; };
  38. スキーマ駆動開発 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある ▪ 確度の高い状態で、バックエンド担当にレビュー依頼 ▪ レビューで受けた指摘のもと、OpenAPI 定義を修正

    ▪ 修正内容を伝搬させるため、fetch client を再生成 ▪ 影響箇所を TypeScript のコンパイルエラーで検知 OpenAPI と Component の疎通齟齬が TypeScript で担保された
  39. 課題「123」の取り組みでコードベースの標準化が整う ✅ 課題1:マークアップから Component への確実なマッピング ✅ 課題2:React Component の一律な実装 ✅

    課題3:OpenAPI 定義検討のフロー 増員とチームビルディング 【課題4】10人並列でも開発可能な体制をつくること
  40. 量産フェーズに入り、大人数での並列開発が開始 ▪ 2人 → 5人 → 10人 と、1ヶ月間隔でメンバーが倍増 ▪ 新規メンバー受入を兼ねながら進行

    ▪ 並行して開発可能な体制を整える 増員とチームビルディング 【課題4】10人並列でも開発可能な体制をつくること
  41. 量産フェーズに入り、大人数での並列開発が開始 ▪ 2人 → 5人 → 10人 と、1ヶ月間隔でメンバーが倍増 ▪ 新規メンバー受入を兼ねながら進行

    ▪ 並行して開発可能な体制を整える 増員とチームビルディング スムーズな増員計画を念頭に 【課題4】10人並列でも開発可能な体制をつくること
  42. hygen による定型出力 大人数での並列開発は、lint・formatter だけでは 防げないガイドライン違反が起こりえます。 また、管理上 OpenAPI と Component の

    命名規則・書式を一律にする必要性がありました。 そこで活用したのが「hygen」です。 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある
  43. $ npm run new:swrfc 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある > [email protected]

    new:swrfc > hygen new swrfc ✔ What is the name of component? · npm scripts に hygen のエントリーポイントを登録 hygen による定型出力
  44. $ npm run new:swrfc 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある > [email protected]

    new:swrfc > hygen new swrfc ✔ What is the name of component? · ModalEditUser CLI に Component 名称を入力すると… hygen による定型出力
  45. $ npm run new:swrfc 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある > [email protected]

    new:swrfc > hygen new swrfc ✔ What is the name of component? · ModalEditUser Loaded templates: .hygen added: src/components/organisms/ModalEditUser/apis.mock.ts added: src/components/organisms/ModalEditUser/ModalEditUser.stories.tsx added: src/components/organisms/ModalEditUser/ModalEditUser.test.tsx added: src/components/organisms/ModalEditUser/ModalEditUser.tsx added: src/components/organisms/ModalEditUser/index.tsx added: /Users/Me/openapi/src/paths/organisms/modal_edit_user.yaml added: src/components/organisms/ModalEditUser/styles.module.scss Component 命名に沿ったファイル群が出力される hygen による定型出力
  46. $ npm run new:swrfc 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある > [email protected]

    new:swrfc > hygen new swrfc ✔ What is the name of component? · ModalEditUser Loaded templates: .hygen added: src/components/organisms/ModalEditUser/apis.mock.ts added: src/components/organisms/ModalEditUser/ModalEditUser.stories.tsx added: src/components/organisms/ModalEditUser/ModalEditUser.test.tsx added: src/components/organisms/ModalEditUser/ModalEditUser.tsx added: src/components/organisms/ModalEditUser/index.tsx added: /Users/Me/openapi/src/paths/organisms/modal_edit_user.yaml added: src/components/organisms/ModalEditUser/styles.module.scss React Component に必要なファイル群と… hygen による定型出力
  47. $ npm run new:swrfc 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある > [email protected]

    new:swrfc > hygen new swrfc ✔ What is the name of component? · ModalEditUser Loaded templates: .hygen added: src/components/organisms/ModalEditUser/apis.mock.ts added: src/components/organisms/ModalEditUser/ModalEditUser.stories.tsx added: src/components/organisms/ModalEditUser/ModalEditUser.test.tsx added: src/components/organisms/ModalEditUser/ModalEditUser.tsx added: src/components/organisms/ModalEditUser/index.tsx added: /Users/Me/openapi/src/paths/organisms/modal_edit_user.yaml added: src/components/organisms/ModalEditUser/styles.module.scss OpenAPI 定義の雛形・MSW ハンドラーも同時に自動生成 hygen による定型出力
  48. ModalEditUser.tsx 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある export const ModalEditUser: React.VFC =

    () => { const { data, error } = useSWR(apiPath(), () => get<ServerProps>(apiPath())); if (error) return <ErrorFallback error={error} />; if (!data) return <>loading...</>; return <ModalEditUserBase {...data} />; }; ファイル命名ミスがなく、コンポーネント書式も一律に hygen による定型出力
  49. ModalEditUser.stories.tsx 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある const defaultArgs: Props = {

    ...data }; const Template: Story<Props> = (args) => ( <ModalEditUserBase {...defaultArgs} {...args} /> ); export const Index: Story<Props> = Template.bind({}); Index.args = {}; export default { title: "organisms/ModalEditUser", } as Meta; Storybook の雛形ファイルも同時に生成 hygen による定型出力
  50. 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある describe("organisms/ModalEditUser", () => { describe("異常系レスポンスの表示", ()

    => { describe("エラーレスポンスが返ってきた時", () => { test.skip("エラーが表示される", async () => {}); }); }); describe("正常系レスポンスの表示", () => { describe("正常レスポンスが返ってきた時", () => { test.skip("コンテンツが表示される", async () => {}); }); }); }); ModalEditUser.test.tsx テストの雛形ファイルも同時に生成 hygen による定型出力
  51. 【課題3】 フロントエンドエンジニアが先行して OpenAPI を定義する必要がある modal_edit_user.yaml post: summary: XXX::画面名称::POST description: 画面概要

    operationId: OrganismsModalEditUserPost tags: - Organisms responses: '201': description: Created content: application/json: schema: $ref: ../../components/common_201.yaml '400': description: Bad Request content: application/json: schema: $ref: ../../components/common_400.yaml '403': description: Forbidden content: application/json: schema: $ref: ../../components/common_403.yaml Component に紐づいた Open API の雛形ファイルも同時に生成 hygen による定型出力 openapi: 3.0.0 info: title: XXX::画面名称 version: '1.0' servers: - url: http://localhost:8080 description: development paths: /organisms/modal_edit_user: get: summary: XXX::画面名称::GET description: 画面概要 operationId: OrganismsModalEditUserGet tags: - Organisms responses: '200': description: OK content: application/json: schema: $ref: ../../components/common_200.yaml