Slide 1

Slide 1 text

新規プロダクトで プロトタイプから製品版まで Next.jsで開発したリアル

Slide 2

Slide 2 text

自己紹介 1/54

Slide 3

Slide 3 text

こんなNext.js は嫌だ 2/54

Slide 4

Slide 4 text

useOshogatsuという西暦を渡すと、その年のお正月 のUnixタイムスタンプを返すhookが公式から提供されている import { useOshogatsu } from 'next/date' const year = 2025 const { gantan } = useOshogatsu(year) console.log("元旦:" + gantan) // 元旦:1745107200 3/54

Slide 5

Slide 5 text

実はPHPには似たようなのが本当にある。あなただけのPHPの面白関数を探してみて⭐ 4/54

Slide 6

Slide 6 text

自己紹介 成果を出す!実践DXを学ぼう 受賞企業 登壇セッション (株)オイシス 生産本部兼はりま工場 統括ライン長 荒尾 和哉様 ファウンテン・デリ(株) 品質管理主任兼カミナシPJチーム事務局 立花 直也様 ■略歴 〜2024年10月:株式会社ゆめみ 2024年10月〜:株式会社カミナシ 〜現在:AIラベル検査の開発 株式会社カミナシ StatHackカンパニー ソフトウェアエンジニア 河野 陸 5/54

Slide 7

Slide 7 text

自己紹介 最近、友達から意識高そうなビジネス書をもらった 6/54

Slide 8

Slide 8 text

自己紹介 最近、友達から意識高そうなビジネス書をもらった コンフォートゾーンを飛び出せ!とのこと 6/54

Slide 9

Slide 9 text

自己紹介 新宿の8席しかない鬼地下のライブハウスで 大爆笑ネタをやってる様子 1000円もらえた 7/54

Slide 10

Slide 10 text

会社紹介 会社紹介 8/54

Slide 11

Slide 11 text

株式会社カミナシ https://kaminashi.jp 9/54

Slide 12

Slide 12 text

カミナシ機能ラインナップ 現場のデジタルインフラとしてお客様のあらゆる業務を解決します Method 作業 Men ⼈ Machine 設備 ペーパーレス化を実現 現場の品質レベルを向上 従業員と会社のやり取りを 1ツールで完結 現場教育を効率的に 動画マニュアルと研修管理 電⼦帳票 故障履歴や保全計画を 設備カルテとして⼀元管理 マニュアル‧研修 コミュニケーション 設備カルテ 現場の共通管理基盤(カミナシID) 1つのID/Passで運⽤で複数のシステムを利⽤ 運⽤の負担軽減とセキュリティ向上 10/54

Slide 13

Slide 13 text

今日話すプロダクトis… カミナシレポートが提供する一つの機能について話します! Method 作業 ペーパーレス化を実現 現場の品質レベルを向上 電⼦帳票 11/54

Slide 14

Slide 14 text

技術スタック プログラミング言語 フロントエンド・モバイル API定義 データベース コード管理・実行環境・ SaaS モニタリング GO TypeScript React Expo Next.js Remix MySQL Datadog GitHub Terraform PostgreSQL Sentry Docker SendGrid 12/54

Slide 15

Slide 15 text

長い前置き 各サービスチームで その時最適なものを 採用する 技術選定の方針 絶対的な標準の技術スタックはない 13/54

Slide 16

Slide 16 text

長い前置き なぜNext.jsが最適 だったのか? AIラベル検査では 全社的にNext.jsを採用しているわけではない 14/54

Slide 17

Slide 17 text

長い前置き 時系列順に ストーリー仕立てで 解説していきます プロトタイプ〜正式リリースまで 15/54

Slide 18

Slide 18 text

動くプロトタイプを5日以内に完成させろ! MISSION1 動くプロトタイプを 5日以内に完成させろ! 16/54

Slide 19

Slide 19 text

動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 PM 17/54

Slide 20

Slide 20 text

動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 PM よし。今はまだ何もないけど 5日後のお客さん訪問で プロトタイプ触ってもらおう! 17/54

Slide 21

Slide 21 text

動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 前提・お気持ち - コアな機能の価値検証を最優先にしたい(だって5日しかない!) - となるとデプロイやインフラに気を遣っている余裕がない! - APIサーバーとFEを別で用意したり開発する余裕もない! 18/54

Slide 22

Slide 22 text

動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 技術選定 1 言語はFE/BEで統一したい → 流石にTypeScriptだろう 2 APIサーバーを(意識して)用意しないで済む仕組み→Server Actions(を使うなら...?) 3 デプロイやインフラを気にせず即公開できるVercel(と相性が良いあいつ...) 19/54

Slide 23

