บทเรียน 70 · Frontend · Next.js

ติดตั้ง Next.js และอ่านโครงโฟลเดอร์ให้ออก

~22 นาที · ควรผ่านบทเรียน 69 (Next.js คืออะไร) มาก่อน · ถ้าเคยผ่านบท 38 (Gin routing) มาแล้วจะได้เปรียบมาก เพราะบทนี้จะเทียบกับมันตรง ๆ

บทนี้มาจากสไลด์ Next.js หน้า 13–20 — ช่วง "Setup & Install" กับ "Folder Structure"
สามสิ่งที่จะได้: สร้างโปรเจกต์ให้สำเร็จจริง · อ่านออกว่าไฟล์ที่มันแถมมาแต่ละอันทำอะไร · และเรื่องใหญ่ที่สุดของบทนี้ — ใน Next.js ชื่อโฟลเดอร์คือ URL
เรื่องสุดท้ายนี้กลับหัวจากที่คุณเพิ่งเรียนมาในบท 38 ที่ Gin ให้คุณพิมพ์ route เอง ทีละบรรทัด · Next.js ไม่มีบรรทัดนั้นให้พิมพ์เลย มันไปอ่านโฟลเดอร์แทน
และมีจุดหนึ่งที่สไลด์พิมพ์คำสั่งผิด — ผมรันให้ดูแล้วว่ามันพังยังไง และพังเพราะอะไร

🧰 ของที่ใช้ตรวจในบทนี้ — ทุกผลรันในหน้านี้ก็อปมาจาก terminal จริง

ของเวอร์ชันที่ใช้รัน
next16.3.4 (Turbopack)
react / react-dom19.2.8
tailwindcss4 (ผ่าน @tailwindcss/postcss)
typescript5
nodev24.14.0

โปรเจกต์ที่ใช้ทดลองสร้างด้วย npx create-next-app@latest --ts --tailwind --eslint --app --no-src-dir จริง
ข้อควรรู้สองอย่างเรื่องผลรันในบทนี้ (บอกไว้ตรง ๆ ดีกว่าให้คุณงงตอนรันเองแล้วเลขไม่ตรง):

  1. พอร์ตในผลรันของผมไม่ใช่ 3000 — โปรเจกต์ทดลองตัวนี้ถูกใช้ร่วมกันหลายบท ผมเลยต้องรันที่พอร์ต 370 (คุณจะเห็นเลขนี้ในผล curl ทุกอัน) ในเครื่องคุณค่าปริยายคือ 3000
  2. ทุกไฟล์ที่ผมสร้างในบทนี้อยู่ใต้ app/t70/ เพราะห้ามชนกับบทอื่น เวลาผมเขียนว่า "สร้าง app/about/page.tsx แล้วได้ /about" ผลรันจริงของผมจะเป็น app/t70/about/page.tsx → /t70/about
    กฎเดียวกันเป๊ะ แค่มีคำว่า t70 คั่นหน้า — ผมจะเทียบให้ดูเป็นตารางในหัวข้อเรื่อง URL

คำสั่งสร้างโปรเจกต์ — และจุดที่สไลด์พิมพ์ผิด

สไลด์หน้า 14 ("Create First NextJS App") ให้คำสั่งมาสามบรรทัด ตามตัวจัดการแพ็กเกจสามเจ้า:

# สามบรรทัดนี้คือสิ่งที่อยู่บนสไลด์หน้า 14 — ก็อปมาตามตัวอักษร
$ npm create-next-app@latest
$ yarn create next-app@latest
$ pnpm create next-app@latest

สังเกตไหมว่าบรรทัดแรกหน้าตาไม่เหมือนอีกสองบรรทัด — yarn กับ pnpm มีเว้นวรรคระหว่าง create กับ next-app แต่บรรทัดของ npm เป็น ขีดกลาง
ตอนอ่านบนสไลด์มันดูเป็นเรื่องเล็กมาก แต่ลองรันดูจริง:

$ npm create-next-app@latest

คิดว่า terminal จะตอบว่าอะไร? มันจะเริ่มถามชื่อโปรเจกต์เหมือนในสไลด์ หรือจะพังก่อน?

พังทันที ไม่ได้ต่อเน็ตด้วยซ้ำ นี่คือผลรันจริง:

Unknown command: "create-next-app@latest"

To see a list of supported npm commands, run:
  npm help

npm ไม่รู้จักคำสั่งชื่อ create-next-app@latest — เพราะมันอ่านทั้งก้อนนั้นเป็นชื่อคำสั่งเดียว

ทำไม yarn/pnpm ถูก แต่ npm ผิด ทั้งที่หน้าตาคล้ายกัน?
คำตอบอยู่ในหน้าช่วยเหลือของ npm เอง — ผลรันจริงอีกอัน:

$ npm create --help

Create a package.json file

Usage:
npm init <package-spec> (same as `npx create-<package-spec>`)
npm init <@scope> (same as `npx <@scope>/create`)

บรรทัดที่เป็นกุญแจคือ npm init <package-spec> (same as `npx create-<package-spec>`)

⚠️ สไลด์หน้า 14 บรรทัดแรกใช้ไม่ได้ — ต้องเติมเว้นวรรค

เขียนแบบนี้ผลจริง
npm create-next-app@latest (ตามสไลด์)❌ Unknown command
npm create next-app@latest✅ มีเว้นวรรค → npm ไปเรียก create-next-app
npx create-next-app@latest✅ ทางที่เอกสารทางการใช้ · ตัวนี้ขีดกลางถูกแล้ว เพราะ npx รับ "ชื่อแพ็กเกจ" ตรง ๆ ไม่ต้องเติมอะไรให้
pnpm create next-app@latest✅ ตามสไลด์ ถูกอยู่แล้ว

กฎที่จำได้ยาว: npx = ขีดกลาง · npm create/pnpm create = เว้นวรรค
โปรเจกต์ทดลองของบทนี้สร้างด้วยบรรทัด npx create-next-app@latest จริง ๆ และมันผ่าน

🧰 เทียบให้เห็นภาพ — ใบสั่งซื้อที่ต้องแยก "ช่องคำสั่ง" กับ "ช่องสินค้า"

