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

Next.js คืออะไร และทำไมไม่ใช่ React เปล่า ๆ

~18 นาที · ต่อจากบท 56–68 (HTML/CSS + TypeScript) · ยังไม่ต้องติดตั้งอะไร บทหน้าค่อยลงมือติดตั้ง

คุณเขียน Go มา 9 วัน เขียน HTML/CSS มาแล้ว เขียน TypeScript มาแล้ว ตอนนี้อาจารย์เอาคำว่า NextJS วางลงบนโต๊ะ พร้อมประโยคเดียวว่า "framework ที่สร้างบน React อีกที"
ปัญหาคือประโยคนั้นจะช่วยอะไรได้ ต้องรู้ก่อนว่า React ทำอะไรได้แค่ไหน แล้วอะไรคือช่องว่างที่ Next.js เข้ามาเติม — ไม่งั้นมันก็เป็นแค่ชื่อสองชื่อที่จำสลับกันได้
บทนี้จะตอบสามคำถามเท่านั้น: (1) React ทำอะไร Next.js ทำอะไรเพิ่ม · (2) คำว่า "component" คืออะไรเมื่อเทียบกับ func ที่คุณเขียนใน Go · (3) ทำไมของชิ้นเดียวถึงเอากลับมาใช้ซ้ำได้
ทุกข้ออ้างในบทนี้ผมรันในโปรเจกต์ Next.js จริงแล้วก็อปผลมา รวมทั้ง โค้ดในสไลด์หน้า 10 ที่คอมไพล์ไม่ผ่านกับ React เวอร์ชันที่เราใช้จริง — จุดนั้นสำคัญ อย่าข้าม

🧰 ของที่ใช้ตรวจในบทนี้

next        16.3.4
react       19.2.8
react-dom   19.2.8
typescript  5.9.3
node        v24.14.0
# โปรเจกต์สร้างด้วย: npx create-next-app@latest --ts --tailwind --eslint --app --no-src-dir
# คำสั่งที่ใช้ตรวจ:  npx tsc --noEmit  ·  npx next build  ·  npx next dev  ·  curl

ทุก error code (TS2503 · TS2339) และทุกก้อน HTML ที่โชว์ในบทนี้ ก็อปมาจากผลรันจริง ไม่ได้พิมพ์จากความจำ
คุณ copy โค้ดไปรันซ้ำเองได้ทุกก้อน — ถ้าได้ผลไม่ตรง มาบอกผมได้เลย

1 · React ทำอะไร แล้ว Next.js ทำอะไรเพิ่ม

เริ่มจากของที่คุณรู้จักอยู่แล้ว: เบราว์เซอร์รู้จักแค่ HTML · CSS · JavaScript
ตอนเรียน HTML/CSS คุณเขียนหน้าเว็บด้วยมือทุกแท็ก อยากได้ปุ่ม 20 ปุ่มก็ก็อป <button> ยี่สิบรอบ แก้ทีต้องไล่แก้ยี่สิบที่

React คือไลบรารีที่แก้ปัญหานั้นข้อเดียว — มันให้คุณเขียน "ของ" หนึ่งชิ้น แล้วเอาไปวางกี่ที่ก็ได้ พร้อมกับคอยอัปเดตหน้าจอให้เองเมื่อข้อมูลเปลี่ยน
แต่ React ทำแค่นั้นจริง ๆ · React ไม่รู้จักคำว่า "หน้าเว็บ" ไม่รู้จัก URL ไม่รู้ว่าจะรวมไฟล์ยังไงให้เบราว์เซอร์โหลดได้ ไม่รู้ว่าจะเอา HTML ออกมาจากเซิร์ฟเวอร์ยังไง ของพวกนั้นคุณต้องหามาต่อเอง

Next.js คือกล่องที่เอา React มาใส่ไว้ แล้วแถมของที่ขาดมาให้ครบ
สไลด์หน้า 6 เขียนไว้ตรง ๆ ว่า "A popular web development framework built on top of React" และหน้าแรกของ nextjs.org พาดหัวว่า "The React Framework for the Web"
คำสำคัญคือ built on top of — Next.js ไม่ได้มาแทน React โค้ด React ที่คุณเขียนใน Next.js ก็คือโค้ด React อันเดิม แค่มีคนจัดโต๊ะให้เรียบร้อยแล้ว

🧰 เทียบให้เห็นภาพ — เครื่องยนต์ กับ รถทั้งคัน

React คือเครื่องยนต์ · มันหมุนได้จริง แรงจริง แต่เครื่องยนต์เปล่า ๆ วางบนพื้น คุณขับไปไหนไม่ได้ ต้องหาแชสซี พวงมาลัย ล้อ ถังน้ำมัน ไฟหน้า มาประกอบเอง และแต่ละชิ้นก็ต้องเลือกยี่ห้อเองว่าจะเข้ากันไหม
Next.js คือรถทั้งคันที่ประกอบมาแล้ว โดยใช้เครื่องยนต์ตัวนั้นแหละ เปิดประตูขึ้นนั่งแล้วขับได้เลย

