웹 개발·4분 읽기

Next.js App Router 완벽 가이드

Next.js 13 이후 도입된 App Router의 핵심 개념과 실전 사용법을 단계별로 알아봅니다. 서버 컴포넌트, 레이아웃, 라우팅 등을 상세히 다룹니다.

공유:

Next.js App Router 완벽 가이드

Next.js는 React 기반의 풀스택 프레임워크로, 2023년 이후 App Router가 기본 라우팅 시스템으로 자리잡았습니다. 기존 Pages Router와 비교하여 더욱 직관적이고 강력한 기능을 제공하는 App Router에 대해 깊이 있게 알아보겠습니다.

App Router란 무엇인가

App Router는 Next.js 13에서 도입된 새로운 라우팅 시스템입니다. app 디렉토리를 기반으로 동작하며, React Server Components를 기본으로 사용합니다. 기존 pages 디렉토리 기반의 라우팅과는 근본적으로 다른 접근 방식을 취합니다.

기존 Pages Router에서는 모든 컴포넌트가 클라이언트에서 렌더링되거나 getServerSideProps, getStaticProps 같은 특수한 함수를 통해 서버 데이터를 가져왔습니다. 반면, App Router에서는 컴포넌트 자체가 서버에서 실행되므로 데이터 페칭이 훨씬 자연스럽습니다.

Pages Router와의 주요 차이점

특징Pages RouterApp Router
디렉토리pages/app/
기본 컴포넌트클라이언트 컴포넌트서버 컴포넌트
데이터 페칭getServerSideProps 등async 컴포넌트 직접 fetch
레이아웃_app.tsx, _document.tsxlayout.tsx (중첩 가능)
로딩 UI수동 구현loading.tsx 자동 지원
에러 처리_error.tsxerror.tsx (경로별 설정)

파일 기반 라우팅 시스템

App Router의 라우팅은 app 디렉토리 내의 폴더 구조를 따릅니다. 각 폴더는 URL 경로의 세그먼트가 되며, 특별한 파일 이름으로 해당 경로의 동작을 정의합니다.

주요 파일 컨벤션

App Router에서 사용하는 특수 파일들은 다음과 같습니다.

  • page.tsx: 해당 경로의 UI를 정의합니다. 이 파일이 있어야 해당 경로가 접근 가능합니다.
  • layout.tsx: 하위 페이지들이 공유하는 레이아웃을 정의합니다. 페이지 전환 시 리렌더링되지 않습니다.
  • loading.tsx: 페이지 로딩 중에 표시할 UI를 정의합니다. React Suspense를 자동으로 감싸줍니다.
  • error.tsx: 에러 발생 시 표시할 UI를 정의합니다. Error Boundary를 자동으로 생성합니다.
  • not-found.tsx: 404 페이지를 커스터마이징합니다.
  • template.tsx: layout과 유사하지만, 네비게이션 시 새 인스턴스가 생성됩니다.

라우팅 예시