คิดว่า npm คือพนักงานหน้าร้านที่มีใบสั่งซื้อสองช่อง: ช่องซ้ายเขียนว่าจะทำอะไร (สั่ง / คืน / เช็คสต็อก) ช่องขวาเขียนว่าของชิ้นไหน
คุณเดินไปบอกว่า "สั่ง-กาแฟเย็น" ติดกันเป็นคำเดียว — พนักงานจะเปิดสมุดหาคำสั่งชื่อ "สั่ง-กาแฟเย็น" แล้วบอกว่า ไม่มีคำสั่งชื่อนี้ในสมุด ทั้งที่เขามีทั้งคำสั่ง "สั่ง" และมีกาแฟเย็นขายอยู่
ที่ไม่เหมือนกัน: พนักงานจริงเดาเจตนาคุณออกแล้วชงกาแฟให้อยู่ดี แต่ npm ไม่เดาให้เลยสักนิด — และนี่คือเรื่องดี เพราะถ้ามันเดา วันหนึ่งมันจะเดาผิดแล้วลงแพ็กเกจที่คุณไม่ได้ขอ ซึ่งอันตรายกว่าเยอะ
อีกจุดที่ไม่เหมือน: npx ไม่ได้มีสองช่องแบบนี้ มันรับ "ชื่อของ" ช่องเดียว ขีดกลางใน npx create-next-app จึงถูกต้องแล้ว ไม่ใช่ข้อยกเว้นมั่ว ๆ

ตอบคำถามตอนติดตั้ง แล้วได้อะไรกลับมาบ้าง

พอคำสั่งรันได้ มันจะถามชื่อโปรเจกต์ก่อน แล้วถามต่อว่าจะใช้ค่าที่แนะนำไหม
สกรีนช็อตบนสไลด์หน้า 14 โชว์ตัวเลือกแรกว่า "Yes, use recommended defaults" พร้อมรายการที่มันจะเปิดให้:

# ข้อความบนสไลด์หน้า 14 ในช่องตัวเลือกแรก
TypeScript, ESLint, No React Compiler, Tailwind, ... directory, App Router, AGENTS.md

อ่านบรรทัดนี้ให้ออกทีละคำ เพราะมันคือสรุปว่าโปรเจกต์คุณจะหน้าตายังไงไปอีกทั้งคอร์ส:

คำบนสไลด์แปลว่า
TypeScriptไฟล์เป็น .tsx ไม่ใช่ .jsx · ได้ tsconfig.json มาด้วย · นี่คือของที่คุณเรียนมาแล้วในบท 63–68
ESLintได้ eslint.config.mjs ติดมาในโปรเจกต์ · ตัวไล่จับโค้ดที่เขียนไม่ดี (ไม่ใช่แค่พิมพ์ผิด)
No React Compilerตัวคอมไพเลอร์ที่ใส่ memo ให้อัตโนมัติ ปิดอยู่ · จำประโยคนี้ไว้ เพราะมันจะสำคัญมากในบทเรื่อง useMemo
Tailwindได้ Tailwind มาพร้อมใช้ · แต่เป็นเวอร์ชัน 4 ซึ่งตั้งค่าไม่เหมือนที่คุณเจอในบทความเก่า ๆ (ดูกล่องเตือนข้างล่าง)
App Routerระบบเส้นทางแบบใหม่ที่ใช้โฟลเดอร์ app/ · คือหัวใจของบทนี้ทั้งบท
AGENTS.mdไฟล์คำแนะนำสำหรับ AI ที่มาช่วยเขียนโค้ดในโปรเจกต์นี้ · เป็นของใหม่ที่เพิ่งมีในเวอร์ชันหลัง ๆ

พอกด Enter เสร็จ นี่คือผลรันจริงในโปรเจกต์ที่ได้ (next 16.3.4) · ผมสั่ง ls -1a — เลข 1 แปลว่า "บรรทัดละชื่อ" ซึ่งอ่านง่ายกว่าแบบหลายคอลัมน์
ผมตัดออกสามบรรทัดที่ไม่ได้มาจาก create-next-app — .next กับ tsconfig.tsbuildinfo ที่เกิดตอนรันโปรเจกต์ และ components ที่ผมสร้างเองทีหลัง

$ ls -1a
.
..
.git
.gitignore
AGENTS.md
CLAUDE.md
README.md
app
eslint.config.mjs
next-env.d.ts
next.config.ts
node_modules
package-lock.json
package.json
postcss.config.mjs
public
tsconfig.json

$ ls -1 app
favicon.ico
globals.css
layout.tsx
page.tsx

สามข้อสังเกตที่ผู้เรียนมักสะดุด:

⚠️ Tailwind ในโปรเจกต์นี้เป็นเวอร์ชัน 4 — ไม่มี tailwind.config.js ให้แก้แล้ว

ถ้าคุณไปหาบทความหรือถาม AI แล้วมันบอกให้ "แก้ tailwind.config.js" คุณจะหาไฟล์นั้นไม่เจอ เพราะมันไม่มีจริง ไม่ใช่เพราะคุณลงพลาด
นี่คือ app/globals.css ตัวจริงในโปรเจกต์ ผลรัน cat สองบรรทัดแรก:

@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #171717;
}

v3 (แบบเก่า) ใช้ @tailwind base; @tailwind components; @tailwind utilities; สามบรรทัด + ไฟล์ config แยก · v4 ใช้ @import "tailwindcss"; บรรทัดเดียว แล้วตั้งค่าสีธีมในไฟล์ CSS เดียวกันนี้เลย · ส่วนตัวต่อกับ build อยู่ใน postcss.config.mjs: "@tailwindcss/postcss": {}

รันเว็บขึ้นมาดู

สไลด์หน้า 15 ให้สามบรรทัดตามตัวจัดการแพ็กเกจ — อันนี้สไลด์ถูกทั้งสามบรรทัด:

$ npm run dev     # หรือ
$ yarn dev        # หรือ
$ pnpm dev

ทำไม npm ต้องมีคำว่า run แต่อีกสองตัวไม่ต้อง? เพราะ dev ไม่ใช่คำสั่งของ npm — มันคือชื่อสคริปต์ที่เขียนไว้ใน package.json ซึ่ง npm ต้องให้บอกก่อนว่าจะ "run" สคริปต์
เปิด package.json จริงในโปรเจกต์นี้ดูก็จะเห็น:

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "eslint"
}

นี่คือผลรันจริงตอนสั่งรัน (ผมมาสก์เลข IP ในวงแลนออกเป็น x.x):

