App Router 구조
App Router의 폴더 세그먼트와 layout·page 파일 규칙을 익히고 서버·클라이언트 컴포넌트의 출발점을 구분합니다.
Next.js 16의 App Router는 파일과 폴더의 위치로 웹 애플리케이션의 라우팅, 레이아웃, 서버 로직을 정의합니다.
2장에서 프로젝트 구조를 간략하게 살펴보았지만, 이 절에서는 App Router의 핵심 원리와 그 구조를 더 깊이 있게 정리하겠습니다.
App Router의 핵심 원리
App Router는 src/app (또는 프로젝트 루트의 app) 디렉터리 내의 파일 시스템을 사용하여 라우트(경로)를 정의합니다.
여기서 가장 중요한 두 가지 규칙이 있습니다.
app디렉터리 안의 일반 폴더는 URL 경로의 한 부분을 나타내는 라우트 세그먼트가 됩니다. 예를 들어,app/dashboard폴더는/dashboard경로의 후보가 됩니다. 다만 실제로 접근 가능한 페이지가 되려면 해당 세그먼트 아래에page.tsx같은 공개 UI 파일이 필요합니다.- 예외도 있습니다.
(marketing)같은 라우트 그룹은 URL에 포함되지 않고,_components같은 private folder는 라우팅에서 제외됩니다.@modal같은 병렬 라우트 슬롯도 URL 세그먼트가 아닙니다.
- 폴더 자체는 UI를 직접 렌더링하지 않습니다. 폴더 안의
page.tsx,layout.tsx와 같은 특정 파일명들이 실제로 브라우저에 표시될 UI를 정의하거나, 해당 라우트에 대한 특별한 동작을 제어합니다.
이 두 가지 규칙을 통해 Next.js는 URL 구조와 UI 역할을 파일 시스템 안에서 함께 표현합니다.
필수 파일: 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가 가장 상위 레이아웃을 정의합니다.각 라우트 트리에는
<html>과<body>를 반환하는 Root Layout이 필요합니다. 라우트 그룹이나[locale]같은 세그먼트 아래에 서로 다른 Root Layout을 두는 고급 구조에서는 루트의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> ); }childrenProp: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 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파일과 달리childrenprop을 받지 않습니다. 오직 자신의 UI만을 렌더링합니다.src/app/page.tsx (루트 페이지) export default function HomePage() { return ( <div> <h1>나 혼자 Next.js!</h1> <p>Next.js 16 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- 자신이 감싸는 하위 컴포넌트의 렌더 오류를 처리합니다. 같은 세그먼트의
layout.tsx와template.tsx는 이 경계 바깥에 있습니다. - React Error Boundary와 유사하게 작동하여 특정 UI 컴포넌트 내부에서 발생하는 자바스크립트 오류를 잡아낼 수 있습니다.
- 오류 경계는 클라이언트 컴포넌트여야 하므로 파일 상단의
"use client"선언이 필요합니다.
- 자신이 감싸는 하위 컴포넌트의 렌더 오류를 처리합니다. 같은 세그먼트의
-
not-found.tsx- 해당 라우트에서 콘텐츠를 찾을 수 없을 때 (예: 404 에러) 보여줄 사용자 정의 UI를 정의합니다.
-
template.tsx- 레이아웃과 유사하지만, 해당 수준의 세그먼트 키가 바뀌면 새 인스턴스로 마운트됩니다. 더 깊은 세그먼트나 검색 매개변수만 바뀌는 모든 이동에서 상위 template가 재마운트되는 것은 아닙니다.
- 애니메이션과 같이 상태를 재설정해야 할 때 유용합니다.
-
default.tsx(병렬 라우트에서 사용)- 전체 페이지를 로드할 때 현재 URL과 맞지 않는 병렬 라우트 슬롯의 활성 상태를 복원할 수 없으면 보여줄 UI를 정의합니다. (고급 주제이므로 나중에 자세히 다룹니다.)
-
route.ts(Route Handler)- 서버 측 HTTP 엔드포인트를 정의합니다.
GET,POST,PUT,DELETE등 HTTP 메서드를 처리하는 함수를 작성합니다. - 같은 라우트 세그먼트에서
page.tsx와route.ts가 동시에 같은 경로를 담당할 수는 없으므로, 화면 라우트와 응답 전용 라우트를 분리해 설계합니다.
- 서버 측 HTTP 엔드포인트를 정의합니다.
-
(folder)(라우트 그룹)- 괄호로 감싼 폴더는 URL 경로에 영향을 주지 않고, 라우트들을 논리적으로 그룹화하거나 레이아웃을 공유할 때 사용합니다. 예를 들어,
app/(marketing)/about/page.tsx는/about경로에 매핑됩니다. - 서로 다른 라우트 그룹이 같은 URL을 만들면 충돌이 발생하므로 그룹 이름은 URL에서 빠진다는 점을 항상 확인해야 합니다.
- 괄호로 감싼 폴더는 URL 경로에 영향을 주지 않고, 라우트들을 논리적으로 그룹화하거나 레이아웃을 공유할 때 사용합니다. 예를 들어,
서버 컴포넌트와 클라이언트 컴포넌트
App Router의 가장 큰 변화 중 하나는 서버 컴포넌트(Server Components)와 클라이언트 컴포넌트(Client Components)의 개념입니다.
page.tsx와 layout.tsx는 기본적으로 서버 컴포넌트입니다. 일반 컴포넌트가 어느 쪽에 포함되는지는 지시어뿐 아니라 누가 가져오는지도 함께 봅니다.
컴포넌트의 경계와 실행 위치의 비교 기준입니다.
| 경계 | 포함되는 코드 | 실행·전달되는 것 |
|---|---|---|
| 서버 컴포넌트 | 기본 page/layout과 서버 쪽에서 가져온 컴포넌트 | 서버에서 데이터와 UI를 구성; 해당 코드 자체는 클라이언트 번들에 넣지 않음 |
| 클라이언트 진입점 | use client 파일과 그 파일이 가져오는 모듈 | 브라우저에서 상태·이벤트 처리; 첫 로드에서는 서버 HTML로 미리 렌더링될 수 있음 |
| 서버에서 넘긴 UI | Client 컴포넌트의 children/props로 넘긴 Server UI | 클라이언트가 직접 import한 모듈과 다름; Server UI는 서버에서 렌더링 |
| 데이터 전달 | 서버에서 Client 컴포넌트로 주는 props | React가 직렬화할 수 있는 값이어야 하며 브라우저에서 읽을 수 있음 |
- 서버 컴포넌트
- 포함되는 코드: 기본 page/layout과 서버 쪽에서 가져온 컴포넌트실행·전달되는 것: 서버에서 데이터와 UI를 구성; 해당 코드 자체는 클라이언트 번들에 넣지 않음
- 클라이언트 진입점
- 포함되는 코드: use client 파일과 그 파일이 가져오는 모듈실행·전달되는 것: 브라우저에서 상태·이벤트 처리; 첫 로드에서는 서버 HTML로 미리 렌더링될 수 있음
- 서버에서 넘긴 UI
- 포함되는 코드: Client 컴포넌트의 children/props로 넘긴 Server UI실행·전달되는 것: 클라이언트가 직접 import한 모듈과 다름; Server UI는 서버에서 렌더링
- 데이터 전달
- 포함되는 코드: 서버에서 Client 컴포넌트로 주는 props실행·전달되는 것: React가 직렬화할 수 있는 값이어야 하며 브라우저에서 읽을 수 있음
DB 접근과 비밀값은 서버에 두되, 렌더된 HTML이나 전달 데이터에 비밀을 포함하지 않습니다. useState·useEffect와 이벤트 핸들러는 클라이언트 경계에서 사용합니다.
실습 재현성을 위해 아래 예시의 http://localhost:4000은 로컬 Mock API 엔드포인트라고 가정합니다.
import Counter from '../components/Counter';
// 데이터 페칭 등 서버에서 처리할 로직 작성 가능
export default async function DashboardPage() {
const data = await fetch('http://localhost:4000/dashboard/data');
const jsonData = await data.json();
return (
<div>
<h1>대시보드 데이터:</h1>
<p>{jsonData.message}</p>
{/* 클라이언트 컴포넌트 사용 */}
<Counter />
</div>
);
}"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>
);
}서버 컴포넌트와 클라이언트 컴포넌트의 개념은 App Router의 핵심이며, 어떤 컴포넌트를 언제 사용해야 하는지에 대한 이해가 프로젝트 구조를 결정합니다.