Next.js 링크와 내비게이션 (Linking and Navigating)
Next.js 공식 문서 Getting Started: Linking and Navigating 내용을 바탕으로, App Router 기준의 내비게이션 동작 원리와 튜닝 포인트를 정리한 자료입니다. citeturn4view0
1. 개요
- Next.js에서는 라우트가 기본적으로 서버에서 렌더링됩니다.
- 이 때문에 새 라우트를 보여 주기 전에 클라이언트가 서버 응답을 기다려야 하는 지연 시간이 발생할 수 있습니다.
- Next.js는 이를 줄이기 위해 다음 기능을 기본 제공하여 빠르고 부드러운 내비게이션을 제공합니다. citeturn4view0
- Prefetching
- Streaming
- Client-side transitions
이 문서에서는 위 기능들이 어떻게 동작하는지, 그리고 동적 라우트나 느린 네트워크 환경에서 어떻게 최적화할 수 있는지를 설명합니다. citeturn4view0
2. 내비게이션 동작 방식 (How navigation works)
Next.js 내비게이션을 이해하려면 다음 네 가지 개념을 알아두면 좋습니다. citeturn4view0
- Server Rendering
- Prefetching
- Streaming
- Client-side transitions
2.1 Server Rendering
- App Router에서 Layouts, Pages는 기본적으로 React Server Component입니다.
- 초기 접속과 그 이후의 내비게이션 모두, 서버에서 Server Component Payload를 생성한 뒤 클라이언트로 전송합니다. citeturn4view0
- 서버 렌더링에는 두 가지 방식이 있습니다.
- Static Rendering (Prerendering)
- 빌드 타임 또는 revalidation 시점에 실행되고,
- 결과가 캐시에 저장됩니다.
- Dynamic Rendering
- 클라이언트 요청이 들어올 때마다 서버에서 렌더링합니다.
- Static Rendering (Prerendering)
정리: 서버 렌더링의 단점은, 새 라우트를 보여 주기 전에 항상 서버 응답을 기다려야 한다는 점입니다. Next.js는 이것을 prefetching + client-side transitions로 보완합니다. citeturn4view0
2.2 Prefetching
Prefetching은 사용자가 실제로 이동하기 전에 백그라운드에서 미리 라우트 데이터를 가져오는 과정입니다. citeturn4view0
- 사용자가 링크를 클릭할 때쯤에는 이미 데이터가 준비되어 있어, 거의 즉시 페이지가 전환되는 느낌을 줍니다.
- Next.js는
next/link의<Link>컴포넌트로 연결된 라우트를, 뷰포트에 들어오거나 hover 되었을 때 자동으로 prefetch합니다. citeturn4view0
// app/layout.tsx
import Link from 'next/link'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<nav>
{/* 뷰포트에 들어오거나 hover되면 자동 prefetch */}
<Link href="/blog">Blog</Link>
{/* a 태그는 prefetch 없음 */}
<a href="/contact">Contact</a>
</nav>
{children}
</body>
</html>
)
}
얼마나 prefetch하는지는 라우트 유형에 따라 다릅니다. citeturn4view0
- 정적 라우트 (Static Route): 전체 라우트를 prefetch
- 동적 라우트 (Dynamic Route): prefetch를 건너뛰거나, 해당 경로에
loading.tsx가 있을 경우 부분 prefetch
요약: 동적 라우트는 필요 이상으로 서버 작업을 하지 않기 위해 prefetch 범위를 줄입니다. 다만 이 경우 **“클릭 후 잠깐 멈춘 것 같은 느낌”**을 줄 수 있어, 아래의 Streaming과 함께 사용하는 것이 중요합니다. citeturn4view0
2.3 Streaming
Streaming은 서버가 전체 페이지를 다 렌더링할 때까지 기다리지 않고, 준비된 부분부터 순차적으로 클라이언트에 전송하는 방식입니다. citeturn4view0
- 사용자는 일부 UI라도 빨리 볼 수 있어 체감 속도가 빨라집니다.
- 특히 동적 라우트에서 유용합니다.
- 공통 레이아웃 영역과 **로딩 스켈레톤(loading skeleton)**을 미리 요청해 둘 수 있습니다. citeturn4view0
Streaming을 사용하려면 해당 라우트 폴더에 loading.tsx 파일을 생성합니다. citeturn4view0
// app/dashboard/loading.tsx
export default function Loading() {
// 이 UI는 라우트가 로딩되는 동안 먼저 보여집니다.
return <LoadingSkeleton />
}
- Next.js는 내부적으로
page.tsx내용을<Suspense>로 감싸고, - prefetch된
loading.tsxUI를 먼저 보여준 뒤, 실제 데이터가 준비되면 최종 콘텐츠로 교체합니다. citeturn4view0
loading.tsx의 장점 citeturn4view0
- 내비게이션 직후 즉시 피드백 제공
- 공통 레이아웃은 그대로 인터랙티브 상태 유지, 내비게이션 중단도 가능
- Core Web Vitals 지표(TTFB, FCP, TTI) 개선
2.4 Client-side transitions
전통적인 서버 렌더링 내비게이션은: citeturn4view0
- 전체 페이지 리로드가 발생하고,
- 상태가 초기화되며,
- 스크롤 위치가 리셋되고,
- 잠시 동안 상호작용이 막힙니다.
Next.js는 <Link> 컴포넌트를 통한 **클라이언트 사이드 전환(client-side transition)**으로 이를 피합니다. citeturn4view0
- 페이지를 리로드하지 않고, 동적으로 콘텐츠만 교체합니다.
- 그 과정에서:
- 공통 레이아웃 및 UI는 유지하고,
- 현재 페이지를 prefetch된 로딩 상태 또는 새 페이지로 교체합니다.
정리: 서버 렌더링 앱이지만, 클라이언트 렌더링 앱 같은 부드러운 라우팅 경험을 제공하며, prefetching과 streaming을 함께 사용하면 동적 라우트에서도 빠른 전환이 가능합니다. citeturn4view0
3. 전환이 느려지는 원인과 대처법 (What can make transitions slow?)
기본 최적화가 있어도, 아래와 같은 경우 전환이 여전히 느리게 느껴질 수 있습니다. 문서에서 제시하는 대표 원인은 다음과 같습니다. citeturn4view0
3.1 loading.tsx가 없는 동적 라우트
- 동적 라우트로 이동할 때, 서버 응답이 올 때까지 보여줄 UI가 없으면 사용자는 앱이 멈춘 것처럼 느낄 수 있습니다. citeturn4view0
- 해결책:
- 해당 동적 라우트(예:
app/blog/[slug]) 폴더에loading.tsx를 추가합니다.
- 해당 동적 라우트(예:
// app/blog/[slug]/loading.tsx
export default function Loading() {
return <LoadingSkeleton />
}
팁: 개발 모드에서는 Next.js Devtools로 해당 라우트가 정적인지(dynamic/static) 확인할 수 있습니다. citeturn4view0
3.2 generateStaticParams가 없는 동적 세그먼트
- 어떤 동적 세그먼트는 사실 빌드 타임에 **미리 렌더링(prerender)**할 수 있습니다.
- 그런데
generateStaticParams가 없으면, Next.js는 해당 라우트를 요청 시점에 동적으로 렌더링하게 됩니다. citeturn4view0 - 해결책:
generateStaticParams를 정의하여 빌드 시점에 가능한 경로를 모두 생성합니다.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
// ...
}
정리: 미리 알 수 있는 동적 경로는
generateStaticParams를 통해 정적 생성 → 캐시 활용을 하는 것이 전환 속도에 유리합니다. citeturn4view0
3.3 느린 네트워크 (Slow networks)
- 네트워크가 느리거나 불안정한 경우, prefetch가 클릭 전에 완료되지 않을 수 있습니다.
- 이때 정적/동적 라우트 모두에서
loading.tsxUI가 바로 보이지 않을 수 있음에 주의해야 합니다. citeturn4view0
이럴 때는 useLinkStatus 훅을 사용하여 전환 상태에 맞는 즉각적인 피드백을 줄 수 있습니다. citeturn4view0
// app/ui/loading-indicator.tsx
'use client'
import { useLinkStatus } from 'next/link'
export default function LoadingIndicator() {
const { pending } = useLinkStatus()
return (
<span
aria-hidden
className={`link-hint ${pending ? 'is-pending' : ''}`}
/>
)
}
- CSS에서 **animation-delay (예: 100ms)**와 opacity 0 → 1을 활용해,
- **일정 시간 이상 걸리는 전환에만 로딩 힌트를 보여주도록 “디바운스”**할 수 있습니다. citeturn4view0
3.4 Prefetch 비활성화 (Disabling prefetching)
<Link>에서 **prefetch={false}**를 설정하면 prefetch를 끌 수 있습니다. citeturn4view0
<Link prefetch={false} href="/blog">
Blog
</Link>
- 장점:
- 매우 많은 링크(예: 무한 스크롤 테이블)에서 불필요한 리소스 사용을 줄일 수 있음
- 단점: citeturn4view0
- 정적 라우트도 클릭 시점에야 데이터를 가져오게 됨
- 동적 라우트는 이동 전에 항상 서버 렌더링을 기다려야 함
Hover 시에만 Prefetch 하기
완전히 끄기보다는, hover 시점에만 prefetch하여 리소스 사용과 체감 속도 사이에서 타협할 수도 있습니다. citeturn4view0
// app/ui/hover-prefetch-link.tsx
'use client'
import Link from 'next/link'
import { useState } from 'react'
function HoverPrefetchLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
const [active, setActive] = useState(false)
return (
<Link
href={href}
prefetch={active ? null : false}
onMouseEnter={() => setActive(true)}
>
{children}
</Link>
)
}
- 뷰포트에 보이는 모든 링크를 prefetch하는 대신,
- 사용자가 실제로 관심을 보인(hover한) 링크만 선택적으로 prefetch합니다.
3.5 Hydration이 끝나지 않은 상태 (Hydration not completed)
<Link>는 Client Component이므로, 하이드레이션이 완료되어야 prefetch를 시작할 수 있습니다.- 초기 방문 시 JS 번들이 너무 크면, 하이드레이션이 지연되고 prefetch 시작도 늦어질 수 있습니다. citeturn4view0
개선 방법: citeturn4view0
@next/bundle-analyzer플러그인으로 번들 크기를 분석하고, 불필요하게 큰 의존성을 줄입니다.- 가능한 로직은 클라이언트 대신 서버 컴포넌트로 옮겨 클라이언트 JS 부담을 줄입니다.
4. 예제: Native History API 활용
Next.js에서는 브라우저의 기본 History API를 사용해도 페이지 리로드 없이 주소를 변경할 수 있습니다.
이때 호출은 Next.js Router와 통합되어, usePathname, useSearchParams와도 연동됩니다. citeturn4view0
4.1 window.history.pushState
- 새 히스토리 엔트리를 추가합니다.
- 사용자는 브라우저 뒤로 가기 버튼으로 이전 상태로 돌아갈 수 있습니다.
예: 상품 목록 정렬 기준을 쿼리스트링으로 반영하기 citeturn4view0
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const params = new URLSearchParams(searchParams.toString())
params.set('sort', sortOrder)
window.history.pushState(null, '', `?${params.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Sort Ascending</button>
<button onClick={() => updateSorting('desc')}>Sort Descending</button>
</>
)
}
4.2 window.history.replaceState
- 현재 히스토리 엔트리를 교체합니다.
- 사용자는 이전 상태로 되돌아갈 수 없습니다. citeturn4view0
예: 현재 pathname을 기준으로 **언어(locale)**를 변경하기 citeturn4view0
'use client'
import { usePathname } from 'next/navigation'
export function LocaleSwitcher() {
const pathname = usePathname()
function switchLocale(locale: string) {
// 예: '/en/about', '/fr/contact'
const newPath = `/${locale}${pathname}`
window.history.replaceState(null, '', newPath)
}
return (
<>
<button onClick={() => switchLocale('en')}>English</button>
<button onClick={() => switchLocale('fr')}>French</button>
</>
)
}
5. 정리
이 문서에서 다룬 핵심 포인트를 요약하면 다음과 같습니다.
- 서버 렌더링 + Prefetch + Streaming + Client-side transitions 조합으로
- 서버 렌더링 앱이지만 클라이언트 앱처럼 빠른 전환을 제공한다.
- 동적 라우트에서는
loading.tsx로 부분 prefetch + 로딩 UI를 제공하고,generateStaticParams로 가능한 경로는 빌드 타임에 정적 생성하는 것이 좋다.
- 리소스 최적화가 필요할 때는
prefetch={false}또는 hover 시 prefetch 전략으로 제어할 수 있다.
- 느린 네트워크나 큰 번들로 인해 전환이 느릴 수 있으므로
useLinkStatus,@next/bundle-analyzer, Server Component 활용 등으로 체감 속도와 번들 크기를 함께 관리해야 한다.
- 필요하다면 브라우저 History API(
pushState,replaceState)를 활용하여- 쿼리스트링이나 locale 변경 등을 라우터와 자연스럽게 연동할 수 있다.
Next.js 내비게이션을 튜닝할 때, 이 문서를 스터디에서 **“내비게이션 구조와 최적화 포인트 체크리스트”**처럼 활용하시면 좋습니다.