icon
3장 : App Router 기초

App Router 구조 이해하기

Next.js 15의 App Router는 이전 Pages Router와는 확연히 다른, 혁신적인 접근 방식을 통해 웹 애플리케이션의 라우팅과 컴포넌트 구조를 정의합니다. 2장에서 프로젝트 구조를 간략하게 살펴보았지만, 이 절에서는 App Router의 핵심 원리와 그 구조를 훨씬 더 깊이 있게 파고들어 보겠습니다.


App Router의 핵심 원리

App Router는 src/app (또는 프로젝트 루트의 app) 디렉터리 내의 파일 시스템을 사용하여 라우트(경로)를 정의합니다. 여기서 가장 중요한 두 가지 규칙이 있습니다.

  1. 폴더(Folder)는 라우트 세그먼트(Route Segment)를 정의한다.

    • app 디렉터리 안에 생성되는 모든 폴더는 URL 경로의 한 부분을 나타내는 라우트 세그먼트가 됩니다. 예를 들어, app/dashboard 폴더는 /dashboard 경로를 의미합니다.
  2. 특정 파일명은 UI를 렌더링하거나 특정 로직을 정의한다.

    • 폴더 자체는 UI를 직접 렌더링하지 않습니다. 폴더 안의 page.tsx, layout.tsx와 같은 특정 파일명들이 실제로 브라우저에 표시될 UI를 정의하거나, 해당 라우트에 대한 특별한 동작을 제어합니다.

이 두 가지 규칙을 통해 Next.js는 매우 직관적이면서도 강력한 라우팅 시스템을 구축합니다.


필수 파일: layout.tsxpage.tsx

App Router 기반의 Next.js 애플리케이션에서 가장 기본이 되는 두 가지 파일은 바로 layout.tsxpage.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이 중첩된 형태로 렌더링됩니다. 즉, RootLayoutchildren으로 DashboardLayout이 들어가고, DashboardLayoutchildren으로 실제 페이지 콘텐츠가 들어가는 구조입니다.

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.tsxpage.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 메서드를 처리하는 함수를 작성합니다.
  • (folder) (라우트 그룹)

    • 괄호로 감싼 폴더는 URL 경로에 영향을 주지 않고, 라우트들을 논리적으로 그룹화하거나 레이아웃을 공유할 때 사용합니다. 예를 들어, app/(marketing)/about/page.tsx/about 경로에 매핑됩니다.

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

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

  • 서버 컴포넌트 (기본값)

    • 별도의 지시어("use client")가 없는 모든 컴포넌트는 기본적으로 서버 컴포넌트로 간주됩니다.
    • 서버에서 렌더링되므로, 클라이언트 측 JavaScript 번들에 포함되지 않아 번들 크기를 줄이고 초기 로딩 속도를 향상시킵니다.
    • 데이터베이스 접근이나 API 키와 같은 민감한 정보를 안전하게 다룰 수 있습니다.
    • 클라이언트 측 상호작용(이벤트 핸들러, useState, useEffect 등)은 불가능합니다.
  • 클라이언트 컴포넌트

    • 파일의 맨 위에 "use client" 지시어를 추가하여 명시적으로 클라이언트 컴포넌트임을 선언합니다.
    • 브라우저(클라이언트)에서 렌더링되므로, 사용자 상호작용(클릭 이벤트, 상태 관리)이 필요한 컴포넌트에 사용됩니다.
    • 번들 크기에 영향을 미치며, 서버 컴포넌트 내에서 클라이언트 컴포넌트를 가져와 사용할 수 있습니다.
src/app/dashboard/page.tsx
// 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
// 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의 핵심이며, 어떤 컴포넌트를 언제 사용해야 하는지에 대한 이해는 매우 중요합니다.