Navigation
Next.js에서는 페이지 이동을 위해 기본 HTML의 <a> 태그 대신, 클라이언트 사이드 네비게이션을 지원하는 전용 기능을 제공한다.
대표적으로 다음 두 가지 방식이 있다.
Link-> 선언적 네비게이션(Declarative Navigation)useRouter-> 프로그래매틱 네비게이션(Programmatic Navigation)
Page Router와 App Router 모두 기본적인 사용 목적은 비슷하지만,useRouter를 제공하는 모듈과 내부 라우팅·렌더링 구조에는 차이가 있다.
| 구분 | Page Router | App Router |
|---|---|---|
| Link | next/link | next/link |
| useRouter | next/router | next/navigation |
| 라우팅 기준 | pages | app |
| Server Component | 지원하지 않음 | 지원 |
| Layout | _app.tsx, 별도 패턴 활용 | layout.tsx |
| 부분 렌더링 | 기본 구조에 포함되지 않음 | Route Segment 단위로 지원 |
1. <a> 태그와 Client-side Navigation
기본 HTML에서는 <a> 태그를 사용해 다른 페이지로 이동한다.
<a href="/test">이동</a>일반적인 <a> 이동은 새로운 문서를 요청하면서 전체 페이지가 다시 로드된다.
이 과정에서 현재 React 애플리케이션의 상태도 초기화될 수 있다.
반면 Next.js의 Link와 router.push()는 애플리케이션 내부 경로를 이동할 때 전체 문서를 새로 로드하지 않고 필요한 페이지 리소스만 가져와 화면을 갱신하는 클라이언트 사이드 네비게이션을 수행할 수 있다.
따라서 페이지 전체를 다시 로드하는 대신 필요한 UI만 갱신하면서 보다 자연스러운 화면 전환을 구현할 수 있다.
2. Link
Next.js는 페이지 이동을 위한 내장 Link 컴포넌트를 제공한다.
import Link from "next/link";
export default function Page() {
return <Link href="/test">이동</Link>;
}Link는 JSX 구조 안에 어디로 이동할 것인지 직접 선언한다.
이처럼 이동 목적지를 UI에 선언하는 방식을 선언적 네비게이션이라고 한다.
ex. 메뉴 / 내비게이션 바 / 게시글 목록 / 상품 목록 / 상세 페이지 이동 버튼
💡 사용자의 클릭 목적이 단순한 페이지 이동이라면 Link를 우선적으로 사용하는 것이 좋다.
Link는 기본적인 링크의 의미와 동작을 유지하면서,
Next.js의 클라이언트 사이드 네비게이션과 Prefetching 기능을 활용할 수 있기 때문이다.
<Link href="/posts/1">게시글 보기</Link>단순한 페이지 이동을 다음과 같이 구현하는 것도 가능하지만,
<button onClick={() => router.push("/posts/1")}>게시글 보기</button>이동 자체가 목적이라면 Link가 해당 UI의 의미를 더 명확하게 표현한다.
Link의 옵션
Link는 이동 방식을 세부적으로 제어할 수 있는 몇 가지 옵션을 제공한다.
<Link href="/test" replace>
이동
</Link>replace를 사용하면 router.replace()처럼 현재 History Entry를 새 경로로 교체한다.
즉, 이동 후 브라우저 뒤로 가기를 눌러도 이전 페이지로 돌아가지 않는다.
<Link href="/test" scroll={false}>
이동
</Link>scroll은 페이지 이동 시 스크롤을 맨 위로 올릴지 여부를 결정한다.
기본값은 true이며, 탭 전환처럼 스크롤 위치를 유지하고 싶은 UI에서는 false로 설정할 수 있다.
Link의 Prefetching
Link의 중요한 특징 중 하나는 Prefetching을 지원한다는 것이다.
Prefetching은 사용자가 실제 페이지로 이동하기 전에 해당 경로에 필요한 리소스를 미리 준비하는 방식이다.
<Link href="/test">이동</Link>Next.js는 Link가 사용자의 뷰포트에 들어오는 등의 조건을 만족하면 연결된 경로를 미리 가져올 수 있다.
따라서 사용자가 실제 링크를 클릭했을 때 처음부터 모든 리소스를 요청하는 것보다 빠르게 화면을 전환할 수 있다. Prefetch를 비활성화할 수도 있다.
// prefetch 비활성화
<Link href="/test" prefetch={false}>
이동
</Link>Prefetching의 세부 동작은 Page Router와 App Router에서 차이가 있으며,
특히 App Router에서는 Static Route와 Dynamic Route에 따라 Prefetch 범위가 달라진다.
3. Programmatic Navigation - useRouter
모든 페이지 이동이 링크 클릭으로 발생하는 것은 아니다.
예를 들어 다음과 같은 상황에서는 특정 로직이 수행된 이후 코드에서 직접 페이지를 변경해야 한다.
- 로그인 성공 후 페이지 이동
- 폼 제출 완료 후 이동
- 특정 조건이 만족되었을 때 이동
- 이벤트 처리 후 이동
- 비동기 작업 완료 후 이동
이처럼 JavaScript 로직을 통해 페이지를 이동하는 방식을 Programmatic Navigation이라고 한다. Next.js에서는 useRouter를 이용해 이를 처리할 수 있다.
3-1. Page Router의 useRouter
Page Router에서는 next/router에서 useRouter를 가져온다.
import { useRouter } from "next/router";
export default function Page() {
const router = useRouter();
const handleClick = () => {
router.push("/test");
};
return <button onClick={handleClick}>이동</button>;
}router.push()는 새로운 경로로 이동하면서 해당 경로를 브라우저의 History Stack에 추가한다.
따라서 이동 후 브라우저의 뒤로 가기를 사용하면 이전 페이지로 돌아갈 수 있다.
🧩 Page Router의 주요 메서드
router.push(): 새로운 경로로 이동하면서 히스토리를 추가한다.router.replace(): 현재 히스토리를 새로운 경로로 교체한다.tsxrouter.replace("/success");push()와 달리 현재 URL을 새로운 URL로 교체하기 때문에 해당 이동에 대한 새로운 History Entry가 추가되지 않는다. 예를 들어 로그인 완료 페이지나 폼 제출 완료 페이지처럼 이전 단계로 다시 이동시키고 싶지 않은 경우 활용할 수 있다.router.back(): 브라우저 히스토리를 기준으로 이전 페이지로 이동한다.
3-2. App Router의 useRouter
App Router에서는 useRouter를 next/navigation에서 가져온다.
또한 useRouter는 React Hook이므로 Client Component에서 사용해야 한다.
"use client";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
const handleClick = () => {
router.push("/test");
};
return <button onClick={handleClick}>이동</button>;
}기본적인 페이지 이동 방법은 Page Router와 비슷하다.
router.push("/test");
router.replace("/test");
router.back();하지만 App Router에서는 Page Router의 router 객체가 담당하던 여러 URL 관련 기능이 별도의 Hook으로 분리되었다.
import { usePathname, useRouter, useSearchParams } from "next/navigation";각 Hook의 역할은 다음과 같다.
useRouter()→ 페이지 이동usePathname()→ 현재 pathname 확인useSearchParams()→ Query String 확인
Page Router에서는 router.pathname, router.query처럼 하나의 router 객체에서 여러 정보를 확인했다면, App Router에서는 목적에 따라 Hook이 분리되어 있다는 차이가 있다.
⚠️ useSearchParams()는 Suspense Boundary가 필요하다
useSearchParams()를 사용하는 컴포넌트는 Suspense로 감싸지 않으면 빌드 시 에러가 발생할 수 있다.
import { Suspense } from "react";
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchBar />
</Suspense>
);
}이는 Next.js가 Static Route에서도 Query String에 접근하는 부분을 별도로 처리하기 때문이다.useRouter(), usePathname()은 이 제약이 없다.
4. Link vs router.push
Link와 router.push() 모두 클라이언트 사이드 네비게이션을 수행할 수 있지만 사용 목적이 다르다.
Link
화면 자체에 이동 목적지가 존재하는 경우 사용한다.
<Link href="/posts/1">게시글 보기</Link>- 선언적 네비게이션
- 일반적인 페이지 링크에 적합
- Prefetching 지원
- 링크라는 UI 의미를 명확하게 표현
router.push()
특정 로직이 수행된 결과로 페이지를 이동해야 하는 경우 사용한다.
const handleSubmit = async () => {
await submitForm();
router.push("/complete");
};- 이벤트 처리 이후 이동
- 조건 기반 이동
- 비동기 작업 완료 후 이동
- 로그인 완료 후 이동
- 폼 제출 이후 이동
💡 즉, 이렇게 생각하면 이해하기 쉽다!
- 사용자가 클릭할 수 있는 링크, 특정 페이지로 이동하는 것 자체가 목적 →
Linktsx<Link href="/mypage">마이페이지</Link> - 어떤 로직이 수행된 결과로 페이지를 이동 →
router.push()tsxawait login(); router.push("/mypage");
5. App Router Navigation
Page Router와 App Router 모두 클라이언트 사이드 네비게이션을 지원하지만
App Router에서는 React Server Components를 기반으로 렌더링 구조가 크게 달라졌다.
App Router의 Navigation을 이해하려면 다음 개념들을 함께 볼 필요가 있다.
Server Rendering
↓
RSC Payload
↓
Prefetching
↓
Streaming
↓
Client-side Transition이 기능들이 결합되어 서버에서 렌더링되는 애플리케이션에서도 빠르고 자연스러운 페이지 이동을 제공한다.
5-1. Navigation의 내부 동작
App Router에서 클라이언트 사이드 네비게이션이 발생하면 Next.js는 기존 페이지 전체를 다시 로드하지 않는다. 대신 새로운 경로에 필요한 정보를 가져와 기존 화면과 결합한다.
이 과정에서 크게 다음과 같은 리소스가 사용된다.
Client Component→ JavaScriptServer Component→ RSC Payload
Client Component는 브라우저에서 실행되어야 하기 때문에 JavaScript가 필요하다.
반면 Server Component는 서버에서 실행되므로 해당 컴포넌트의 JavaScript 코드를 그대로 브라우저에서 실행할 필요가 없다.
서버에서 처리된 결과가 RSC Payload 형태로 전달되고, React는 이를 Client Component와 결합해 필요한 UI를 갱신한다.
5-2. Server Component와 Client Component
App Router에서는 page.tsx와 layout.tsx가 기본적으로 Server Component다.
컴포넌트는 크게 다음과 같이 구분할 수 있다.
Server Component→ 서버에서 실행Client Component→ 브라우저에서 실행
클라이언트에서는 JavaScript를 통해 Hydration되고 상호작용을 담당
Client Component는 다음과 같이 사용자 상호작용이나 브라우저 기능이 필요한 경우 사용한다.
ex. useState / useEffect / onClick / onChange / window / localStorage
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}반대로 Server Component는 브라우저에서 실행하기 위한 컴포넌트 JavaScript를 클라이언트 번들에 추가할 필요가 없다.
이를 통해 다음과 같은 이점을 기대할 수 있다.
- 클라이언트 JavaScript 감소
- JavaScript 파싱 및 실행 비용 감소
- 브라우저 메인 스레드 부담 감소
- 서버에서 데이터 소스에 직접 접근 가능
즉, Server Component의 중요한 특징 중 하나는
클라이언트에서 실행해야 하는 JavaScript의 범위를 줄일 수 있다는 것이다.
5-3. RSC Payload
Server Component의 렌더링 결과를 클라이언트로 전달하기 위해 RSC Payload(React Server Component Payload)가 사용된다.
RSC Payload는 React Server Component Tree의 렌더링 결과를 표현하는 데이터 형식이다.
여기에는 대표적으로 다음 정보가 포함된다.
- Server Component의 렌더링 결과
- Client Component가 렌더링될 위치
- Client Component의 JavaScript 파일에 대한 참조
- Server Component에서 Client Component로 전달되는 Props
즉, Server Component의 JavaScript를 그대로 브라우저에서 실행하는 것이 아니라 서버에서 처리한 결과와 필요한 참조 정보를 클라이언트에 전달한다.
Server Component
↓
서버에서 렌더링
↓
RSC Payload 생성
↓
클라이언트로 전달
↓
Client Component Tree와 조합
↓
UI 업데이트Initial Load
App Router의 첫 페이지 요청에서는 서버에서 HTML과 RSC Payload가 함께 활용된다.
서버
│
├─ Server Component 렌더링
│
├─ RSC Payload 생성
│
└─ HTML 생성
↓
브라우저브라우저에서는 먼저 HTML을 이용해 화면을 표시한다.
이후 RSC Payload를 이용해 Server Component와 Client Component Tree를 조정하고, JavaScript를 이용해 Client Component를 Hydration하여 상호작용 가능한 상태로 만든다.
Subsequent Navigation
이미 애플리케이션에 들어온 이후 Link 등을 통해 다른 경로로 이동할 때는 전체 HTML 문서를 다시 요청하지 않는다. 대신 Next.js는 새로운 경로에 필요한 RSC Payload 등을 활용해 현재 화면을 업데이트한다.
현재 페이지
↓
Link 클릭
↓
새 Route에 필요한 정보 사용
↓
RSC Payload와 Client Component 결합
↓
필요한 UI 업데이트이것이 App Router가 Server Component를 사용하면서도 SPA와 비슷하게 부드러운 페이지 이동을 제공할 수 있는 이유 중 하나다.
5-4. Prefetching
App Router에서도 Link를 사용하면 Prefetching을 사용할 수 있다.
<Link href="/posts/1">게시글 보기</Link>Link가 사용자의 뷰포트에 들어오면 Next.js는 해당 경로를 백그라운드에서 미리 가져올 수 있다. 하지만 모든 Route가 동일한 범위로 Prefetch되는 것은 아니다.
Static Route
정적으로 생성할 수 있는 Route는 전체 Route를 Prefetch할 수 있다.
Link가 Viewport에 진입 -> Route Prefetch -> 사용자 클릭 -> 준비된 Route 사용이미 필요한 정보가 클라이언트에 준비되어 있기 때문에 빠른 페이지 전환이 가능하다.
Dynamic Route
요청 시점에 서버 렌더링이 필요한 Dynamic Route는 기본적으로 Prefetch가 생략될 수 있다.
다만 해당 Route에 loading.tsx가 존재하면 일부 Route를 미리 Prefetch할 수 있다.
app
└── blog
└── [slug]
├── page.tsx
└── loading.tsx이 경우 공유되는 Layout이나 Loading UI 등을 미리 준비하고 실제 페이지 콘텐츠는 서버에서 준비되는 대로 받아올 수 있다. 이렇게 Dynamic Route 전체가 준비될 때까지 기다리지 않고 일부 UI를 먼저 준비할 수 있다.
Client-side Router Cache
Prefetch된 Route는 브라우저 메모리에 일정 시간 저장되는데, 이를 Client-side Router Cache라고 한다.
같은 Route로 다시 이동할 때 서버에 다시 요청하지 않고 캐시된 정보를 재사용할 수 있다.
- Static Route → 상대적으로 오래 캐시됨
- Dynamic Route → 상대적으로 짧게 캐시됨
💡 캐시 지속 시간(staleTime)은 Next.js 버전에 따라 계속 변경되어 왔으므로, 정확한 값은 사용 중인 버전의 공식 문서를 확인하는 것이 안전하다.
Prefetch가 체감 성능에 미치는 영향
Prefetching이 없다면 사용자가 링크를 클릭한 이후 필요한 Route를 가져오기 시작한다.
Click
↓
Route 요청
↓
응답 대기
↓
렌더링반면 Prefetching을 통해 필요한 정보가 미리 준비되어 있다면 다음과 같이 동작할 수 있다.
Prefetch
↓
Click
↓
준비된 정보 사용
↓
빠른 화면 전환사용자 입장에서는 다음과 같은 차이를 느낄 수 있다.
- 클릭 이후 반응 속도 향상
- 화면 전환 지연 감소
- 자연스러운 페이지 이동
Prefetching은 단순히 네트워크 요청을 미리 보내는 기능이 아니라 사용자가 페이지 이동을 기다리는 시간을 줄이는 Navigation 최적화 전략이라고 볼 수 있다.
5-5. Streaming
Dynamic Route는 데이터를 처리하거나 서버에서 렌더링하는 시간이 필요할 수 있다.
만약 페이지 전체가 완성될 때까지 기다린 후 응답한다면 사용자는 클릭 이후 한동안 아무 변화가 없는 것처럼 느낄 수 있다.
Click -> 서버에서 전체 Route 렌더링 -> 완료될 때까지 대기 -> 화면 표시App Router에서는 Streaming을 이용해 이러한 문제를 줄일 수 있다.Streaming은 전체 Route의 렌더링이 완료될 때까지 기다리지 않고 준비된 부분부터 클라이언트로 전달하는 방식이다.
Click -> Loading UI -> 준비된 UI부터 전달 -> 나머지 콘텐츠 전달App Router에서는 loading.tsx를 이용해 Route Segment에 Loading UI를 정의할 수 있다.
export default function Loading() {
return <div>Loading...</div>;
}Next.js는 내부적으로 페이지 콘텐츠를 Suspense Boundary로 감싸 Loading UI를 먼저 보여주고 실제 콘텐츠가 준비되면 이를 교체할 수 있다.
따라서 사용자에게 즉각적인 시각적 피드백을 제공하면서 서버 렌더링을 계속 진행할 수 있다.
5-6. Partial Rendering
App Router에서는 페이지 이동 시 모든 UI를 다시 렌더링하는 것이 아니라 변경된 Route Segment를 중심으로 갱신한다. 이를 Partial Rendering이라고 한다.
예를 들어 다음과 같은 구조가 있다고 가정
📁 app
└── dashboard
├── layout.tsx
├── settings
│ └── page.tsx
└── analytics
└── page.tsx사용자가 다음과 같이 이동한다면 /dashboard/settings -> /dashboard/analytics
두 Route 모두 dashboard/layout.tsx를 공유한다.
`dashboard/layout.tsx` → 유지
`settings/page.tsx` → 제거
`analytics/page.tsx` → 렌더링따라서 페이지 이동 시 공유 Layout은 유지되고 변경되는 Route Segment를 중심으로 화면이 갱신된다.
이러한 구조 덕분에 불필요한 UI 작업을 줄일 수 있으며 공유 Layout 내부의 상태와 사용자 인터랙션 역시 유지할 수 있다.
not-found.tsx와 error.tsx
Navigation 도중 발생할 수 있는 예외 상황도 Route Segment 단위로 처리된다.
not-found.tsx→ 존재하지 않는 경로로 이동했을 때 표시error.tsx→ 렌더링 중 에러가 발생했을 때 표시
app
└── blog
└── [slug]
├── page.tsx
├── loading.tsx
├── not-found.tsx
└── error.tsxloading.tsx가 Suspense Boundary로 로딩 상태를 감싸는 것처럼, error.tsx는 내부적으로 Error Boundary로 해당 Route Segment를 감싸 에러가 상위 트리 전체로 전파되지 않도록 한다.
5-7. Client-side Transition
일반적인 서버 렌더링 페이지로 이동하면 브라우저는 새로운 HTML 문서를 요청하는 Full Page Navigation을 수행한다.
Page A -> 새 문서 요청 -> Page BNext.js App Router에서는 Link를 이용한 애플리케이션 내부 이동 시 Client-side Transition을 수행한다.
Page A -> Link -> 공유 Layout 유지 -> 변경되는 UI 교체 -> Page B따라서 서버에서 렌더링되는 Route를 사용하면서도 전체 페이지를 새로 로드하지 않고 SPA와 유사한 부드러운 페이지 전환을 제공할 수 있다.
Prefetching과 Streaming까지 함께 사용하면 다음과 같은 구조가 된다.
Prefetch -> 사용자 Click -> Client-side Transition -> 공유 Layout 유지 -> Loading UI 표시 -> Server Rendering / Streaming -> 실제 콘텐츠로 교체즉, App Router의 빠른 Navigation은 하나의 기능 때문이 아니라 여러 기능이 함께 작동한 결과다.
정리
Next.js의 Navigation은 크게 두 가지 방식으로 나눌 수 있다.
Link→ 선언적 네비게이션useRouter→ 프로그래매틱 네비게이션
단순한 페이지 이동이라면 Link를 사용하고, 특정 로직이 수행된 결과로 이동해야 한다면 useRouter를 사용할 수 있다.
App Router에서는 Navigation 과정에 Server Component, RSC Payload, Streaming, Partial Rendering, Prefetching, Client-side Transition 등이 결합된다. 이를 통해 서버 중심의 렌더링 구조를 사용하면서도 전체 페이지를 매번 다시 로드하지 않고 필요한 Route Segment를 중심으로 화면을 갱신할 수 있다.
또한 Server Component를 활용해 브라우저에서 실행해야 하는 JavaScript의 범위를 줄이고, Prefetching과 Streaming을 통해 페이지 이동 과정에서 발생하는 사용자 대기 시간을 줄일 수 있다.