ที่ไม่เหมือนกัน: ซื้อรถมาแล้วคุณถอดเครื่องออกไปใช้เดี่ยว ๆ ไม่ได้ แต่ในโลกโค้ด React ยังอยู่ตรงนั้นให้คุณเรียกใช้ตรง ๆ ได้ทุกบรรทัด — ใน Next.js คุณ import { useState } from 'react' ได้ตามปกติ ไม่มีอะไรถูกซ่อน · และอีกจุด: รถประกอบมาแล้วเปลี่ยนล้อได้ง่าย ๆ แต่ Next.js มีความเห็น ว่าโฟลเดอร์ต้องชื่ออะไร ไฟล์ต้องชื่ออะไร ถ้าคุณอยากทำนอกกรอบนั้น คุณจะเหนื่อยกว่าเขียน React เปล่า ๆ

ตารางที่ต้องจำจากบทนี้ — อะไรต้องทำเอง อะไรได้ฟรี

นี่คือใจความทั้งหมดของบท ถ้าจำได้ตารางเดียวก็ถือว่าคุ้มแล้ว
คอลัมน์ซ้ายคือ "ถ้าคุณตั้งโปรเจกต์ React เปล่า ๆ เอง" คอลัมน์ขวาคือ "ถ้าคุณใช้ Next.js"

งานที่หน้าเว็บหนึ่งหน้าต้องมี React เปล่า ๆ Next.js
วาด UI จากข้อมูล · อัปเดตหน้าจอเมื่อข้อมูลเปลี่ยน ✅ ได้ฟรี — นี่คืองานของ React แท้ ๆ ✅ ได้ฟรี — เพราะมันคือ React ตัวเดียวกัน
Routing — URL /about ต้องไปโผล่หน้าไหน ❌ ต้องลงไลบรารีเพิ่มเอง (เช่น React Router) แล้วเขียนตารางเส้นทางเอง ✅ ได้จากชื่อโฟลเดอร์ — สร้าง app/about/page.tsx แล้ว /about ใช้ได้ทันที ไม่ต้องประกาศที่ไหนอีก
Bundling — รวมไฟล์ .tsx หลายสิบไฟล์ให้เบราว์เซอร์โหลดได้ ❌ ต้องเลือกและตั้งค่า bundler เอง (Vite / webpack) + ตั้งค่า TypeScript เอง ✅ มาให้แล้ว (เวอร์ชันนี้ใช้ Turbopack) · npx next dev คำสั่งเดียวจบ
Server rendering — ให้ HTML ที่มีเนื้อหาจริงออกจากเซิร์ฟเวอร์ ❌ เบราว์เซอร์ได้ <div id="root"></div> เปล่า ๆ แล้วรอ JS วาดทีหลัง ✅ เป็นค่าเริ่มต้น — พิสูจน์ในหัวข้อ 5 ด้วย curl
Route Handler — เขียน API endpoint ในโปรเจกต์เดียวกัน ❌ ต้องแยกโปรเจกต์ backend ต่างหาก (แบบ Go ที่คุณเขียนมา) ✅ วางไฟล์ route.ts ในโฟลเดอร์ ก็เป็น endpoint แล้ว (สไลด์หน้า 7)
ปรับรูป · ฟอนต์ · สคริปต์ ให้โหลดเร็ว ❌ ต้องหา plugin มาต่อเอง หรือทำมือ ✅ สไลด์หน้า 7 เรียกว่า Build-in Optimizations — มีมาให้ในกล่อง

อ่านตารางนี้ยังไงให้ถูก — ช่อง ❌ ไม่ได้แปลว่า React ทำไม่ได้ ทุกช่องทำได้หมด แค่คุณต้องเป็นคนเลือกเครื่องมือและต่อสายเอง
นี่คือความต่างระหว่างคำว่า library กับ framework ซึ่งจะติดป้ายให้ในกล่องถัดไป

🏷️ ศัพท์ที่เอาไปคุยกับ senior ได้
library (ไลบรารี)
กองเครื่องมือที่คุณเป็นคนเรียกใช้ — คุณคุมลำดับ คุณคุมโครงสร้างโปรเจกต์ · React คือ library
ถาม senior ได้ว่า "ที่นี่ใช้ React เปล่า ๆ หรือใช้ framework ครอบอยู่ครับ"
framework (เฟรมเวิร์ก)
กรอบที่มันเป็นคนเรียกโค้ดคุณ — มันกำหนดว่าไฟล์ต้องอยู่ตรงไหน ชื่ออะไร แล้วมันไปหยิบมาใช้เอง · Next.js คือ framework
พิมพ์บอก AI ได้ว่า "อธิบายให้หน่อยว่า Next.js เป็น framework ต่างจาก React ที่เป็น library ตรงไหนบ้าง ขอเป็นข้อ ๆ"
App Router
ระบบจัดเส้นทางแบบใหม่ของ Next.js ที่อ่านโครงโฟลเดอร์ app/ เป็นแผนที่ URL (ของเก่าชื่อ Pages Router ใช้โฟลเดอร์ pages/ — เจอในบทความเก่า ๆ เยอะมาก)
ถาม senior ได้ว่า "โปรเจกต์นี้เป็น App Router หรือ Pages Router ครับ" — คำตอบเปลี่ยนวิธีเขียนแทบทุกอย่าง ถามก่อนเสมอ
SSR / Server-Side Rendering
การให้ เซิร์ฟเวอร์วาด HTML เสร็จก่อนส่งให้เบราว์เซอร์ แทนที่จะส่งหน้าเปล่าไปให้ JavaScript วาดเอาเอง
พิมพ์บอก AI ได้ว่า "หน้านี้ควรเป็น server render หรือ client render ดี เพราะอะไร"

2 · "component" คืออะไร เทียบกับ func ที่คุณเขียนใน Go

