App Router 구조 이해하기
Next.js 15의 App Router는 이전 Pages Router와는 확연히 다른, 혁신적인 접근 방식을 통해 웹 애플리케이션의 라우팅과 컴포넌트 구조를 정의합니다. 2장에서 프로젝트 구조를 간략하게 살펴보았지만, 이 절에서는 App Router의 핵심 원리와 그 구조를 훨씬 더 깊이 있게 파고들어 보겠습니다.
App Router의 핵심 원리
App Router는 src/app
(또는 프로젝트 루트의 app
) 디렉터리 내의 파일 시스템을 사용하여 라우트(경로)를 정의합니다. 여기서 가장 중요한 두 가지 규칙이 있습니다.
-
폴더(Folder)는 라우트 세그먼트(Route Segment)를 정의한다.
app
디렉터리 안에 생성되는 모든 폴더는 URL 경로의 한 부분을 나타내는 라우트 세그먼트가 됩니다. 예를 들어,app/dashboard
폴더는/dashboard
경로를 의미합니다.
-
특정 파일명은 UI를 렌더링하거나 특정 로직을 정의한다.
- 폴더 자체는 UI를 직접 렌더링하지 않습니다. 폴더 안의
page.tsx
,layout.tsx
와 같은 특정 파일명들이 실제로 브라우저에 표시될 UI를 정의하거나, 해당 라우트에 대한 특별한 동작을 제어합니다.
- 폴더 자체는 UI를 직접 렌더링하지 않습니다. 폴더 안의
이 두 가지 규칙을 통해 Next.js는 매우 직관적이면서도 강력한 라우팅 시스템을 구축합니다.
필수 파일: layout.tsx
와 page.tsx
App Router 기반의 Next.js 애플리케이션에서 가장 기본이 되는 두 가지 파일은 바로 layout.tsx
와 page.tsx
입니다.
layout.tsx
(공유 레이아웃)
layout.tsx
파일은 해당 폴더와 그 하위 폴더의 모든 라우트 세그먼트에 적용되는 공유 UI(Shared UI) 를 정의합니다.
-
최상위
layout.tsx
(Root Layout):src/app/layout.tsx
파일은 애플리케이션의 가장 상위 레이아웃을 정의합니다. 이곳은 모든 Next.js 애플리케이션에서 필수적으로 존재해야 합니다.src/app/layout.tsx // src/app/layout.tsx import './globals.css'; // 전역 스타일 임포트 export default function RootLayout({ children, // 필수 prop: 중첩된 라우트 세그먼트 또는 페이지가 여기에 렌더링됨 }: { children: React.ReactNode; }) { return ( <html lang="ko"> <body>{children}</body> </html> ); }
children
Prop:layout.tsx
컴포넌트는 반드시children
이라는 prop을 받아야 합니다. 이children
은 해당 레이아웃이 감싸는 하위 라우트 세그먼트 또는page.tsx
파일의 내용이 렌더링될 위치를 나타냅니다.<html lang="ko">
와<body>
태그: 루트 레이아웃은 반드시<html>
과<body>
태그를 포함해야 합니다.
-
중첩 레이아웃 (Nested Layouts):
app
디렉터리 내의 어떤 폴더에서도layout.tsx
파일을 생성할 수 있습니다. 예를 들어,src/app/dashboard/layout.tsx
를 만들면, 이 레이아웃은/dashboard
경로와 그 하위 모든 경로(예:/dashboard/settings
)에 적용됩니다.src/app/dashboard/layout.tsx // src/app/dashboard/layout.tsx import Sidebar from '../../components/Sidebar'; // 가정: 사이드바 컴포넌트 export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="flex"> <Sidebar /> <main className="flex-1">{children}</main> </div> ); }
이 경우,
/dashboard
및 그 하위 페이지들은RootLayout
안에DashboardLayout
이 중첩된 형태로 렌더링됩니다. 즉,RootLayout
의children
으로DashboardLayout
이 들어가고,DashboardLayout
의children
으로 실제 페이지 콘텐츠가 들어가는 구조입니다.
page.tsx
(페이지 UI)
page.tsx
파일은 특정 라우트 세그먼트의 고유한 UI(Unique UI) 를 렌더링합니다.
-
라우트의 최종 UI: 폴더 안에
page.tsx
파일이 있어야만 해당 폴더 경로가 접근 가능한 페이지(URL)가 됩니다. -
단독 렌더링:
page.tsx
파일은layout.tsx
파일과 달리children
prop을 받지 않습니다. 오직 자신의 UI만을 렌더링합니다.src/app/page.tsx // src/app/page.tsx (루트 페이지) export default function HomePage() { return ( <div> <h1>나 혼자 Next.js!</h1> <p>Next.js 15 App Router와 함께하는 웹 개발 여정</p> </div> ); }
// src/app/dashboard/page.tsx (대시보드 페이지) export default function DashboardPage() { return ( <div> <h2>환영합니다, 대시보드입니다!</h2> <p>여기에 대시보드 콘텐츠가 표시됩니다.</p> </div> ); }
App Router의 파일 컨벤션 (Convention)
layout.tsx
와 page.tsx
외에도 App Router는 다양한 특수 파일명들을 제공하여 라우트별로 특정 UI나 로직을 정의할 수 있게 합니다.
-
loading.tsx
- 해당 라우트 세그먼트의 데이터 로딩이 완료될 때까지 보여줄 로딩 스피너나 플레이스홀더 UI를 정의합니다.
- React의 Suspense와 함께 작동합니다.
-
error.tsx
- 해당 라우트 세그먼트에서 에러가 발생했을 때 보여줄 에러 UI를 정의합니다.
- React Error Boundary와 유사하게 작동하여 특정 UI 컴포넌트 내부에서 발생하는 자바스크립트 오류를 잡아낼 수 있습니다.
-
not-found.tsx
- 해당 라우트에서 콘텐츠를 찾을 수 없을 때 (예: 404 에러) 보여줄 사용자 정의 UI를 정의합니다.
-
template.tsx
- 레이아웃과 유사하지만, 라우트가 변경될 때마다 새로운 인스턴스가 마운트됩니다.
- 애니메이션과 같이 상태를 재설정해야 할 때 유용합니다.
-
default.tsx
(병렬 라우트에서 사용)- 병렬 라우트(Parallel Routes)가 활성화되지 않았을 때 대신 렌더링될 폴백(fallback) UI를 정의합니다. (고급 주제이므로 나중에 자세히 다룹니다.)
-
route.ts
(API 라우트)- API 엔드포인트를 정의합니다.
GET
,POST
,PUT
,DELETE
등 HTTP 메서드를 처리하는 함수를 작성합니다.
- API 엔드포인트를 정의합니다.
-
(folder)
(라우트 그룹)- 괄호로 감싼 폴더는 URL 경로에 영향을 주지 않고, 라우트들을 논리적으로 그룹화하거나 레이아웃을 공유할 때 사용합니다. 예를 들어,
app/(marketing)/about/page.tsx
는/about
경로에 매핑됩니다.
- 괄호로 감싼 폴더는 URL 경로에 영향을 주지 않고, 라우트들을 논리적으로 그룹화하거나 레이아웃을 공유할 때 사용합니다. 예를 들어,
서버 컴포넌트와 클라이언트 컴포넌트
App Router의 가장 큰 변화 중 하나는 서버 컴포넌트(Server Components) 와 클라이언트 컴포넌트(Client Components) 의 개념입니다.
-
서버 컴포넌트 (기본값)
- 별도의 지시어(
"use client"
)가 없는 모든 컴포넌트는 기본적으로 서버 컴포넌트로 간주됩니다. - 서버에서 렌더링되므로, 클라이언트 측 JavaScript 번들에 포함되지 않아 번들 크기를 줄이고 초기 로딩 속도를 향상시킵니다.
- 데이터베이스 접근이나 API 키와 같은 민감한 정보를 안전하게 다룰 수 있습니다.
- 클라이언트 측 상호작용(이벤트 핸들러,
useState
,useEffect
등)은 불가능합니다.
- 별도의 지시어(
-
클라이언트 컴포넌트
- 파일의 맨 위에
"use client"
지시어를 추가하여 명시적으로 클라이언트 컴포넌트임을 선언합니다. - 브라우저(클라이언트)에서 렌더링되므로, 사용자 상호작용(클릭 이벤트, 상태 관리)이 필요한 컴포넌트에 사용됩니다.
- 번들 크기에 영향을 미치며, 서버 컴포넌트 내에서 클라이언트 컴포넌트를 가져와 사용할 수 있습니다.
- 파일의 맨 위에
// src/app/dashboard/page.tsx (기본적으로 서버 컴포넌트)
// 데이터 페칭 등 서버에서 처리할 로직 작성 가능
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard/data');
const jsonData = await data.json();
return (
<div>
<h1>대시보드 데이터:</h1>
<p>{jsonData.message}</p>
{/* 클라이언트 컴포넌트 사용 */}
<Counter />
</div>
);
}
// src/app/components/Counter.tsx (클라이언트 컴포넌트)
"use client"; // 이 지시어가 있으면 클라이언트 컴포넌트로 동작
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
서버 컴포넌트와 클라이언트 컴포넌트의 개념은 Next.js 15의 핵심이며, 어떤 컴포넌트를 언제 사용해야 하는지에 대한 이해는 매우 중요합니다.