C
Next.js/Basics/Lesson 01

App Router Basics — How It Differs from Pages Router

40 min·theory
This chapter
1/3
TypeScript

App Router Basics — How It Differs from Pages Router

💡 Why Learn This? — The Pages Router Era Is Winding Down

🎯 App Router has been the new standard since Next.js 13 (2022). From versions 14 and 15 onward, the official documentation is written primarily around App Router.
💼 Pages in App Router are **Server Components by default** — the client-side JS bundle starts at zero, and only the parts that need it are switched to `'use client'`.
Boilerplate from Pages Router such as `getServerSideProps`, `getStaticProps`, `_app.tsx`, and `_document.tsx` disappears. Inside a Server Component, a simple `await fetch()` is all you need.
🔗 Shared layout, loading, and error UI are defined at the folder level (`layout.tsx`, `loading.tsx`, `error.tsx`) — naturally inherited by every nested route.
📈 'Next.js App Router experience' has become a standard requirement in real-world job postings.
🏢 실무에서는
codemaster40 is also built on Next.js 15 App Router (the `src/app/` structure). This very learning page is rendered via a dynamic route at `src/app/study/[category]/[slug]/page.tsx`, and data fetching is done with direct imports inside Server Components. Code that would have required funneling data through props with `getServerSideProps` in Pages Router — in App Router, the component function simply becomes `async` and a single `await` inside it is all it takes.

4 Core Concepts of App Router — Folders Are Routes

1. File-Based Routing (Folders as URLs)

The folder structure under src/app/ maps directly to URLs.

code
src/app/
├── page.tsx                       →  /
├── about/page.tsx                 →  /about
├── study/page.tsx                 →  /study
├── study/[category]/page.tsx      →  /study/javascript
└── study/[category]/[slug]/page.tsx →  /study/javascript/promise
  • Only page.tsx becomes a route (files with other names do not).
  • [name] folders are dynamic segments — they come in as parameters.

2. Server Components Are the Default

Components inside App Router are Server Components by default unless explicitly marked otherwise.

tsx
// app/users/[id]/page.tsx — this component runs on the server only
export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await fetch(`https://api/users/${params.id}`).then(r => r.json());
  return <h1>{user.name}</h1>;
}
  • Only the fully rendered HTML is sent to the browser → 0KB of client-side JS bundle.
  • Functions can be async. You can use await directly inside a component.
  • Accessing DBs, the filesystem, and environment variables is fine (none of it leaks to the browser).

3. Client Components Are Explicitly Opted In

When browser features like useState, onClick, or useEffect are needed, add 'use client' at the top of the file:

tsx
'use client';
import { useState } from 'react';

export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}
  • Only this component is sent to the client — the parent Server Component stays on the server.
  • The Server ↔ Client boundary becomes explicit → bundle size stays under control.

4. layout.tsx — Shared UI per Folder