Slide 23 text

動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 技術選定 1 言語はFE/BEで統一したい → 流石にTypeScriptだろう 2 APIサーバーを(意識して)用意しないで済む仕組み→Server Actions(を使うなら...?) 3 デプロイやインフラを気にせず即公開できるVercel(と相性が良いあいつ...) 朧げながら浮かんできました 20/54

Slide 24

Slide 24 text

動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 フロントエンド&バックエンド ホスティング データベース・ストレージ 手軽さと開発スピード重視の技術スタックに 21/54

Slide 25

Slide 25 text

動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 デプロイ(CI/CD)やインフラに気を使わず、 Full TypeScript ✖Server Actionsで考えられうる最高速度で開発できた! 1 フィードバックがあったらすぐ開発して、すぐ反映して〜の爆速サイクル 結果 2 素早く価値検証できて、正式開発への意思決定に貢献できた 22/54

Slide 26

Slide 26 text

MISSION2 製品版を開発せよ 製品版を開発せよ 23/54

Slide 27

Slide 27 text

製品版のプロダクト構成 製品版を開発せよ 24/54

Slide 28

Slide 28 text

製品版を開発せよ 検査画面 UX的に既存プロダクトに実装する 管理画面 既存プロダクトとは別に新しく作る 25/54

Slide 29

Slide 29 text

製品版を開発せよ 検査画面 UX的に既存プロダクトに実装する 管理画面を新しく別のアプリで作る必要あり 25/54

Slide 30

Slide 30 text

管理画面を新しく別で作る必要あり プロトタイプのコードは捨てて0から作 る必要がある 製品版を開発せよ 検査画面 UX的に既存プロダクトに実装する 25/54

Slide 31

Slide 31 text

製品版を開発せよ UXの都合上 検査機能は既存プロダクトに実装する必要あり 管理画面 既存プロダクトとは別に新しく作る 25/54

Slide 32

Slide 32 text

製品版を開発せよ 検査機能は既存のプロダクトに実装 プロトの中でかなり洗練されたので 機能的、コード品質が十分なレベルに 管理画面 既存プロダクトとは別に新しく作る 25/54

Slide 33

Slide 33 text

選択と集中... 製品版を開発せよ 26/54

Slide 34

Slide 34 text

選択と集中... 製品版を開発せよ 不確実性の高い 検査画面の組み込みに時間を 使うべき 26/54

Slide 35

Slide 35 text

管理画面は そのままNext.jsで いくことにする...! インフラはAWSに 移行したよ 製品版を開発せよ 管理画面 27/54

Slide 36

Slide 36 text

製品版においては プロトタイプ開発で不満なくいけた ので、そのまま使ってます...!という 感じ! 製品版を開発せよ つまり、ぶっちゃけ Next.jsじゃないと満たせない要件があるとかではない 28/54

Slide 37

Slide 37 text

製品版を開発せよ 29/54

Slide 38

Slide 38 text

Next.jsを採用した理由(背景)まとめ 素早い開発が求められるなか、 Full TypeScript ✖ Server Actionsで爆速機能開発できて、 Vercelでゼロコンフィグでデプロイできる。 この辺がマッチした。 特に不満なく開発できたので、課題に集中するため、そのまま採用した! 色々言ってるけど、僕が使ってみたかったのが7割かも 製品版を開発せよ 30/54

Slide 39

Slide 39 text

SaaSはリリースしたら MISSION3へ続く... 31/54

Slide 40

Slide 40 text

SaaSはリリースしたら 終わりではない... MISSION3へ続く... 31/54

Slide 41

Slide 41 text

SaaSはリリースしたら 終わりではない... MISSION3へ続く... 31/54

Slide 42

Slide 42 text

MISSION3 プロトタイプからの 技術的課題を 解決せよ プロトタイプからの技術的課題を解決せよ 32/54

Slide 43

Slide 43 text

プロトタイプからの技術的課題を解決せよ MISSION3 FE/BEで型を共通化しすぎてつらい case1 Progressively Enhanced Form、複雑なFormはどうしたら...? case2 プロトタイプからの技術的課題を解決せよ 33/54

Slide 44

Slide 44 text

FE/BEで型を共通化しすぎてつらい case1 特にレイヤを意識せずにServer Actionsを使うとこうなる(こうなった) // server/type.ts type User = { id: string; name: string hashedPassword: string; createdAt: string; settingId: number; } // Server Actions export const getUsers = async (): Promise => { 'use server'; // ユーザーを取得して返却する } プロトタイプからの技術的課題を解決せよ export const Page = async () => { const users = await getUsers(); return } 'use client' export const UserList = ({users}: {users: server.User[]}) => { // クライアントにhashedPasswordが漏れてるけど大丈夫? // クライアントにとって不要なフィールドはない? return
    {users.map((user) => (
  • {user.name}
  • ))}
} 34/54