▲ Next.js 16.3.4 (Turbopack)
- Local:         http://localhost:370
- Network:       http://192.168.x.x:370
✓ Ready in 156ms
✓ Running next.config.ts took 12ms

หน้าที่ได้จะเป็นหน้าต้อนรับที่มีข้อความ "To get started, edit the page.tsx file." (ตรงกับสกรีนช็อตบนสไลด์หน้า 15 เป๊ะ) — ประโยคนี้ไม่ได้เขียนเล่น ๆ มันกำลังบอกคุณว่าไฟล์ไหนคือหน้าแรก ซึ่งเป็นเรื่องของหัวข้อถัดไป

🗣 ศัพท์ที่เพิ่งเรียนไป
scaffold
การปั๊มโครงโปรเจกต์เปล่า ๆ ขึ้นมาให้ทั้งชุด — create-next-app คือตัวทำ scaffold · คำนี้ใช้กับทุกภาษา ไม่ใช่แค่ Next.js · ถาม senior ได้ว่า “ทีมเรา scaffold โปรเจกต์ใหม่ด้วยอะไร มีเทมเพลตของทีมไหม” — หลายทีมมีตัวปั๊มของตัวเองที่ใส่ config ของบริษัทมาให้แล้ว จะได้ไม่ต้องนั่งตั้งใหม่
dev server
เซิร์ฟเวอร์ที่รันเฉพาะตอนพัฒนา (จาก npm run dev) — มันคอยดูไฟล์ ถ้าคุณแก้แล้วเซฟ หน้าเว็บจะอัปเดตให้เอง · คนละตัวกับ npm start ที่เป็นเซิร์ฟเวอร์ของของจริง ซึ่งต้อง npm run build ก่อนถึงจะรันได้ · เจอคำถาม “ทำไมของบนเซิร์ฟเวอร์จริงช้ากว่า/ต่างจากในเครื่อง” ให้ถามกลับก่อนเลยว่าเทียบกับ dev หรือ build
Turbopack
ตัวแปลงและรวมโค้ดที่ Next 16 ใช้เป็นค่าปริยาย (แทน webpack ของเดิม) — คือเหตุผลที่ผลรันข้างบนขึ้น Ready in 156ms · พิมพ์บอก AI ได้ว่า “โปรเจกต์นี้เป็น Next 16 ที่ใช้ Turbopack” เพราะคำแนะนำเรื่องปรับ config ของยุค webpack หลายอย่างใช้กับมันไม่ได้แล้ว

plugin ที่แนะนำ — และอันไหนที่ "ลงแล้วไม่พอ"

สไลด์หน้า 16 แนะนำสี่ตัว ผมเพิ่มคอลัมน์ว่ามันทำอะไรให้จริง ๆ:

ส่วนขยาย VS Codeได้อะไรต้องมีอะไรในโปรเจกต์ด้วยไหม
React Snippets
(Simple React Snippets / ES7+)
พิมพ์ตัวย่อสั้น ๆ แล้วมันกางโครง component ให้ · ประหยัดการพิมพ์ซ้ำ ไม่ต้อง เป็นเรื่องของ editor ล้วน ๆ
ESLint ขีดเส้นหยักใต้โค้ดที่ผิดกฎ — ไม่ใช่แค่พิมพ์ผิด แต่รวมถึง "เขียนแบบนี้แล้วจะมีปัญหาทีหลัง" ต้องมี · ส่วนขยายแค่เอากฎมาแสดงบนจอ กฎตัวจริงอยู่ใน eslint.config.mjs ในโปรเจกต์ ซึ่ง create-next-app ใส่มาให้แล้ว
Prettier จัดหน้าโค้ดให้สวยเท่ากันทั้งทีม — เว้นวรรค ขึ้นบรรทัด วงเล็บ ไม่บังคับ แต่ทีมส่วนใหญ่จะมีไฟล์ตั้งค่าไว้ให้เหมือนกันทุกเครื่อง
Tailwind CSS IntelliSense เดาชื่อคลาส Tailwind ให้ตอนพิมพ์ + โชว์ว่าคลาสนั้นแปลเป็น CSS อะไร ต้องมี Tailwind ในโปรเจกต์จริง มันถึงจะรู้ว่าจะเดาอะไรให้

ประเด็นที่สไลด์ไม่ได้พูด แต่ทำให้คนใหม่งงบ่อยที่สุด: ส่วนขยายใน editor กับเครื่องมือในโปรเจกต์ เป็นคนละอย่างกัน
ลง ESLint ใน VS Code แล้วเส้นหยักไม่ขึ้น ไม่ได้แปลว่าลงพลาด — ให้ไปดูก่อนว่าโปรเจกต์มี eslint.config.mjs ไหม
และกลับกัน: โปรเจกต์ที่ไม่ได้ลงส่วนขยายเลยก็ยัง lint ได้ ด้วยคำสั่ง npm run lint ใน terminal · ส่วนขยายทำแค่ย้ายผลมาไว้ตรงหน้าคุณเร็วขึ้น

เรื่องใหญ่ที่สุดของบทนี้ — ชื่อโฟลเดอร์คือ URL

ในบท 38 คุณเขียน route ของ Gin ด้วยมือ ทีละบรรทัด แบบนี้:

// Gin — คุณเป็นคนบอกเองว่า path ไหนไปเรียกฟังก์ชันไหน
r.GET("/users/:id", getUserHandler)

ใน Next.js บรรทัดแบบนี้ไม่มี — และไม่มีที่ให้เขียนด้วย
Next.js ไปไล่อ่านโฟลเดอร์ใต้ app/ แล้วสร้าง route ให้เองจาก ชื่อโฟลเดอร์ · กฎมีสองข้อเท่านั้น:

  1. ชื่อโฟลเดอร์ = ชิ้นส่วนหนึ่งของ URL · ซ้อนโฟลเดอร์ = ซ้อน path
  2. โฟลเดอร์จะกลายเป็นหน้าเว็บได้ ก็ต่อเมื่อในนั้นมีไฟล์ชื่อ page.tsx · โฟลเดอร์เปล่า ๆ หรือมีไฟล์ชื่ออื่น ไม่ใช่หน้าเว็บ

ผมสร้างของจริงในโปรเจกต์ทดลองสี่โฟลเดอร์ แล้วรันดู นี่คือสิ่งที่สร้าง:

app/
└── t70/                    ← ชื่อโฟลเดอร์ที่ผมใช้กันชนกับบทอื่น
    ├── layout.tsx          ← ไม่ใช่หน้าเว็บ (หัวข้อถัดไปจะพูดถึง)
    ├── page.tsx            ← หน้าเว็บ  →  /t70
    ├── about/
    │   └── page.tsx        ← หน้าเว็บ  →  /t70/about
    ├── blog/                ← ไม่มี page.tsx ของตัวเอง เป็นแค่ทางผ่าน  →  ???
    │   └── hello/
    │       └── page.tsx    ← หน้าเว็บ  →  /t70/blog/hello
    └── secret/
        └── note.tsx        ← ไฟล์ธรรมดา ชื่อไม่ใช่ page.tsx  →  ???

เนื้อในแต่ละ page.tsx สั้นมาก แค่พอให้แยกออกว่าอันไหนเป็นอันไหน — นี่คือไฟล์ app/t70/about/page.tsx ทั้งไฟล์:

export default function About() {
  return <h1>PAGE: /t70/about</h1>;
}

สองอย่างที่ page.tsx ต้องมี ห้ามขาด: ต้อง export default (ไม่ใช่ export เฉย ๆ) และของที่ export ต้องเป็นฟังก์ชันที่คืน JSX
ชื่อฟังก์ชัน (About) จะตั้งว่าอะไรก็ได้ Next.js ไม่ได้ดูชื่อ มันดูว่า "ตัวที่ export default ออกมา" คืออะไร

# รันเว็บแล้วยิงทั้งสี่ที่อยู่ดู
$ curl -o /dev/null -w "HTTP %{http_code}\n"  http://localhost:370/t70
$ curl -o /dev/null -w "HTTP %{http_code}\n"  http://localhost:370/t70/about
$ curl -o /dev/null -w "HTTP %{http_code}\n"  http://localhost:370/t70/blog/hello
$ curl -o /dev/null -w "HTTP %{http_code}\n"  http://localhost:370/t70/secret
$ curl -o /dev/null -w "HTTP %{http_code}\n"  http://localhost:370/t70/blog

สามอันแรกคง 200 อยู่แล้ว — แต่สองอันท้ายล่ะ?
secret/ มีโฟลเดอร์จริง มีไฟล์ .tsx อยู่ข้างในจริง แค่ชื่อไม่ใช่ page.tsx
blog/ ก็มีโฟลเดอร์จริง แถมข้างในมี hello/page.tsx ที่เปิดได้อยู่ด้วย — แล้วตัว /t70/blog เองล่ะ เปิดได้ไหม?

ผลรันจริง — ทั้งสองอันได้ 404 และไม่มี error ใด ๆ ใน terminal ด้วย:

/t70                   HTTP 200
/t70/about             HTTP 200
/t70/blog/hello        HTTP 200
/t70/secret            HTTP 404
/t70/blog              HTTP 404

หน้าที่ได้จาก /t70/secret คือหน้า 404 มาตรฐานของ Next.js (ในเนื้อหน้ามีข้อความ This page could not be found)

/t70/blog ได้ 404 คือจุดที่คนพลาดกันมากที่สุด — โฟลเดอร์ blog/ เป็นแค่ทางผ่าน มันไม่มี page.tsx ของตัวเอง
ถ้าอยากให้ /t70/blog เปิดได้ด้วย ก็แค่เพิ่ม app/t70/blog/page.tsx เข้าไปอีกไฟล์ จบ

สำคัญ: ทั้งสองกรณีไม่ใช่ error · การวางไฟล์ .tsx อื่น ๆ ไว้ในโฟลเดอร์ของ route เป็นเรื่องที่ทำได้และทำกันปกติ (เช่นวาง component ที่ใช้เฉพาะหน้านั้น) · โฟลเดอร์ที่เป็นทางผ่านเฉย ๆ ก็ปกติ — Next.js แค่ไม่นับสองอย่างนี้เป็นหน้าเว็บ

อีกหลักฐานที่หนักแน่นกว่า curl คือ ตารางที่ next build พิมพ์ออกมาเอง ตอนจบ · ผลรันจริง (ผมตัดบรรทัดของบทอื่นที่อยู่ในโปรเจกต์เดียวกันออก เหลือเฉพาะของบทนี้):

$ npx next build

✓ Compiled successfully in 647ms
  Running TypeScript ...
✓ Generating static pages using 9 workers (30/30) in 138ms

Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /t70
├ ○ /t70/about
├ ○ /t70/blog/hello
└ ...

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

/t70/secret ไม่มีอยู่ในตารางนี้ — Next.js ไม่รู้จักมันตั้งแต่ตอน build แล้ว ไม่ใช่ไปพังตอนมีคนกดเข้ามา · ตารางนี้แหละคือ "รายการ route" ของโปรเจกต์คุณ ถ้าอยากรู้ว่าเว็บมีหน้าอะไรบ้าง สั่ง npx next build แล้วอ่านตารางนี้

บรรทัด /_not-found ไม่ใช่ของที่คุณสร้าง — Next.js ใส่มาให้เองทุกโปรเจกต์ มันคือหน้า 404 มาตรฐานที่คุณเพิ่งเห็นตอน curl /t70/secret นั่นแหละ
ตอนรันเองแล้วเจอบรรทัดนี้โผล่มาในตาราง อย่าเพิ่งคิดว่าตัวเองทำอะไรพลาด · ถ้าอยากได้หน้า 404 หน้าตาของตัวเอง ให้สร้างไฟล์ app/not-found.tsx แล้วมันจะมาแทนที่อันนี้

ตารางเทียบ: path ของไฟล์ ↔ URL ที่ได้

แถวที่มี ✅ คือของที่ผม curl จริงแล้วได้ 200 · คอลัมน์ขวาสุดคือหน้าตาที่คุณจะเจอในโปรเจกต์ของคุณเอง ที่ไม่มีชั้น t70 คั่น