code
app/
├── layout.tsx          ← root (inherited by all pages)
└── study/
    ├── layout.tsx      ← inherited only by /study/*
    └── [category]/page.tsx
tsx
// app/study/layout.tsx
export default function StudyLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <aside>Sidebar</aside>
      <main>{children}</main>
    </div>
  );
}
  • No _app.tsx or _document.tsx.
  • Each nested route naturally inherits the layout.tsx in its own folder.
  • loading.tsx, error.tsx, and not-found.tsx in the same folder are also applied automatically.
💻 🅰️ Legacy Pages Router (Next.js 12 and below)
// ============================================
// ❌ Pages Router — Boilerplate, Client Components by default
// ============================================

// 📁 pages/users/[id].tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';

interface User {
  id: number;
  name: string;
}

// 1. Fetch data on the server in advance — must be separated into a distinct function
export const getServerSideProps: GetServerSideProps<{ user: User }> = async (ctx) => {
  const id = ctx.params?.id as string;
  const res = await fetch(`https://api/users/${id}`);
  const user: User = await res.json();
  return { props: { user } };
};

// 2. The component is a Client Component — renders the received props
export default function UserPage({
  user,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return <h1>{user.name}</h1>;
}

// 📁 pages/_app.tsx — Common shell for all pages
import type { AppProps } from 'next/app';
import Layout from '../components/Layout';

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

// 📁 pages/_document.tsx — Customize <html>, <body>
import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html lang="ko">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

// Disadvantages:
// - Data fetching = separate function (getServerSideProps / getStaticProps)
// - Component remains a 'props container' — cannot use await inside
// - Global files like _app.tsx / _document.tsx are mandatory
// - Page component code is entirely included in the client bundle
💻 🅱️ App Router (Next.js 13+) — Same Screen, Half the Code
// ============================================
// ✅ App Router — Server Component by default, async + await directly
// ============================================

// 📁 app/users/[id]/page.tsx
interface User {
  id: number;
  name: string;
}

// 1. Component is directly async — await inside
export default async function UserPage({
  params,
}: {
  params: { id: string };
}) {
  const res = await fetch(`https://api/users/${params.id}`);
  const user: User = await res.json();
  return <h1>{user.name}</h1>;
}
// 👆 That's it. No getServerSideProps.
//   This component runs only on the server → 0KB client bundle.

// 📁 app/layout.tsx — Root layout
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>{children}</body>
    </html>
  );
}
// 👆 _app.tsx + _document.tsx combined into one file.

// 📁 app/users/layout.tsx — Layout applied only to /users/*
export default function UsersLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <section>
      <nav>User Menu</nav>
      {children}
    </section>
  );
}

// 📁 app/users/[id]/loading.tsx — Automatic Suspense just by placing it in the same folder
export default function Loading() {
  return <p>Loading...</p>;
}

// 📁 app/users/[id]/error.tsx — Error Boundary in the same folder
'use client'; // Error boundary marked as a Client Component
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

// 📁 app/users/[id]/Counter.tsx — Only the interactive parts are Client
'use client';
import { useState } from 'react';

export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>Click: {n}</button>;
}
// 👆 You can just import and embed <Counter /> inside page.tsx (Server).
//   Parent is Server, only child Counter is Client.

// Advantages:
// - Data and rendering in one component — boilerplate disappears
// - Default Server Component → automatic client bundle minimization
// - layout.tsx / loading.tsx / error.tsx naturally inherit by folder unit
// - Direct DB/environment variable access OK (does not leak to the browser)

💡 💡 5 Common Gotchas When Moving from Pages to App

1. 'use client' Goes Only Where Needed — It Does Not Spread Upward
Even if a Server Component imports a Client Component, the parent remains a Server Component. The reverse is not allowed (Client → Server imports are not permitted).

2. async Components Are Only Possible in Server Components
Client Components cannot be async functions — they conflict with useState and useEffect. Keep data fetching on the server side and pass the results down as props.

3. fetch Is Automatically Cached and Deduplicated
Next.js deduplicates fetch calls to the same URL within the same render. You can disable this with cache: 'no-store' or set an ISR interval with next: { revalidate: 60 }.

4. Client-Only Hooks Must Stay in Client Components
useState, useEffect, useContext, usePathname, and useSearchParams are all client-only. Using them in a Server Component will cause a build error.

5. Environment Variables: the NEXT_PUBLIC_ Prefix Rule Still Applies
Server Components can access all env vars. Client Components can only see those prefixed with NEXT_PUBLIC_ — the same rule as in Pages Router.

⚡ Try It Yourself — Next.js Router Simulation

Next.js code cannot run directly in the browser (it requires a server runtime). Instead, try the simulation to see **how App Router maps URLs to folders**.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

In Next.js App Router, where does a component run by default if `'use client'` is not present at the top of the file?
💡 Every component in App Router is a **Server Component by default**. Only the server-rendered HTML is sent to the browser; the component function's own JS is never included in the client bundle. Only when browser-specific APIs such as `useState`, `onClick`, or `useEffect` are needed do you add `'use client'` at the top of the file to convert it to a Client Component.
App Router Basics — Differences from Pages Router - Next.js