인프런 "한 입 크기로 잘라먹는 Next.js" 수강
리액트에서 자주 사용하던 리액트 라우터처럼, 특정 조건을 기준으로 웹 서비스 내 페이지를 분할하고 이동을 처리할 수 있도록 해주는 기능이다.
Next.js에서는
pages폴더 구조를 기반으로 페이지 라우팅을 지원한다.
파일명 뿐만 아니라 폴더명도 인식index.js 파일이 있으면, 해당 폴더명이 라우팅 경로가 되고 index.js의 컴포넌트가 페이지로 렌더링됨
Page Router는 이전 버전에서 주로 사용되던 방식이다.
Next.js(v15)를 설치하면 기본적으로 App Router 구조가 생성되므로, Page Router로 사용하려면 프로젝트 생성 시 옵션을 조정해야 한다.
npx create-next-app@latest my-app
설치 과정에서 TypeScript / ESLint / App Router 사용 여부 등을 선택할 수 있는데, App Router를 No로 설정하면 Page Router 구조를 사용할 수 있다.
Page Router는 Next.js에서 제공하는 전통적인 라우팅 방식pages 폴더 구조와 파일명/폴더명으로 라우팅을 관리Next.js 최신 버전에서도 옵션을 통해 Page Router 프로젝트 생성 가능Next.js의
Page Router는pages폴더 구조에 따라 URL 경로를 자동으로 매핑한다.
예를 들어 search 폴더 안에 index.tsx 파일을 작성하면 /search 경로로 접근할 수 있다.
// pages/search/index.tsx
export default function Page() {
return <h1>Search</h1>;
}
👉 브라우저에서 http://localhost:3000/search로 접속하면 Search라는 텍스트가 표시된다.
search폴더 안에setting.tsx파일을 추가하면/search/setting경로가 자동으로 생성된다.
// pages/search/setting.tsx
export default function Page() {
return <h1>search/setting</h1>;
}
👉 http://localhost:3000/search/setting 경로로 접속 시 아래와 같이 렌더링된다.

추가로 search/setting/index.tsx 형태로 폴더 안에 index.tsx를 두면, /search/setting이 기본 경로로 인식된다. ⚠️ 이때 주의할 점은 index 파일이 항상 우선 적용된다는 점!!!
Page Router에서는useRouter훅을 사용해 URL 쿼리스트링을 쉽게 처리할 수 있다.
// pages/search/index.tsx
import { useRouter } from "next/router";
export default function Page() {
const router = useRouter();
const { q } = router.query;
return <h1>Search {q}</h1>;
}
👉 http://localhost:3000/search?q=이민제로 접근하면 아래처럼 표시된다.

router 객체를 출력해보면, pathname, query, asPath 등 다양한 정보를 확인할 수 있다.

Page Router에서는 대괄호(
[])를 사용해 동적 경로를 정의할 수 있다.
/pages
/index.js // 기본 경로 (/)
/about.js // /about
/posts
┣ index.js // /posts
┗ [id].js // /posts/1, /posts/2 ...
[id].js 파일을 만들면, /posts/1, /posts/2 와 같은 동적 경로를 처리할 수 있다.
// pages/book/[id].tsx
import { useRouter } from "next/router";
export default function Page() {
const router = useRouter();
const { id } = router.query;
return <h1>Book: {id}</h1>;
}
👉 /book/1, /book/13과 같이 경로를 입력하면 각각 Book: 1, Book: 13으로 출력된다.

만약
/book/13/14/15처럼 여러 파라미터가 들어오는 경우, 파일명을[...id].tsx로 작성하면 배열 형태로 받을 수 있다.
// pages/book/[...id].tsx
import { useRouter } from "next/router";
export default function Page() {
const router = useRouter();
const { id } = router.query;
return <h1>Book: {id}</h1>;
}
👉 http://localhost:3000/book/13/14/15로 접근 시 Book: 13,14,15 형태로 출력된다.

/book자체도 유효하게 처리하고 싶다면, 파일명을 대괄호 두 겹으로 감싼[[...id]].tsx를 사용한다.
이 경우 세그먼트가 없으면 query.id가 undefined가 되어 기본 화면을 표시할 수 있다.
// pages/book/[[...id]].tsx
import { useRouter } from "next/router";
export default function Page() {
const { query } = useRouter();
const ids = query.id as string[] | undefined;
// 세그먼트가 없으면 기본 안내, 있으면 값 표시
if (!ids) return <h1>Book:</h1>;
return <h1>Book: {ids.join(", ")}</h1>;
}