ไฟล์ในโปรเจกต์ทดลองURL ที่ได้จริงในโปรเจกต์คุณจะเขียนแค่
app/page.tsx✅ /เหมือนกัน — นี่คือหน้าแรก
app/t70/page.tsx✅ /t70app/dashboard/page.tsx → /dashboard
app/t70/about/page.tsx✅ /t70/aboutapp/about/page.tsx → /about
app/t70/blog/hello/page.tsx✅ /t70/blog/helloapp/blog/hello/page.tsx → /blog/hello
app/t70/blog/ (ไม่มี page.tsx ของตัวเอง)❌ 404โฟลเดอร์ที่เป็นแค่ทางผ่าน ไม่ใช่หน้า
app/t70/secret/note.tsx❌ 404ไฟล์ที่ไม่ได้ชื่อ page.tsx ไม่เป็น URL
app/t70/layout.tsx— ไม่เป็น URL ของตัวเองมันไปครอบหน้าอื่นแทน (หัวข้อถัดไป)

เทียบกับ Gin ที่เพิ่งเรียนมา

เรื่องGin (บท 38)Next.js App Router
บอก path ยังไง พิมพ์เป็นสตริง r.GET("/users/:id", h) ตั้งชื่อโฟลเดอร์ — ไม่มีสตริงให้พิมพ์
อยากรู้ว่าเว็บมี route อะไรบ้าง อ่านไฟล์ที่รวม r.GET/r.POST ไว้ ดูโครงโฟลเดอร์ หรืออ่านตารางจาก next build
พิมพ์ path ผิด (เช่นตกตัวอักษร) คอมไพล์ผ่าน แต่ยิงไปแล้วได้ 404 ตอนรัน ก็ได้ 404 เหมือนกัน — แต่คุณเห็นมันตั้งแต่ตอนตั้งชื่อโฟลเดอร์ใน editor
มีสอง route ชื่อชนกัน เป็นไปได้ — ต้องระวังลำดับที่ลงทะเบียน เป็นไปไม่ได้ — ระบบไฟล์ไม่ยอมให้มีสองโฟลเดอร์ชื่อเดียวกันในที่เดียวกันอยู่แล้ว
ย้าย route ไป path ใหม่ แก้สตริงในโค้ด เปลี่ยนชื่อโฟลเดอร์ · ไม่ต้องแตะโค้ดข้างในเลยสักบรรทัด
🧰 เทียบให้เห็นภาพ — สมุดสารบัญ กับ ป้ายหน้าห้อง

Gin เหมือนตึกที่มีสมุดสารบัญเล่มเดียวแขวนไว้ที่ล็อบบี้ — เขียนไว้ว่า "ห้อง 301 = ฝ่ายบัญชี" · ใครย้ายห้องต้องกลับมาแก้สมุด ถ้าลืมแก้ สมุดกับความจริงจะไม่ตรงกัน แล้วคนจะเดินหลง
Next.js เหมือนตึกที่ไม่มีสมุดสารบัญเลย แต่ทุกห้องมีป้ายติดหน้าประตู — อยากรู้ว่ามีห้องอะไรบ้าง เดินดูเอา · ย้ายฝ่ายบัญชีไปชั้นสี่ = ยกป้ายไปติดที่ประตูใหม่ จบ ไม่มีสมุดให้ลืมแก้
ที่ไม่เหมือนกัน: ป้ายหน้าห้องจริง ๆ ใครก็แขวนมั่วได้ แต่ Next.js บังคับว่าห้องจะ "มีอยู่" ก็ต่อเมื่อข้างในมีเฟอร์นิเจอร์ชื่อ page.tsx — โฟลเดอร์เปล่าจึงไม่ใช่หน้าเว็บ ซึ่งไม่มีอะไรในโลกจริงเทียบได้ตรง ๆ
อีกจุด: สารบัญของ Gin เขียนอะไรก็ได้ที่คุณคิดออก (เช่นเงื่อนไขแปลก ๆ) ส่วนป้ายหน้าห้องต้องเล่นตามกฎการตั้งชื่อโฟลเดอร์ — ยืดหยุ่นน้อยกว่า แลกกับความคาดเดาได้

🗣 ศัพท์ที่เพิ่งเรียนไป
file-system routing
ระบบที่ใช้ตำแหน่งของไฟล์เป็นตัวกำหนด URL แทนการเขียนตารางเส้นทางด้วยมือ — ทั้งหัวข้อนี้อธิบายคำนี้คำเดียว · ถาม senior ได้ว่า "โปรเจกต์นี้ใช้ file-system routing หรือประกาศ route เอง" เป็นคำถามแรกที่ควรถามเวลาเข้าโปรเจกต์ frontend ใหม่ เพราะคำตอบเปลี่ยนวิธีหาไฟล์ทั้งหมด
route segment
ชิ้นส่วนหนึ่งของ URL ที่คั่นด้วย / — /blog/hello มีสอง segment และในโปรเจกต์ก็คือสองชั้นโฟลเดอร์พอดี · พิมพ์บอก AI ได้ว่า "ช่วยเพิ่ม route segment settings ใต้ dashboard ให้หน่อย" แล้วมันจะรู้ทันทีว่าให้สร้าง app/dashboard/settings/page.tsx
App Router
ชื่อของระบบเส้นทางแบบที่ใช้โฟลเดอร์ app/ ตัวนี้ · Next.js เคยมีอีกแบบชื่อ Pages Router (ใช้โฟลเดอร์ pages/) ซึ่งกฎต่างกันพอสมควร · เวลาค้นหาคำตอบให้พิมพ์คำว่า "App Router" ต่อท้ายเสมอ ไม่งั้นคุณจะเจอคำตอบของระบบเก่าปนมาแล้วทำตามไม่ได้

layout.tsx ต่างจาก page.tsx ยังไง

สไลด์หน้า 19 สรุปไว้สองบรรทัดสั้น ๆ: "layout.tsx: root layout, wraps every page" และ "page.tsx: page level to render interface"
ถูกทั้งคู่ แต่สั้นเกินกว่าจะเอาไปใช้จริง ผมขอแตกให้ครบ:

page.tsxlayout.tsx
เป็น URL ของตัวเองไหม✅ ใช่ — มันคือ "หน้า"❌ ไม่ · มันคือ "กรอบ"
รับอะไรเข้ามาไม่ต้องรับอะไรก็ได้รับ children = หน้าที่จะมาอยู่ข้างใน
ครอบใครบ้าง—ทุกหน้าในโฟลเดอร์เดียวกันและโฟลเดอร์ลูกทั้งหมด
ตอนเปลี่ยนหน้าถูกแทนที่ด้วยหน้าใหม่อยู่เฉย ๆ ไม่ถูกวาดใหม่
เอาไปใส่อะไรเนื้อหาของหน้านั้น ๆเมนู · แถบบน · footer · ของที่ต้องอยู่ทุกหน้า

