Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
App Router入門 Next.js App Router@Recruit bootcam...
Search
Recruit
PRO
August 09, 2024
Technology
17
1.9k
App Router入門 Next.js App Router@Recruit bootcamp2024
2024年度リクルート エンジニアコース新人研修の講義資料です
Recruit
PRO
August 09, 2024
Tweet
Share
More Decks by Recruit
See All by Recruit
スタサプ ForSCHOOLアプリのシンプルな設計
recruitengineers
PRO
3
680
リクルート流データ基盤塾~鶴谷と学ぶ~
recruitengineers
PRO
4
60
『SUUMO』 スマホサイト デザインリニューアルへの挑戦
recruitengineers
PRO
5
230
『リクルートダイレクトスカウト』 のリニューアルから振り返る: ビジョンドリブンの可能性
recruitengineers
PRO
3
210
負債あるモノリスのオブザーバビリティに組織で向き合う
recruitengineers
PRO
9
350
あなたの知らないiOS開発の世界
recruitengineers
PRO
4
280
大規模プロダクトにおける組織作りと技術ポートフォリオマネジメント
recruitengineers
PRO
4
390
OR学会2024秋_短期収益と将来のオフ方策評価性能を考慮したクーポン割当方策混合比の決定
recruitengineers
PRO
5
610
リクルート新人研修2024 テキスト生成AI活用
recruitengineers
PRO
12
810
Other Decks in Technology
See All in Technology
ドキュメントとの付き合い方を考える
leveragestech
1
150
LINEヤフー新卒採用 コーディングテスト解説 実装問題編
lycorp_recruit_jp
1
13k
O'Reilly Superstream: Building a RAG App to Chat with Your Data
pamelafox
0
120
DenoでもViteしたい!インポートパスのエイリアスを指定してラクラクアプリ開発
bengo4com
2
2k
業務ヒアリングと知識の呪い
tamai_63
0
290
プロダクト開発の貢献をアピールするための目標設計や認知活動 / Goal design and recognition activities to promote product development contributions.
oomatomo
5
920
怖くないオフライン機能開発 〜基本的な技術で実現する現場向けオフライン機能 / Developing offline functions without fear ~ Offline functions for the field realized with basic technology
kaminashi
1
110
「ばん・さく・つき・たー!」にならないためにSHIROBAKOから 学んだこと
ysknsid25
3
700
ガバメントクラウド開発と変化と成長する組織 / Organizational change and growth in developing a government cloud
kazeburo
4
990
Kubernetes Meetup Tokyo #67 - KEP-3619: Fine-grained SupplementalGroups Control / k8sjp67-kep-3619
everpeace
0
150
Oracle GoldenGate 23ai 導入Tips
oracle4engineer
PRO
1
280
今こそ変化対応力を向上させるとき 〜ログラスが FAST に挑戦する理由〜 / Why Loglass is Talking on the Challenge of Agile Framework FAST
shioyang
0
130
Featured
See All Featured
The Invisible Side of Design
smashingmag
297
50k
Embracing the Ebb and Flow
colly
84
4.4k
Unsuck your backbone
ammeep
668
57k
Art, The Web, and Tiny UX
lynnandtonic
296
20k
Docker and Python
trallard
40
3k
4 Signs Your Business is Dying
shpigford
180
21k
What's in a price? How to price your products and services
michaelherold
243
11k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.8k
WebSockets: Embracing the real-time Web
robhawkes
59
7.3k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
41
9.2k
Scaling GitHub
holman
458
140k
VelocityConf: Rendering Performance Case Studies
addyosmani
325
23k
Transcript
App Router 入門 Next.js App Router@Recruit bootcamp2024
Profile Name: 佐藤 昭文(Akifumi Sato ) 所属: 横断エンジニアリング部 APソリューショングループ(通称ASG )
Web Engineer Work 『レジュメ』 『リクルートダイレクトスカウト』 Activity https://zenn.dev/akfm JS Conf 2023 Vercel meetup Node 学園42 Rust 入門本の執筆
Agenda 話さないこと Pages Router ・React の基礎 experimental な機能、不安定な機能 話すこと Next.js
の歴史 これからのNext.js(App Router) App Router Short Tutorial 話すこと・話さないこと
Next.js の歴史
Next.js の歴史(1) 年 イベント 補足 2013/05 React が公開 2016/10 Next.js
が公開 React +SSR が広く認知される 2018~2019 Gatsby.js の台頭 SSR->SSG が注目される 2019/07
[email protected]
dynamic ルーティング・Typescript 対応 2020/03
[email protected]
SSG 対応 2020/07
[email protected]
ISR 対応
Next.js の歴史(2) 年 イベント 補足 2021~2022 Next.js@v10~12 パフォーマンス改善・次世代コンパイラの開発に着手 2022/05 Layout
RFC 発表 後のApp Router 2022/10
[email protected]
App Router(Beta) 2023/05
[email protected]
App Router(Stable) 、Server Actions(Beta) 2023/10
[email protected]
Server Actions(Stable) 2024/05
[email protected]
Partial Pre Rendering
Next.js の歴史に見る進化 Next.js は常に高いパフォーマンスを追い求めてきた コードスプリッティングや静的化 画像やfont の最適化( next/image 、 next/fotn
)など Next.js は常により良い DX も追い求めてきた SWC をベースとしたRust コンパイラを内包し、コンパイル速度改善 webpack 作者sokra 氏をVercel に迎え、Rust 製の後継バンドラー開発 2024 年現在、Next.js はApp Router への過渡期である RSC などのReact の最新機能を取り入れた、設計思想の大幅なアップデート Next.js は破壊的変更ではなく段階的移行という手段を選んだ Next.js はいかにして進化してきたか
Pages -> App Router
App Router とは ( 公式ドキュメントより)Next.js App Router は、Server Components 、Streaming
with Suspense 、Server Actions といったReact の最新機 能を使ったアプリケーション構築のための新しいモデル 新たなNext.js のアプリケーション構築方法 pages ではなく、 app ディレクトリ配下に配置する フレームワークとしてはほとんど別物レベル React の新機能(主にサーバー側機能)が使える React Server Components Server Actions 強力なCache その他諸々新機能(多くて混乱するので割愛)
Pages Router の現在 Pages Router において、新規機能開発の予定はない 深刻な脆弱性対応やパフォーマンス改善は取り込まれるかもしれない 予期せずApp Router 側の修正がPages
Router に影響してしまうこともしばしば… Pages Router で利用できるライブラリは多く、いい意味で「枯れてる」状態 しかし徐々にライブラリ群もApp Router のみ対応している状況になっていくであろうことが予想される いずれはNext.js ユーザーはApp Router へ移行することになる React のClass Component -> Functional Component への移行に近いかもしれない 実態としてはdeprecated に近い
App Router の現状 Next.js としてはApp Router はstable と宣言してるが、実際には未成熟な状態 stable 宣言当初、バグがかなり多くプロダクションReady
とは言えなかった 今日では致命的なバグについて、徐々に解消しつつある ただし、まだ一部機能ではバグが多いため、機能ごとに見極めが必要 関連ライブラリも増えてきたものの、発展途上 学習コスト・対応コスト高 Vercel 以外で利用するには深い理解が必要となる(Cache Handler 周りとか) 現状社内の一部プロジェクトでApp Router が使われているが、実験的採用に止まる App Router はエコシステム含め未成熟な段階
React の新機能
React Server Components(RSC) Server Component: サーバー側でのみレンダリングされる Component Client Component: 従来からあるクライアント・サーバーどちらでもレンダリング可能なComponent
今後RSC をサポートするフレームワークは増えていくと予想される Remix Vike React における新たなアーキテクチャ
RSC のモチベーション デフォルトでより良いパフォーマンスの達成 ハイドレーション処理・バンドルサイズを減らせる クライアント<-> サーバーの通信を減らし、より高速でセキュアに バックエンドへのフルアクセス より簡単にComponent が必要なデータを取得できるようになった 中間層(gSSP,
tRPC, GraphQL, API Routes など) の実装や設計が不要に 参考: https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#motivation 従来からあったReact の問題を解決したかった
Server Components App Router ではデフォルトでServer Components となる クライアントサイドでの動作前提な useState など一部
hooks は利用できない Component 自体を async にすることが可能で、 fetch を直接扱える Client Components/Server Components どちらも含めることができる サーバーでのみ実行されるComponent import { Counter } from "@/components/Counter"; // Client Component <Counter /> export default async function Page() { // 今までできなかったが、直接await できる const todo = await fetch('https://dummyjson.com/todos/random').then(res => res.json()) return ( <div> <pre> <code>todo: {JSON.stringify(todo)}</code> </pre> </div> ) }
SSR との違い SSR: ハードナビゲーション( 初期表示) のみ実行される SC: ハードナビゲーションでもソフトナビゲーション( Link の遷移)
でも実行される 従来からあるSSR(Server Side Rendering) と混乱しやすいが、Server Components(SC) は全く異なる概念
Client Components ファイルの先頭に "use client" があるとそのComponent 以下は全てClient Component となる Next.js
においてはSSR 時にもレンダリングされる 従来からあるComponent import { useState } from "react"; const [count, setCount] = useState(0) // Client Component のみで利用可能 "use client" export function Counter() { return ( <div> <p>count: >{count}</p> <button onClick={() => setCount(prev => prev + 1)}>increment</button> </div> ) }
Client Components children ( などのprops) を除き、Server Components を含むことはできない 従来からあるComponent children:
React.ReactNode; // Server Components も可! <div>{children}</div> "use client" import { useState } from "react"; export function Accordion({ children, }: { }) { const [isOpen, setIsOpen] = useState(false) return ( <div> <button>toggle</button> </div> ) }
Server Actions ファイルや関数の先頭行に "use server" があるとServer Actions となる Client Components
からサーバー側の関数を実行できる <form action={create}> // action.ts "use server" async function create(formData: FormData) { const text = formData.get('name') // ...API へPOST したりDB に保存するなど... } // page.tsx export default function Page() { return ( <input type="text" name="name" /> <button type="submit">Create</button> </form> ) }
App Router 固有の特徴
強力なCache 4 層のCache 層があり、多くがデフォルトで有効なためパフォーマンスが非常に良い
その他多くの機能 Nested Layout Error/Loading UI Dynamic Routes revalidate Parallel Routes
Intercepting Routes etc… 他にも細々多くの機能が含まれ( 詳細はshort tutorial で)
Short Tutorial
Hello world. package.json https://github.com/vercel/next.js/tree/canary/examples/hello-world # pnpm を未インストール時のみ $ npm install
-g pnpm # プロジェクトの作成 $ pnpm create next-app --example hello-world hello-world-app $ cd hello-world-app # Next.js はv14.2.3 を使うこと $ pnpm dev "dependencies": { - "next": "latest", + "next": "14.2.3", "react": "^18.2.0", "react-dom": "^18.2.0" },
Hello world. local サーバーを起動 $ pnpm dev
Hello world. ファイル保存時に自動整形されるようにIDE を設定 VSCode の設定方法 WebStorm の設定方法 フォーマット揃える手間を省くために、prettier をインストールしIDE
に自動整形を設定 $ pnpm add -D prettier
file conventions App Router のルーティグは page.tsx で定義される
file conventions https://nextjs.org/docs/app/building-your-application/routing/defining-routes layout.js page.js error.js loading.js etc… App Router
は特定のファイル名規役に応じてコンポーネントを処理する
Nested layout layout.tsx を修正して共通のヘッダーを作成 <header>all page's header</header> // app/layout.tsx export
default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="ja"> <body> {children} </body> </html> ); }
New page ディレクトリを作成して新しいページを作成 import Link from "next/link"; <Link href="/todos">/todos</Link> //
app/page.tsx export default function Page() { return ( <> <h1>Hello, Next.js!</h1> </> ); }
New page ディレクトリを作成して新しいページを作成 // app/todos/layout.tsx export default function TodosLayout({ children,
}: { children: React.ReactNode; }) { return ( <> {children} <footer>todos page's footer</footer> </> ); }
New page ディレクトリを作成して新しいページを作成 // app/todos/page.tsx import Link from "next/link"; export
default function Page() { return ( <> <h1>Hello, Todos page!</h1> <Link href="/todos/random">/todos/random</Link> </> ); }
New page ディレクトリを作成して新しいページを作成 // app/todos/random/page.tsx export default function Page() {
return <h1>Hello, Random todo</h1>; }
Server Components + fetch dummy データをfetch して表示 const todo =
await fetch('https://dummyjson.com/todos/random') .then((res) => res.json()) .finally(() => console.log('>>> fetch dummyjson')); // app/todos/random/page.tsx export default async function Page() { return ( <> <h1>Hello, Random todo</h1> <pre> <code>todos: {JSON.stringify(todo, null, 2)}</code> </pre> </> ); }
Error UI page.tsx でエラーが起きた時のUI は、 error.tsx で定義可能 // .then((res) =>
res.json()) .then((res) => { throw new Error('always error') }) // app/todos/random/page.tsx export default async function Page() { const todo = await fetch('https://dummyjson.com/todos/random') .finally(() => console.log('>>> fetch dummyjson')); // ... }
Error UI ※ pnpm dev 中に error.tsx を追加すると無限リロードが発生することがあるが、再起動で解消する page.tsx でエラーが起きた時のUI
は、 error.tsx で定義可能 // app/todos/error.tsx "use client" export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return ( <div> <h2>Error!</h2> <button onClick={() => reset()}>Try again</button> <pre>{error.message}</pre> </div> ) }
Loading UI 長い非同期処理を含む場合には、 loading.tsx でLoading UI を定義可能 await setTimeout(1000); //
app/todos/random/page.tsx import { setTimeout } from "timers/promises"; export default async function Page() { const todo = await fetch('https://dummyjson.com/todos/random') .then(async (res) => { return res.json(); }) .finally(() => console.log('>>> fetch dummyjson')); // ... }
Loading UI 長い非同期処理を含む場合には、 loading.tsx でLoading UI を定義可能 // app/todos/random/loading.tsx export
default function Loading() { return <h1>Loading...</h1>; }
Client Components + useState useState を使うために "use client" を追加 "use
client"; import { useState } from "react"; const [count, setCount] = useState(0); // app/components/counter.tsx export function Counter() { return ( <div> <p>count: {count}</p> <button onClick={() => setCount(count + 1)}>increment</button> </div> ); }
Client Components + useState useState を使うために "use client" を追加 import
{ Counter } from "./components/counter"; <Counter /> // app/page.tsx import Link from "next/link"; export default function Page() { return ( <> <h1>Hello, Next.js!</h1> <Link href="/todos">/todos</Link> </> ); }
Server Actions form からサーバー側の関数を実行 // app/action.ts "use server"; export async
function action(data: FormData) { // skip validation const name = data.get("name"); console.log(`action called with name: "${name}"`); // ...API へPOST したりDB に保存するなど... }
Server Actions form からサーバー側の関数を実行 import { action } from "./action";
<form action={action}> // app/page.tsx import Link from "next/link"; import { Counter } from "./components/counter"; export default function Page() { return ( <> <h1>Hello, Next.js!</h1> <Link href="/todos">/todos</Link> <Counter /> name: <input type="text" name="name" /> <button>submit</button> </form> </> ); }
Dynamic Routes 従来同様、 [id] のようなページも作成可能 // app/todos/[id]/page.tsx export default function
Page({ params }: { params: { id: string } }) { return <h1>Todo ID: {params.id} page!</h1>; }
Dynamic Routes 従来同様、 [id] のようなページも作成可能 // app/todos/page.tsx import Link from
"next/link"; export default function Page() { return ( <> <h1>Hello, Todos page!</h1> <ul> <li> <Link href="/todos/random">/todos/random</Link> </li> <li> <Link href="/todos/111">/todos/111</Link> </li> </ul> </> ); }
Full Route Cache のrevalidate https://nextjs.org/docs/app/building-your-application/caching#full-route-cache // `revalidate` をexport することで、レンダリングのキャッシュ(Full Route
Cache) のrevalidate を設定可能 // pnpm dev ではなく、pnpm build && pnpm start でないと動作しないので要注意 // 3.0s ごとにrevalidate export const revalidate = 3; // app/todos/random/page.tsx export default async function Page() { const todo = await fetch('https://dummyjson.com/todos/random') .then((res) => res.json()) .finally(() => console.log('>>> fetch dummyjson')); return ( <> <h1>Hello, Random todo</h1> <pre> <code>todos: {JSON.stringify(todo, null, 2)}</code> </pre> </> ); }
Data Cache のrevalidate https://nextjs.org/docs/app/building-your-application/caching#data-cache revalidate: 3, // 3 秒でrevalidate //
export const revalidate = 3; // app/todos/random/page.tsx export default async function Page() { const todo = await fetch('https://dummyjson.com/todos/random', { next: { }, }) .then((res) => res.json()) .finally(() => console.log('>>> fetch dummyjson')); return ( <> <h1>Hello, Random todo</h1> <pre> <code>todos: {JSON.stringify(todo, null, 2)}</code> </pre> </> ); }
Router Cache のrevalidate https://nextjs.org/docs/app/building-your-application/caching#router-cache import { revalidatePath } from "next/cache";
// Full Route Cache/Data Cache/Router Cache がrevalidate される revalidatePath("/todos/random"); // app/todos/random/action.ts "use server"; export async function revalidateTodo() { console.log("revalidatePath: /todos/random"); }
Router Cache のrevalidate https://nextjs.org/docs/app/building-your-application/caching#router-cache <form action={revalidateTodo}> <button>revalidate</button> </form> // app/todos/random/page.tsx
import { revalidateTodo } from "./action"; export default async function Page() { // ... return ( <> <h1>Hello, Random todo</h1> <pre> <code>todos: {JSON.stringify(todo, null, 2)}</code> </pre> </> ); } // export const revalidate = 3;
Request Memorization レンダリング中は fetch はメモ化されるので、1 回しか通信されない const todo = await
fetchTodo(); // fetch 1 const todo = await fetchTodo(); // fetch 2 // app/todos/random/page.tsx function fetchTodo() { return fetch("https://dummyjson.com/todos/random", { next: { revalidate: 3 }, }).then((res) => res.json()) as Promise<{ todo: string }>; } export async function generateMetadata() { return { // API 的にはリクエストごとにrandom なTODO が帰ってくるが // Memorization により1 回にまとまられるので画面とタイトルで齟齬が起きない title: `Todo: ${todo.todo}`, description: "This is a Next.js app.", }; } export default async function Page() { // ... }
ハンズオン課題 https://github.com/recruit-tech/bootcamp-2024-nextjs Pages Router で作ったblog をApp Router で書き換えてみましょう 近くの席の人同士で議論しながら進めましょう 佐藤・吉井さんが歩き回ってるので質問や不明点あれば気軽に聞いてください
blog-app-router に参考実装があるのでどうしても参考実装がみたいというときはどうぞ Pages Router で作ったBlog アプリをApp Router で書き換えてみよう
付録1: 4 種類のCache ※ 時間が余ったら説明します
App Router におけるCache の種類 Mechanism What Where Purpose Duration Request
Memoization 関数の戻り値 Server React Component tree にお けるdata の再利用 リクエストごと Data Cache API レスポンスやデータベ ースアクセスの結果 Server ユーザーやデプロイをまたぐ データの再利用 永続化 (revalidate 可) Full Route Cache HTML やRSC payload Server レンダリングコストやパフォ ーマンスの向上 永続化 (revalidate 可) Router Cache RSC Payload Client ナビゲーションごとのリクエ スト削減 ユーザーセッショ ン・時間
Request Memoization レンダリング時のリクエストをメモ化する
Data Cache データの再利用を目的としたCache
Data Cache データの再利用を目的としたCache
Data Cache データの再利用を目的としたCache
Full Route Cache HTML やRSC payload をキャッシュする
Router Cache RSC Payload をキャッシュする
付録2: Client Boundary ※ 時間が余ったら説明します
Client Boundary 出典: https://postd.cc/server-components/ Client Component でimport されたComponent は全てClient Component
となる
Client Boundary 出典: https://postd.cc/server-components/ Client Component でimport されたComponent は全てClient Component
となる
Client Boundary 出典: https://postd.cc/server-components/ Client Component でimport されたComponent は全てClient Component
となる
❌: Client がServer をimport 当然ながら、Client 側にServer 側のモジュールはimport できない // You
cannot import a Server Component into a Client Component. import ServerComponent from "./Server-Component"; <ServerComponent /> "use client"; export default function ClientComponent({ children, }: { children: React.ReactNode; }) { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>{count}</button> </> ); }
✅: Props でServer Component を渡す Props を通じてServer Component を渡すことは可能 children,
children: React.ReactNode; // Server Components も可! "use client"; import { useState } from "react"; export default function ClientComponent({ }: { }) { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>{count}</button> {children} </> ); }
付録3: RSC のテストと Presentational/Container Component ※ 時間が余ったら説明します
Presentational/Container Component React Server Component のテストと Container / Presentation Separation
概要 Server Component を動作させて、結果の DOM やインタラクションを一気通貫でインテグレーションテス トすることは現状では難しい サーバー側の処理の妥当性と、DOM 描画・操作の妥当性は、それぞれ別のユニットテストとして分離する 「分離」には、Container / Presentation パターンを用いる Container / Presentation: Redux 全盛期に流行した見た目(presentational )と振る舞い(container )を 分離するパターン 「hooks 登場以前のモデルだから間に受けないで欲しい」と今では書かれてる RSC は現代における新たなContainer / Presentation になりうると提唱者も言っている quramy さんの以下の記事の要約なので、詳しくは以下を参照ください
Presentational Component のテスト Presentational Component のテストは従来通りのテスト方法で問題ない import "@testing-library/jest-dom"; import {
screen, render } from "@testing-library/react"; import { ArtistPagePresentation } from "./page"; describe(ArtistPagePresentation, () => { it("renders artist's name", () => { render( <ArtistPagePresentation artist={{ name: "John Coltrane", biography: "" }} /> ); expect(screen.getByText("John Coltrane")).toBeInTheDocument(); }); });
Container Component のテスト Container Component は単なる関数として実行して振る舞いを検証する // ... jest.mock('next/navigation', ()
=> ({ notFound: jest.fn() })) jest.mock('@/prismaClient', () => ({ prisma: jestPrisma.client })) describe(ArtistPage, () => { beforeEach(() => initialize({ prisma: jestPrisma.client })) // ... it('passes fetched data to presentational component for existing id', async () => { const created = await ArtistFactory.create() const { type, props } = await ArtistPage({ params: { artistId: created.id } }) expect(type).toBe(ArtistPagePresentation) expect(props).toMatchObject({ artist: { name: created.name, biography: created.biography, }, } satisfies React.ComponentProps<typeof ArtistPagePresentation>) }) })
最後に エンジニアは常に新たな知識を学ばなければならず、限られた時間の中では学ぶ内容の選択が重要(技術選 定の審美眼 by t-wada さん) App Router はReact の新機能を取り入れており、学んだ内容は他のReact
フレームワークでも生かすことが できる可能性が高い フロントエンドの進化と未来に興味がある方はぜひ、一緒にApp Router を追いかけましょう! App Router のすゝめ