🧠App Router의 기본 개념
App Router는 트리(Tree) 구조로 라우팅 및 컴포넌트 계층을 시각화한다.
- app/ 디렉토리가 루트(Root)
- 그 하위에 있는 폴더들(dashboard, blog, [slug] 등)이 세그먼트(segment)
- 세그먼트는 또 다른 하위 세그먼트를 가질 수 있으며, 이를 통해 서브트리(subtree)를 형성한다.
- 더 이상 하위 폴더가 없는 말단 세그먼트는 잎(leaf) 노드이다.
- 여기에 주로 page.tsx가 위치한다.
- Domain
- URL 경로(Path): 도메인 다음에 오는 URL의 일부 (세그먼트로 구성)
- URL 세그먼트(Segment): 슬래시로 구분된 URL 경로의 일부
폴더와 파일
Next.js는 파일 시스템 기반 라우터를 사용한다.
- 폴더(Folders): 라우트를 정의하며, 라우트는 루트 디렉토리에서 시작해 page.js가 위치한 잎(leaf) 폴더까지의 폴더 경로로 구성된다.
- 즉, 폴더 계층 구조가 곧 라우트 경로이다.
- 파일(Files): 각 라우트 세그먼트에 대해 실제 화면에 표시될 UI를 정의하는 데 사용된다.
파일
Next.js는 중첩된 라우트에서 특정 동작을 가진 UI를 생성하기 위해 아래와 같은 일련의 특별한 파일을 제공한다.
- layout: 세그먼트와 그 자식들에 대한 공유 UI
- page: 라우트의 고유한 UI를 만들고 공개적으로 접근 가능하게 만드는 파일
- loading: 세그먼트와 그 자식들에 대한 로딩 UI
- not-found: 세그먼트와 그 자식들에 대한 404 UI
- error: 세그먼트와 그 자식들에 대한 에러 UI
- global-error: 글로벌 에러 UI
- route: 서버 측 API 엔드포인트 (기존 pages의 api 폴더 역할)
- template: 커스텀 된 (리렌더링) 레이아웃 UI
- default: 병렬 라우트에 대한 fallback UI
- .js, .jsx, .tsx 파일 확장자 모두 사용 가능
라우트 세그먼트
- App Router에서 각 폴더는 하나의 라우트 세그먼트(route segment)를 나타내며, 이는 URL 경로의 해당 부분과 직접 연결된다.
App Router 구성 원칙
📦 Colocation 원칙
- app/ 디렉토리 내부에서는 꼭 라우팅을 위한 page.js가 아니더라도, 해당 경로에 관련된 컴포넌트, 스타일, 테스트 파일 등을 함께 위치시킬 수 있다. (→ Colocation)
- 라우팅은 폴더 단위로 정의되며, page.js, route.js 등 특정 파일만이 실제 라우팅 엔드포인트가 된다.
- 폴더 내 다른 파일들은 외부에서 직접 접근되지 않으며, 해당 경로를 구성하는 구성 요소로서만 사용된다.
📄 Page vs Layout
Pages
- 경로(Route)에서 UI를 실제로 렌더링하는 유일한 파일이다.
- 기본적으로 서버 컴포넌트이다.
- 필요 시 "use client"로 클라이언트 컴포넌트 전환 가능
- 데이터 페칭이 가능하다.
Layouts
- 여러 페이지 간 공유되는 UI 영역
- 예: 사이드바, 헤더, 네비게이션 바 등
- 항상 children props 필요로 한다.
// app/dashboard/layout.tsx
export default function DashboardLayout({ children: React.ReactNode; }) {
return <section>{children}</section>
}
- 특징
- 상태(state)를 유지
- 인터랙티브 요소가 리렌더링되지 않음
- 기본적으로 서버 컴포넌트 (원하면 "use client"로 클라이언트 전환 가능)
- 데이터 페칭 가능하지만, 부모 → 자식 간 직접 데이터 전달은 불가
- app 디렉토리 루트에 필수로 루트 레이아웃을 정의해야한다.
📚 Nested Layouts (중첩 레이아웃)
- 레이아웃은 기본적으로 하위 경로에 중첩된다.
- 즉, 상위 layout → 하위 layout → page.js 순으로 감싸게된다.
- 항상 children props을 통해 트리처럼 구성
📋 Templates
- layout처럼 UI를 감싸지만, 매 페이지 이동 시 새 인스턴스 생성한다.
- 레이아웃과 달리, 상태와 DOM이 유지되지 않고 재설정된다.
- 사용자가 template를 공유하는 페이지를 변경하면, 컴포넌트의 새로운 인스턴스가 마운트되어 DOM 요소가 다시 생성된다.
// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
Next.js에서는 아래와 같은 특별한 상황이 아닌 경우 Layout 사용을 권장하지만, 템플릿이 필요한 사례들이 있을 것이다.
- 애니메이션 시작/ 종료
- 페이지를 변경할 때 마다 매번 Suspense fallback 보여주고 싶을 때
- 강제로 컴포넌트 초기화가 필요한 경우 </aside>
✅ Route Groups
- 기본적으로 app/ 내부의 모든 폴더는 URL 경로로 매핑된다.
- 그러나 괄호 ( )로 감싼 Route Group 폴더는 URL 경로에 포함되지 않으면서도 폴더 구조를 논리적으로 정리할 수 있게 해준다.
app/
├── (marketing)/ │
├── layout.tsx │
└── home/ │
└── page.tsx → URL: /home
├── (shop)/ │
├── layout.tsx │
└── home/ │
└── page.tsx → URL: /home
- 각각의 Route Group 마다 같은 URL 계층을 가져도, 다른 layout을 적용할 수 있다.
- 위 예시처럼 (marketing), (shop)은 app 하단의 최상위 루트지만, Route Group을 이용해서 별개의 레이아웃 구성할 수 있다.
‼️ 하지만, Next.js는 동일한 URL 경로(/home)에 대해 “하나의 페이지”만 선택적으로 렌더링한다.
즉, (marketing)/home과 (shop)/home을 동시에 존재시켜도, 둘 다 /home에서 접근할 수는 없다.
→ URL을 다르게 만들어야만 각각 접근 가능하다.
렌더링과 UX 최적화
🕒 즉시 로딩 상태 (Instant Loading States)
- loading.tsx 파일을 만들어 해당 경로에서 즉시 보여줄 로딩 UI를 정의할 수 있다.
- 이 UI는 React의 Suspense Boundary 안에서 자동으로 동작하며, page.tsx의 비동기 데이터가 준비되는 동안 표시된다.
- 보통 스켈레톤 UI, 스피너, 썸네일 등을 넣는다.
// app/dashboard/loading.tsx
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <LoadingSkeleton />}
layout.tsx
└─ Suspense
├─ loading.tsx (즉시 렌더됨)
└─ page.tsx (비동기 데이터 처리 후 등장)
커스텀 Suspense 사용
- loading.tsx를 사용하지 않고도 컴포넌트 내부에서 직접 React.Suspense를 사용하여 부분 로딩 구현도 가능하다.
import { Suspense } from 'react';
import Comments from './Comments';
import CommentsSkeleton from './CommentsSkeleton';
export default function PostPage() {
return (
<div>
<h1>게시글</h1>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
🚰 Streaming
- 기존 SSR 방식에서는 모든 데이터를 서버에서 받아야 클라이언트에 HTML을 렌더링할 수 있었다.
- 👉 페이지 전송 전까지 전체 데이터 대기 = 느린 초기 로딩
- React 18과 Next.js App Router는 HTML을 청크 단위로 점진적으로 전송 가능해졌다.
- 즉, 페이지 전체 데이터를 기다리지 않고, 일부 UI를 먼저 렌더링한 뒤, 데이터에 의존하는 컴포넌트는 비동기 로딩 후 순차적으로 하이드레이션된다.
Streaming은 HTML을 작은 청크로 나눠 서버에서 클라이언트로 순차 전송함으로써, 초기 로딩 속도를 개선한다
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>)
}
- 위 예시에서 <Suspense>는 비동기 작업(예: 데이터 가져오기)을 수행하는 컴포넌트를 감싸고, 작업이 진행되는 동안 대체 UI(예: 스켈레톤, 스피너)를 표시한 다음 작업이 완료되면 컴포넌트를 교체하는 방식으로 동작한다
Streaming의 장점
- 스트리밍 서버 렌더링: 서버에서 클라이언트로 HTML을 점진적으로 렌더링한다.
- 선택적 하이드레이션: React는 사용자 상호작용에 기반하여 먼저 상호작용 가능한 컴포넌트를 우선적으로 처리할 수 있다.
💥 Error Handling
error.tsx 파일을 생성하면, 해당 세그먼트(segment)와 그 하위 요소들을 자동으로 React Error Boundary로 감싼다.
오류가 발생한 컴포넌트만 fallback되며, 나머지 앱은 정상 유지된다.
- 즉, 사용자는 전체 페이지 새로고침 없이 오류 복구를 시도할 수 있다.
- // app/dashboard/error.tsx 'use client' // error.tsx는 반드시 클라이언트 컴포넌트여야 함 import { useEffect } from 'react'; export default function Error({ error, reset, }: { error: Error; reset: () => void; }) { useEffect(() => { console.error(error); // 에러 로깅 }, [error]); return ( <div> <h2>문제가 발생했습니다!</h2> <button onClick={() => reset()}>다시 시도</button> </div> ); }
// app/dashboard/error.tsx
'use client'
// error.tsx는 반드시 클라이언트 컴포넌트여야 함
import { useEffect } from 'react';
export default function Error({ error, reset, }: { error: Error; reset: () => void; }) {
useEffect(() => {
console.error(error); // 에러 로깅
}, [error]);
return (
<div>
<h2>문제가 발생했습니다!</h2>
<button onClick={() => reset()}>다시 시도</button>
</div>
);
}
⚙️에러 발생 시 흐름
- 특정 세그먼트 내 런타임 에러 발생
- 해당 위치의 error.tsx 자동 렌더링 → 레이아웃이나 상위 세그먼트는 유지되고, 오류 발생 영역만 대체
- 사용자 reset() 호출 시 세그먼트 리렌더 → 복구 시도
Root Layout Error 처리
❗ app/layout.tsx 자체에서 발생하는 에러는 일반 error.tsx로는 처리할 수 없다.
app/global-error.tsx
- 루트 레이아웃 또는 템플릿(layout.tsx, template.tsx)에서 발생하는 에러를 처리하려면,
- app/global-error.tsx 파일을 만들어야 한다.
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>)
}
👩👧👧 App Router에서 개선된 또 다른 기능들
Data Fetching
- 모든 컴포넌트에서 fetch() Web API를 사용할 수 있다.
- React와 Next.js는 내부적으로 fetch() 호출을 감지해서 중복 제거 / 캐싱 / 서버 요청 타이밍 제어를 해준다.
- fetch() 자체를 쓰는 방식이지만, 내부적으로는 Next.js가 알아서 SSG/SSR/ISR로 처리한다
// SSG처럼 정적으로 생성하고 싶다면
fetch(URL, { cache: 'force-cache' }); // 기본값
// SSR처럼 매 요청마다 최신 데이터를 받고 싶다면
fetch(URL, { cache: 'no-store' });
// ISR처럼 일정 시간마다 재생성하려면
fetch(URL, { next: { revalidate: 10 } });
New next/Image
기존에 <img> 태그로 이미지를 넣으면 크기 지정 누락, 느린 로딩, 레이아웃 밀림(Layout Shift) 등이 생기곤 했지만,
아래와 같이 개선되어 이전 next/image와 다르게 더 가볍고, 쓰기 쉬우며, 접근성을 확보하였다.
- width, height 필수 지정: 레이아웃 안정성 확보
- alt 필수: 웹 접근성 향상
- lazy loading 기본값 내장: 초기 렌더링 속도 향상
- hydration 없이 작동: 클라이언트와 서버 간 DOM 일치 문제 줄어듦
- 스타일링이 더 쉬워짐 (Tailwind나 className 바로 적용 가능)
- import Image from 'next/image'; <Image src="/profile.png" alt="사용자 프로필 이미지" width={200} height={200} />
New @next/font (폰트 최적화)
기존에는 구글 폰트를 <link>로 가져오거나, 외부 CDN에서 받아왔지만, 네트워크 요청이 느리면 Layout Shift 발생하였다.
- 폰트를 프로젝트에 직접 포함시키며, 외부 요청 없이 동작하게 하고
- CSS의 size-adjust 속성을 활용해 텍스트 높이 차이로 인한 밀림을 제거
- 자체 호스팅으로 개인정보 보호
import { Inter } from '@next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function Layout({ children }) {
return <html className={inter.className}>{children}</html>;
}
⇒ 지금은 CSS가 아닌 JS로 폰트를 불러오는 방식이지만, 속도 및 안정성 측면에서 더 좋아졌다.
Improved next/link
- <Link> 컴포넌트에 더 이상 수동으로 <a> 태그를 하위에 추가할 필요가 없어졌다.
- <Link>는 이제 기본값으로 <a> 태그를 렌더링하며, <a> 태그의 Props들을 정상적으로 전달할 수 있다.
import Link from 'next/link'
// Next.js 12: `<a>` has to be nested otherwise it's excluded
<Link href="/about">
<a>About</a>
</Link>// Next.js 13: `<Link>` always renders `<a>`
<Link href="/about">
About
</Link>
[Next.js App Router + Server Component 기준] 전체 흐름 정리
[빌드 시점:next build]
- Next.js는 각 경로(page.tsx)를 검사
- 정적 생성 가능한가?
- fetch()에 revalidate, cookies(), headers() 등이 없는가?
- YES → SSG or ISR로 처리
- NO → SSR 대상 (Dynamic Rendering) 으로 분류
- fetch()에 revalidate, cookies(), headers() 등이 없는가?
1️⃣ [서버: HTML 생성]
- 서버는 React 컴포넌트를 실행하여 초기 HTML을 생성함
- 이 HTML은 사용자가 보는 페이지의 기본 뼈대가 됨
2️⃣ [클라이언트: HTML 수신 및 렌더링]
- 브라우저는 먼저 HTML을 받아 화면에 표시함
- 이 시점에는 이벤트 핸들러나 동적 인터랙션은 아직 불가능함
3️⃣ [JS 번들 청크 로딩 시작]
- 브라우저는 HTML 내부 <script> 태그를 통해 필요한 JavaScript 파일들을 청크 단위로 요청
- Webpack에 의해 자동으로 쪼개진 번들(app/products/page.js, Button.client.js 등)을 로드함
- 청크Next.js는 경로별, 컴포넌트별로 JS 파일을 분할(chunking) 함👉 이 덕분에
- 필요한 컴포넌트만 로드함 → 초기 로딩 속도 향상
- 서버 컴포넌트는 번들 자체가 없음 → 전송되지 않음번들 분할 이유</aside>
- 👉 초기 로딩 최적화: code splitting
- <aside> ➕
- /_next/static/chunks/app/products/page-9ab3f.js /_next/static/chunks/Button.client-f20c9.js /_next/static/chunks/vendors-node_modules_react.js
- 🧱 청크(chunk)의 예시
- 청크Next.js는 경로별, 컴포넌트별로 JS 파일을 분할(chunking) 함👉 이 덕분에
4️⃣ [Hydration (Client Component만)]
- React가 해당 HTML에 대응하는 컴포넌트를 찾아 이벤트 연결, 상태 초기화 등을 진행
- 이 과정이 완료되면 사용자 인터랙션이 가능해짐