Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
新規プロダクトでプロトタイプから正式リリースまでNext.jsで開発したリアル
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
かわりく / Kawano Riku
September 16, 2025
Technology
1.8k
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
新規プロダクトでプロトタイプから正式リリースまでNext.jsで開発したリアル
https://findy.connpass.com/event/365033/
こちらのイベントの登壇資料になります
かわりく / Kawano Riku
September 16, 2025
More Decks by かわりく / Kawano Riku
See All by かわりく / Kawano Riku
TSで型安全なエラーハンドリング〜まずはBranded Typeで始めてみては?〜
kawanoriku0
0
390
Other Decks in Technology
See All in Technology
omasushiというライブラリを作った
polidog
PRO
0
150
なぜSRE・セキュリティは評価されないのか?守りの組織を事業成長エンジンに変えた実践
cscengineer
PRO
3
2.4k
Reactの設計論
uhyo
10
3.2k
幾何アルゴリズムで なめらかなピン操作を / iOSDC Japan 2026 / smoothpin
kazumanagano
0
220
DEFCON34-Write-up_HYCu-MYCu
daikiokazaki
0
110
多摩川(.dev)ランニング入門 / Tamagawa.dev#3
fujiwara3
3
420
作り直せるコードは迅速に 作り直せないDBは慎重に - AI時代のプロダクトエンジニアが「判断の不可逆性」で開発速度を変える話
kinosuke01
0
280
10分で知る最近のOmarchy
komagata
0
150
Bet AI Day 2026丨AIによって本質に戻るシステムリスク管理
layerx
PRO
0
1.3k
Snowflakeのコスト最適化を支えるアーキテクチャ設計
ktatsuya
0
970
深夜のクラウド懺悔室 1:29:300 or 1:0:0
kazzpapa3
0
200
Tab5をRubyで動くパソコンにする
kishima
2
270
Featured
See All Featured
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
4.7k
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
3k
WCS-LA-2024
lcolladotor
0
820
Navigating Team Friction
lara
192
16k
Building Adaptive Systems
keathley
44
3.2k
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
2.2k
How GitHub (no longer) Works
holman
316
150k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
340
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
210
Building a Scalable Design System with Sketch
lauravandoore
463
34k
Transcript
新規プロダクトで プロトタイプから製品版まで Next.jsで開発したリアル
自己紹介 1/54
こんなNext.js は嫌だ 2/54
useOshogatsuという西暦を渡すと、その年のお正月 のUnixタイムスタンプを返すhookが公式から提供されている import { useOshogatsu } from 'next/date' const year
= 2025 const { gantan } = useOshogatsu(year) console.log("元旦:" + gantan) // 元旦:1745107200 3/54
実はPHPには似たようなのが本当にある。あなただけのPHPの面白関数を探してみて⭐ 4/54
自己紹介 成果を出す!実践DXを学ぼう 受賞企業 登壇セッション (株)オイシス 生産本部兼はりま工場 統括ライン長 荒尾 和哉様 ファウンテン・デリ(株) 品質管理主任兼カミナシPJチーム事務局
立花 直也様 ▪略歴 〜2024年10月:株式会社ゆめみ 2024年10月〜:株式会社カミナシ 〜現在:AIラベル検査の開発 株式会社カミナシ StatHackカンパニー ソフトウェアエンジニア 河野 陸 5/54
自己紹介 最近、友達から意識高そうなビジネス書をもらった 6/54
自己紹介 最近、友達から意識高そうなビジネス書をもらった コンフォートゾーンを飛び出せ!とのこと 6/54
自己紹介 新宿の8席しかない鬼地下のライブハウスで 大爆笑ネタをやってる様子 1000円もらえた 7/54
会社紹介 会社紹介 8/54
株式会社カミナシ https://kaminashi.jp 9/54
カミナシ機能ラインナップ 現場のデジタルインフラとしてお客様のあらゆる業務を解決します Method 作業 Men ⼈ Machine 設備 ペーパーレス化を実現 現場の品質レベルを向上
従業員と会社のやり取りを 1ツールで完結 現場教育を効率的に 動画マニュアルと研修管理 電⼦帳票 故障履歴や保全計画を 設備カルテとして⼀元管理 マニュアル‧研修 コミュニケーション 設備カルテ 現場の共通管理基盤(カミナシID) 1つのID/Passで運⽤で複数のシステムを利⽤ 運⽤の負担軽減とセキュリティ向上 10/54
今日話すプロダクトis… カミナシレポートが提供する一つの機能について話します! Method 作業 ペーパーレス化を実現 現場の品質レベルを向上 電⼦帳票 11/54
技術スタック プログラミング言語 フロントエンド・モバイル API定義 データベース コード管理・実行環境・ SaaS モニタリング GO TypeScript
React Expo Next.js Remix MySQL Datadog GitHub Terraform PostgreSQL Sentry Docker SendGrid 12/54
長い前置き 各サービスチームで その時最適なものを 採用する 技術選定の方針 絶対的な標準の技術スタックはない 13/54
長い前置き なぜNext.jsが最適 だったのか? AIラベル検査では 全社的にNext.jsを採用しているわけではない 14/54
長い前置き 時系列順に ストーリー仕立てで 解説していきます プロトタイプ〜正式リリースまで 15/54
動くプロトタイプを5日以内に完成させろ! MISSION1 動くプロトタイプを 5日以内に完成させろ! 16/54
動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 PM 17/54
動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 PM よし。今はまだ何もないけど 5日後のお客さん訪問で プロトタイプ触ってもらおう! 17/54
動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 前提・お気持ち - コアな機能の価値検証を最優先にしたい(だって5日しかない!) - となるとデプロイやインフラに気を遣っている余裕がない! - APIサーバーとFEを別で用意したり開発する余裕もない!
18/54
動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 技術選定 1 言語はFE/BEで統一したい → 流石にTypeScriptだろう 2 APIサーバーを(意識して)用意しないで済む仕組み→Server
Actions(を使うなら...?) 3 デプロイやインフラを気にせず即公開できるVercel(と相性が良いあいつ...) 19/54
動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 技術選定 1 言語はFE/BEで統一したい → 流石にTypeScriptだろう 2 APIサーバーを(意識して)用意しないで済む仕組み→Server
Actions(を使うなら...?) 3 デプロイやインフラを気にせず即公開できるVercel(と相性が良いあいつ...) 朧げながら浮かんできました 20/54
動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 フロントエンド&バックエンド ホスティング データベース・ストレージ 手軽さと開発スピード重視の技術スタックに 21/54
動くプロトタイプを5日以内に完成させろ! 動くプロトタイプを5日以内に完成させろ! MISSION1 デプロイ(CI/CD)やインフラに気を使わず、 Full TypeScript ✖Server Actionsで考えられうる最高速度で開発できた! 1 フィードバックがあったらすぐ開発して、すぐ反映して〜の爆速サイクル
結果 2 素早く価値検証できて、正式開発への意思決定に貢献できた 22/54
MISSION2 製品版を開発せよ 製品版を開発せよ 23/54
製品版のプロダクト構成 製品版を開発せよ 24/54
製品版を開発せよ 検査画面 UX的に既存プロダクトに実装する 管理画面 既存プロダクトとは別に新しく作る 25/54
製品版を開発せよ 検査画面 UX的に既存プロダクトに実装する 管理画面を新しく別のアプリで作る必要あり 25/54
管理画面を新しく別で作る必要あり プロトタイプのコードは捨てて0から作 る必要がある 製品版を開発せよ 検査画面 UX的に既存プロダクトに実装する 25/54
製品版を開発せよ UXの都合上 検査機能は既存プロダクトに実装する必要あり 管理画面 既存プロダクトとは別に新しく作る 25/54
製品版を開発せよ 検査機能は既存のプロダクトに実装 プロトの中でかなり洗練されたので 機能的、コード品質が十分なレベルに 管理画面 既存プロダクトとは別に新しく作る 25/54
選択と集中... 製品版を開発せよ 26/54
選択と集中... 製品版を開発せよ 不確実性の高い 検査画面の組み込みに時間を 使うべき 26/54
管理画面は そのままNext.jsで いくことにする...! インフラはAWSに 移行したよ 製品版を開発せよ 管理画面 27/54
製品版においては プロトタイプ開発で不満なくいけた ので、そのまま使ってます...!という 感じ! 製品版を開発せよ つまり、ぶっちゃけ Next.jsじゃないと満たせない要件があるとかではない 28/54
製品版を開発せよ 29/54
Next.jsを採用した理由(背景)まとめ 素早い開発が求められるなか、 Full TypeScript ✖ Server Actionsで爆速機能開発できて、 Vercelでゼロコンフィグでデプロイできる。 この辺がマッチした。 特に不満なく開発できたので、課題に集中するため、そのまま採用した!
色々言ってるけど、僕が使ってみたかったのが7割かも 製品版を開発せよ 30/54
SaaSはリリースしたら MISSION3へ続く... 31/54
SaaSはリリースしたら 終わりではない... MISSION3へ続く... 31/54
SaaSはリリースしたら 終わりではない... MISSION3へ続く... 31/54
MISSION3 プロトタイプからの 技術的課題を 解決せよ プロトタイプからの技術的課題を解決せよ 32/54
プロトタイプからの技術的課題を解決せよ MISSION3 FE/BEで型を共通化しすぎてつらい case1 Progressively Enhanced Form、複雑なFormはどうしたら...? case2 プロトタイプからの技術的課題を解決せよ 33/54
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<User[]> => { 'use server'; // ユーザーを取得して返却する } プロトタイプからの技術的課題を解決せよ export const Page = async () => { const users = await getUsers(); return <UserList users={users} /> } 'use client' export const UserList = ({users}: {users: server.User[]}) => { // クライアントにhashedPasswordが漏れてるけど大丈夫? // クライアントにとって不要なフィールドはない? return <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> } 34/54
1 とにかく開発早い 共通化のメリット 2 コードもファイルも少ない プロトタイプからの技術的課題を解決せよ FE/BEで型を共通化しすぎてつらい case1 3 隅から隅まで型補完が効く
35/54
1 FEに公開させたくないフィールドを意図せず公開してしまう恐れあり 2 BEとFE常に一緒に変更しないといけなくなる(密結合) 3 UIコンポーネント側でロジックが増える 共通化の課題 プロトタイプからの技術的課題を解決せよ FE/BEで型を共通化しすぎてつらい case1
36/54
解決方法 // 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
解決方法 import * as client from '@/client/types' import { getUsersService
} from '@/server/service' // server actions はFE/BEの境界としてデータの変換 export const getUsers = async (): Promise<client.User[]> => { 'use server'; // ユーザー取得する処理 const users = getUsersService() // clientの型に変換する return convertToClientUser(users) } それぞれに最適な型を定義して疎結合にした。Server Actionsとして実装する関数はあくまでも境界に プロトタイプからの技術的課題を解決せよ FE/BEで型を共通化しすぎてつらい case1 38/54
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 ( <form action={formAction}> <Name errors={errors.name} /> <Age errors={errors.age} /> <SubmitButton isPending={isPending} /> </form> ); } Server Actions が最初に出てきた時にインパクトあったやつですね プロトタイプからの技術的課題を解決せよ 39/54
1 状態管理しなくて良い!!! 2 典型的なフォームハンドリングのボイラープレートが不要になる (状態管理→バリデーションして→APIの型に変換して〜みたいな) メリット(PE自体のメリットは割愛。DX的な目線で) プロトタイプからの技術的課題を解決せよ case2 Progressively Enhanced
Form、複雑なFormはどうしたら...? 40/54
1 クライアントJSに頼らないためリッチ、複雑なFormが提供できない 課題 2 FormData APIが string 中心のため、型の恩恵が薄く、静的チェックが効かない プロトタイプからの技術的課題を解決せよ case2
Progressively Enhanced Form、複雑なFormはどうしたら...? 41/54
複雑なFormは従来の状態管理に戻した。シンプルなやつはそのままPE 解決法 1 2 一部で状態管理のつらみは戻ってきたけど、即時バリデーションエラーなどリッ チなFormが提供できるようになった PEに統一していた時はできなかった、複雑なFormを実装できた プロトタイプからの技術的課題を解決せよ case2 Progressively
Enhanced Form、複雑なFormはどうしたら...? 42/54
複雑なFormは従来の状態管理に戻した。シンプルなやつはそのままPE 解決法 1 2 一部で状態管理のつらみは戻ってきたけど、即時バリデーションエラーなどリッ チなFormが提供できるようになった PEに統一していた時はできなかった、複雑なFormを実装できた プロトタイプからの技術的課題を解決せよ case2 Progressively
Enhanced Form、複雑なFormはどうしたら...? 具体的な方法は最近の開発パートで話します 42/54
最近の開発 最近の開発 43/54
最近の開発 Extra nuqsでクエリパラメータの管理を安全かつシンプルに case1 Zustand + Valibotによるフォーム実装 case2 最近の開発 44/54
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
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
最近の開発 Zustand + Valibotによるフォーム実装 case2 47/54
最近の開発 Zustand + Valibotによるフォーム実装 case2 元はというと 47/54
最近の開発 さっきお話ししたやつです Zustand + Valibotによるフォーム実装 case2 48/54
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
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
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<string>(''); return ( <Input name="name" label="ユーザー名" value={value} onChange={(e) => setValue(e.target.value)} onBlur={() => setErrorMsg(getErrors(value)?.[0] ?? '')} required errorMessage={errorMsg} error={errorMsg !== ''} /> ); }; 最近の開発 Zustand Slice Patternなるものを使ってみた Zustand + Valibotによるフォーム実装 case2 51/54
export type UserStore = NameSlice & AgeSlice & StatusSlice export
const createUserStore = (initial: InitialValue) => { const { name, age, status } = initial; return create<UserStore>()((...args) => ({ ...createNameSlice({ name })(...args), ...createAgeSlice({ age })(...args), ...createStatusSlice({ status })(...args), })); }; 最近の開発 Zustand Slice Patternなるものを使ってみた Zustand + Valibotによるフォーム実装 case2 52/54
- UXの良いForm(即時バリデーション) - フィールドごとに凝集度が高く、めちゃくちゃ可読性高い、追記しやすい - Formの入力状態によって分岐するような複雑なFormも簡単に実装できた 最近の開発 Zustand + Valibotによるフォーム実装
case2 53/54
最近の開発 https://zenn.dev/yumemi_inc/articles/26ff46875f3cd0 詳しくはこちらを参照! 54/54
告知 extra ぜひ一度カジュアル面談でお話ししましょう!
告知 extra ぜひ一度カジュアル面談でお話ししましょう!
告知 相方 extra