Next.js 14 App Router 완벽 가이드 — 서버 컴포넌트부터 라우팅까지
기존 Pages Router에 익숙한 팀에게 App Router가 어렵게 느껴지는 이유도 여기 있습니다. 폴더 구조만 바뀐 것이 아니라, 서버 컴포넌트, 스트리밍, 캐싱, 레이아웃 지속성 같은 개념이 함께 들어오기 때문입니다.
이 글은 App Router의 디렉토리 문법을 나열하기보다, 실무에서 어떤 기준으로 구조를 잡아야 하는지에 초점을 맞춥니다.
App Router를 이해하는 가장 좋은 출발점
app/
├── layout.tsx
├── page.tsx
├── loading.tsx
├── error.tsx
├── blog/
│ ├── page.tsx
│ └── [slug]/
│ └── page.tsx
└── api/
└── posts/
└── route.ts
이 구조는 단순히 파일명을 외우는 문제가 아닙니다. 각 파일은 렌더링 생명주기에서 맡는 역할이 다릅니다.
layout.tsx: 유지되는 UI 프레임page.tsx: 실제 route entryloading.tsx: Suspense 기반 로딩 UIerror.tsx: 세그먼트 단위 오류 복구route.ts: 서버 핸들러
즉, App Router는 라우팅이 아니라 UI와 서버 로직의 경계 배치 시스템에 가깝습니다.
기본값은 서버 컴포넌트라는 점이 가장 중요하다
async function PostList() {
const posts = await db.posts.findMany()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
App Router에서 컴포넌트는 기본적으로 서버 컴포넌트입니다. 이 말은 곧 다음을 의미합니다.
- 브라우저 번들에 꼭 포함되지 않아도 된다
- 서버 전용 코드와 가까이 둘 수 있다
- 데이터를 가져오는 코드와 UI를 더 가깝게 작성할 수 있다
이것이 App Router의 가장 큰 장점입니다. 다만 동시에 “무조건 서버에서 돌린다”가 아니라, 브라우저에 있어야 할 것만 클라이언트로 내리는 감각이 중요해집니다.
클라이언트 컴포넌트는 예외적으로 사용해야 한다
'use client'
import { useState } from 'react'
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked((value) => !value)}>
{liked ? '❤️' : '🤍'}
</button>
)
}
'use client'를 붙이는 순간 그 파일과 하위 의존성은 클라이언트 번들의 영향을 받습니다. 그래서 좋은 기준은 간단합니다.
- 상태와 이벤트가 필요한가
- 브라우저 API가 필요한가
- 인터랙션이 실제로 필요한가
버튼 하나 때문에 페이지 전체를 클라이언트 컴포넌트로 만드는 실수는 매우 흔합니다. 가능하면 경계를 늦게 내리는 편이 좋습니다.
Layout은 공통 UI가 아니라 지속되는 UI다
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
)
}
App Router의 Layout은 단순 공통 래퍼가 아닙니다. 세그먼트 이동 시 재사용되며, 상태와 렌더링 비용에 영향을 줍니다. 그래서 Layout에는 보통 아래가 잘 맞습니다.
- 전역 프레임
- 공통 내비게이션
- 섹션별 지속 레이아웃
반대로 매번 바뀌는 데이터나 화면별 특수 로직을 Layout에 과도하게 넣으면 구조가 금방 복잡해집니다.
동적 라우트는 URL 매핑보다 데이터 전략과 함께 봐야 한다
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
여기서 중요한 질문은 단순히 slug를 매핑하는 것이 아닙니다.
- 이 경로는 정적으로 생성할 것인가
- 요청 시점마다 새로 렌더링할 것인가
- 일부만 재검증할 것인가
App Router에서는 라우팅과 렌더링 전략이 분리되지 않습니다. URL 구조를 잡을 때부터 캐싱과 생성 전략까지 같이 생각하는 편이 좋습니다.
loading.tsx와 error.tsx는 UX 설계 도구다
export default function Loading() {
return <div className="animate-pulse">로딩 중...</div>
}
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<p>오류가 발생했습니다: {error.message}</p>
<button onClick={reset}>다시 시도</button>
</div>
)
}
이 파일들은 문법 요소가 아니라 사용자 경험 제어점입니다. 특히 App Router에서는 스트리밍과 세그먼트 단위 렌더링이 가능하므로, 로딩과 에러 경험을 더 세밀하게 나눌 수 있습니다.
좋은 기준은 이렇습니다.
- 로딩은 너무 큰 단위보다 사용자 의미 단위로 나눈다
- 에러 복구는 가능한 한 국소화한다
- 전체 앱이 아니라 실패한 세그먼트만 다시 시도하게 만든다
Route Handler는 API 라우트의 새 문법이지만 역할은 여전히 중요하다
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await db.posts.findMany()
return NextResponse.json(posts)
}
export async function POST(request: Request) {
const body = await request.json()
const post = await db.posts.create({ data: body })
return NextResponse.json(post, { status: 201 })
}
App Router에서는 페이지 컴포넌트에서 직접 데이터를 가져오는 경우가 많아져서, Route Handler의 역할을 오해하기 쉽습니다. 하지만 여전히 다음 용도에서는 중요합니다.
- 외부 클라이언트가 호출하는 HTTP API
- 웹훅/콜백 엔드포인트
- 브라우저 외부와 통신하는 명시적 서버 인터페이스
즉, 서버 컴포넌트가 있다고 해서 모든 API 레이어가 사라지는 것은 아닙니다.
Metadata도 렌더링 파이프라인의 일부다
export async function generateMetadata({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.description,
openGraph: {
images: [post.ogImage],
},
}
}
App Router에서는 메타데이터도 route 단위에서 데이터와 함께 구성할 수 있습니다. 콘텐츠 기반 서비스에서는 특히 중요합니다. 다만 이 역시 결국 데이터 fetch 비용과 연결되므로, 메타데이터 생성이 과도하게 무거워지지 않게 조심해야 합니다.
App Router 실무에서 자주 놓치는 것: 캐싱 모델
App Router를 쓰다 보면 가장 자주 나오는 질문이 있습니다.
- 왜 데이터가 생각보다 안 바뀌지?
- 왜 매번 다시 불리지?
- 왜 로컬과 프로덕션 동작이 다르지?
이건 대부분 캐싱과 렌더링 전략을 명확히 이해하지 못해서 생깁니다. App Router는 단순 SSR이 아니라, 정적 생성, 재검증, 서버 fetch 캐시, 스트리밍이 함께 섞여 있습니다. 그래서 화면마다 아래를 먼저 정하는 편이 좋습니다.
- 항상 최신이어야 하는가
- 일정 시간 캐시 가능할까
- 정적 생성 가치가 큰가
- 사용자별 동적 응답이 필요한가
즉, App Router는 코드 구조보다 데이터 생명주기 설계가 더 중요합니다.
App Router가 특히 잘 맞는 경우
- 콘텐츠 중심 서비스
- 커머스/마케팅 사이트
- 관리자 화면처럼 서버 데이터 비중이 높은 UI
- SEO와 초기 로딩 품질이 중요한 서비스
반대로 복잡한 실시간 협업 앱, 클라이언트 상태가 매우 두꺼운 제품은 App Router의 장점보다 클라이언트 중심 구조의 단순함이 더 크게 느껴질 수도 있습니다.
마무리
Next.js App Router의 본질은 새로운 폴더 규칙이 아니라, React 앱의 렌더링 위치와 데이터 흐름을 다시 설계하게 만드는 모델입니다. 서버 컴포넌트, 레이아웃 지속성, 스트리밍, 캐싱을 함께 이해할 때 비로소 장점이 드러납니다.
실무에서는 파일명을 외우는 것보다 “이 화면은 어디서 렌더링되어야 하고, 어떤 데이터 생명주기를 가져야 하는가”를 먼저 묻는 습관이 더 중요합니다. App Router는 그 질문에 정교하게 답할수록 강해집니다.
운영 환경에서 어려워지는 지점
- App Router는 강한 프리미티브를 주지만, 그 힘만큼 서버 경계, 캐시, route segment 동작을 더 엄격히 이해해야 한다.
- 팀이 서버 액션, 클라이언트 상태, 재검증을 일관된 데이터 흐름 없이 섞기 시작하면 빠르게 꼬인다.
- layout 중첩, segment 설정, 캐시 무효화가 함께 얽히면 프레임워크의 마법 같은 편의성이 곧 디버깅 난이도로 바뀐다.
중요한 아키텍처 결정
- 어떤 데이터는 layout에서, 어떤 데이터는 page나 leaf component에서 가져올지 이유와 함께 정한다.
- server action은 mutation 흐름을 개선하는 곳에만 쓰고, 모든 API 설계를 대체하는 만능 도구로 보지 않는다.
- route 계열별 캐시와 재검증 정책을 정해 신선도 기대치를 분명히 한다.
실무 예시
깔끔한 App Router 설계는 안정적인 shell 책임을 layout에 두고 변동성이 큰 데이터는 트리 아래쪽에 둔다.
app/
dashboard/layout.tsx
dashboard/page.tsx
dashboard/@activity/page.tsx
dashboard/settings/page.tsx
피해야 할 안티패턴
- 한 자식의 상호작용 때문에 큰 트리를 client component로 표시하는 것.
- 재시도, 인증, 에러 의미 정의 없이 server action을 사용하는 것.
- 팀 누구도 설명하지 못하는 암묵적 캐시 동작에 의존하는 것.
운영 체크리스트
- route segment 설정과 캐시 기대치를 문서화한다.
- client component와 hydration의 번들 영향을 측정한다.
- 동시 mutation 상황에서 재검증 동작을 테스트한다.
- layout 수준 데이터 패칭이 무관한 route를 조용히 막고 있지 않은지 본다.
최종 판단
App Router는 layout, 서버 렌더링, 캐시 정책을 일급 아키텍처로 다룰 때 가장 강하다. 가볍게 쓰면 초반에는 생산적이지만 나중에는 설명하기 어려운 구조가 된다.
Continue Reading
다음으로 읽기 좋은 글
React 아키텍처 설계 가이드
React 애플리케이션을 유지보수 가능한 시스템으로 설계하는 방법을 정리합니다. 컴포넌트 경계, 상태 계층, 서버 상태, 렌더링 모델, 팀 협업 구조까지 실무 중심으로 다룹니다.
🖥️ FrontendReact Server Components 완전 정복 — RSC와 Server Actions
React Server Components를 개념 소개 수준이 아니라 아키텍처 선택의 관점에서 정리합니다. Client Component와의 경계 설정, Server Actions, 캐싱, 스트리밍, 실무에서 자주 겪는 함정까지 Next.js App Router 기준으로 설명합니다.
📈 최신 동향React Foundation 이 엔지니어링 팀에 의미하는 것
React Foundation 소식이 단순 거버넌스 뉴스가 아니라 프레임워크 생태계의 장기 예측 가능성에 어떤 의미를 갖는지 정리한 글입니다.
🧪 TestReact Testing Library 실전 설계 가이드
React Testing Library를 사용자 중심 테스트 도구로 활용하는 방법을 정리합니다. query 우선순위, 상호작용 테스트, 비동기 UI, provider wrapper, 과도한 mocking 회피까지 실무 기준으로 다룹니다.
다음 탐색