본문으로 건너뛰기

안동민 개발노트

본문 시작

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 역할을 파일 시스템 안에서 함께 표현합니다.

App Router는 폴더를 URL 후보로 읽고 예약 파일이 있을 때 화면과 동작을 만든다

폴더 이름만으로 화면이 생기지 않는다. URL에 포함되는 폴더와 제외되는 폴더, 그리고 page/layout 같은 예약 파일을 함께 봐야 한다.

구조라우팅 의미화면 생성 조건주의할 예외
app/dashboard/dashboard 후보 세그먼트page.tsx가 있으면 접근 가능폴더만 있으면 페이지가 아님
page.tsx해당 세그먼트의 고유 UIURL의 최종 화면children prop을 받지 않음
layout.tsx하위 세그먼트를 감싸는 공유 UIchildren 위치가 필요루트 layout은 html/body 필수
(marketing)URL에 빠지는 라우트 그룹안쪽 page가 실제 경로 생성같은 URL 충돌 주의
_components라우팅 제외 private folder직접 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가 가장 상위 레이아웃을 정의합니다.

    각 라우트 트리에는 <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이 중첩된 형태로 렌더링됩니다.

    즉, 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의 예약 파일이 어떤 UI 또는 서버 책임을 맡는지 정리한 것입니다.

App Router 예약 파일은 route의 상태별 UI를 나눈다

같은 폴더 안의 파일이 URL 도착부터 성공·대기·실패까지 어떤 역할을 맡는지 트리로 읽는다.

  1. success
    page.tsx

    URL이 최종적으로 렌더할 화면

  2. shared
    layout.tsx

    하위 route 사이에 유지되는 공통 UI

  3. pending
    loading.tsx

    segment가 준비되는 동안의 fallback

  4. failure
    error.tsx

    하위 렌더 오류를 잡고 재시도 제공

  5. missing
    not-found.tsx

    대상이 없다는 명시적 결과

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 (기본적으로 서버 컴포넌트)
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의 핵심이며, 어떤 컴포넌트를 언제 사용해야 하는지에 대한 이해가 프로젝트 구조를 결정합니다.

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

요청 URL은 세그먼트, layout, page, 서버/클라이언트 경계를 거쳐 렌더 트리로 조립된다

App Router의 구조를 이해하려면 파일이 URL을 만드는 과정과 컴포넌트가 서버 또는 브라우저에서 실행되는 경계를 같이 봐야 한다.

단계읽는 대상결정되는 것확인할 파일
1. URL 매칭/dashboard/settingsdashboard, 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 여부서버에서 남을 코드와 브라우저로 갈 코드상호작용 컴포넌트

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

세그먼트, 예약 파일, 서버/클라이언트 경계를 함께 정해야 App Router 구조가 흔들리지 않는다

라우팅만 맞아도 상호작용 위치나 데이터 접근 위치가 틀리면 구조가 금방 흐려진다.

설계 질문결정 기준잘 맞은 상태틀렸을 때 신호
URL에 들어갈 폴더인가일반 폴더 또는 route group원하는 URL과 폴더가 대응예상과 다른 경로 생성
화면인가 응답인가page.tsx 또는 route.tsUI와 API 책임 분리같은 경로에서 책임 충돌
공유 UI가 필요한가layout.tsx 위치하위 URL에만 공통 UI 적용원치 않는 페이지까지 영향
브라우저 상호작용이 필요한가use client 선언상태/이벤트가 필요한 곳만 클라이언트useState 오류 또는 번들 증가
데이터는 어디서 읽나서버 컴포넌트 우선비밀값과 DB 접근이 서버에 남음API 키 노출 위험

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

하나의 URL은 세그먼트 매칭, layout 중첩, page 렌더링, 상태 UI 적용 순서로 화면이 된다

문제가 생기면 이 순서대로 보면 어느 파일에서 화면이 달라졌는지 빠르게 좁힐 수 있다.

순서Next.js가 보는 것결과디버깅 포인트
1URL 경로세그먼트 후보 선택폴더 이름과 동적 라우트 확인
2상위 layout.tsx공통 UI를 바깥부터 감쌈children 누락 여부 확인
3최종 page.tsx고유 화면 렌더링page 파일 존재 여부 확인
4loading/error/not-found상태별 대체 UI해당 세그먼트에 파일이 있는지 확인
5client boundary브라우저 상호작용 영역 hydrateuse client 위치 확인

서버/클라이언트 경계를 잘못 나누면 번들 크기, 보안, 상호작용 위치가 모두 흔들립니다.

서버·클라이언트 경계는 번들·권한·데이터 흐름을 함께 바꾼다

상호작용이 필요한 가장 작은 subtree만 클라이언트로 보내고 데이터 접근은 서버에 남긴다.

  1. Server
    데이터와 secret

    DB·권한·큰 의존성을 서버에서 실행

  2. Serialized props
    경계 통과

    클라이언트에 필요한 최소 데이터만 직렬화

  3. Client
    상호작용

    state·event·browser API가 필요한 UI

  4. 영향
    bundle scope

    use client 아래 import가 브라우저 번들에 포함

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

layout은 공유 껍데기, page는 고유 화면, 서버 컴포넌트는 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상태/이벤트가 필요한지