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

コンパウンドプロダクトの画面を Schema-Driven UI で描く

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for Masayuki Takahashi Masayuki Takahashi
November 02, 2026
38

コンパウンドプロダクトの画面を Schema-Driven UI で描く

Avatar for Masayuki Takahashi

Masayuki Takahashi

November 02, 2026

Transcript

  1. 自己紹介 • 業務 ◦ ◦ HR領域のプロダクト開発エンジニア プロダクト全体のUIUX改善 • 経歴 ◦

    ◦ ◦ 2021/04 ASP事業会社 2022/04 株式会社HRBrain 2025/10 Dress Code株式会社 • 技術 ◦ ◦ ユーザー体験とか考えるのが好き フロント専任からフルスタックに挑戦中 もず(@mozu1206) • 趣味 ◦ コーヒー / スプラトゥーン CONFIDENTIAL | © Dress Code Inc . All rights reserved. 2
  2. 会社概要 Company Name / 会社名 Dress Code 株式会社 CEO /

    代表取締役 江尻 祐樹 Date of establishment / 設立年月 2024年9月(正式創業 2025年4月) Location / 所在地 東京都中央区築地2-1-4 銀座PREX East 8F Member / メンバー数 56名 Pre Seed&Seed Round Number of companies Number of countries 14.1億円 250+社 5カ国 資金調達を実施 が利用中 で事業を展開 CONFIDENTIAL | © Dress Code Inc . All rights reserved. 3
  3. Schema-Driven UI とは 一般の呼び名 私たちの呼び方 Server-Driven UI Schema-Driven UI サーバーがデータだけでなく画面の形まで返し、クライアントは

    それを描く スキーマ(zod の契約)を正とし、Backend はそれに従って 返し、Frontend はそれに従って描く 採用例: Airbnb(Ghost Platform)、Lyft ねらい: 誰が書いても同じ形になる ねらい: アプリのリリースを待たずに画面を変える 参考: Airbnb Tech Blog「A Deep Dive into Airbnb’s Server-Driven UI System」(2021、medium.com/airbnb-engineering) Lyft Engineering「The Journey to Server Driven UI At Lyft Bikes and Scooters」(eng.lyft.com) 仕組みは Server-Driven UI。スキーマを正とするから Schema-Driven UI CONFIDENTIAL | © Dress Code Inc . All rights reserved. 9
  4. これまで 1 カラムを 1 つ足すたびに、3 か所を揃えていた Frontend 2 Backend 3

    Frontend カラム定義を足す 返すデータを足す セルを選ぶ 見出し・順番・ソート可否 クエリと DTO に項目を増やす 日付は DateCell、人は PersonCell </ routes/<<./columns.tsx(Frontend) const columns = [ { id: 'name', header: '氏名', cell: PersonCell, enableSorting: true }, { id: 'joinedAt', header: '入社日', cell: DateCell, enableSorting: true }, ] 普通のやり方。ただ、揃えるのは書き手の注意力まかせで、書き手が増えるほどズレる CONFIDENTIAL | © Dress Code Inc . All rights reserved. 11
  5. いま Backend が、カラム定義とセルの種類まで返す Backend 一覧画面 名前 種別 状態 担当者 購入日

    ノート PC 001 PC 貸出中 山田 太郎 2025/04/01 カラム定義 見出し・順番・ソート可否 返すデータ モニター 014 周辺機器 在庫 - 2025/06/12 スマホ 007 モバイル 貸出中 佐藤 花子 2024/11/20 ノート PC 023 PC 修理中 鈴木 一郎 2023/09/05 キーボード 102 周辺機器 在庫 - 2026/01/15 行ごとの値 セルの種類 cellType Frontend タブレット 003 モバイル 貸出中 高橋 健 2025/02/28 cellType に部品を当てて描くだけ 一覧画面のイメージ(データは架空)。見出し・順番・セルの種類は Backend が返す 3 か所が Backend の 1 か所に集まった。カラム定義はフロントが触らない CONFIDENTIAL | © Dress Code Inc . All rights reserved. 12
  6. カラムは Backend が決め、Frontend は写すだけ Backend:カラムを定義する Frontend:写して部品を当てる </ カラム定義(Backend) export const

    COLUMNS = { name: { cellType: CellType.PERSON_NAME, sortable: true, sortKey: 'NAME', pinned: ColumnPin.LEFT, minWidth: '240px', }, </ <<. }; </ カラム定義を写す(Frontend) export function buildDynamicColumns(columns) { return columns.map((col) <> ({ id: col.fieldKey, accessorFn: (r) <> r.fieldValues[col.fieldKey], enableSorting: col.sortable, cell: getCellRenderer(col), })); } </ cellType から部品を引く STATIC_RENDERERS.get(col.cellType) <? FallbackCell fieldKey ごとの if はない。cellType で部品を引くだけ CONFIDENTIAL | © Dress Code Inc . All rights reserved. 13
  7. 一覧画面のフロントは、カラム以外だけを書く Frontend:ある一覧画面 この画面で </ 一覧画面のコンポーネント(Frontend、46 行) const { data, isPending,

    tableProps } = useListDynamicTable({ search, navigate, </ 行クリックの遷移はフロントから渡す onNavigateToDetail: (id) <> navigate({ to: '/items/$id', params: { id } }), }); 書かない return <DynamicDataTable data={data} isPending={isPending} {<<.tableProps} <>; 検索条件と URL の同期、行クリックの遷移先(Backend は行の id だけ返す)、カラムの表示切替ボタン(右端の操 作カラム) カラムの見出し・順番・幅、セルの選び方、翻訳キー。 Backend が返す 書く 画面ごとに書くのは、その画面だけの振る舞い。データのカラム定義は 1 行も書かない CONFIDENTIAL | © Dress Code Inc . All rights reserved. 14
  8. カラムの追加が、Backend の PR 1 本で終わる </ カラム定義 + ItemCount: {

    + cellType: CellType.TEXT, sortable: false, minWidth: '100px', + }, </ 行の値への変換 + ItemCount: (row) <> String(row.itemCount), </ 一覧を取ってくるクエリ - select: { name: true }, + select: { name: true, items: true }, + itemCount: r._count.items, </ 契約 + itemCount: z.number(), 5 ファイル Backend の変更(テストを除く) 0行 Frontend の変更 取ってくる、返すを 1 つ増やして、カラムとセルを選ぶ。それで終わり CONFIDENTIAL | © Dress Code Inc . All rights reserved. 15
  9. 知らない値が来ても、フロントは止まらない Backend:契約 配り方 </ 契約(Backend) const ColumnSchema = z.object({ fieldKey:

    z.string(), fieldName: z.string(), cellType: z.string(), </ enum にしない sortable: z.boolean(), pinned: z.string().nullable(), }); Backend の契約ファイルに zod で書く enum にしない理由 enum で縛ると、Backend と Frontend を同時にデプロイしないと壊れる。 書く側(Backend)は CellType の定数で縛る。ゆるいのは配る型だけ。 知らない値は FallbackCell で描く ビルドで集めて型にし、GitHub Packages へ publish Frontend は契約に沿った型で受け取る 契約の正本は Backend に 1 つ。型はゆるく配って、描く側で守る CONFIDENTIAL | © Dress Code Inc . All rights reserved. 16
  10. フロントは、届いたカラムをそのまま描くだけ これまで:カラム定義はフロント いま:カラム定義は Backend const allColumns = [<<.]; </ カラム定義はフロント

    </ 権限を見て、カラムを間引く const columns = allColumns.filter((col) <> canView(user, col.fieldKey), ); </ 並び替え・絞り込みでも、同じ判定をもう一度 </ カラム定義は Backend から届く const columns = buildDynamicColumns(schema.columns); </ 権限の判定は、フロントにはない 見せないカラムは、Backend が最初から返さない カラム定義も、値も、そのカラムでの並び替え・絞り込みの条件も、まとめて外して返す 出す・出さないはフロントで考えない。届いたものを、そのまま描く CONFIDENTIAL | © Dress Code Inc . All rights reserved. 17
  11. 合う部品がないときだけ、フロントで作る Backend Frontend カラム定義と値を返す 新しいセルを作って登録する いつもどおりカラム定義に足し、新しい cellType を指定する 全 cellType

    に描画があるかを、テストで確かめる 部品をどう作り、どんな契約で配るか。それが dresscode-ui 詳しくは山口さんの「契約で守るコンパウンドプロダクトのデザインシステム」へ CONFIDENTIAL | © Dress Code Inc . All rights reserved. 18
  12. 詳細画面は、セクションに属性が並ぶだけ 詳細画面 セクション 名前 colspan 3 種別 colspan 6 説明

    電話番号 colspan 3 colspan 3 状態 詳細画面のイメージ。valueType は一覧の cellType、colspan は幅 colspan 3 </ 属性の定義(Backend) Name: b.define({ attributeKey: AttributeKey.NAME, valueType: ValueType.TEXT, colspan: 3, }), Category: b.define({ <<. colspan: 3 }), Description: b.define({ <<. colspan: 6 }), </ 詳細画面のセクション(Frontend) <FormSection label={section.name}> <AttributeRenderer attributes={section.attributes} attributeRenderInterceptor={<<.} <> </FormSection> valueType ごとの分岐は共通部品に集め、画面側はその画面だけの上書きを書く CONFIDENTIAL | © Dress Code Inc . All rights reserved. 20
  13. Thank you for your attention. We are Hiring! CONFIDENTIAL |

    © Dress Code Inc . All rights reserved. 25