/book → Book: (기본 페이지 렌더)/book/1 → Book: 1/book/13/14/15 → Book: 13, 14, 15존재하지 않는 경로를 처리하려면
pages/404.tsx파일을 생성하면 된다.
// pages/404.tsx
export default function Page() {
return <h1>존재하지 않는 페이지입니다.</h1>;
}
👉 http://localhost:3000/book/없는값 같은 잘못된 경로 접근 시 자동으로 404 페이지가 렌더링된다.

pages 폴더 구조를 기반으로 URL이 자동 생성된다.폴더/index.tsx = 해당 경로의 기본 페이지파일명.tsx = 파일명과 동일한 경로 생성[id].tsx = 동적 파라미터 라우팅[...id].tsx = 여러 파라미터 처리 (Catch All Routes)[[...id]].tsx = 세그먼트 0개도 허용(옵셔널)404.tsx = 에러 페이지 처리Next.js에서 페이지 간 이동을 구현할 때 단순히 <a> 태그를 사용할 수도 있지만, 이는 CSR 방식과 충돌하며 불필요한 리렌더링을 발생시킬 수 있다.
그래서 Next.js는 자체적으로 최적화된
Link컴포넌트를 제공하며, 사용법은<a>와 거의 동일하다.
// pages/_app.tsx
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import Link from 'next/link';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<header>
<Link href="/">Index</Link>
<Link href="/search">Search</Link>
<Link href="/book/1">Book/1</Link>
</header>
<Component {...pageProps} />
</>
);
}

👉 위처럼 Link 컴포넌트를 사용하면, 클라이언트 사이드 라우팅 방식으로 부드럽게 페이지 전환이 이루어진다.
경우에 따라서는 단순히 <Link>로 이동하지 않고, 버튼 클릭이나 특정 로직에 따라 프로그래매틱하게 페이지를 이동해야 할 수도 있다. 이때는 useRouter 훅을 활용!
// pages/_app.tsx
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const onClickButton = () => {
router.push('/test'); // 원하는 경로로 이동
};
return (
<>
<header>
<Link href="/">Index</Link>
<Link href="/search">Search</Link>
<Link href="/book/1">Book/1</Link>
<button onClick={onClickButton}>/test 페이지 이동</button>
</header>
<Component {...pageProps} />
</>
);
}

router.push('/path') → 새로운 경로로 이동router.replace('/path') → 현재 기록을 덮어쓰기 (뒤로가기 불가)router.back() → 이전 페이지로 이동<Link href="..."> 컴포넌트를 사용useRouter().push() replace(), back() 등을 활용Next.js Page Router에서는 사용자가 현재 보고 있는 페이지에서 이동할 가능성이 있는 다른 페이지를 미리 불러오는 기능을 제공하며, 이를 프리패칭(Pre-Fetching)이라고 한다.


초기 렌더링 시 모든 자바스크립트 코드를 한 번에 내려주는 것이 아니라, 페이지 단위로 필요한 JS Bundle만 전달된다.
이렇게 하면 서버에서 불필요한 자바스크립트 양을 줄일 수 있고, Hydration 속도도 빨라진다.
⚠️ 하지만 문제는 이후 Client Side Rendering(CSR) 방식으로 이동할 때 발생한다.
import { useEffect } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
export default function App() {
const router = useRouter();
const onClickButton = () => {
router.push("/test");
};
useEffect(() => {
router.prefetch("/test"); // 사전 로드
}, []);
return (
<header>
<Link href="/">Index</Link>
<Link href="/search">Search</Link>
<Link href="/book/1">Book/1</Link>
<div>
<button onClick={onClickButton}>/test 페이지 이동</button>
</div>
</header>
);
}
Next.js의 Link 컴포넌트는 기본적으로 프리패칭이 활성화되어 있는데, 경우에 따라 프리패칭을 막고 싶다면 prefetch={false} 옵션을 사용하면 된다.
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const onClickButton = () => {
router.push('/test');
};
useEffect(() => {
router.prefetch('/test');
}, []);
return (
<>
<header>
<Link href={'/'}>Index</Link>
<Link href={'search'}>Search</Link>
<Link href={'/book/1'}>Book/1</Link>
<div>
<button onClick={onClickButton}>/test 페이지 이동</button>
</div>
</header>
<Component {...pageProps} />
</>
);
}

Link 컴포넌트는 기본적으로 프리패칭을 수행하며, 필요할 경우 prefetch={false}로 끌 수 있다.router.prefetch()를 이용하면 특정 경로를 직접 프리패칭할 수도 있다.