สไลด์หน้า 10 นิยามไว้ประโยคเดียว: "It takes types props, manages typed state, and returns JSX."
ตัดศัพท์ออกให้หมดแล้วเหลือแค่นี้ — component คือฟังก์ชันธรรมดา ๆ ที่คืน "หน้าตา" แทนที่จะคืนข้อมูล

เทียบกับของที่คุณเขียนมาแล้วเก้าวัน:

ฟังก์ชันใน Gocomponent ใน React
รับอะไรเข้าพารามิเตอร์ที่มี typeprops — ก็คือพารามิเตอร์ที่มี type เหมือนกัน
คืนอะไรออกข้อมูล — string, error, structJSX — คำอธิบายหน้าตาของ UI
ใครเรียกมันคุณเรียกเอง Greeting("DevPool")React เรียกให้ คุณแค่วางแท็ก <Greeting name="DevPool" />
เรียกกี่ครั้งเท่าที่คุณเขียนไว้เรียกใหม่ทุกครั้งที่ข้อมูลเปลี่ยน — จุดนี้ต่างที่สุด

แถวสุดท้ายคือแถวที่ทำให้คนมาจากฝั่ง backend งงที่สุด
ใน Go คุณเรียก CalculateBill() หนึ่งครั้ง ได้ผลลัพธ์ แล้วจบ
ใน React ฟังก์ชันเดียวกันนั้นถูกเรียกซ้ำแล้วซ้ำอีก ทุกครั้งที่ข้อมูลขยับ แล้ว React ก็เอาผลลัพธ์รอบใหม่ไปเทียบกับรอบก่อน แล้วแก้หน้าจอเฉพาะจุดที่ต่าง
เพราะแบบนี้ component จึงต้องให้ผลเหมือนเดิมเสมอเมื่อ input เหมือนเดิม — ซึ่งก็คือ pure function ที่คุณเพิ่งเรียนไปในบท 65–67 นั่นแหละ ไม่ใช่ของใหม่

โค้ดในสไลด์หน้า 10 — และเหตุผลที่มันคอมไพล์ไม่ผ่านกับของจริง

สไลด์เขียนโค้ดนี้ไว้ (ผมพิมพ์ตามที่เห็นในสกรีนช็อตทุกตัวอักษร):

// components/t69/Greeting.tsx — ตามสไลด์หน้า 10 เป๊ะ ๆ
function Greeting(
  { name }: { name: string }
): JSX.Element {
  return <h1>Hello, {name}</h1>;
}

โปรเจกต์ที่เราใช้เป็น React 19.2.8 + TypeScript 5.9.3
รัน npx tsc --noEmit แล้วคิดว่าได้อะไร?

ไม่ผ่าน ได้ error จริงบรรทัดนี้:

components/t69/Greeting.tsx(3,4): error TS2503: Cannot find namespace 'JSX'.

สาเหตุ: React 19 เอา JSX ที่เป็นชื่อระดับ global ออกไปแล้ว
สมัย React 17–18 แค่ import React เข้ามา ชื่อ JSX ก็ลอยอยู่ให้ใช้ทั่วไฟล์ พอ React 19 ย้ายมันไปอยู่ใต้ React ชื่อลอย ๆ ตัวนั้นก็หายไป TypeScript เลยหาไม่เจอ

วิธีแก้ที่สั้นที่สุด — import ชื่อนั้นเข้ามาเอง (ผมรันแล้ว ผ่านจริง exit=0):

import type { JSX } from "react";   // ← บรรทัดที่สไลด์ไม่มี

function Greeting(
  { name }: { name: string }
): JSX.Element {
  return <h1>Hello, {name}</h1>;
}

export default Greeting;

หรือจะลบ : JSX.Element ทิ้งไปเลยก็ได้ — และนี่คือสิ่งที่คนส่วนใหญ่ทำจริง เพราะ TypeScript เดา return type ให้เองอยู่แล้วจากตัว return เขียนกำกับไว้ก็แค่ให้คนอ่านสบายใจ ไม่ได้เพิ่มความปลอดภัยอะไร

⚠️ สไลด์หน้า 10 กับของจริงไม่ตรงกัน — และจะเจอแบบนี้อีกเรื่อย ๆ

สไลด์เขียนถูกตามยุคของมัน แต่โปรเจกต์ที่ create-next-app สร้างให้วันนี้ได้ React 19 ซึ่งย้ายของชิ้นนี้ไปแล้ว
บทเรียนที่แพงกว่าตัว error: เวลาก็อปโค้ดจากสไลด์ · บล็อก · หรือคำตอบของ AI มาแล้วมันแดง คำถามแรกที่ควรถามคือ "อันนี้เขียนไว้ตอน React เวอร์ชันอะไร" ไม่ใช่ "ผมพิมพ์ผิดตรงไหน"
เช็คของตัวเองได้ด้วย node -e "console.log(require('react/package.json').version)"

