Skip to content

App Router

App Router 기존 Page Router와 다른 부분

변경되거나 추가되는 부분

  • 페이지 라우팅 설정 방식 변경
  • 레이아웃 설정 방식 변경
  • 데이터 페칭 방식 변경
  • React 18 신규 기능 활용 (Streaming, Suspense 등)

크게 변경되지 않는 사항

  • 네비게이팅 방식
  • 프리페칭 동작
  • 사전 렌더링 개념 (SSR / SSG / ISR)

App Router의 기본 구조

App Router는 app 폴더를 기준으로 동작한다.

less
📁 app
├── page.tsx // ~/
├── 📁 search
│   └── page.tsx // ~/search
└── 📁 book
    ├── page.tsx // ~/book
    └── 📁 [id]
        └── page.tsx // ~/book/1
  • app 폴더 구조를 기반으로 URL이 자동 매핑된다.
  • 하지만 page 이름의 파일만 실제 페이지로 인식된다.
  • 폴더는 경로 역할, page.tsx는 화면 역할을 담당한다.

동적 경로 (Dynamic Route)

URL 파라미터를 사용하는 동적 경로는 대괄호 문법 [param]을 사용한다.

less
📁 book
└── 📁 [id]
    └── page.tsx
  • 위 구조는 /book/1, /book/2, /book/3 url에 대응된다.

파라미터 접근 방식

App Router에서 페이지 컴포넌트에 전달되는 props를 콘솔로 확인해보면 다음과 같은 형태를 볼 수 있다.

tsx
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q: string }>;
}) {
  console.log({ searchParams });

  const { q } = await searchParams;
  return <div>Search 페이지 : {q}</div>;
}

bash
{
  params: Promise { ... },
  searchParams: Promise { ... }
}
🔍 console.log가 복잡하게 보이는 이유
bash
{
  params: Promise {
    {},
    [Symbol(async_id_symbol)]: 96763,
    [Symbol(trigger_async_id_symbol)]: 96762,
    [Symbol(kResourceStore)]: {
      isStaticGeneration: false,
      page: '/search/page',
      route: '/search',
      incrementalCache: [IncrementalCache],
      cacheLifeProfiles: [Object],
      isRevalidate: false,
      isBuildTimePrerendering: undefined,
      hasReadableErrorStacks: undefined,
      fetchCache: undefined,
      isOnDemandRevalidate: false,
      isDraftMode: false,
      isPrefetchRequest: false,
      buildId: 'development',
      reactLoadableManifest: {},
      assetPrefix: '',
      afterContext: [AfterContext],
      cacheComponentsEnabled: false,
      dev: true,
      previouslyRevalidatedTags: [],
      refreshTagsByCacheKind: [Map],
      runInCleanSnapshot: [Function],
      shouldTrackFetchMetrics: true,
      fetchMetrics: []
    },
    [Symbol(kResourceStore)]: {
      type: 'request',
      phase: 'render',
      implicitTags: [Object],
      url: [Object],
      rootParams: {},
      headers: [Getter],
      cookies: [Getter/Setter],
      mutableCookies: [Getter],
      userspaceMutableCookies: [Getter],
      draftMode: [Getter],
      renderResumeDataCache: null,
      isHmrRefresh: true,
      serverComponentsHmrCache: [LRUCache],
      devFallbackParams: null
    },
    [Symbol(kResourceStore)]: undefined,
    [Symbol(kResourceStore)]: undefined,
    [Symbol(kResourceStore)]: undefined
  },
  searchParams: Promise {
    <pending>,
    q: [Getter/Setter],
    [Symbol(async_id_symbol)]: 96686,
    [Symbol(trigger_async_id_symbol)]: 96680,
    [Symbol(kResourceStore)]: {
      isStaticGeneration: false,
      page: '/search/page',
      route: '/search',
      incrementalCache: [IncrementalCache],
      cacheLifeProfiles: [Object],
      isRevalidate: false,
      isBuildTimePrerendering: undefined,
      hasReadableErrorStacks: undefined,
      fetchCache: undefined,
      isOnDemandRevalidate: false,
      isDraftMode: false,
      isPrefetchRequest: false,
      buildId: 'development',
      reactLoadableManifest: {},
      assetPrefix: '',
      afterContext: [AfterContext],
      cacheComponentsEnabled: false,
      dev: true,
      previouslyRevalidatedTags: [],
      refreshTagsByCacheKind: [Map],
      runInCleanSnapshot: [Function],
      shouldTrackFetchMetrics: true,
      fetchMetrics: []
    },
    [Symbol(kResourceStore)]: {
      type: 'request',
      phase: 'render',
      implicitTags: [Object],
      url: [Object],
      rootParams: {},
      headers: [Getter],
      cookies: [Getter/Setter],
      mutableCookies: [Getter],
      userspaceMutableCookies: [Getter],
      draftMode: [Getter],
      renderResumeDataCache: null,
      isHmrRefresh: true,
      serverComponentsHmrCache: [LRUCache],
      devFallbackParams: null
    },
    [Symbol(kResourceStore)]: undefined,
    [Symbol(kResourceStore)]: undefined,
    [Symbol(kResourceStore)]: undefined
  }
}