นี่คือ app/t70/layout.tsx ตัวจริงที่ผมใช้ทดลอง ตัดให้เหลือแก่น:

export default function T70Layout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <header>
        <strong>HEADER-FROM-LAYOUT</strong>
        <LayoutCounter />
      </header>
      <main>{children}</main>          {/* ← หน้าเว็บมาโผล่ตรงนี้ */}
      <footer>FOOTER-FROM-LAYOUT</footer>
    </div>
  );
}

{children} คือช่องว่างที่บอกว่า "เอาหน้ามาวางตรงนี้" — ไม่มีบรรทัดนี้ หน้าเว็บจะหายไปเฉย ๆ
ลอง curl หน้า /t70/blog/hello ดู นี่คือ HTML จริงที่ได้กลับมา (ผมตัดเฉพาะช่วงที่ layout สร้าง):

$ curl -s http://localhost:370/t70/blog/hello

<div><header><strong>HEADER-FROM-LAYOUT</strong>
<button id="cnt">layout counter = <!-- -->0</button>
<nav><a href="/t70">home</a> <a href="/t70/about">about</a>
<a href="/t70/blog/hello">blog/hello</a></nav></header>
<main><h1>PAGE: /t70/blog/hello</h1></main>
<footer>FOOTER-FROM-LAYOUT</footer></div>

HEADER-FROM-LAYOUT กับ FOOTER-FROM-LAYOUT โผล่มาด้วย ทั้งที่ไฟล์ app/t70/blog/hello/page.tsx มีแค่ <h1> บรรทัดเดียว — และ hello อยู่ลึกลงไปสองชั้นจากตัว layout ด้วยซ้ำ
นี่คือความหมายของคำว่า "ครอบทุกหน้า": ครอบลูกหลานทุกชั้น ไม่ใช่แค่ชั้นเดียว

✅ layout ซ้อน layout ได้ และมันซ้อนกันจริง — หลักฐาน: หน้า /t70/about ของผมยังได้แท็กนี้มาด้วย
<html lang="en" class="... h-full antialiased"> และ <title>Create Next App</title>
ทั้งสองอย่างนี้ไม่ได้อยู่ใน app/t70/layout.tsx ของผมเลย — มันมาจาก app/layout.tsx ซึ่งเป็น root layout ของทั้งโปรเจกต์
แปลว่าหน้าหนึ่งหน้าถูกครอบด้วย layout ทุกชั้นที่อยู่เหนือมัน เรียงจากนอกเข้าใน

ข้อที่คนพลาดบ่อยที่สุด: layout ไม่ถูกวาดใหม่ตอนเปลี่ยนหน้า

ประโยคนี้พูดกันเยอะแต่ไม่ค่อยมีใครพิสูจน์ให้ดู ผมพิสูจน์ให้:
ผมใส่ปุ่มนับเลขไว้ใน layout (ไฟล์ components/t70/LayoutCounter.tsx) ปุ่มนี้จำเลขไว้ในตัวมันเอง · ถ้า layout ถูกวาดใหม่เมื่อไหร่ เลขจะกลับไปเป็น 0

// ลำดับที่ทำในเบราว์เซอร์จริง
1. เปิด /t70            → กดปุ่มจนเลขขึ้นเป็น 2
2. กดลิงก์ "about" ใน layout  → URL เปลี่ยนเป็น /t70/about
3. อ่านเลขบนปุ่มอีกครั้ง

ข้อ 3 จะได้เลขอะไร? — 0 (เพราะเปลี่ยนหน้าแล้วทุกอย่างเริ่มใหม่) หรือ 2 (เพราะ layout ไม่ได้ถูกแตะ)?
แล้วถ้าแทนที่จะกดลิงก์ เรากด F5 โหลดหน้าใหม่ทั้งหน้า ล่ะ จะได้เลขเดียวกันไหม?

ผลรันจริงจากเบราว์เซอร์ ตอนกดลิงก์:

{
  "path": "/t70/about",
  "h1": "PAGE: /t70/about",
  "counter": "layout counter = 2",
  "sameButtonNode": true
}

ได้ 2 — และบรรทัด sameButtonNode: true คือหมัดเด็ด: ผมเก็บตัวปุ่มไว้ก่อนเปลี่ยนหน้า แล้วเทียบกับปุ่มหลังเปลี่ยนหน้า มันคือปุ่มก้อนเดียวกันในหน้าเว็บ ไม่ใช่ปุ่มใหม่ที่หน้าตาเหมือนกัน
ส่วน h1 เปลี่ยนจาก PAGE: /t70 เป็น PAGE: /t70/about เรียบร้อย → เนื้อหาเปลี่ยน กรอบไม่เปลี่ยน

แต่พอสั่งโหลดหน้าใหม่ทั้งหน้า (เท่ากับกด F5):

{
  "path": "/t70/about",
  "counter": "layout counter = 0"
}

กลับเป็น 0 · เพราะการโหลดใหม่ทั้งหน้าคือการโยนทุกอย่างทิ้งแล้วเริ่มใหม่จากศูนย์ — layout กันการวาดใหม่ได้เฉพาะตอนเปลี่ยนหน้าผ่านลิงก์ในเว็บ ไม่ใช่กัน F5
นี่คือเหตุผลว่าทำไมเมนูใน Next.js ถึงควรใช้ <Link> ไม่ใช่ <a> ธรรมดา (จะลงรายละเอียดในบทเรื่อง navigation)

🧰 เทียบให้เห็นภาพ — กรอบรูปกับรูปที่สลับได้

layout.tsx คือกรอบรูปที่แขวนติดผนัง · page.tsx คือรูปที่ใส่อยู่ในกรอบ
เปลี่ยนรูป = ถอดรูปเก่าออก ใส่รูปใหม่เข้าไป · กรอบไม่ได้ถูกถอดลงจากผนังเลย ตะปูก็ยังอยู่ที่เดิม ฝุ่นบนกรอบก็ยังอยู่ที่เดิม
ปุ่มนับเลขในการทดลองข้างบนคือ "ฝุ่นบนกรอบ" นั่นแหละ — เลข 2 ไม่หายเพราะกรอบไม่ได้ถูกถอด
ที่ไม่เหมือนกัน: กรอบรูปจริงมีอันเดียวชั้นเดียว แต่ layout ของ Next.js ซ้อนกันหลายชั้นได้ (root layout ครอบ layout ของ t70 ครอบหน้าอีกที) เหมือนกรอบซ้อนกรอบซ้อนกรอบ
อีกจุดที่ต่าง: การรื้อบ้านใหม่ทั้งหลัง (กด F5) กรอบก็หลุดด้วย — ความ "ไม่ถูกวาดใหม่" มีผลแค่ในช่วงที่ผู้ใช้เดินอยู่ในเว็บเท่านั้น