🏷️ ศัพท์ที่เอาไปคุยกับ senior ได้
component (คอมโพเนนต์)
ฟังก์ชันที่คืน UI · ชื่อต้องขึ้นต้นด้วยตัวพิมพ์ใหญ่ (เหตุผลอยู่ในหัวข้อ 6)
ถาม senior ได้ว่า "อันนี้ควรแตกเป็น component ใหม่ หรือรับ prop เพิ่มดีครับ" — เป็นคำถามที่ senior ตอบได้ทันทีและสอนอะไรได้เยอะ
props
ย่อจาก properties · คือพารามิเตอร์ของ component ส่งเข้าไปทางแอตทริบิวต์ของแท็ก · ในฝั่ง component มันมาถึงเป็น object ก้อนเดียว เลยเห็นเขียน { name } แกะออกมาแบบนั้น
พิมพ์บอก AI ได้ว่า "ช่วยเขียน type ของ props ให้ component นี้หน่อย"
JSX
ไวยากรณ์ที่ให้เขียนแท็กแบบ HTML ปนลงไปในไฟล์ TypeScript ได้ · มันไม่ใช่ HTML และไม่ใช่ string — ตอน build มันถูกแปลงเป็นการเรียกฟังก์ชันธรรมดา
ถาม senior ได้ว่า "JSX ตรงนี้กลายเป็นโค้ดอะไรตอน build ครับ"
type inference
ความสามารถของ TypeScript ที่เดาชนิดข้อมูลให้เองโดยไม่ต้องเขียนกำกับ — เหตุผลที่ลบ : JSX.Element ทิ้งได้แล้วยังปลอดภัยเท่าเดิม
(คำนี้คุณเจอมาแล้วในบท 63 · ที่นี่มันกลับมาใช้ได้พอดี)

3 · ทำไม component ถึงเอากลับมาใช้ซ้ำได้

คำตอบสั้นมาก: เพราะมันเป็นฟังก์ชัน · ฟังก์ชันเรียกซ้ำได้ด้วยอาร์กิวเมนต์คนละชุด component ก็เหมือนกันเป๊ะ แค่ "อาร์กิวเมนต์" เขียนอยู่ในแท็ก

ผมเขียนไฟล์นี้ไว้จริงในโปรเจกต์ — สิบสี่บรรทัด เขียนครั้งเดียว:

// components/t69/PriceTag.tsx
import type { JSX } from "react";

type PriceTagProps = {
  label: string;
  baht: number;
};

export default function PriceTag({ label, baht }: PriceTagProps): JSX.Element {
  return (
    <p>
      {label}: {baht.toLocaleString("th-TH")} บาท
    </p>
  );
}

แล้วเรียกใช้สามครั้งด้วยข้อมูลคนละชุด:

// app/t69/page.tsx
import Greeting from "@/components/t69/Greeting";
import PriceTag from "@/components/t69/PriceTag";

export default function Page() {
  return (
    <main>
      <Greeting name="DevPool" />
      <PriceTag label="ค่าไฟเดือนนี้" baht={1250} />
      <PriceTag label="ค่าน้ำเดือนนี้" baht={340} />
      <PriceTag label="ค่าเน็ตเดือนนี้" baht={599} />
    </main>
  );
}

HTML ที่ออกมาจริง (ผม curl มาแล้วตัดเอาเฉพาะ <main>)
ของจริงเป็นบรรทัดเดียวยาว ๆ ผมแค่กด Enter คั่นให้อ่านง่าย ไม่ได้ตัดตัวอักษรตัวไหนออกเลย — รวมทั้งของแปลก ๆ ที่คุณกำลังจะเห็น:

<main><h1>Hello, <!-- -->DevPool</h1>
<p>ค่าไฟเดือนนี้<!-- -->: <!-- -->1,250<!-- --> บาท</p>
<p>ค่าน้ำเดือนนี้<!-- -->: <!-- -->340<!-- --> บาท</p>
<p>ค่าเน็ตเดือนนี้<!-- -->: <!-- -->599<!-- --> บาท</p></main>

⚠️ อะไรคือ <!-- --> ที่โผล่มาเต็มไปหมด

ผมเกือบลบมันทิ้งเพื่อให้บทอ่านง่าย แล้วนึกได้ว่าถ้าลบ คุณจะรันตามแล้วเห็นไม่ตรงกับบท
มันคือ คอมเมนต์ HTML ว่าง ๆ ที่ React ใส่เองตอน server render ทุกครั้งที่ข้อความสองก้อนมาต่อกัน — ในที่นี้คือ {label} · : · {baht...} · บาท สี่ก้อนติดกัน
มันมีไว้บอกเบราว์เซอร์ว่า "ตรงนี้เป็นข้อความคนละก้อนนะ อย่าเผลอเชื่อมกัน" เพื่อให้ตอน JavaScript โหลดเสร็จแล้วมาต่อยอด (hydrate) React นับก้อนได้ตรงกับตอนที่มันวาดไว้
คนอ่านมองไม่เห็นมันเลย เพราะคอมเมนต์ไม่แสดงผล · ถ้าคุณ curl แล้วไม่เจอ แปลว่าหน้านั้นไม่ได้ผ่านมือ React — ตัวมันเองก็เลยเป็นหลักฐานอย่างหนึ่ง

สังเกตเครื่องหมายจุลภาคใน 1,250 — ผมไม่ได้พิมพ์มันลงไป toLocaleString("th-TH") เป็นคนใส่ให้ · แปลว่าโค้ดจัดรูปแบบตัวเลข เขียนที่เดียวใน PriceTag.tsx แล้วมีผลกับทั้งสามบรรทัด
ถ้าพรุ่งนี้อยากเปลี่ยนเป็น ฿1,250 คุณแก้ไฟล์เดียว ไม่ต้องไล่หาสามที่ — นี่คือ "ใช้ซ้ำได้" ที่แปลว่าอะไรจริง ๆ ไม่ใช่แค่ประหยัดการพิมพ์