실제로 출력된 객체를 보면 위와 같이 정체를 알기 어려운 값들이 포함되어 있다.
이는 개발자가 사용하는 데이터가 아니라, Next.js가 서버 렌더링 과정에서 사용하는 내부 실행 컨텍스트 정보이다.

즉, 현재 요청 상태 / 캐싱 정보 / 렌더링 모드 / 개발 환경 설정 등이 포함된 메타데이터라고 볼 수 있다.

왜 Promise 형태로 전달될까?

App Router에서는 페이지 렌더링 과정에서 필요한 값들이 비동기적으로 처리되도록 설계되어 있다. 따라서 params, searchParams는 즉시 사용 가능한 객체가 아니라 Promise 형태로 전달될 수 있다.

tsx
const { id } = await params;
const { q } = await searchParams;

디버깅 시 권장 방식

Promise 객체 자체를 확인하기보다는 resolve된 값만 확인하는 것이 훨씬 직관적이다.

tsx
const resolvedParams = await params;
console.log(resolvedParams);

const resolvedSearchParams = await searchParams;
console.log(resolvedSearchParams);

정리

  • App Router의 params, searchParams는 Promise 형태로 전달될 수 있다.
  • 콘솔에 보이는 복잡한 Symbol 값들은 Next.js 내부 구현이다.
  • 실제 사용 시에는 await 후 값만 사용하는 것이 일반적인 패턴이다.

Layout 적용 - layout.tsx

less
📁 app
├── page.tsx // ~/
└── 📁 search
    ├── page.tsx // ~/search
    ├── layout.tsx
    └── 📁 setting
        ├── page.tsx
        └── layout.tsx

/search 페이지 하위에 layout.tsx를 넣으면 레이아웃 안에 칠드런으로 page.tsx 컴포넌트가 렌더링되게 된다. 해당 경로의 레이아웃으로 자동 설정된다.

  • /search 경로로 시작하는 모든 페이지의 레이아웃으로 적용된다.
  • /search/setting/search로 시작하는 경로이므로 레아아웃 자동 적용
  • 만약 안에 레이아웃을 또 넣으면 중첩된다.

Route Group

Route Group은 URL 경로에는 영향을 주지 않는 폴더 구조다.

less
📁 app
└── 📁 (with-searchbar)
  • 소괄호 ( )로 감싼 폴더는 실제 경로에 포함되지 않는다.
  • 화면 구조를 정리하거나 레이아웃을 그룹화하기 위한 용도로 사용된다.

즉, 파일 구조 정리용 폴더 ≠ 라우팅 경로


less
📁 app
└── 📁 (auth)
    ├── login
    │   └── page.tsx   → /login
    └── register
        └── page.tsx   → /register
  • (auth) 폴더는 URL에 나타나지 않는다.
  • 하지만 내부 페이지들은 정상적으로 라우팅된다.