🗣 ศัพท์ที่เพิ่งเรียนไป
root layout
app/layout.tsx — layout ชั้นนอกสุดที่ครอบทุกหน้าในเว็บ · เป็นที่เดียวที่มีแท็ก <html> กับ <body> และ Next.js บังคับให้ต้องมี · ถาม senior ได้ว่า "ของที่ต้องมีทุกหน้าจริง ๆ ควรไปอยู่ root layout หรือ layout ย่อย" — คำตอบมักคือ "ให้ต่ำที่สุดเท่าที่ทำได้" เพื่อไม่ให้หน้าที่ไม่เกี่ยวต้องแบกไปด้วย
nested layout
layout ที่วางไว้ในโฟลเดอร์ย่อย แล้วมันไปซ้อนอยู่ข้างใน layout ของชั้นบน · พิมพ์บอก AI ได้ว่า "ทำ nested layout ใต้ app/dashboard ให้มีเมนูซ้ายทุกหน้าในนั้น" แล้วมันจะรู้ว่าต้องสร้าง app/dashboard/layout.tsx ไม่ใช่ไปยุ่งกับ root layout
children
ชื่อของ "ของที่ถูกครอบ" ที่ layout รับเข้ามา · เอาไปวางตรงไหนในโครงก็ได้ ตรงนั้นแหละที่หน้าจะไปโผล่ · ถ้าเปิดหน้าแล้วเจอแต่เมนูแต่เนื้อหาหาย ให้เช็คก่อนเลยว่า layout ลืมเขียน {children} หรือเปล่า — เป็นบั๊กที่หาไม่เจอง่าย ๆ เพราะ Next.js ไม่ฟ้องอะไรทั้งนั้น

แล้ว component ควรไปอยู่ไหน

สไลด์หน้า 20 เสนอโครงเพิ่มอีกสองโฟลเดอร์ ซึ่งไม่ใช่กฎของ Next.js แต่เป็นข้อตกลงของทีม (Next.js ไม่รู้จักชื่อพวกนี้เลย มันสนใจแค่ page.tsx/layout.tsx):

โฟลเดอร์สไลด์บอกว่าแปลเป็นภาษาคน
components/ pure and presentational of component ชิ้นส่วนที่ไม่จำอะไรเอง — ได้ข้อมูลอะไรมาก็วาดตามนั้น เช่นปุ่ม การ์ด ป้าย
containers/ Stateful composition of components by business requirement ชิ้นส่วนที่จำสถานะเองและเอา component เล็ก ๆ มาประกอบตามงานจริง เช่น "ฟอร์มสมัครสมาชิก"

สไลด์ยังมีกล่อง Tips ว่า "Component and container in the app/ folder are only used in themselves." — แปลว่า ถ้าชิ้นส่วนนั้นใช้ที่เดียว ให้วางไว้ข้าง ๆ หน้าที่ใช้มันเลย (ใน app/) แต่ถ้าใช้หลายที่ ให้ยกออกมาไว้ที่ components/ ระดับบนสุด

ในบทนี้ผมทำตามข้อตกลงนั้นจริง — ปุ่มนับเลขที่ใช้พิสูจน์เรื่อง layout ผมวางไว้ที่ components/t70/LayoutCounter.tsx (นอก app/) แล้ว import เข้ามาด้วย @/components/t70/LayoutCounter
เครื่องหมาย @/ คือทางลัดที่ชี้ไปรากโปรเจกต์ ตั้งไว้ใน tsconfig.json: "paths": { "@/*": ["./*"] } — มีมาให้ตั้งแต่ create-next-app แล้ว
ประโยชน์: ไฟล์อยู่ลึกแค่ไหนก็เขียน import เหมือนกันหมด ไม่ต้องนับ ../../..

เช็คความเข้าใจ

ในโปรเจกต์ Next.js App Router คุณสร้างไฟล์ app/dashboard/settings/page.tsx · URL ที่เปิดได้คืออะไร

ตัวเลือกที่ 2 ถูก · โฟลเดอร์ทุกชั้นใต้ app/ ต่อกันเป็น URL — ในบทนี้ผม curl พิสูจน์แบบสามชั้นมาแล้วด้วยซ้ำ (app/t70/blog/hello/page.tsx → /t70/blog/hello → HTTP 200)
ตัวเลือกที่ 3 ผิดเพราะ app/ เป็น "จุดตั้งต้น" ไม่ได้นับเป็นชิ้นส่วนของ URL · ตัวเลือกที่ 4 คือวิธีคิดแบบ Gin (บท 38) ซึ่งไม่มีใน Next.js — ไม่มีไฟล์ config ให้ลงทะเบียน route

โฟลเดอร์ app/reports/ มีอยู่จริง ข้างในมีไฟล์ ReportTable.tsx ไฟล์เดียว ไม่มี page.tsx · เปิด /reports จะได้อะไร

ตัวเลือกที่ 3 ถูก · ผมทดลองด้วยของจริงในบทนี้แล้ว: app/t70/secret/note.tsx (มีไฟล์ ไม่มี page.tsx) → curl ได้ HTTP 404
และมันไม่ใช่ error — next build ก็ไม่บ่นสักคำ ตารางเส้นทางแค่ไม่มีบรรทัดนี้เท่านั้น · การวางไฟล์ component ปนไว้ในโฟลเดอร์ route แบบนี้ทำได้ตามปกติ
ตัวเลือกที่ 4 ผิดชัดเจน: Next.js ดูที่ชื่อไฟล์ ไม่ได้ดูว่าในโฟลเดอร์มีกี่ไฟล์

ใน app/shop/layout.tsx มีปุ่มที่จำเลขไว้ในตัวเอง · ผู้ใช้กดจนเลขเป็น 5 แล้วกด <Link> ไปหน้า /shop/cart · เลขบนปุ่มจะเป็นเท่าไร