"Everything is component"

สไลด์หน้า 11 ทั้งหน้ามีข้อความเดียว: "Everything is component" แล้วหน้า 12 ก็เอาสกรีนช็อต Outlook มาตีกรอบสีทับทุกส่วน — แถบบนหนึ่งกรอบ · เมนูซ้ายหนึ่งกรอบ · รายการเมลหนึ่งกรอบ · เมลแต่ละฉบับในรายการอีกกรอบซ้อนเข้าไป

หน้าเว็บหนึ่งหน้า ไม่ได้มองเป็น "หน้า" แต่มองเป็นต้นไม้ของกล่อง

  <Page>
   ├── <TopBar />              ← กล่องเดียว ใช้ทุกหน้า
   ├── <Sidebar>
   │     ├── <FolderItem name="Inbox"      count={517} /> ┐
   │     ├── <FolderItem name="Junk Email" count={2}   /> ├ ไฟล์เดียวกัน
   │     └── <FolderItem name="Drafts"     count={0}   /> ┘ ใช้ 3 ครั้ง
   └── <MailList>
         ├── <MailRow ... />   ┐
         ├── <MailRow ... />   ├ ไฟล์เดียวกัน จะมี 5 หรือ 500 แถวก็ได้
         └── <MailRow ... />   ┘

ประโยคนี้ฟังดูเวอร์ แต่มันเป็นวิธีมองงาน ไม่ใช่คำโฆษณา
เวลาได้ดีไซน์มาแผ่นหนึ่ง งานแรกไม่ใช่ลงมือเขียน HTML — งานแรกคือลากกรอบทับดีไซน์ว่าอะไรซ้ำบ้าง ของที่ซ้ำคือ component ส่วนที่ต่างกันระหว่างของซ้ำ ๆ นั้นคือ props
ในตัวอย่างข้างบน FolderItem ต่างกันแค่ชื่อกับตัวเลข — สองอย่างนั้นจึงเป็น props

4 · Routing มาจากโฟลเดอร์ — พิสูจน์

สไลด์หน้า 7 เรียกว่า "Create routes using the file system"
แปลว่า ไม่มีไฟล์ตารางเส้นทางให้แก้ · ที่อยู่ของไฟล์คือ URL

ผมสร้างแค่โฟลเดอร์ app/t69/ แล้ววางไฟล์ page.tsx ลงไปไฟล์เดียว ไม่ได้ไปประกาศอะไรเพิ่มที่ไหนเลย แล้วยิงคำสั่งนี้:

$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:PORT/t69
200

$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:PORT/t69/settings
404

/t69 ได้ 200 เพราะมีโฟลเดอร์ · /t69/settings ได้ 404 เพราะไม่มีโฟลเดอร์ settings ข้างใน · แค่นั้นเลย ไม่มีความลับอะไรมากกว่านี้

ตอนสั่ง npx next build Next.js พิมพ์ตารางเส้นทางที่มันหาเจอออกมาให้ดูด้วย:

▲ Next.js 16.3.4 (Turbopack)
✓ Compiled successfully in 358ms

Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /t69
└ ...

○  (Static)  prerendered as static content

หมายเหตุความซื่อสัตย์ — โปรเจกต์ทดสอบนี้มีบทอื่นใช้ร่วมกันอยู่ ตารางจริงจึงยาวกว่านี้ (มี /t70, /t71, … ของบทถัด ๆ ไป) ผมตัดเหลือเฉพาะบรรทัดที่เกี่ยวกับบทนี้ ไม่ได้แต่งบรรทัดขึ้นมาใหม่
ด้วยเหตุผลเดียวกัน เลขพอร์ตในคำสั่ง curl ข้างบนผมเขียนเป็น PORT — เพราะพอร์ตจริงตอนผมรันถูกจองไว้โดยเซิร์ฟเวอร์ของบทอื่น ตัวเลขนั้นไม่มีความหมายกับคุณ

5 · "Server rendering" คืออะไร — พิสูจน์ด้วยไฟล์ที่มันทิ้งไว้

บรรทัดสุดท้ายของตารางข้างบนเขียนว่า ○ (Static) prerendered as static content
คำว่า prerendered แปลว่า "วาดเสร็จไว้ล่วงหน้าแล้ว" และมันไม่ใช่คำโฆษณา — มันทิ้งไฟล์ไว้ให้ดูจริง ๆ

$ ls .next/server/app/ | grep t69
t69
t69.html
t69.meta
t69.rsc
t69.segments

$ cat .next/server/app/t69.html   # (ตัดเอาเฉพาะ <main>)
<main><h1>Hello, <!-- -->DevPool</h1><p>ค่าไฟเดือนนี้<!-- -->: <!-- -->1,250<!-- --> บาท</p>…</main>

หยุดตรงนี้แล้วดูให้ดี: ข้อความ "Hello, DevPool" อยู่ในไฟล์ .html จริง ๆ ตั้งแต่ตอน build เสร็จ ก่อนที่จะมีเบราว์เซอร์ตัวไหนเปิดหน้านี้ด้วยซ้ำ
ถ้าเป็น React เปล่า ๆ ตามท่ามาตรฐาน สิ่งที่อยู่ในไฟล์ HTML จะเป็น <div id="root"></div> ว่าง ๆ แล้วรอ JavaScript โหลดเสร็จค่อยวาด

