Next.js를 처음 공부할 때 가장 헷갈리는 부분 중 하나가 라우팅 구조입니다.
예전에는 pages 디렉터리를 기준으로 페이지를 만들었지만, Next.js 13 이후부터는 app 디렉터리를 사용하는 App Router 방식이 등장했습니다.
App Router는 단순히 폴더 이름으로 URL을 만드는 것에서 끝나지 않습니다.
페이지, 레이아웃, 로딩 화면, 에러 화면, API 요청 처리까지 하나의 구조 안에서 관리할 수 있도록 설계되어 있습니다.
1. App Router란?
App Router는 Next.js의 새로운 라우팅 방식입니다.
기존 Pages Router는 pages 폴더 안에 파일을 만들면 URL이 생성되는 구조였습니다.
pages/
index.tsx
about.tsx
board/
index.tsx
[id].tsx
반면 App Router는 app 폴더를 기준으로 라우트를 구성합니다.
app/
page.tsx
about/
page.tsx
board/
page.tsx
[id]/
page.tsx
즉, App Router에서는 폴더가 URL 경로가 되고, 각 경로의 실제 화면은 page.tsx 파일이 담당합니다.
2. 기본 폴더 구조
Next.js App Router 프로젝트의 기본 구조는 보통 다음과 같습니다.
src/
app/
layout.tsx
page.tsx
globals.css
components/
lib/
types/
public/
각 폴더의 역할은 다음과 같습니다.
app/ 페이지와 라우팅을 관리하는 핵심 폴더
components/ 공통 컴포넌트 관리
lib/ API, DB, 유틸 함수 관리
types/ TypeScript 타입 관리
public/ 이미지, 정적 파일 관리
여기서 가장 중요한 폴더는 app입니다.
App Router는 app 폴더 안에서 URL, 화면, 레이아웃, 로딩, 에러 처리를 구성합니다.
3. page.tsx
page.tsx는 실제 페이지 화면을 담당하는 파일입니다.
예를 들어 다음과 같은 구조가 있다고 가정해보겠습니다.
app/
page.tsx
about/
page.tsx
board/
page.tsx
이 구조는 다음 URL과 연결됩니다.
app/page.tsx → /
app/about/page.tsx → /about
app/board/page.tsx → /board
예시 코드는 다음과 같습니다.
// app/about/page.tsx
export default function AboutPage() {
return (
<main>
<h1>소개 페이지</h1>
<p>이 페이지는 /about 경로에서 보여집니다.</p>
</main>
);
}
App Router에서는 폴더 이름이 URL 경로가 되고, 그 안의 page.tsx가 화면이 됩니다.
4. layout.tsx
layout.tsx는 여러 페이지에서 공통으로 사용하는 레이아웃을 담당합니다.
예를 들어 모든 페이지에 공통 Header와 Footer를 넣고 싶다면 app/layout.tsx를 사용합니다.
// app/layout.tsx
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<header>공통 헤더</header>
{children}
<footer>공통 푸터</footer>
</body>
</html>
);
}
여기서 children에는 현재 URL에 해당하는 페이지가 들어갑니다.
예를 들어 /about으로 접속하면 구조는 다음과 같이 렌더링됩니다.
RootLayout
└── AboutPage
즉, layout.tsx는 페이지를 감싸는 공통 틀입니다.
5. 중첩 Layout 구조
App Router의 장점 중 하나는 중첩 레이아웃을 쉽게 만들 수 있다는 점입니다.
예를 들어 관리자 페이지에만 별도의 사이드바를 붙이고 싶다면 다음처럼 구성할 수 있습니다.
app/
layout.tsx
page.tsx
admin/
layout.tsx
page.tsx
users/
page.tsx
app/layout.tsx는 전체 페이지에 적용되고,app/admin/layout.tsx는 /admin 하위 페이지에만 적용됩니다.
// app/admin/layout.tsx
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="admin-layout">
<aside>관리자 메뉴</aside>
<section>{children}</section>
</div>
);
}
이 경우 /admin/users 페이지는 다음 구조로 렌더링됩니다.
RootLayout
└── AdminLayout
└── AdminUsersPage
이처럼 App Router는 페이지 단위뿐 아니라 경로 단위로 레이아웃을 분리할 수 있습니다.
6. 동적 라우팅
게시글 상세 페이지처럼 URL의 일부가 계속 바뀌는 경우가 있습니다.
예를 들어 다음과 같은 URL이 있다고 가정해보겠습니다.
/board/1
/board/2
/board/100
이런 경우에는 대괄호를 사용해 동적 라우트를 만들 수 있습니다.
app/
board/
[id]/
page.tsx
코드는 다음과 같이 작성할 수 있습니다.
// app/board/[id]/page.tsx
type Props = {
params: {
id: string;
};
};
export default function BoardDetailPage({ params }: Props) {
return (
<main>
<h1>게시글 상세 페이지</h1>
<p>게시글 ID: {params.id}</p>
</main>
);
}
/board/10으로 접속하면 params.id에는 "10"이 들어옵니다.
동적 라우팅은 게시판 상세, 상품 상세, 사용자 프로필 페이지 등에 자주 사용됩니다.
7. loading.tsx
loading.tsx는 페이지가 로딩 중일 때 보여줄 UI를 담당합니다.
app/
board/
loading.tsx
page.tsx
예시는 다음과 같습니다.
// app/board/loading.tsx
export default function Loading() {
return <div>게시글 목록을 불러오는 중입니다...</div>;
}
board/page.tsx에서 데이터를 가져오는 시간이 오래 걸리면, 사용자는 빈 화면 대신 로딩 화면을 보게 됩니다.
실무에서는 스켈레톤 UI를 넣는 경우가 많습니다.
export default function Loading() {
return (
<div>
<div className="h-6 w-40 bg-gray-200 animate-pulse" />
<div className="mt-4 h-4 w-full bg-gray-200 animate-pulse" />
<div className="mt-2 h-4 w-3/4 bg-gray-200 animate-pulse" />
</div>
);
}
8. error.tsx
error.tsx는 특정 경로에서 에러가 발생했을 때 보여줄 UI를 담당합니다.
app/
board/
error.tsx
page.tsx
예시는 다음과 같습니다.
'use client';
// app/board/error.tsx
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>문제가 발생했습니다.</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>다시 시도</button>
</div>
);
}
주의할 점은 error.tsx는 Client Component로 작성해야 한다는 점입니다.
그래서 파일 상단에 'use client'를 추가합니다.
reset() 함수는 에러가 발생한 페이지를 다시 렌더링할 때 사용합니다.
9. not-found.tsx
존재하지 않는 페이지나 데이터를 찾을 수 없는 경우에는 not-found.tsx를 사용할 수 있습니다.
app/
not-found.tsx
// app/not-found.tsx
export default function NotFound() {
return (
<main>
<h1>페이지를 찾을 수 없습니다.</h1>
<p>요청하신 페이지가 존재하지 않습니다.</p>
</main>
);
}
게시글 상세 페이지에서 특정 게시글이 없을 때도 사용할 수 있습니다.
import { notFound } from 'next/navigation';
type Props = {
params: {
id: string;
};
};
export default async function BoardDetailPage({ params }: Props) {
const post = null;
if (!post) {
notFound();
}
return <div>{post.title}</div>;
}
이렇게 하면 데이터가 없을 때 Next.js가 자동으로 not-found.tsx 화면을 보여줍니다.
10. Route Handler
App Router에서는 route.ts 파일을 사용해서 API를 만들 수 있습니다.
예를 들어 /api/hello API를 만들고 싶다면 다음과 같이 구성합니다.
app/
api/
hello/
route.ts
// app/api/hello/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({
message: 'Hello Next.js App Router',
});
}
브라우저에서 /api/hello로 접속하면 다음과 같은 JSON 응답을 받을 수 있습니다.
{
"message": "Hello Next.js App Router"
}
POST 요청도 만들 수 있습니다.
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({
message: '게시글이 등록되었습니다.',
data: body,
});
}
Route Handler는 간단한 API, 인증 처리, 외부 API 프록시, 서버 로직 처리 등에 사용할 수 있습니다.
11. Server Component와 Client Component
App Router에서는 기본적으로 컴포넌트가 Server Component로 동작합니다.
Server Component는 서버에서 렌더링되며, 브라우저에 전달되는 JavaScript 양을 줄일 수 있습니다.
// 기본적으로 Server Component
export default async function PostPage() {
const posts = await fetch('https://example.com/api/posts').then((res) =>
res.json()
);
return (
<main>
<h1>게시글 목록</h1>
{posts.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</main>
);
}
하지만 useState, useEffect, onClick 같은 브라우저 기능이 필요하다면 Client Component를 사용해야 합니다.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
클릭 수: {count}
</button>
);
}
정리하면 다음과 같습니다.
Server Component
- 기본값
- 서버에서 실행
- DB 조회, API 호출에 적합
- useState, useEffect 사용 불가
Client Component
- 'use client' 필요
- 브라우저에서 실행
- 클릭 이벤트, 상태 관리에 적합
- useState, useEffect 사용 가능
실무에서는 가능한 Server Component를 기본으로 사용하고, 상호작용이 필요한 부분만 Client Component로 분리하는 것이 좋습니다.
12. 실무형 App Router 예시 구조
게시판 프로젝트를 만든다면 다음과 같은 구조로 시작할 수 있습니다.
src/
app/
layout.tsx
page.tsx
board/
page.tsx
loading.tsx
error.tsx
[id]/
page.tsx
admin/
layout.tsx
page.tsx
posts/
page.tsx
api/
posts/
route.ts
posts/
[id]/
route.ts
components/
Header.tsx
Footer.tsx
PostCard.tsx
lib/
api.ts
db.ts
types/
post.ts
위 구조를 URL로 보면 다음과 같습니다.
/ 메인 페이지
/board 게시글 목록
/board/1 게시글 상세
/admin 관리자 메인
/admin/posts 관리자 게시글 관리
/api/posts 게시글 API
/api/posts/1 게시글 상세 API
이처럼 App Router는 화면과 API를 모두 app 폴더 안에서 관리할 수 있습니다.
13. App Router를 사용할 때 자주 하는 실수
1. page.tsx 없이 폴더만 만드는 경우
app/
about/
이렇게 폴더만 만들면 /about 페이지가 생성되지 않습니다.
반드시 다음처럼 page.tsx가 있어야 합니다.
app/
about/
page.tsx
2. Client Component가 필요한데 'use client'를 빼는 경우
다음 코드는 에러가 발생합니다.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>클릭</button>;
}
useState를 사용하려면 파일 상단에 'use client'를 추가해야 합니다.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>클릭</button>;
}
3. layout.tsx와 page.tsx의 역할을 혼동하는 경우
layout.tsx는 공통 구조를 담당하고,page.tsx는 실제 페이지 내용을 담당합니다.
layout.tsx → Header, Footer, Sidebar 같은 공통 영역
page.tsx → 현재 URL에서 보여줄 실제 화면
정리하자면 Next.js App Router는 app 폴더를 기준으로 라우팅을 구성하는 방식입니다.
핵심 파일은 다음과 같습니다.
page.tsx 실제 페이지 화면
layout.tsx 공통 레이아웃
loading.tsx 로딩 화면
error.tsx 에러 화면
not-found.tsx 404 화면
route.ts API 요청 처리
App Router를 이해할 때 가장 중요한 기준은 다음 세 가지입니다.
첫째, 폴더가 URL 경로가 됩니다.
둘째, page.tsx가 실제 화면이 됩니다.
셋째, layout.tsx는 하위 페이지를 감싸는 공통 구조입니다.
이 구조만 이해해도 Next.js 프로젝트의 전체 흐름을 훨씬 쉽게 파악할 수 있습니다.
처음에는 단순한 게시판 구조로 연습하는 것이 좋습니다.
/board
/board/[id]
/admin/posts
/api/posts
이 정도만 직접 만들어봐도 App Router의 기본 개념을 충분히 익힐 수 있습니다.