Slide 45

Slide 45 text

1 とにかく開発早い 共通化のメリット 2 コードもファイルも少ない プロトタイプからの技術的課題を解決せよ FE/BEで型を共通化しすぎてつらい case1 3 隅から隅まで型補完が効く 35/54

Slide 46

Slide 46 text

1 FEに公開させたくないフィールドを意図せず公開してしまう恐れあり 2 BEとFE常に一緒に変更しないといけなくなる(密結合) 3 UIコンポーネント側でロジックが増える 共通化の課題 プロトタイプからの技術的課題を解決せよ FE/BEで型を共通化しすぎてつらい case1 36/54

Slide 47

Slide 47 text

解決方法 // server/type.ts type User = { id: string; name: string hashedPassword: string; createdAt: string; settingId: number; } // client/type.ts type User = { id: string; name: string } それぞれに最適な型を定義して疎結合にした。Server Actionsとして実装する関数はあくまでも境界に プロトタイプからの技術的課題を解決せよ FE/BEで型を共通化しすぎてつらい case1 // server/service/user.ts import type { User } from '../server/types' const getUsersService = (): User[] => { // dbアクセスやロジックなど } 37/54

Slide 48

Slide 48 text

解決方法 import * as client from '@/client/types' import { getUsersService } from '@/server/service' // server actions はFE/BEの境界としてデータの変換 export const getUsers = async (): Promise => { 'use server'; // ユーザー取得する処理 const users = getUsersService() // clientの型に変換する return convertToClientUser(users) } それぞれに最適な型を定義して疎結合にした。Server Actionsとして実装する関数はあくまでも境界に プロトタイプからの技術的課題を解決せよ FE/BEで型を共通化しすぎてつらい case1 38/54

Slide 49

Slide 49 text

Progressively Enhanced Form、複雑なFormはどうしたら...? case2 export const createUser = async (_prev: unknown, fd: FormData) => { "use server"; const name = fd.get("name") as string; const age = fd.get("age") as string; const parsed = parseUser(name, age); if (!parsed.success) { return parsed.errors } const userId = await userRepo.create({ name, age }) redirect("/users/" + userId); } "use client"; export default function UserCreateForm() { const [errors, formAction, isPending] = useActionState(createUser, {name: [], age: []}); return ( ); } Server Actions が最初に出てきた時にインパクトあったやつですね プロトタイプからの技術的課題を解決せよ 39/54

Slide 50

Slide 50 text

1 状態管理しなくて良い!!! 2 典型的なフォームハンドリングのボイラープレートが不要になる (状態管理→バリデーションして→APIの型に変換して〜みたいな) メリット(PE自体のメリットは割愛。DX的な目線で) プロトタイプからの技術的課題を解決せよ case2 Progressively Enhanced Form、複雑なFormはどうしたら...? 40/54

Slide 51

Slide 51 text

1 クライアントJSに頼らないためリッチ、複雑なFormが提供できない 課題 2 FormData APIが string 中心のため、型の恩恵が薄く、静的チェックが効かない プロトタイプからの技術的課題を解決せよ case2 Progressively Enhanced Form、複雑なFormはどうしたら...? 41/54

Slide 52

Slide 52 text

複雑なFormは従来の状態管理に戻した。シンプルなやつはそのままPE 解決法 1 2 一部で状態管理のつらみは戻ってきたけど、即時バリデーションエラーなどリッ チなFormが提供できるようになった PEに統一していた時はできなかった、複雑なFormを実装できた プロトタイプからの技術的課題を解決せよ case2 Progressively Enhanced Form、複雑なFormはどうしたら...? 42/54

Slide 53

Slide 53 text

複雑なFormは従来の状態管理に戻した。シンプルなやつはそのままPE 解決法 1 2 一部で状態管理のつらみは戻ってきたけど、即時バリデーションエラーなどリッ チなFormが提供できるようになった PEに統一していた時はできなかった、複雑なFormを実装できた プロトタイプからの技術的課題を解決せよ case2 Progressively Enhanced Form、複雑なFormはどうしたら...? 具体的な方法は最近の開発パートで話します 42/54

Slide 54

Slide 54 text

最近の開発 最近の開発 43/54

Slide 55

Slide 55 text

最近の開発 Extra nuqsでクエリパラメータの管理を安全かつシンプルに case1 Zustand + Valibotによるフォーム実装 case2 最近の開発 44/54

Slide 56

Slide 56 text