ตัวเลือกที่ 1 ถูก · ผลรันจริงในบทนี้: กดจนเลขเป็น 2 → กดลิงก์ไป /t70/about → เลขยังเป็น 2 และตัวปุ่มเป็นก้อนเดิมในหน้าเว็บ (sameButtonNode: true) ส่วน <h1> ของหน้าเปลี่ยนไปแล้ว
แต่มีข้อยกเว้นที่ต้องจำคู่กันเสมอ: ถ้าโหลดหน้าใหม่ทั้งหน้า (F5 หรือพิมพ์ URL แล้ว Enter) เลขกลับเป็น 0 — ผมรันพิสูจน์ไว้ในกล่องเฉลยเรื่อง layout แล้วเหมือนกัน
ตัวเลือกที่ 4 ผิด: เรื่องนี้ไม่เกี่ยวกับ dev หรือ production เลย

พิมพ์ npm create-next-app@latest แล้วได้ Unknown command: "create-next-app@latest" · สาเหตุคืออะไร

ตัวเลือกที่ 4 ถูก · หลักฐานคือหน้าช่วยเหลือของ npm เอง: npm init <package-spec> (same as `npx create-<package-spec>`) — create คือชื่อคำสั่ง ส่วน next-app@latest คือของที่ส่งให้มัน
เขียนติดกันจึงกลายเป็นการเรียกคำสั่งชื่อ create-next-app@latest ที่ไม่มีอยู่ · ที่ถูกคือ npm create next-app@latest (เว้นวรรค) หรือ npx create-next-app@latest (ขีดกลาง เพราะ npx รับชื่อแพ็กเกจตรง ๆ)
ตัวเลือกที่ 2 เป็นความเชื่อที่พบบ่อยแต่ผิด — npx/npm create โหลดมาใช้ให้เองอยู่แล้ว ไม่ต้องลงถาวร

🛠 ภารกิจ — สร้างสองหน้าแล้วพิสูจน์ด้วยผลรัน ไม่ใช่แค่ดูในเบราว์เซอร์แล้วพอใจ

ทำตามลำดับ ห้ามข้ามข้อ 5 กับ 6 เพราะสองข้อนั้นคือ "หลักฐาน" ส่วนข้อ 1–4 คือแค่ "รู้สึกว่าน่าจะได้"

  1. สร้างโปรเจกต์ใหม่ด้วย npx create-next-app@latest เลือก "Yes, use recommended defaults" · แล้ว npm run dev ให้ขึ้นที่ localhost:3000
  2. สร้าง app/about/page.tsx ที่คืน <h1>ABOUT-PAGE</h1>
  3. สร้าง app/blog/first-post/page.tsx ที่คืน <h1>FIRST-POST</h1>
  4. สร้าง app/about/layout.tsx ที่ห่อ {children} ไว้ใน <div> และมี <footer>MY-LAYOUT</footer> ต่อท้าย
  5. พิสูจน์เรื่อง URL ด้วย curl — รันสี่บรรทัดนี้แล้วจดผลไว้:
    curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/about
    curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/blog/first-post
    curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/blog
    curl -s http://localhost:3000/about | grep -o "MY-LAYOUT"
    คุณต้องอธิบายให้ได้ว่าทำไมบรรทัดที่ 3 ถึงได้เลขนั้น ก่อนไปข้อต่อไป
  6. พิสูจน์เรื่อง layout ครอบใครบ้าง — รัน curl -s http://localhost:3000/blog/first-post | grep -c "MY-LAYOUT"
    ทำนายก่อนกด Enter ว่าจะได้ 0 หรือ 1 แล้วค่อยรัน
  7. ปิด dev server แล้วสั่ง npx next build · เทียบตาราง Route ที่มันพิมพ์ออกมา กับรายชื่อโฟลเดอร์ที่คุณสร้าง ว่าตรงกันทุกบรรทัดไหม

ข้อ 5 — /about ได้ 200 · /blog/first-post ได้ 200

แต่ /blog ต้องได้ 404 · เพราะโฟลเดอร์ app/blog/ ไม่มี page.tsx ของตัวเอง มีแต่โฟลเดอร์ลูก — โฟลเดอร์กลาง ๆ ที่เป็นแค่ทางผ่านไม่ใช่หน้าเว็บ
บรรทัดที่ 4 ต้องพิมพ์คำว่า MY-LAYOUT ออกมา (layout ครอบหน้า /about อยู่)

ข้อ 6 — ต้องได้ 0 · เพราะ app/about/layout.tsx ครอบเฉพาะสิ่งที่อยู่ใต้โฟลเดอร์ about/ เท่านั้น · blog/first-post เป็นคนละสาย มันจึงไม่โดนครอบ
ถ้าอยากให้ครอบทั้งคู่ ต้องเลื่อน layout ขึ้นไปที่ app/layout.tsx — นี่คือความหมายจริงของคำว่า "ครอบทุกหน้า" ที่สไลด์เขียนสั้น ๆ ไว้: ครอบทุกหน้าที่อยู่ใต้ตัวมัน
ข้อ 7 — ตาราง Route ควรมี / · /about · /blog/first-post และไม่มี /blog ซึ่งตรงกับผล curl ในข้อ 5 พอดี
จะมีอีกบรรทัดที่คุณไม่ได้สร้างคือ /_not-found — อันนั้นถูกแล้ว Next.js แถมมาให้เองทุกโปรเจกต์ (ดูกล่องอธิบายในหัวข้อเรื่อง URL)

💬 ติดตรงไหนถามได้เลย — โดยเฉพาะสามคำถามนี้ที่ผู้เรียนถามซ้ำทุกรุ่น:
“ผมสร้างโฟลเดอร์แล้วแต่เปิดไม่เจอหน้า เพราะอะไร” · “ทำไม layout ของผมไม่ครอบหน้าที่ผมอยากให้ครอบ” · “ตอนนี้จะเอาเมนูไปไว้ตรงไหนดี root layout หรือ layout ย่อย”
ถามพร้อมโครงโฟลเดอร์ที่คุณสร้างไว้จริง (พิมพ์ tree app หรือถ่ายรูปแถบ EXPLORER มา) จะตอบได้ตรงจุดกว่าเยอะ เพราะปัญหาเกือบทั้งหมดของบทนี้อ่านออกจากโครงโฟลเดอร์

สรุปที่เอาไปใช้พรุ่งนี้ได้

อ้างอิง