app/
├── page.tsx                    → /
├── about/
│   └── page.tsx                → /about
├── blog/
│   ├── page.tsx                → /blog
│   └── [slug]/
│       └── page.tsx            → /blog/:slug
├── shop/
│   └── [...categories]/
│       └── page.tsx            → /shop/* (catch-all)
└── (marketing)/
    ├── layout.tsx              → 마케팅 그룹 레이아웃
    └── campaigns/
        └── page.tsx            → /campaigns

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

App Router의 가장 큰 변화는 React Server Components(RSC)를 기본으로 사용한다는 점입니다. 서버 컴포넌트는 서버에서만 렌더링되며, JavaScript 번들에 포함되지 않아 성능상 큰 이점을 제공합니다.

서버 컴포넌트의 장점

서버 컴포넌트를 사용하면 여러 가지 이점이 있습니다. 첫째, 데이터베이스나 파일 시스템에 직접 접근할 수 있습니다. 둘째, 클라이언트로 전송되는 JavaScript 양이 줄어들어 초기 로딩 속도가 빨라집니다. 셋째, 민감한 정보(API 키, 데이터베이스 토큰 등)가 클라이언트에 노출되지 않습니다.

// 서버 컴포넌트 (기본값 - 별도 선언 불필요)
async function PostList() {
  const posts = await fetch('https://api.example.com/posts');
  const data = await posts.json();

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

클라이언트 컴포넌트 사용

사용자 상호작용이 필요한 경우(클릭 이벤트, 상태 관리, 브라우저 API 사용 등)에는 클라이언트 컴포넌트를 사용합니다. 파일 상단에 "use client" 지시어를 추가하면 됩니다.

"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>
  );
}

컴포넌트 선택 기준

서버 컴포넌트와 클라이언트 컴포넌트를 선택할 때는 다음 기준을 참고하세요.

서버 컴포넌트를 사용하는 경우:

  • 데이터 페칭이 주된 목적인 컴포넌트
  • 백엔드 리소스에 직접 접근해야 하는 경우
  • 민감한 정보를 서버에서만 처리해야 하는 경우
  • 큰 의존성 패키지를 사용하는 경우 (클라이언트 번들 축소)

클라이언트 컴포넌트를 사용하는 경우:

  • onClick, onChange 등 이벤트 리스너가 필요한 경우
  • useState, useEffect 등 React 훅을 사용하는 경우
  • 브라우저 전용 API를 사용하는 경우 (localStorage, window 등)
  • 커스텀 훅이 상태나 효과에 의존하는 경우

데이터 페칭 전략

App Router에서의 데이터 페칭은 이전보다 훨씬 단순해졌습니다. 서버 컴포넌트에서 async/await를 직접 사용할 수 있기 때문입니다.

기본 데이터 페칭

async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    cache: 'force-cache',  // 정적 데이터 (기본값)
  });
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();

return (

상품 목록

{products.map((product) => ( ))}
); }


### 캐싱 전략

Next.js App Router에서는 `fetch` 함수에 캐싱 옵션을 지정할 수 있습니다.

- **`cache: 'force-cache'`**: 정적 데이터로 빌드 시 캐시됩니다. SSG와 유사합니다.
- **`cache: 'no-store'`**: 매 요청마다 새로 가져옵니다. SSR과 유사합니다.
- **`next: { revalidate: 60 }`**: 60초마다 데이터를 재검증합니다. ISR과 유사합니다.

## 레이아웃과 중첩 라우팅

App Router의 레이아웃 시스템은 매우 강력합니다. 레이아웃은 여러 페이지에 걸쳐 공유되며, 페이지 전환 시에도 상태를 유지합니다.

### 루트 레이아웃

// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode; }) { return (

{children}
푸터 영역
); }


### 중첩 레이아웃

대시보드 같은 복잡한 UI에서는 중첩 레이아웃이 유용합니다. 각 경로 세그먼트마다 독립적인 레이아웃을 가질 수 있으며, 이들이 자연스럽게 중첩됩니다.

// app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return (

{children}
); }


## 미들웨어와 인터셉팅 라우트

### 미들웨어 활용

미들웨어는 요청이 완료되기 전에 실행되는 코드입니다. 인증 확인, 리다이렉트, 요청 헤더 수정 등에 활용할 수 있습니다.

// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) { const token = request.cookies.get('auth-token');

if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); }

return NextResponse.next(); }

export const config = { matcher: '/dashboard/:path*', };


### 병렬 라우트와 인터셉팅 라우트

App Router는 병렬 라우트(`@folder`)와 인터셉팅 라우트(`(.)folder`)를 지원합니다. 이를 통해 모달, 탭 UI 등 복잡한 라우팅 패턴을 구현할 수 있습니다.

## Server Actions

Server Actions는 서버에서 실행되는 비동기 함수로, 폼 제출이나 데이터 변경 작업을 처리하는 데 사용됩니다. 별도의 API 라우트를 만들지 않아도 됩니다.

// app/actions.ts "use server";

export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string;

await db.post.create({ data: { title, content }, });

revalidatePath('/posts'); }

// app/posts/new/page.tsx import { createPost } from '../actions';

export default function NewPostPage() { return (