🧰 เทียบให้เห็นภาพ — ร้านข้าวแกงกับร้านตามสั่ง

Next.js = ร้านข้าวแกง · กับข้าวทำเสร็จตั้งถาดรอไว้ตั้งแต่เช้า ลูกค้าเดินเข้ามาปุ๊บ ตักใส่จานเสร็จใน 10 วินาที — เห็นของทันที
React เปล่า ๆ = ร้านตามสั่ง · ลูกค้าเข้ามาเจอครัวว่างเปล่า สั่งแล้วนั่งรอให้ผัดให้ดู — ได้ของร้อนกว่า แต่ช่วงแรกลูกค้ามองเห็นแค่จานเปล่า
คนที่รอไม่ไหวคือ Google — บอตของมันมักไม่นั่งรอให้ผัดเสร็จ ก็เลยเก็บได้แต่จานเปล่าไปทำ SEO

ที่ไม่เหมือนกัน: ข้าวแกงตั้งไว้นานแล้วเย็น แต่หน้าเว็บที่ prerender ไว้ ไม่ได้ค้างอยู่แบบนั้นถาวร — Next.js มีวิธีสั่งให้ทำใหม่ตามรอบเวลา (สไลด์หน้า 7 เรียกว่า Incremental Static Regeneration / ISR)
และที่สำคัญกว่านั้น: มันไม่ได้เลือกอย่างใดอย่างหนึ่งทั้งหน้า — หน้าเดียวกันผสมได้ ส่วนที่นิ่งก็ตั้งถาดไว้ ส่วนที่ต้องกดได้ก็ผัดสด ตรงนี้คือเรื่องของ 'use client' ซึ่งจะเจอเต็ม ๆ ในบทถัด ๆ ไป — บทนี้แค่ให้รู้ว่ามันมีอยู่

6 · กับดักแรกที่จะเจอ: ตัวพิมพ์ใหญ่ตัวเดียว

ในเมื่อ component เป็นแค่ฟังก์ชัน แล้ว React รู้ได้ยังไงว่า <PriceTag /> คือ component ของเรา ส่วน <p> คือแท็ก HTML?
คำตอบคือมันดูตัวอักษรตัวแรก ตัวพิมพ์ใหญ่ = component ของคุณ · ตัวพิมพ์เล็ก = แท็ก HTML ที่เบราว์เซอร์รู้จัก

// app/t69/page.tsx — ผมเปลี่ยนแค่ตัว P เป็นตัวเล็ก
import PriceTag from "@/components/t69/PriceTag";

const priceTag = PriceTag;   // ← ของอันเดียวกันเป๊ะ แค่ชื่อขึ้นต้นตัวเล็ก

export default function Page() {
  return (
    <main>
      <priceTag label="ค่าไฟเดือนนี้" baht={1250} />
    </main>
  );
}

ตัวแปร priceTag ชี้ไปที่ฟังก์ชันเดียวกันเป๊ะ ไม่มีอะไรเปลี่ยนเลยนอกจากตัวอักษรตัวเดียว
รัน npx tsc --noEmit แล้วคิดว่าได้อะไร?

พังทันที — นี่คือผลรันจริง:

app/t69/page.tsx(9,7): error TS2339: Property 'priceTag' does not exist on type 'JSX.IntrinsicElements'.

อ่านคำว่า JSX.IntrinsicElements ให้ออกแล้วจะเข้าใจทั้งหมด — มันคือรายชื่อแท็ก HTML ที่มีอยู่จริงในโลก (div, p, h1, …)
พอเจอชื่อขึ้นต้นด้วยตัวเล็ก JSX ไม่ได้มองว่านี่คือตัวแปรของคุณ มันไปเปิดรายชื่อแท็ก HTML แทน แล้วรายงานว่าไม่มีแท็กชื่อ priceTag ในโลกนี้

ทำไมต้องรู้เรื่องนี้ตั้งแต่วันแรก: ข้อความ error ไม่มีคำว่า "ตัวพิมพ์ใหญ่" อยู่เลยสักคำ · ถ้าไม่เคยเจอมาก่อน คุณจะไปนั่งไล่ดู props ไล่ดู import ไล่ดู type ทั้งที่คำตอบคือกด Shift ตัวเดียว
และถ้าเขียนเป็น .jsx ธรรมดาที่ไม่มี TypeScript คอยจับให้ มันจะไม่ error เลยด้วยซ้ำ — เบราว์เซอร์จะได้แท็กประหลาดที่ไม่แสดงผลอะไร แล้วคุณจะนั่งงงว่าทำไมหน้าว่าง · TypeScript ช่วยคุณไว้ตรงนี้

✅ กฎที่ปลอดภัยเสมอ — ตั้งชื่อไฟล์ component ให้ตรงกับชื่อ component และขึ้นต้นด้วยตัวพิมพ์ใหญ่ทั้งคู่: PriceTag.tsx ข้างในมี PriceTag
ทำแบบนี้แล้วคุณจะไม่มีวันเจอ TS2339 ตัวนี้ เพราะชื่อที่ import มาก็เป็นตัวใหญ่อยู่แล้วโดยอัตโนมัติ

7 · สรุปเป็นสามประโยค

  React    =  วิธีเขียน UI เป็นชิ้น ๆ ที่ประกอบกันได้     (ไลบรารี)
  Next.js  =  React + routing + rendering + bundling   (เฟรมเวิร์ก)
  component = ฟังก์ชันที่รับ props แล้วคืน JSX          (ไม่ใช่ของใหม่ — มันคือฟังก์ชัน)

