loading.tsx
export default function Loading() { return <PostListSkeleton />}이 파일이 있으면 Next.js가 page.tsx를 자동으로 Suspense로 감싼다.
설정 파일 대신 폴더 구조가 라우팅을 정의한다
Next.js는 라우팅 설정 파일을 쓰지 않는다. 폴더 이름이 경로가 되고, 파일 이름이 역할이 된다.
//posts/posts/hello-world/about폴더는 경로를 만들고, page.tsx가 있어야 실제로 접근 가능해진다.
page.tsx 없는 폴더는 URL이 되지 않는다 — 조각을 모아두는 용도로 쓸 수 있다.
| 파일 | 역할 |
|---|---|
layout.tsx |
자식 라우트를 감싸는 공유 UI. 이동해도 리렌더되지 않는다 |
page.tsx |
해당 경로의 실제 화면. 이게 있어야 URL이 생긴다 |
loading.tsx |
자동으로 Suspense 경계를 만들어 준다 |
error.tsx |
자동으로 에러 경계를 만든다. 클라이언트 컴포넌트여야 한다 |
not-found.tsx |
notFound() 호출 또는 매칭 실패 시 |
template.tsx |
layout과 비슷하지만 이동할 때마다 새로 마운트된다 |
route.ts |
페이지 대신 HTTP 핸들러 (REST API) |
default.tsx |
병렬 라우트에서 매칭 실패 시 대체 UI |
App Router의 중요한 장점인데 잘 알려져 있지 않다.
flowchart TB
A["app/layout.tsx<br/>사이드바 · 스크롤 위치 유지"] --> B["app/dashboard/layout.tsx<br/>탭 상태 유지"]
B --> C["dashboard/reports/page.tsx<br/>← 이동 시 이것만 교체"]
B --> D["dashboard/settings/page.tsx<br/>← 이것으로"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
class A,B ok
class C,D warn
/dashboard/reports → /dashboard/settings 이동 시 바뀌는 건 page뿐이다app/posts/[slug]/page.tsx → /posts/abc params.slug = 'abc'app/shop/[...cat]/page.tsx → /shop/a/b/c params.cat = ['a','b','c']app/docs/[[...path]]/page.tsx → /docs 또는 /docs/a (optional catch-all)// params는 Promise다 — Next.js 15부터 바뀌었다. await 해야 한다.export default async function PostPage({ params }: PageProps<'/posts/[slug]'>) { const { slug } = await params const post = await getPost(slug) if (!post) notFound() return <article>{post.title}</article>}
// 빌드 타임에 미리 생성할 경로를 알려준다export async function generateStaticParams() { const posts = await getAllPosts() return posts.map((p) => ({ slug: p.slug }))}PageProps<'/posts/[slug]'>는 Next.js가 자동 생성해 주는 타입이다. 직접 정의할 필요가 없다.
export default async function SearchPage({ searchParams }: PageProps<'/search'>) { const { q, page } = await searchParams const results = await search(q, Number(page ?? 1)) return <ResultList items={results} />}괄호로 감싼 폴더는 URL에 나타나지 않는다. 레이아웃을 나눌 때 쓴다.
//pricing/dashboard/settings랜딩 페이지와 로그인 후 화면의 껍데기가 완전히 다른 경우가 대부분이다. 이 구조가 사실상 표준 패턴이라고 봐도 된다.
이름은 어렵지만 용도는 명확하다.
app/dashboard/├─ layout.tsx├─ @team/page.tsx├─ @analytics/page.tsx└─ page.tsxexport default function Layout({ children, team, analytics }) { return <>{children}{team}{analytics}</>}한 화면에 독립적인 영역 여러 개를 둔다.
각 슬롯이 자기 loading과 error를 따로 가진다 — 하나가 느려도 나머지는 뜬다.
app/├─ feed/page.tsx├─ photo/[id]/page.tsx└─ feed/(.)photo/[id]/page.tsx피드에서 사진을 클릭하면 모달로 뜨고, 같은 URL을 직접 열거나 새로고침하면 전체 페이지로 뜬다.
인스타그램의 그 동작이다. (.)는 같은 레벨, (..)는 한 단계 위를 가로챈다.
export async function GET(request: Request) { const { searchParams } = new URL(request.url) const limit = Number(searchParams.get('limit') ?? 10) const posts = await db.post.findMany({ take: limit }) return Response.json(posts)}
export async function POST(request: Request) { const body = await request.json() const created = await db.post.create({ data: body }) return Response.json(created, { status: 201 })}import Link from 'next/link'
// 기본 — 뷰포트에 들어오면 자동으로 프리페치된다<Link href="/posts/hello">읽기</Link><Link href="/posts/hello" prefetch={false}>프리페치 끄기</Link>// 프로그래매틱 이동은 클라이언트 컴포넌트에서'use client'import { useRouter } from 'next/navigation'
export function SaveButton() { const router = useRouter() return <button onClick={() => router.push('/done')}>저장</button>}loading.tsx
export default function Loading() { return <PostListSkeleton />}이 파일이 있으면 Next.js가 page.tsx를 자동으로 Suspense로 감싼다.
error.tsx
'use client' // 필수
export default function Error({ error, reset }) { return ( <div> <p>불러오지 못했습니다</p> <button onClick={reset}>다시 시도</button> </div> )}Next.js 16에서 middleware.ts가 proxy.ts로 이름이 바뀌었다.
런타임 기본값도 Edge에서 Node.js로 바뀌었다.
// proxy.ts (프로젝트 루트, app/ 과 같은 레벨)import { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) { if (!request.cookies.get('session')) { return NextResponse.redirect(new URL('/login', request.url)) }}
export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],}npx @next/codemod@canary middleware-to-proxy . # 자동 마이그레이션Next.js 팀이 문서에서 명시적으로 경고하는 지점이다.
flowchart TD
R["요청"] --> P{"proxy.ts matcher 에<br/>걸리는가"}
P -->|"걸린다"| C1["쿠키 확인 → 리다이렉트"]
P -->|"안 걸린다"| C2["그냥 통과"]
C1 --> D["데이터에 닿는 지점"]
C2 --> D
D --> A{"여기서 다시<br/>인증·인가를 하는가"}
A -->|"한다"| OK["안전 ✅"]
A -->|"proxy 를 믿는다"| BAD["Server Function 직접 호출로 우회 가능 ❌"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class OK ok
class BAD bad
class R,P,C1,C2,D,A mute
page.tsx가 있어야 접근 가능해진다params / searchParams는 Promise다. await한다(name) 으로 껍데기를 분리하는 게 표준 패턴middleware.ts → proxy.ts. 인증의 최종 방어선으로 쓰지 말 것