안동민 개발노트

안동민 개발노트

App Router 구조페이지 생성레이아웃 컴포넌트페이지 간 링크 생성
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 3장 : App Router 기초
  5. App Router 구조
  1. Next.js
  2. App Router 구조

App Router 구조

App Router의 폴더 세그먼트와 layout·page 파일 규칙을 익히고 서버·클라이언트 컴포넌트의 출발점을 구분합니다.

Next.js 16의 App Router는 파일과 폴더의 위치로 웹 애플리케이션의 라우팅, 레이아웃, 서버 로직을 정의합니다.

2장에서 프로젝트 구조를 간략하게 살펴보았지만, 이 절에서는 App Router의 핵심 원리와 그 구조를 더 깊이 있게 정리하겠습니다.


App Router의 핵심 원리

App Router는 src/app (또는 프로젝트 루트의 app) 디렉터리 내의 파일 시스템을 사용하여 라우트(경로)를 정의합니다.

여기서 가장 중요한 두 가지 규칙이 있습니다.

폴더(Folder)는 라우트 세그먼트(Route Segment)를 만든다.
  • app 디렉터리 안의 일반 폴더는 URL 경로의 한 부분을 나타내는 라우트 세그먼트가 됩니다. 예를 들어, app/dashboard 폴더는 /dashboard 경로의 후보가 됩니다. 다만 실제로 접근 가능한 페이지가 되려면 해당 세그먼트 아래에 page.tsx 같은 공개 UI 파일이 필요합니다.
  • 예외도 있습니다. (marketing) 같은 라우트 그룹은 URL에 포함되지 않고, _components 같은 private folder는 라우팅에서 제외됩니다. @modal 같은 병렬 라우트 슬롯도 URL 세그먼트가 아닙니다.
특정 파일명은 UI를 렌더링하거나 특정 로직을 정의한다.
  • 폴더 자체는 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>
      );
    }
    • 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
    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 (루트 페이지)
    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가 동시에 같은 경로를 담당할 수는 없으므로, 화면 라우트와 응답 전용 라우트를 분리해 설계합니다.
  • (folder) (라우트 그룹)
    • 괄호로 감싼 폴더는 URL 경로에 영향을 주지 않고, 라우트들을 논리적으로 그룹화하거나 레이아웃을 공유할 때 사용합니다. 예를 들어, app/(marketing)/about/page.tsx는 /about 경로에 매핑됩니다.
    • 서로 다른 라우트 그룹이 같은 URL을 만들면 충돌이 발생하므로 그룹 이름은 URL에서 빠진다는 점을 항상 확인해야 합니다.

서버 컴포넌트와 클라이언트 컴포넌트

App Router의 가장 큰 변화 중 하나는 서버 컴포넌트(Server Components)와 클라이언트 컴포넌트(Client Components)의 개념입니다.

page.tsx와 layout.tsx는 기본적으로 서버 컴포넌트입니다. 일반 컴포넌트가 어느 쪽에 포함되는지는 지시어뿐 아니라 누가 가져오는지도 함께 봅니다.

컴포넌트의 경계와 실행 위치

컴포넌트의 경계와 실행 위치의 비교 기준입니다.

컴포넌트의 경계와 실행 위치
경계포함되는 코드실행·전달되는 것
서버 컴포넌트기본 page/layout과 서버 쪽에서 가져온 컴포넌트서버에서 데이터와 UI를 구성; 해당 코드 자체는 클라이언트 번들에 넣지 않음
클라이언트 진입점use client 파일과 그 파일이 가져오는 모듈브라우저에서 상태·이벤트 처리; 첫 로드에서는 서버 HTML로 미리 렌더링될 수 있음
서버에서 넘긴 UIClient 컴포넌트의 children/props로 넘긴 Server UI클라이언트가 직접 import한 모듈과 다름; Server UI는 서버에서 렌더링
데이터 전달서버에서 Client 컴포넌트로 주는 propsReact가 직렬화할 수 있는 값이어야 하며 브라우저에서 읽을 수 있음
서버 컴포넌트
포함되는 코드: 기본 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 엔드포인트라고 가정합니다.

src/app/dashboard/page.tsx (기본적으로 서버 컴포넌트)
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>
  );
}
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>
  );
}

서버 컴포넌트와 클라이언트 컴포넌트의 개념은 App Router의 핵심이며, 어떤 컴포넌트를 언제 사용해야 하는지에 대한 이해가 프로젝트 구조를 결정합니다.

개발 서버 실행 및 기본 설정

이전 페이지

페이지 생성

다음 페이지

이 페이지의 목차

App Router의 핵심 원리필수 파일: layout.tsx와 page.tsxlayout.tsx (공유 레이아웃)page.tsx (페이지 UI)App Router의 파일 컨벤션 (Convention)서버 컴포넌트와 클라이언트 컴포넌트