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 역할을 파일 시스템 안에서 함께 표현합니다.
폴더 이름만으로 화면이 생기지 않는다. URL에 포함되는 폴더와 제외되는 폴더, 그리고 page/layout 같은 예약 파일을 함께 봐야 한다.
| 구조 | 라우팅 의미 | 화면 생성 조건 | 주의할 예외 |
|---|---|---|---|
| app/dashboard | /dashboard 후보 세그먼트 | page.tsx가 있으면 접근 가능 | 폴더만 있으면 페이지가 아님 |
| page.tsx | 해당 세그먼트의 고유 UI | URL의 최종 화면 | children prop을 받지 않음 |
| layout.tsx | 하위 세그먼트를 감싸는 공유 UI | children 위치가 필요 | 루트 layout은 html/body 필수 |
| (marketing) | URL에 빠지는 라우트 그룹 | 안쪽 page가 실제 경로 생성 | 같은 URL 충돌 주의 |
| _components | 라우팅 제외 private folder | 직접 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)
아래 표는 App Router의 예약 파일이 어떤 UI 또는 서버 책임을 맡는지 정리한 것입니다.
같은 폴더 안의 파일이 URL 도착부터 성공·대기·실패까지 어떤 역할을 맡는지 트리로 읽는다.
- successpage.tsx
URL이 최종적으로 렌더할 화면
- sharedlayout.tsx
하위 route 사이에 유지되는 공통 UI
- pendingloading.tsx
segment가 준비되는 동안의 fallback
- failureerror.tsx
하위 렌더 오류를 잡고 재시도 제공
- missingnot-found.tsx
대상이 없다는 명시적 결과
layout.tsx와 page.tsx 외에도 App Router는 다양한 특수 파일명들을 제공하여 라우트별로 특정 UI나 로직을 정의할 수 있게 합니다.
-
loading.tsx- 해당 라우트 세그먼트의 데이터 로딩이 완료될 때까지 보여줄 로딩 스피너나 플레이스홀더 UI를 정의합니다.
- React의 Suspense와 함께 작동합니다.
-
error.tsx- 해당 라우트 세그먼트에서 에러가 발생했을 때 보여줄 에러 UI를 정의합니다.
- React Error Boundary와 유사하게 작동하여 특정 UI 컴포넌트 내부에서 발생하는 자바스크립트 오류를 잡아낼 수 있습니다.
- 사용자 상호작용으로 복구를 시도할 수 있어 보통 파일 상단에
"use client"를 선언합니다.
-
not-found.tsx- 해당 라우트에서 콘텐츠를 찾을 수 없을 때 (예: 404 에러) 보여줄 사용자 정의 UI를 정의합니다.
-
template.tsx- 레이아웃과 유사하지만, 라우트가 변경될 때마다 새로운 인스턴스가 마운트됩니다.
- 애니메이션과 같이 상태를 재설정해야 할 때 유용합니다.
-
default.tsx(병렬 라우트에서 사용)- 병렬 라우트(Parallel Routes)가 활성화되지 않았을 때 대신 렌더링될 폴백(fallback) 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)의 개념입니다.
-
서버 컴포넌트 (기본값)
- 별도의 지시어(
"use client")가 없는 모든 컴포넌트는 기본적으로 서버 컴포넌트로 간주됩니다. - 서버에서 렌더링되므로, 클라이언트 측 JavaScript 번들에 포함되지 않아 번들 크기를 줄일 수 있습니다.
- 데이터베이스 접근이나 API 키와 같은 민감한 정보를 안전하게 다룰 수 있습니다.
- 클라이언트 측 상호작용(이벤트 핸들러,
useState,useEffect등)은 불가능합니다.
- 별도의 지시어(
-
클라이언트 컴포넌트
- 파일의 맨 위에
"use client"지시어를 추가하여 명시적으로 클라이언트 컴포넌트임을 선언합니다. - 브라우저에서 hydrate되어 상호작용하므로, 클릭 이벤트나 상태 관리가 필요한 컴포넌트에 사용됩니다. 초기 요청에서는 서버에서 HTML로 미리 렌더링될 수 있습니다.
- 번들 크기에 영향을 미치며, 서버 컴포넌트 내에서 클라이언트 컴포넌트를 가져와 사용할 수 있습니다.
- 파일의 맨 위에
실습 재현성을 위해 아래 예시의 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의 핵심이며, 어떤 컴포넌트를 언제 사용해야 하는지에 대한 이해가 프로젝트 구조를 결정합니다.
아래 다이어그램은 요청 URL이 세그먼트 트리, 예약 파일, 서버/클라이언트 경계를 거쳐 실제 렌더 트리로 조립되는 과정을 정리한 것입니다.
App Router의 구조를 이해하려면 파일이 URL을 만드는 과정과 컴포넌트가 서버 또는 브라우저에서 실행되는 경계를 같이 봐야 한다.
| 단계 | 읽는 대상 | 결정되는 것 | 확인할 파일 |
|---|---|---|---|
| 1. URL 매칭 | /dashboard/settings | dashboard, settings 세그먼트 | src/app/dashboard/settings |
| 2. layout 누적 | 상위 layout.tsx | 공통 UI가 바깥에서 안쪽으로 중첩 | root layout, segment layout |
| 3. page 선택 | 최종 page.tsx | 해당 URL의 고유 화면 | settings/page.tsx |
| 4. 상태 파일 적용 | loading/error/not-found | 대기, 오류, 404 UI | 같은 세그먼트의 예약 파일 |
| 5. 경계 분리 | use client 여부 | 서버에서 남을 코드와 브라우저로 갈 코드 | 상호작용 컴포넌트 |
아래 다이어그램은 세그먼트, 예약 파일, 서버/클라이언트 경계를 실제 설계 기준으로 다시 압축해 보여줍니다.
라우팅만 맞아도 상호작용 위치나 데이터 접근 위치가 틀리면 구조가 금방 흐려진다.
| 설계 질문 | 결정 기준 | 잘 맞은 상태 | 틀렸을 때 신호 |
|---|---|---|---|
| URL에 들어갈 폴더인가 | 일반 폴더 또는 route group | 원하는 URL과 폴더가 대응 | 예상과 다른 경로 생성 |
| 화면인가 응답인가 | page.tsx 또는 route.ts | UI와 API 책임 분리 | 같은 경로에서 책임 충돌 |
| 공유 UI가 필요한가 | layout.tsx 위치 | 하위 URL에만 공통 UI 적용 | 원치 않는 페이지까지 영향 |
| 브라우저 상호작용이 필요한가 | use client 선언 | 상태/이벤트가 필요한 곳만 클라이언트 | useState 오류 또는 번들 증가 |
| 데이터는 어디서 읽나 | 서버 컴포넌트 우선 | 비밀값과 DB 접근이 서버에 남음 | API 키 노출 위험 |
아래 다이어그램은 하나의 요청 URL이 App Router 트리에서 어떤 순서로 화면 구조로 조립되는지 단계별로 정리합니다.
문제가 생기면 이 순서대로 보면 어느 파일에서 화면이 달라졌는지 빠르게 좁힐 수 있다.
| 순서 | Next.js가 보는 것 | 결과 | 디버깅 포인트 |
|---|---|---|---|
| 1 | URL 경로 | 세그먼트 후보 선택 | 폴더 이름과 동적 라우트 확인 |
| 2 | 상위 layout.tsx | 공통 UI를 바깥부터 감쌈 | children 누락 여부 확인 |
| 3 | 최종 page.tsx | 고유 화면 렌더링 | page 파일 존재 여부 확인 |
| 4 | loading/error/not-found | 상태별 대체 UI | 해당 세그먼트에 파일이 있는지 확인 |
| 5 | client boundary | 브라우저 상호작용 영역 hydrate | use client 위치 확인 |
서버/클라이언트 경계를 잘못 나누면 번들 크기, 보안, 상호작용 위치가 모두 흔들립니다.
마지막으로 layout.tsx, page.tsx, 서버 컴포넌트 기본값의 관계를 App Router 관점에서 정리합니다.
세 개의 관계를 잡으면 App Router 구조를 읽는 기준이 선명해진다.
| 요소 | 맡는 역할 | 필수 규칙 | 처음 확인할 것 |
|---|---|---|---|
| Root layout | 모든 페이지를 감싸는 최상위 HTML 구조 | html/body와 children 포함 | src/app/layout.tsx |
| Nested layout | 특정 세그먼트 아래 공통 UI | 하위 경로에만 영향 | 어느 폴더에 놓였는지 |
| page.tsx | 해당 URL의 고유 화면 | 접근 가능한 페이지를 만듦 | URL과 폴더의 대응 |
| Server Component | 기본 렌더링 위치 | 비밀값과 데이터 접근을 서버에 유지 | use client가 없는 파일 |
| Client Component | 브라우저 상호작용 | 파일 상단 use client | 상태/이벤트가 필요한지 |