บทหน้าเราจะลงมือติดตั้งจริง แล้วกางโครงโฟลเดอร์ที่ create-next-app สร้างให้ดูทีละไฟล์ ว่าอันไหนแตะได้ อันไหนอย่าเพิ่งแตะ

🛠️ ภารกิจ — ลากกรอบก่อนเขียนโค้ด แล้วพิสูจน์ด้วยผลรันจริง

บทนี้ยังไม่ต้องติดตั้งอะไร ภารกิจจึงแบ่งเป็นสองส่วน: ส่วนที่ทำได้เดี๋ยวนี้ กับ ส่วนที่ทำหลังจบบท 70

  1. ทำได้เดี๋ยวนี้ — ฝึกสายตา: เปิดเว็บที่คุณใช้ทุกวัน (Facebook · YouTube · หรือหน้า index ของคอร์สนี้เอง) แล้วเขียนลงกระดาษว่ามันมีกี่ component
    ตอบสามข้อนี้ให้ได้: (ก) อันไหนที่ปรากฏซ้ำมากกว่า 3 ครั้งในหน้าเดียว · (ข) ของซ้ำนั้นอะไรที่ต่างกัน (นั่นคือ props) · (ค) อันไหนที่ปรากฏทุกหน้าไม่ว่าคุณจะกดไปไหน (นั่นคือของที่จะไปอยู่ใน layout — เจอในบท 70)
  2. ทำได้เดี๋ยวนี้ — ตรวจเวอร์ชันเป็น: ถ้าเครื่องคุณมี Node อยู่แล้ว รัน node --version แล้วจดไว้
    นิสัยนี้จะช่วยคุณตลอดคอร์ส — เพราะทุกครั้งที่โค้ดจากอินเทอร์เน็ตพัง คำถามแรกคือ "เวอร์ชันตรงกันไหม" อย่างที่เห็นในหัวข้อ 2
  3. หลังจบบท 70 (ติดตั้งเสร็จแล้ว) — พิสูจน์ตารางในหัวข้อ 1 ด้วยมือตัวเอง:
    ก. สร้าง components/PriceTag.tsx ตามโค้ดในหัวข้อ 3 แล้วเรียกใช้ 3 ครั้ง ด้วยข้อมูลคนละชุด
    ข. ตรวจ type ก่อน: npx tsc --noEmit ต้องได้ผลว่าไม่มี error
    ค. พิสูจน์ว่า routing มาจากโฟลเดอร์จริง: รัน npx next dev แล้ว curl -o /dev/null -w "%{http_code}\n" ยิงไปที่ path ที่มีโฟลเดอร์ กับ path ที่ไม่มี — ต้องได้ 200 กับ 404
    ง. พิสูจน์ว่ามัน render ที่ server จริง: รัน npx next build แล้วเปิด .next/server/app/<ชื่อ route>.html ขึ้นมาอ่าน
    เกณฑ์ผ่าน: คุณต้องเห็นข้อความภาษาไทยของคุณเองอยู่ในไฟล์ .html นั้น ถ้าเห็นแล้ว แปลว่าคุณพิสูจน์แถว "Server rendering" ในตารางหัวข้อ 1 ด้วยตัวเองเรียบร้อย
    จ. ทำให้มันพังเพื่อพิสูจน์ว่าตัวตรวจทำงาน: เปลี่ยน <PriceTag> เป็น <priceTag> หนึ่งที่ แล้วรัน npx tsc --noEmit ซ้ำ — ต้องได้ TS2339 · ถ้าไม่ได้ error แปลว่าคุณตรวจผิดไฟล์ ให้ย้อนไปดูใหม่

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

1. เพื่อนบอกว่า "โปรเจกต์นี้ใช้ Next.js เลยไม่ได้ใช้ React แล้ว" — ควรตอบว่าอะไร?

สไลด์หน้า 6 เขียนไว้ตรง ๆ ว่า built on top of React และ nextjs.org พาดหัวว่า "The React Framework for the Web" — Next.js คือ React ที่มีคนจัดโต๊ะให้ ไม่ใช่ของที่มาแทน
ในโปรเจกต์จริงคุณ import { useState } from 'react' ได้ตามปกติ และ package.json ของ sandbox ที่ใช้ตรวจบทนี้ก็มี react: 19.2.8 อยู่ในรายการ dependency ตรง ๆ
ตัวเลือกที่ 1 ผิดสองชั้น — Next.js ไม่ใช่ภาษา (มันคือ package บน npm) และไม่ได้มาแทน React
ตัวเลือกที่ 3 ผิดตรง "ไม่เกี่ยวกับตอนทำงานจริง" — Next.js ทำงานตอนรันจริงด้วย มันคือตัวที่ render หน้าเว็บฝั่ง server ให้ ไม่ใช่แค่เครื่องมือ build
ตัวเลือกที่ 4 ผิดตรงไวยากรณ์ — โค้ดในบทนี้ทุกก้อนเขียนด้วย JSX ของ React มาตรฐาน

2. ในโปรเจกต์ Next.js ที่ใช้ App Router อยากให้ URL /report เปิดได้ ต้องทำอะไร?

