안동민 개발노트 아이콘

안동민 개발노트

3장 : App Router 기초

App Router 구조 이해하기

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.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
    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이 중첩된 형태로 렌더링됩니다. 즉, 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 (루트 페이지)
    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 구조 이해하기에서 라우팅, 렌더링 경계, 배포 기준을 정리한 것입니다.

layout.tsxpage.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.tsxroute.ts가 동시에 같은 경로를 담당할 수는 없으므로, 화면 라우트와 응답 전용 라우트를 분리해 설계합니다.
  • (folder) (라우트 그룹)
    • 괄호로 감싼 폴더는 URL 경로에 영향을 주지 않고, 라우트들을 논리적으로 그룹화하거나 레이아웃을 공유할 때 사용합니다. 예를 들어, app/(marketing)/about/page.tsx/about 경로에 매핑됩니다.
    • 서로 다른 라우트 그룹이 같은 URL을 만들면 충돌이 발생하므로 그룹 이름은 URL에서 빠진다는 점을 항상 확인해야 합니다.

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

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

  • 서버 컴포넌트 (기본값)
    • 별도의 지시어("use client")가 없는 모든 컴포넌트는 기본적으로 서버 컴포넌트로 간주됩니다.
    • 서버에서 렌더링되므로, 클라이언트 측 JavaScript 번들에 포함되지 않아 번들 크기를 줄일 수 있습니다.
    • 데이터베이스 접근이나 API 키와 같은 민감한 정보를 안전하게 다룰 수 있습니다.
    • 클라이언트 측 상호작용(이벤트 핸들러, useState, useEffect 등)은 불가능합니다.
  • 클라이언트 컴포넌트
    • 파일의 맨 위에 "use client" 지시어를 추가하여 명시적으로 클라이언트 컴포넌트임을 선언합니다.
    • 브라우저에서 hydrate되어 상호작용하므로, 클릭 이벤트나 상태 관리가 필요한 컴포넌트에 사용됩니다. 초기 요청에서는 서버에서 HTML로 미리 렌더링될 수 있습니다.
    • 번들 크기에 영향을 미치며, 서버 컴포넌트 내에서 클라이언트 컴포넌트를 가져와 사용할 수 있습니다.

실습 재현성을 위해 아래 예시의 http://localhost:4000은 로컬 Mock API 엔드포인트라고 가정합니다.

src/app/dashboard/page.tsx (기본적으로 서버 컴포넌트)
// 데이터 페칭 등 서버에서 처리할 로직 작성 가능
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의 핵심이며, 어떤 컴포넌트를 언제 사용해야 하는지에 대한 이해가 프로젝트 구조를 결정합니다.

아래 다이어그램은 요청 URL이 세그먼트 트리, 예약 파일, 서버/클라이언트 경계를 거쳐 실제 렌더 트리로 조립되는 과정을 정리한 것입니다.

아래 다이어그램은 세그먼트, 예약 파일, 서버/클라이언트 경계를 실제 설계 기준으로 다시 압축해 보여줍니다.

아래 다이어그램은 하나의 요청 URL이 App Router 트리에서 어떤 순서로 화면 구조로 조립되는지 단계별로 정리합니다.

App Router 구조 이해하기에서는 라우팅, 렌더링, 데이터 경계가 실제 서비스 품질에 어떤 순서로 영향을 주는지 점검합니다.

마지막으로 layout.tsx, page.tsx, 서버 컴포넌트 기본값의 관계를 App Router 관점에서 정리합니다.