페이지 라우팅 설정하기
Next.js의 Page Router는 pages 폴더 내의 파일 구조를 기반으로 자동으로 라우팅을 생성하는 시스템이다. 별도의 라우팅 설정 파일 없이 파일과 폴더만으로 URL 경로를 정의할 수 있다.
Page Router vs App Router
Next.js 13부터 App Router가 도입되었지만, Page Router는 여전히 안정적이고 널리 사용된다. App Router는 Server Components를 기본으로 하는 새로운 패러다임이며, Page Router는 기존의 Client-side Navigation을 중심으로 한다.
기본 라우팅
pages 폴더 내의 파일이 자동으로 라우트가 된다. 파일명이 곧 URL 경로다.
pages/
├── index.tsx → /
├── about.tsx → /about
├── contact.tsx → /contact
└── products.tsx → /products
위 구조에서 index.tsx는 루트 경로 /에 매핑되고, about.tsx는 /about 경로에 매핑된다.
// pages/index.tsx
export default function Home() {
return <h1>홈 페이지</h1>;
}
// pages/about.tsx
export default function About() {
return <h1>소개 페이지</h1>;
}
각 파일은 기본적으로 React 컴포넌트를 default export 해야 한다. Next.js는 이 컴포넌트를 자동으로 페이지로 렌더링한다.
중첩 라우팅
폴더를 사용하면 중첩된 경로를 만들 수 있다.
pages/
├── index.tsx → /
├── blog/
│ ├── index.tsx → /blog
│ ├── first-post.tsx → /blog/first-post
│ └── second-post.tsx → /blog/second-post
└── products/
├── index.tsx → /products
└── electronics.tsx → /products/electronics
폴더 내의 index.tsx는 해당 폴더 경로의 기본 페이지가 된다. 예를 들어 blog/index.tsx는 /blog 경로에 매핑된다.
// pages/blog/index.tsx
export default function Blog() {
return <h1>블로그 목록</h1>;
}
// pages/blog/first-post.tsx
export default function FirstPost() {
return <h1>첫 번째 포스트</h1>;
}
이렇게 폴더 구조만으로 직관적인 URL 계층을 구성할 수 있다.
동적 라우팅 (URL Parameter)
동적 경로를 갖는 페이지를 생성하려면 파일명을 대괄호로 감싸면 된다. [id].tsx와 같이 작성하면 해당 부분이 동적 세그먼트가 된다.
pages/
└── posts/
├── index.tsx → /posts
└── [id].tsx → /posts/:id
[id].tsx 파일은 /posts/1, /posts/abc, /posts/hello 등 모든 경로에 매칭된다.
// pages/posts/[id].tsx
import { useRouter } from 'next/router';
export default function Post() {
const router = useRouter();
const { id } = router.query;
return <h1>포스트 ID: {id}</h1>;
}
위 코드에서 useRouter 훅을 통해 URL의 동적 파라미터에 접근할 수 있다. router.query 객체에서 파일명에 정의한 id를 구조 분해 할당으로 가져온다.
사용자가 /posts/123에 접속하면 id는 "123"이 되고, /posts/hello에 접속하면 id는 "hello"가 된다.
여러 동적 세그먼트
여러 동적 세그먼트를 조합할 수도 있다.
pages/
└── posts/
└── [category]/
└── [id].tsx → /posts/:category/:id
// pages/posts/[category]/[id].tsx
import { useRouter } from 'next/router';
export default function Post() {
const router = useRouter();
const { category, id } = router.query;
return (
<div>
<h1>카테고리: {category}</h1>
<p>포스트 ID: {id}</p>
</div>
);
}
/posts/tech/123에 접속하면 category는 "tech", id는 "123"이 된다.
Query String
Query String은 페이지 경로에 영향을 주지 않으며, ? 뒤에 오는 파라미터들을 의미한다. 예를 들어 /search?q=nextjs&sort=latest에서 q와 sort가 Query String이다.
별도의 설정 없이 useRouter 훅으로 접근할 수 있다.
주의: next/router 사용useRouter는 next/router에서 import 해야 한다. next/navigation의 useRouter는 App Router 전용이므로 Page Router에서는 사용할 수 없다.
// pages/search.tsx
import { useRouter } from 'next/router';
export default function Search() {
const router = useRouter();
const { q, sort } = router.query;
return (
<div>
<h1>검색어: {q}</h1>
<p>정렬: {sort}</p>
</div>
);
}
사용자가 /search?q=nextjs&sort=latest에 접속하면 q는 "nextjs", sort는 "latest"가 된다.
Query String은 동적 라우팅 파라미터와 함께 사용할 수도 있다. 모든 파라미터는 router.query에 통합된다.
// pages/posts/[id].tsx
import { useRouter } from 'next/router';
export default function Post() {
const router = useRouter();
const { id, comments } = router.query;
return (
<div>
<h1>포스트 ID: {id}</h1>
{comments && <p>댓글 표시 모드</p>}
</div>
);
}
/posts/123?comments=true에 접속하면 id는 "123", comments는 "true"가 된다.
Catch-all Routes
단일 동적 세그먼트는 한 단계의 경로만 매칭한다. /posts/[id].tsx는 /posts/123은 매칭하지만 /posts/123/comments는 매칭하지 못한다.
여러 단계의 경로를 한 번에 처리하려면 Catch-all Routes를 사용한다. 대괄호 안에 ...을 붙이면 된다.
pages/
└── docs/
└── [...slug].tsx → /docs/* (모든 하위 경로)
[...slug].tsx는 /docs/a, /docs/a/b, /docs/a/b/c 등 모든 하위 경로를 매칭한다.
// pages/docs/[...slug].tsx
import { useRouter } from 'next/router';
export default function Docs() {
const router = useRouter();
const { slug } = router.query;
// slug는 배열로 전달됨
return (
<div>
<h1>문서 경로</h1>
<p>세그먼트: {Array.isArray(slug) ? slug.join(' / ') : slug}</p>
</div>
);
}
위 코드에서 slug는 경로 세그먼트를 배열로 담는다.
/docs/introduction접속 시:slug = ["introduction"]/docs/getting-started/installation접속 시:slug = ["getting-started", "installation"]/docs/api/hooks/useRouter접속 시:slug = ["api", "hooks", "useRouter"]
배열의 각 요소가 경로의 각 세그먼트에 해당한다. 이를 활용해 동적으로 컨텐츠를 렌더링하거나 파일 시스템에서 마크다운 파일을 읽어올 수 있다.
Catch-all Routes의 한계
하지만 [...slug].tsx는 최소 하나의 세그먼트가 필요하다. /docs에는 매칭되지 않고 /docs/something부터 매칭된다.
/docs도 같은 컴포넌트로 처리하고 싶다면 두 가지 방법이 있다.
docs/index.tsx를 별도로 만든다- Optional Catch-all Routes를 사용한다
Optional Catch-all Routes
대괄호를 한 번 더 감싸면 Optional Catch-all Routes가 된다.
pages/
└── docs/
└── [[...slug]].tsx → /docs 및 /docs/* (모든 하위 경로)
[[...slug]].tsx는 /docs도 매칭하고, /docs/a, /docs/a/b 등 모든 하위 경로도 매칭한다.
// pages/docs/[[...slug]].tsx
import { useRouter } from 'next/router';
export default function Docs() {
const router = useRouter();
const { slug } = router.query;
if (!slug) {
// /docs 경로
return <h1>문서 홈</h1>;
}
// /docs/* 경로
return (
<div>
<h1>문서 경로</h1>
<p>세그먼트: {slug.join(' / ')}</p>
</div>
);
}
/docs접속 시:slug = undefined/docs/introduction접속 시:slug = ["introduction"]/docs/api/hooks접속 시:slug = ["api", "hooks"]
이렇게 하면 하나의 컴포넌트로 모든 문서 경로를 처리할 수 있다.
Link 컴포넌트
Next.js에서 페이지 간 이동은 next/link의 Link 컴포넌트를 사용한다. HTML의 <a> 태그와 달리 클라이언트 사이드 네비게이션을 제공한다.
import Link from 'next/link';
export default function Nav() {
return (
<nav>
<Link href="/">홈</Link>
<Link href="/about">소개</Link>
<Link href="/blog">블로그</Link>
</nav>
);
}
Link 컴포넌트를 사용하면 페이지 전체를 새로고침하지 않고 필요한 부분만 업데이트한다. 이를 통해 빠른 페이지 전환과 상태 유지가 가능하다.
동적 경로에 Link 사용
동적 경로는 문자열 보간이나 객체 형태로 전달할 수 있다.
import Link from 'next/link';
export default function PostList() {
const posts = [
{ id: 1, title: '첫 번째 포스트' },
{ id: 2, title: '두 번째 포스트' },
];
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
객체 형태로도 전달할 수 있다.
<Link
href={{
pathname: '/posts/[id]',
query: { id: post.id },
}}
>
{post.title}
</Link>
Prefetching
Link 컴포넌트는 기본적으로 viewport에 보이는 링크를 자동으로 prefetch한다. 사용자가 링크를 클릭하기 전에 미리 페이지를 로드해두기 때문에 즉각적인 페이지 전환이 가능하다.
프로덕션 빌드에서만 동작하며, prefetch={false}로 비활성화할 수 있다.
<Link href="/about" prefetch={false}>
소개
</Link>
useRouter 훅
useRouter 훅은 현재 라우터 정보에 접근하고 프로그래매틱하게 네비게이션을 제어할 수 있게 한다.
import { useRouter } from 'next/router';
export default function Page() {
const router = useRouter();
console.log(router.pathname); // 현재 경로 (예: /posts/[id])
console.log(router.asPath); // 브라우저에 표시되는 전체 경로 (예: /posts/123?sort=latest)
console.log(router.query); // 쿼리 파라미터 객체 (예: { id: '123', sort: 'latest' })
return <div>페이지</div>;
}
주요 속성은 다음과 같다.
pathname: 현재 페이지의 경로 패턴 (동적 세그먼트는 대괄호 형태로 표시됨)asPath: 실제 브라우저에 표시되는 전체 경로query: URL 파라미터와 Query String을 담은 객체route:pathname과 동일
프로그래매틱 네비게이션
Link 컴포넌트 외에 코드로 페이지를 이동할 수 있다. router.push()와 router.replace()를 사용한다.
router.push()
router.push()는 새로운 URL을 브라우저 히스토리에 추가하고 이동한다.
import { useRouter } from 'next/router';
export default function LoginPage() {
const router = useRouter();
const handleLogin = async () => {
// 로그인 로직...
// 로그인 성공 후 대시보드로 이동
router.push('/dashboard');
};
return <button onClick={handleLogin}>로그인</button>;
}
Query String이나 동적 세그먼트도 전달할 수 있다.
// 문자열 형태
router.push('/posts/123?sort=latest');
// 객체 형태
router.push({
pathname: '/posts/[id]',
query: { id: '123', sort: 'latest' },
});
router.replace()
router.replace()는 현재 히스토리를 대체한다. 뒤로가기를 눌렀을 때 이전 페이지로 돌아가지 않는다.
const handleRedirect = () => {
// 히스토리에 추가하지 않고 대체
router.replace('/new-page');
};
로그인 후 리다이렉트처럼 사용자가 뒤로가기로 돌아가지 못하게 하고 싶을 때 유용하다.
기타 메서드
router.back(): 브라우저 히스토리에서 뒤로 이동router.reload(): 현재 페이지 새로고침router.prefetch(url): 특정 페이지를 미리 로드
라우팅 우선순위
여러 라우트 패턴이 동일한 URL에 매칭될 수 있는 경우, Next.js는 다음 우선순위로 라우트를 결정한다.
- 정적 라우트 (우선순위 가장 높음)
- 동적 라우트
- Catch-all 라우트 (우선순위 가장 낮음)
pages/
├── posts/
│ ├── index.tsx (1) /posts
│ ├── create.tsx (2) /posts/create
│ ├── [id].tsx (3) /posts/:id
│ └── [...slug].tsx (4) /posts/*
위 구조에서:
/posts접속 시:index.tsx매칭/posts/create접속 시:create.tsx매칭 (정적 라우트가 우선)/posts/123접속 시:[id].tsx매칭/posts/a/b/c접속 시:[...slug].tsx매칭
create.tsx가 있다면 /posts/create는 [id].tsx보다 우선한다. 정적 라우트가 동적 라우트보다 우선순위가 높기 때문이다.
404 페이지
존재하지 않는 경로에 접속할 때 표시되는 404 페이지를 커스터마이징할 수 있다. pages 폴더 최상위에 404.tsx 파일을 만들면 된다.
// pages/404.tsx
import Link from 'next/link';
export default function Custom404() {
return (
<div>
<h1>404 - 페이지를 찾을 수 없습니다</h1>
<p>요청하신 페이지가 존재하지 않습니다.</p>
<Link href="/">홈으로 돌아가기</Link>
</div>
);
}
빌드 시 자동으로 정적 페이지로 생성되므로 추가 서버 부하 없이 빠르게 제공된다.