프리페칭(Prefetching)은 애플리케이션 내에서 다른 경로(페이지)로 이동할 때 마치 즉각적으로 이동하는 것처럼 느끼게 만들어주는 마법 같은 기술이에요. Next.js는 여러분의 애플리케이션 코드에 사용된 링크들을 기반으로, 기본적으로 아주 똑똑하게 프리페칭을 시도한답니다.
이 가이드에서는 프리페칭이 내부적으로 어떻게 작동하는지 설명하고, 실무에서 자주 쓰이는 일반적인 구현 패턴들을 보여드릴 거예요.
사용자가 다른 라우트(페이지)로 이동할 때, 브라우저는 보통 새 페이지를 그리기 위해 HTML이나 JavaScript 파일 같은 자원(assets)을 요청하게 됩니다. 여기서 프리페칭(Prefetching)이란, 여러분이 새로운 라우트로 실제로 이동하기 이전에 미리 이러한 리소스들을 가져오는(fetching) 과정을 말해요.
💡 강사의 보충 설명:
여러분, 전통적인 SPA(Single Page Application)인 순수 React를 생각해 볼까요? 초기 로딩 때 거대한 자바스크립트 덩어리(번들)를 한 번에 다 다운받아야 해서 첫 로딩이 꽤 느렸죠. 하지만 Next.js는 다릅니다!
Next.js는 라우트(경로)를 기준으로 애플리케이션을 자동으로 작은 JavaScript 조각(청크)들로 나눕니다(이를 Code Splitting이라고 해요). 전통적인 SPA처럼 모든 코드를 초기에 한꺼번에 불러오는 대신, 현재 라우트에 필요한 코드만 먼저 불러옵니다. 이렇게 하면 초기 로딩 시간을 엄청나게 줄일 수 있고, 그동안 백그라운드에서는 앱의 다른 부분들을 조용히 불러오게 됩니다.
결과적으로, 사용자가 링크를 클릭할 때쯤이면 새 라우트에 필요한 리소스가 이미 브라우저 캐시에 모두 다운로드되어 있는 상태가 됩니다.
새로운 페이지로 이동할 때, 화면이 전체적으로 깜빡이면서 새로고침(full page reload)되거나 브라우저 탭에 빙글빙글 도는 로딩 스피너가 나타나지 않아요. 대신 Next.js는 클라이언트 사이드 전환(client-side transition)을 수행해서, 페이지 이동이 마치 앱처럼 순식간에 일어나는 것처럼 느끼게 해줍니다.
Next.js에서는 페이지가 정적인지 동적인지에 따라 프리페칭 동작 방식이 다릅니다. 아래 표를 잘 살펴볼까요?
| 정적 페이지 (Static page) | 동적 페이지 (Dynamic page) | |
|---|---|---|
| 프리페치 범위 (Prefetched) | 예, 라우트 전체 (Yes, full route) | 아니요, 단 loading.js 파일이 있는 경우는 예외 |
| 클라이언트 캐시 수명 (Client Cache TTL) | 5분 (기본값) | 꺼짐(Off), 단 활성화(enabled)한 경우는 예외 |
| 클릭 시 서버 왕복 (Server roundtrip on click) | 아니요 (No) | 예, 쉘(shell) 이후에 스트리밍됨 |
알아두면 좋은 정보 (Good to know): > 사용자가 웹사이트에 처음 진입(초기 내비게이션)할 때 브라우저는 HTML, JavaScript, 그리고 React Server Components (RSC) 페이로드를 가져옵니다. 그 이후의 내비게이션부터는, 브라우저가 서버 컴포넌트용 RSC 페이로드와 클라이언트 컴포넌트용 JS 번들만 쏙쏙 골라서 가져오게 됩니다.
💡 강사님의 실무 팁 🎯:
실무에서 동적 페이지(Dynamic page)는 사용자마다 다른 데이터를 보여줘야 하므로 전체를 미리 다 가져올 수 없어요. 대신loading.js를 사용해 로딩 UI(뼈대 화면 등)까지만 미리 가져오고, 클릭하는 순간 서버에 진짜 데이터를 요청해서 부드럽게 화면을 채워줍니다. 이 차이를 명확히 아셔야 네트워크를 효율적으로 쓸 수 있어요!
Next.js에서 <Link> 컴포넌트를 사용하면 기본적으로 이 자동 프리페칭이 적용됩니다.
import Link from 'next/link'
export default function NavLink() {
return <Link href="/about">About</Link>
}
import Link from 'next/link'
export default function NavLink() {
return <Link href="/about">About</Link>
}
이때, loading.js의 유무에 따라 미리 가져오는 데이터의 범위가 달라집니다.
| 상황 (Context) | 프리페치되는 페이로드 (Prefetched payload) | 클라이언트 캐시 수명 (Client Cache TTL) |
|---|---|---|
loading.js가 없을 때 | 페이지 전체 (Entire page) | 앱이 새로고침 될 때까지 (Until app reload) |
loading.js가 있을 때 | 첫 번째 로딩 경계선(boundary)까지의 레이아웃 | 30초 (설정 가능(configurable)) |
자동 프리페칭은 프로덕션(production) 환경에서만 실행됩니다. (개발 모드에서는 체감하기 어려울 수 있어요!) 프리페칭을 끄고 싶다면 prefetch={false} 속성을 추가하거나, 아래에 나올 프리페칭 비활성화 (Disabled Prefetch) 섹션에 있는 래퍼(wrapper) 컴포넌트를 사용하시면 됩니다.
코드를 통해 수동으로 프리페칭을 지시하려면 next/navigation에서 제공하는 useRouter 훅을 불러온 뒤, router.prefetch() 메서드를 호출하면 됩니다. 이 방법은 화면(뷰포트) 바깥에 있는 라우트를 미리 데우거나(warm), 애널리틱스 응답, 마우스 오버(hover), 스크롤 등의 이벤트에 반응해서 프리페칭할 때 유용해요.
'use client'
import { useRouter } from 'next/navigation'
import { CustomLink } from '@components/link'
export function PricingCard() {
const router = useRouter()
return (
<div onMouseEnter={() => router.prefetch('/pricing')}>
{/* 다른 UI 요소들 */}
<CustomLink href="/pricing">View Pricing</CustomLink>
</div>
)
}
만약 컴포넌트가 로드될 때 특정 URL을 프리페칭하는 것이 목적이라면, 아래 나오는 '링크 확장 또는 이젝트' 예시를 참고해 주세요.
💡 강사님의 실무 팁 🎯:
수동 프리페칭은 언제 쓰일까요? 기본<Link>태그를 사용할 수 없는 경우, 예를 들어 복잡한 폼(Form)의 제출 버튼을 누르기 전에 유효성 검사를 하는 동안 다음 이동할 페이지를 미리 받아두고 싶을 때router.prefetch()를 쓰면 사용자가 지연 시간을 전혀 느끼지 못하게 할 수 있습니다!
⚠️ 주의해서 진행하세요 (Proceed with caution):
기본<Link>를 확장(Extending)한다는 것은, 여러분 스스로가 프리페칭 주기, 캐시 무효화(invalidation), 그리고 접근성 문제 등을 직접 책임지고 유지보수하겠다는 뜻입니다. Next.js의 기본 설정으로 충분하지 않을 때만 신중하게 진행하세요.
Next.js는 기본적으로 올바른 프리페칭을 하려고 노력하지만, 고급 사용자(power users)라면 필요에 따라 이 기능을 해제(eject)하고 수정할 수 있습니다. 성능(performance)과 리소스 소비(resource consumption) 사이에서 컨트롤을 할 수 있게 되는 거죠.
예를 들어, 기본 동작인 '링크가 화면(뷰포트)에 들어올 때'가 아니라 '사용자가 링크에 마우스를 올렸을 때(hover)'만 프리페칭을 실행하고 싶을 수 있습니다.
'use client'
import Link from 'next/link'
import { useState } from 'react'
export 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={null}은 사용자가 마우스를 올려 의도(intent)를 보였을 때, Next.js의 기본(정적) 프리페칭 동작으로 복원시키는 역할을 합니다.
여러분의 프로젝트에 맞게 나만의 맞춤형 프리페칭 전략을 세우기 위해 <Link> 컴포넌트를 확장할 수도 있습니다. 예를 들어 사용자의 마우스 커서 이동 방향을 예측해서 링크를 프리페칭해주는 ForesightJS 같은 외부 라이브러리를 사용할 수도 있겠죠.
다른 방법으로는, useRouter를 사용해서 네이티브 <Link>의 동작을 일부 재현할 수도 있습니다. 하지만 명심하세요, 이 방법을 선택하면 프리페칭과 캐시 무효화 처리를 여러분이 직접 관리해야 합니다.
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
function ManualPrefetchLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
const router = useRouter()
useEffect(() => {
let cancelled = false
const poll = () => {
if (!cancelled) router.prefetch(href, { onInvalidate: poll })
}
poll()
return () => {
cancelled = true
}
}, [href, router])
return (
<a
href={href}
onClick={(event) => {
event.preventDefault()
router.push(href)
}}
>
{children}
</a>
)
}
여기서 onInvalidate는 캐시된 데이터가 오래되었다(stale)고 Next.js가 판단할 때 호출되는 함수입니다. 이를 통해 프리페치를 새로고침(refresh)할 수 있습니다.
알아두면 좋은 정보 (Good to know): > 기본
<a>태그를 그냥 쓰면 브라우저는 도착지 라우트로 전체 페이지를 새로고침(full page navigation) 해버립니다. 그래서onClick이벤트에서event.preventDefault()를 사용해 전체 새로고침을 막고,router.push를 호출하여 부드럽게 이동하게 만들어야 해요.
리소스 소비를 훨씬 더 세밀하게 제어하고 싶다면 특정 라우트에 대한 프리페칭을 완전히 꺼버릴 수 있습니다.
'use client'
import Link, { LinkProps } from 'next/link'
function NoPrefetchLink({
prefetch,
...rest
}: LinkProps & { children: React.ReactNode }) {
return <Link {...rest} prefetch={false} />
}
예를 들어, 애플리케이션 전체에서 <Link>를 일관성 있게 사용하고는 싶지만, 화면 맨 아래(Footer)에 있는 링크들까지 화면에 보인다고 해서 굳이 프리페칭할 필요는 없을 수 있거든요.
💡 강사님의 실무 팁 🎯:
Footer 영역에 있는 '이용약관', '개인정보 처리방침', '회사소개' 같은 페이지들은 사용자들이 빈번하게 들어가지 않습니다. 이런 페이지들까지 뷰포트에 들어올 때마다 다운로드하면 불필요한 트래픽(돈!)이 낭비되겠죠. 실무에서는 이런 곳에 반드시prefetch={false}를 걸어두어 최적화를 합니다!
Next.js는 프리페치된 React Server Component(RSC) 페이로드를 메모리 안에 저장하며, 이때 라우트의 세그먼트(조각)들을 키(key)로 사용합니다. 만약 형제 라우트 간에 이동할 때(예를 들어 /dashboard/settings 에서 /dashboard/analytics로 이동), 부모 레이아웃(이 경우 /dashboard)은 재사용하고 변경된 말단(leaf) 페이지만 새로 가져옵니다. 이렇게 하면 네트워크 트래픽을 아끼고 화면 이동 속도가 획기적으로 개선됩니다.
Next.js 내부에 작은 작업 대기열(task queue)이 있어서, 다음과 같은 우선순위로 프리페칭을 진행합니다:
이 스케줄러 덕분에 다운로드 낭비를 최소화하면서도 사용자가 클릭할 확률이 가장 높은 곳들을 우선적으로 준비할 수 있습니다.
PPR 기능이 활성화되면, 페이지는 두 부분으로 나뉩니다. 정적인 뼈대(static shell)와 스트리밍 방식으로 채워지는 동적 섹션(streamed dynamic section)입니다:
revalidateTag나 revalidatePath 등에 의해)되면, 그와 연관된 프리페치된 정보들도 백그라운드에서 조용히 새로고침됩니다.이 부분은 정말 중요합니다! 실무에서 자주 마주치는 문제들이니 집중해 주세요.
만약 여러분의 레이아웃이나 페이지가 순수(pure)하지 않고 부수 효과(side-effects, 예를 들어 구글 애널리틱스 트래킹 등)를 가지고 있다면, 사용자가 실제로 그 페이지를 방문했을 때가 아니라 해당 라우트가 화면에 보여 프리페칭 될 때 이 효과가 실행되어 버리는 참사가 일어날 수 있습니다.
이런 불상사를 막기 위해서는 부수 효과를 useEffect 훅 내부로 이동시키거나, 클라이언트 컴포넌트에서 트리거되는 서버 액션(Server Action)으로 옮기셔야 합니다.
변경 전 (Before - 이렇게 하시면 안 됩니다!):
//filename="app/dashboard/layout.tsx" switcher
import { trackPageView } from '@/lib/analytics'
export default function Layout({ children }: { children: React.ReactNode }) {
// 경고: 사용자가 클릭도 안 했는데 프리페치 단계에서 이 코드가 실행되어 버립니다!
trackPageView()
return <div>{children}</div>
}
//filename="app/dashboard/layout.js" switcher
import { trackPageView } from '@/lib/analytics'
export default function Layout({ children }) {
// 경고: 사용자가 클릭도 안 했는데 프리페치 단계에서 이 코드가 실행되어 버립니다!
trackPageView()
return <div>{children}</div>
}
💡 강사님의 실무 팁 🎯:
이거 정말 많은 주니어 개발자분들이 놓치는 실수입니다! 메인 화면을 스크롤만 했는데, 아래쪽 링크들이 프리페치되면서 트래커가 작동해버리면 "방문자 수" 데이터가 완전히 뻥튀기되고 엉망이 되겠죠? 트래킹 코드는 반드시 실제로 렌더링(마운트)될 때 동작하게 만들어야 합니다.
변경 후 (After - 올바른 방법!):
이러한 로직을 처리하는 별도의 클라이언트 컴포넌트를 만들어 줍니다.
'use client'
import { useEffect } from 'react'
import { trackPageView } from '@/lib/analytics'
export function AnalyticsTracker() {
useEffect(() => {
// 이제 실제로 페이지가 브라우저에 마운트(사용자가 방문)될 때만 실행됩니다!
trackPageView()
}, [])
return null
}
'use client'
import { useEffect } from 'react'
import { trackPageView } from '@/lib/analytics'
export function AnalyticsTracker() {
useEffect(() => {
trackPageView()
}, [])
return null
}
그리고 레이아웃에서 그 컴포넌트를 불러와 사용합니다.
import { AnalyticsTracker } from '@/app/ui/analytics-tracker'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<AnalyticsTracker />
{children}
</div>
)
}
import { AnalyticsTracker } from '@/app/ui/analytics-tracker'
export default function Layout({ children }) {
return (
<div>
<AnalyticsTracker />
{children}
</div>
)
}
Next.js는 <Link> 컴포넌트를 쓸 때 화면에 보이는 링크들을 자동으로 싹 다 프리페치합니다.
하지만 리소스 낭비를 피하기 위해 이를 막고 싶은 상황도 분명 있습니다. 예를 들어 스크롤을 내릴수록 계속 데이터가 불러와지는 '무한 스크롤 게시판(infinite scroll table)'에 수백 개의 링크가 있다면 말이죠.
이럴 때는 <Link> 컴포넌트의 prefetch 속성을 false로 설정하여 프리페칭을 완전히 끌 수 있습니다.
<Link prefetch={false} href={`/blog/${post.id}`}>
{post.title}
</Link>
하지만, 주의하세요! 이렇게 설정하면 정적 라우트는 클릭할 때만 페칭되고, 동적 라우트는 화면을 이동하기 전 서버가 렌더링을 마칠 때까지 사용자가 멍하니 기다려야 할 수도 있습니다.
프리페칭 기능을 완전히 끄지 않으면서 리소스 사용량도 줄이려면 어떻게 해야 할까요? 바로 사용자가 링크 위에 마우스를 올릴 때(hover)까지 프리페칭을 지연(defer)시키는 겁니다. 이렇게 하면 사용자가 진짜 방문할 것 같은 링크들만 전략적으로 페칭할 수 있어요.
'use client'
import Link from 'next/link'
import { useState } from 'react'
export 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>
)
}
'use client'
import Link from 'next/link'
import { useState } from 'react'
export function HoverPrefetchLink({ href, children }) {
const [active, setActive] = useState(false)
return (
<Link
href={href}
prefetch={active ? null : false}
onMouseEnter={() => setActive(true)}
>
{children}
</Link>
)
}
모든 문서의 의미론적(semantic) 개요를 보려면, https://nextjs.org/docs/sitemap.md를 참조하세요.
사용 가능한 모든 문서의 인덱스를 보려면, https://nextjs.org/docs/llms.txt를 참조하세요.