สไลด์หน้า 7 เรียกความสามารถนี้ว่า "Create routes using the file system" — ที่อยู่ของไฟล์คือ URL ไม่มีตารางเส้นทางให้แก้
ในบทนี้ผมสร้างแค่โฟลเดอร์ app/t69/ กับไฟล์ page.tsx ไฟล์เดียว แล้ว curl /t69 ได้ 200 ส่วน curl /t69/settings ได้ 404 เพราะไม่มีโฟลเดอร์ settings — ไม่ได้ประกาศอะไรเพิ่มเลยสักบรรทัด
ตัวเลือกที่ 1 ผิด — next.config.ts ของโปรเจกต์นี้ไม่ได้ตั้งค่าอะไรไว้เลย ทั้งไฟล์มีแค่ const nextConfig: NextConfig = { /* config options here */ }; แล้ว export default ออกไป — ไม่มีตารางเส้นทางสักบรรทัด แต่ /t69 ก็ยังใช้ได้
ตัวเลือกที่ 2 คือท่าของ React เปล่า ๆ ตามคอลัมน์กลางของตารางในหัวข้อ 1 — ถูกสำหรับ React แต่ผิดสำหรับ Next.js
ตัวเลือกที่ 4 ได้ไฟล์ HTML นิ่ง ๆ ที่ไม่ใช่ component และไม่มี React อยู่ในนั้นเลย

3. เอาโค้ดจากสไลด์มาวางแล้วได้ TS2503: Cannot find namespace 'JSX' — ควรทำอะไรก่อน?

error ตัวนี้ผมเจอจริงตอนรัน npx tsc --noEmit กับโค้ดสไลด์หน้า 10 แบบไม่แก้อะไรเลย สาเหตุคือ React 19 ย้ายชื่อ JSX ออกจาก global ไปไว้ใต้ React โค้ดที่เขียนไว้สมัย React 17–18 จึงหาไม่เจอ
แก้ได้ด้วยการเติม import type { JSX } from "react"; หรือลบ : JSX.Element ทิ้งไปเลย
ตัวเลือกที่ 1 คือการปิดตัวช่วย — และทำให้แย่ลงจริง ๆ เพราะกับดักตัวพิมพ์เล็กในหัวข้อ 6 จะเงียบสนิทถ้าไม่มี TypeScript
ตัวเลือกที่ 2 คือกับดักที่กินเวลามากที่สุด — สไลด์กับเครื่องคุณคนละเวอร์ชันกันได้เสมอ
ตัวเลือกที่ 3 เป็นท่าที่คนชอบทำเวลาจนตรอก แต่ที่นี่ node_modules ไม่ได้พัง มันติดตั้งเวอร์ชันที่ถูกต้องตามที่ขอไว้แล้ว — ลบแล้วลงใหม่ก็ได้ error เดิม เสียเวลาเปล่า

4. curl ไปที่หน้า Next.js แล้วเห็นข้อความจริงอยู่ใน HTML ที่ได้กลับมา แปลว่าอะไร?

curl ไม่รัน JavaScript — มันขอไฟล์มาแล้วพิมพ์ตามที่ได้ ตรงไปตรงมา ดังนั้นอะไรที่เห็นใน curl คือสิ่งที่เซิร์ฟเวอร์ส่งมาจริง ก่อน JS ทำงาน
ในบทนี้ผม curl แล้วได้ข้อความ Hello, DevPool ในแท็ก <h1> มาเต็ม ๆ (มี <!-- --> ของ React คั่นอยู่ตามที่เห็นในหัวข้อ 3) และไฟล์ .next/server/app/t69.html ที่ build ทิ้งไว้ก็มีข้อความเดียวกัน
ตัวเลือกที่ 2 ผิดที่ใจกลาง — ถ้า curl รัน JS ได้ การทดสอบนี้ก็จะพิสูจน์อะไรไม่ได้เลย ความที่มันไม่รัน JS นั่นแหละคือเหตุผลที่ใช้มันเป็นหลักฐานได้
ตัวเลือกที่ 3 กลับหัวกลับหาง — HTML ที่ว่างเปล่าคือลักษณะของ React เปล่า ๆ ที่ไม่มี server rendering ส่วนหน้านี้ใช้ React และมีเนื้อหาครบ เพราะ Next.js วาดให้ที่ server
ตัวเลือกที่ 4 ผิด — โค้ดต้นทางคือ <PriceTag> สามตัวที่รับ props คนละชุด แถวที่ออกมาเป็น <p> ก็เพราะ component วาดให้ ไม่มีใครพิมพ์ HTML นั้นด้วยมือ

💬 ติดตรงไหนถามได้ — ถ้ารันตามในบทแล้วได้ผลไม่ตรงกับที่เขียนไว้ ให้ส่งมาทั้งโค้ดที่รัน · คำสั่งที่ใช้ · ข้อความที่ขึ้นเต็ม ๆ และคราวนี้เพิ่มอีกอย่างคือเวอร์ชันของ next กับ react (node -e "console.log(require('react/package.json').version)")
เพราะอย่างที่เห็นในหัวข้อ 2 — ครึ่งหนึ่งของปัญหาในโลก React คือเรื่องเวอร์ชันล้วน ๆ
และคำถามที่คุ้มที่สุดที่ถามได้กับบทนี้คือ "หน้านี้ควรให้ server วาด หรือให้เบราว์เซอร์วาดดี" — มันไม่มีคำตอบเดียว และมันคือคำถามที่คุณจะต้องตอบซ้ำ ๆ ตลอดกลุ่มนี้

อ้างอิง