const { query, push } = useRouter(); const limit = !Array.isArray(query.limit) ? parseInt(query.limit ?? '50', 10) : 50; // … const sp = new URLSearchParams(location.search); sp.set('limit', String(nextLimit)); // 全部 string に変換必須… push(`?${sp.toString()}`); 最近の開発 before nuqsでクエリパラメータの管理を安全かつシンプルに case1 - router.queryのナロイングが必須&複雑になりがち - URLSearchParamsの操作はバグりがち - 全てstringに変換、削除や未設定時の対応など... 45/54

Slide 57

Slide 57 text

import { useQueryState, parseAsInteger, withDefault } from 'nuqs'; const [limit, setLimit] = useQueryState( 'limit', withDefault(parseAsInteger.withOptions({ min: 1 }), 50) ); // … setLimit(newLimit); // 直列化・更新方式はフックが面倒見てくれる 最近の開発 after nuqsでクエリパラメータの管理を安全かつシンプルに case1 - 組み込みのparser 👌 - クエリパラメータの更新も宣言的にかける。気にすることが大幅減 46/54

Slide 58

Slide 58 text

最近の開発 Zustand + Valibotによるフォーム実装 case2 47/54

Slide 59

Slide 59 text

最近の開発 Zustand + Valibotによるフォーム実装 case2 元はというと 47/54

Slide 60

Slide 60 text

最近の開発 さっきお話ししたやつです Zustand + Valibotによるフォーム実装 case2 48/54

Slide 61

Slide 61 text

export const createUser = async (_prev: unknown, fd: FormData) => { "use server"; const name = fd.get("name") as string; const age = fd.get("age") as string; const parsed = parseUser(name, age); if (!parsed.success) { return parsed.errors } const userId = await userRepo.create({ name, age }) redirect("/users/" + userId); } - Server Actionsに全て投げてバリデーション、エラーがあったら一気にFEに返して表示 最近の開発 Zustand + Valibotによるフォーム実装 case2 before 49/54

Slide 62

Slide 62 text

Zustand Slice Patternなるものを使ってみた import type { FormInputSliceCreator } from '@/common/form'; import { validateLength } from '@/common/form/validation'; export type NameSlice = { name: string; setName: (name: string) => void; getNameErrorMessages: (value: string) => string[]; getNameIsValid: () => boolean; }; export const createNameSlice: FormInputSliceCreator< NameSlice, { name: string } > = (initialValue) => (set, get) => ({ name: initialValue.name, setName: (name) => set({ name }), getNameErrorMessages: (name) => [ ...validateLength({ value: name, minLength: 1, maxLength: 50 }), ], getNameIsValid: () => get().getNameErrorMessages(get().name).length === 0, }); 最近の開発 Zustand + Valibotによるフォーム実装 case2 50/54

Slide 63

Slide 63 text

import { useState } from 'react'; import { Input } from '@/common/components/FormInput'; import { useFormStore } from '../FormStoreProvider'; export const Name = () => { const value = useFormStore((state) => state.name); const setValue = useFormStore((state) => state.setName); const getErrors = useFormStore((state) => state.getNameErrorMessages); const [errorMsg, setErrorMsg] = useState(''); return ( setValue(e.target.value)} onBlur={() => setErrorMsg(getErrors(value)?.[0] ?? '')} required errorMessage={errorMsg} error={errorMsg !== ''} /> ); }; 最近の開発 Zustand Slice Patternなるものを使ってみた Zustand + Valibotによるフォーム実装 case2 51/54

Slide 64

Slide 64 text

export type UserStore = NameSlice & AgeSlice & StatusSlice export const createUserStore = (initial: InitialValue) => { const { name, age, status } = initial; return create()((...args) => ({ ...createNameSlice({ name })(...args), ...createAgeSlice({ age })(...args), ...createStatusSlice({ status })(...args), })); }; 最近の開発 Zustand Slice Patternなるものを使ってみた Zustand + Valibotによるフォーム実装 case2 52/54

Slide 65

Slide 65 text

- UXの良いForm(即時バリデーション) - フィールドごとに凝集度が高く、めちゃくちゃ可読性高い、追記しやすい - Formの入力状態によって分岐するような複雑なFormも簡単に実装できた 最近の開発 Zustand + Valibotによるフォーム実装 case2 53/54

Slide 66

Slide 66 text

最近の開発 https://zenn.dev/yumemi_inc/articles/26ff46875f3cd0 詳しくはこちらを参照! 54/54

Slide 67

Slide 67 text

告知 extra ぜひ一度カジュアル面談でお話ししましょう!

Slide 68

Slide 68 text

告知 extra ぜひ一度カジュアル面談でお話ししましょう!

Slide 69

Slide 69 text

告知 相方 extra