Linking and Navigating

김동현·2026년 3월 4일

Next.js에서는 기본적으로 모든 라우트(경로)가 서버에서 렌더링됩니다. 이 말은 즉, 새로운 화면을 보여주기 전에 클라이언트(브라우저)가 서버의 응답을 먼저 기다려야 한다는 뜻이죠. 일반적인 서버 렌더링 방식이라면 화면이 전환될 때마다 깜빡임이 생기고 느리게 느껴질 텐데요.

Next.js는 내비게이션이 빠르고 즉각적으로 반응하도록 보장하기 위해 프리패칭(prefetching), 스트리밍(streaming), 그리고 클라이언트 사이드 전환(client-side transitions) 이라는 강력한 기능들을 기본적으로 내장하고 있습니다.

이 가이드에서는 Next.js에서 내비게이션이 어떻게 작동하는지 설명하고, 동적 라우트(dynamic routes)느린 네트워크(slow networks) 환경에서 내비게이션을 어떻게 최적화할 수 있는지 알려드릴게요.

💡 강사의 부연 설명:
과거의 MPA(Multi Page Application) 방식은 링크를 클릭할 때마다 매번 서버에서 새로운 HTML을 통째로 받아와야 해서 화면이 하얗게 깜빡이는 현상(White Screen of Death)이 있었습니다. 반면 React 기반의 SPA(Single Page Application)는 화면 깜빡임 없이 부드럽게 전환되죠. Next.js는 기본적으로 서버에서 화면을 그리지만(SSR), 막상 화면을 이동할 때는 마치 SPA처럼 부드럽고 빠르게 동작합니다. 이 가이드는 바로 그 '마법 같은 부드러움'이 어떤 원리로 일어나는지 설명하는 핵심 파트입니다!


내비게이션 작동 원리 (How navigation works)

Next.js에서 내비게이션이 어떻게 작동하는지 제대로 이해하려면, 다음 개념들과 친해질 필요가 있습니다.

서버 렌더링 (Server Rendering)

Next.js App 라우터 환경에서, 레이아웃과 페이지(Layouts and Pages) 는 기본적으로 React 서버 컴포넌트(React Server Components) 입니다.

사용자가 웹사이트에 처음 접속할 때나 그 이후에 다른 페이지로 이동할 때, 서버 컴포넌트는 클라이언트로 보내지기 전에 먼저 서버에서 렌더링 작업을 거칩니다. 이때 서버에서 만들어진 특별한 데이터 형식인 서버 컴포넌트 페이로드(Server Component Payload) 가 생성되어 클라이언트(브라우저)로 전송되죠.

서버 렌더링은 그 작업이 '언제' 일어나는지에 따라 두 가지 종류로 나뉩니다.

  • 정적 렌더링 (Static Rendering 또는 Prerendering): 빌드하는 시점(build time)이나 재검증(revalidation) 과정 중에 렌더링이 일어나고, 그 결과물이 캐싱(저장)됩니다. 한 번 만들어두고 여러 사람에게 계속 보여주는 방식이죠.
  • 동적 렌더링 (Dynamic Rendering): 사용자가 요청을 보낼 때마다(request time) 실시간으로 렌더링이 일어납니다. 사용자마다 다른 데이터를 보여줘야 할 때 쓰여요.

서버 렌더링의 단점(트레이드오프)은 클라이언트가 새로운 화면을 보여주기 위해 반드시 서버의 응답을 기다려야 한다는 점입니다. Next.js는 이 대기 시간으로 인해 생기는 지연을 해결하기 위해, 사용자가 방문할 가능성이 높은 라우트를 미리 가져오는 프리패칭(prefetching) 기능과, 부드럽게 화면을 교체하는 클라이언트 사이드 전환(client-side transitions) 을 수행합니다.

알아두면 좋은 정보(Good to know): 첫 방문 시점(Initial visit)에는 초기 화면을 빠르게 보여주기 위해 HTML 파일도 함께 생성되어 클라이언트로 전송됩니다.

💡 강사의 팁:
"어? 서버에서 렌더링하면 결국 로딩을 기다려야 하잖아요?" 라고 물어보시는 수강생분들이 많습니다. 맞아요! 서버에서 DB도 다녀오고 API도 찔러봐야 하니 시간이 걸릴 수밖에 없죠. 그래서 Next.js가 머리를 쓴 겁니다. "사용자가 클릭하기 전에 몰래 미리 받아오자!" 이게 바로 다음에 설명할 '프리패칭'입니다.

프리패칭 (Prefetching)

프리패칭은 사용자가 특정 라우트(페이지)로 이동하기 전에, 브라우저 백그라운드에서 해당 라우트의 데이터를 미리 로드하는 과정입니다. 이렇게 하면 사용자가 링크를 클릭하는 순간, 다음 라우트를 그리기 위한 데이터가 이미 클라이언트 측에 준비되어 있기 때문에 체감상 내비게이션이 아주 즉각적(Instant)으로 느껴지게 됩니다.

Next.js는 <Link> 컴포넌트로 연결된 라우트들이 사용자의 화면(viewport) 영역에 들어올 때 자동으로 프리패칭을 수행합니다. 스크롤을 내리다가 화면에 링크가 보이면 Next.js가 알아서 뒤에서 데이터를 받아오기 시작하는 거죠.

import Link from 'next/link'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <nav>
          {/* 링크에 마우스를 올리거나 화면(viewport)에 나타날 때 미리 로드(Prefetch)됩니다 */}
          <Link href="/blog">Blog</Link>
          {/* 이건 일반 a 태그라서 프리패칭이 되지 않습니다! */}
          <a href="/contact">Contact</a>
        </nav>
        {children}
      </body>
    </html>
  )
}
import Link from 'next/link'

export default function Layout() {
  return (
    <html>
      <body>
        <nav>
          {/* 링크에 마우스를 올리거나 화면(viewport)에 나타날 때 미리 로드(Prefetch)됩니다 */}
          <Link href="/blog">Blog</Link>
          {/* 이건 일반 a 태그라서 프리패칭이 되지 않습니다! */}
          <a href="/contact">Contact</a>
        </nav>
        {children}
      </body>
    </html>
  )
}

💡 강사의 팁:
정말 중요한 포인트입니다! Next.js 프로젝트 안에서 내부 페이지로 이동할 때는 절대로 기본 HTML 태그인 <a>를 사용하지 마세요. 반드시 next/link에서 불러온 <Link> 컴포넌트를 사용해야 Next.js의 강력한 프리패칭 최적화와 SPA 같은 화면 전환 효과를 누릴 수 있습니다. <a> 태그를 쓰면 브라우저가 화면을 완전히 새로고침(Full refresh) 해버려요! 외부 사이트(예: https://google.com)로 나갈 때만 <a> 태그를 쓰시면 됩니다.

라우트를 어느 정도까지 미리 가져오느냐(프리패칭 범위)는 해당 라우트가 정적(Static) 인지 동적(Dynamic) 인지에 따라 달라집니다.

  • 정적 라우트 (Static Route): 라우트 전체 데이터를 전부 프리패칭합니다.
  • 동적 라우트 (Dynamic Route): 기본적으로 프리패칭을 건너뜁니다(skipped). 하지만 만약 폴더 안에 loading.tsx 파일이 있다면, 해당 라우트의 일부만(부분적으로) 프리패칭합니다.

동적 라우트의 프리패칭을 건너뛰거나 부분적으로만 수행하는 이유는, 사용자가 어쩌면 영영 방문하지 않을 수도 있는 라우트를 위해 서버가 불필요하게 많은 작업을 하는 것을 방지하기 위해서입니다. 서버 자원을 아끼는 것이죠.

하지만, 서버 응답을 기다렸다가 화면을 전환하면 사용자 입장에서는 "어? 앱이 먹통인가? 멈췄나?" 하고 답답함을 느낄 수 있습니다.

스트리밍이 없는 서버 렌더링 상황

동적 라우트 환경에서 내비게이션 경험을 개선하고 싶다면, 바로 다음에 설명할 스트리밍(streaming) 기술을 활용하면 됩니다.

스트리밍 (Streaming)

스트리밍은 전체 라우트 렌더링이 다 끝날 때까지 서버가 가만히 기다리는 것이 아니라, 준비가 완료된(ready) 동적 라우트의 일부분부터 클라이언트로 즉시 쪼개서(stream) 보내주는 기능입니다.
이 기능을 사용하면, 페이지의 일부가 아직 로딩 중이라고 하더라도 사용자는 완성된 부분부터 먼저 볼 수 있기 때문에 체감 속도가 훨씬 빠릅니다.

동적 라우트의 관점에서 보면, 이는 곧 라우트를 부분적으로 프리패칭(partially prefetched) 할 수 있다는 의미입니다. 즉, 공통으로 사용하는 레이아웃(shared layouts)이나 로딩 상태를 보여주는 스켈레톤(loading skeletons) UI를 사전에 미리 요청해 둘 수 있다는 것이죠.

스트리밍이 있는 서버 렌더링의 작동 원리

스트리밍을 사용하는 방법은 아주 간단합니다. 라우트 폴더 안에 loading.tsx 파일을 만들기만 하면 됩니다.

loading.js 특수 파일의 모습

export default function Loading() {
  // 실제 라우트 데이터가 로딩되는 동안 보여줄 폴백(fallback) UI를 추가하세요.
  // 보통 스켈레톤 UI(LoadingSkeleton)나 스피너를 많이 넣습니다.
  return <LoadingSkeleton />
}
export default function Loading() {
  // 실제 라우트 데이터가 로딩되는 동안 보여줄 폴백(fallback) UI를 추가하세요.
  return <LoadingSkeleton />
}

내부적으로(Behind the scenes), Next.js는 page.tsx의 내용을 React의 <Suspense> 경계(boundary)로 자동으로 감싸줍니다. 이렇게 되면 라우트가 진짜 데이터를 로딩하는 동안, 미리 프리패치해 둔 폴백 UI(loading.tsx의 내용)를 먼저 보여주다가 데이터 로딩이 다 끝나면 실제 콘텐츠로 부드럽게 싹(swapped) 교체해 줍니다.

알아두면 좋은 정보(Good to know): 파일 기반의 loading.tsx 말고도, 컴포넌트 내부에서 더 세밀하게 로딩 상태를 관리하고 싶다면 React의 기본 기능인 <Suspense> 컴포넌트를 직접 사용하여 중첩된 로딩 UI를 만들 수도 있습니다.

loading.tsx 를 사용했을 때의 장점:

  • 사용자가 링크를 클릭하자마자 즉각적으로 내비게이션이 실행되고 로딩 UI라는 시각적 피드백을 바로 받을 수 있습니다. (답답함 제로!)
  • 사이드바, 헤더 같은 공통 레이아웃(Shared layouts)은 여전히 사용자 조작(interactive)이 가능한 상태로 남아있으며, 로딩 중에도 다른 메뉴를 클릭하는 등 내비게이션을 중단(interruptible)할 수 있습니다.
  • 가장 중요한 성능 지표인 핵심 웹 바이탈(Core Web Vitals) 수치가 크게 개선됩니다. 특히 TTFB (Time To First Byte), FCP (First Contentful Paint), TTI (Time to Interactive) 에 좋습니다.

그리고 여기서 끝이 아닙니다. Next.js는 내비게이션 경험을 한 차원 더 끌어올리기 위해 <Link> 컴포넌트를 통한 클라이언트 사이드 전환(client-side transitions) 까지 함께 수행합니다.

클라이언트 사이드 전환 (Client-side transitions)

전통적인 웹 개발 방식에서는, 서버 렌더링 된 페이지로 이동할 때 전체 페이지가 새로고침(full page load)되는 현상이 발생합니다. 이렇게 되면 브라우저가 관리하던 기존 상태(state)가 다 날아가 버리고, 사용자가 내려놨던 스크롤 위치도 맨 위로 초기화되며, 화면이 깜빡이는 동안 사용자와의 상호작용(interactivity)도 꽉 막혀버리죠.

하지만 Next.js는 <Link> 컴포넌트를 활용한 클라이언트 사이드 전환(client-side transitions) 으로 이 문제를 우아하게 회피합니다. 페이지 전체를 새로고침하는 대신, 아래와 같은 방식으로 화면의 내용을 동적으로 부드럽게 업데이트합니다.

  • 사이드바나 내비게이션 바처럼 여러 페이지에서 공유하는 레이아웃과 UI는 그대로 유지합니다. (렌더링 낭비 방지!)
  • 바뀌는 부분(현재 페이지 내용)만 미리 준비된(prefetched) 로딩 상태나 새로운 페이지 콘텐츠로 쏙 교체합니다.

바로 이 '클라이언트 사이드 전환' 기술 덕분에, 서버에서 렌더링 되는(Server-rendered) Next.js 앱이 마치 클라이언트 측에서만 동작하는(Client-rendered) SPA(Single Page Application)처럼 가볍고 매끄럽게 느껴지는(feel) 것입니다. 여기에 앞서 설명한 프리패칭스트리밍 기술까지 찰떡궁합으로 결합되면, 데이터를 그때그때 받아와야 하는 동적 라우트 상황에서도 엄청나게 빠른 전환 속도를 자랑하게 됩니다.

또한, 클라이언트 사이드 전환 중에는 페이지 최상단으로 자동 스크롤(scrolling to the top of the page) 하는 기능도 Next.js가 알아서 처리해 줍니다. 혹시 상단에 고정된 헤더(sticky/fixed header)가 있어서 페이지 이동 후 콘텐츠가 헤더 뒤로 숨어버리는 문제가 발생한다면, CSS 속성인 scroll-padding-top 을 사용해서 간편하게 해결할 수 있습니다.

💡 강사의 팁:
"서버에서 렌더링하면 깜빡인다며? SPA가 최고 아니야?" 라고 묻던 시대는 지났습니다. Next.js의 App 라우터 방식은 첫 화면은 서버에서 완성된 HTML로 쏴줘서(SEO 최적화 & 초기 로딩 속도 향상) 가져오고, 그 이후의 화면 이동은 React의 가상 DOM을 활용해 변경된 부분만 클라이언트 단에서 교체해 줍니다. 두 마리 토끼를 다 잡은 완벽한 하이브리드 아키텍처라고 볼 수 있죠!


무엇이 화면 전환을 느리게 만들까요? (What can make transitions slow?)

지금까지 설명한 Next.js의 눈부신 최적화 기능들 덕분에 내비게이션은 빠르고 쾌적하게 동작합니다. 하지만, 특정 조건에서는 여전히 화면 전환이 느리게 느껴질 수 있습니다. 실무에서 정말 자주 부딪히는 문제들이니 집중해 주세요! 흔한 원인들과 그에 대한 해결책(UX 개선 방법)을 알려드리겠습니다.

1. loading.tsx가 없는 동적 라우트 (Dynamic routes without loading.tsx)

동적 라우트(예: 데이터베이스에서 실시간으로 글을 읽어와야 하는 게시글 페이지 등)로 이동할 때, 클라이언트는 화면에 결과를 띄우기 전 서버가 데이터를 다 처리하고 응답할 때까지 멍하니 기다려야 합니다.
만약 아무런 피드백이 없다면, 사용자는 "어? 내 클릭이 안 먹혔나? 앱이 멈췄나?" 라고 착각하기 십상이죠.

강력히 권장하는 방법은, 동적 라우트 폴더에 무조건 loading.tsx 파일을 추가하는 것입니다. 이 파일 하나만 추가해도 라우트의 일부를 미리 프리패칭할 수 있게 되고, 클릭 즉시 화면 전환이 시작되며, 실제 서버 렌더링이 완료되는 동안 사용자에게 예쁜 로딩 UI를 보여줄 수 있습니다.

export default function Loading() {
  return <LoadingSkeleton />
}
export default function Loading() {
  return <LoadingSkeleton />
}

알아두면 좋은 정보(Good to know): 개발(development) 모드에서는 화면 우측 하단에 뜨는 Next.js Devtools(번개 아이콘)를 통해 이 페이지가 정적(Static)으로 렌더링되었는지, 동적(Dynamic)으로 렌더링되었는지 시각적으로 확인할 수 있습니다. 자세한 내용은 devIndicators 문서를 참고해 보세요.

2. generateStaticParams가 없는 동적 세그먼트 (Dynamic segments without generateStaticParams)

동적 세그먼트(예: [slug], [id] 형태의 폴더)를 가진 페이지라도, 들어갈 수 있는 데이터 목록이 정해져 있다면 사전에 미리(Prerender) 만들어 둘 수 있습니다. 하지만, 어떤 값들이 존재하는지 알려주는 generateStaticParams 함수를 빼먹었다면? Next.js는 어쩔 수 없이 사용자가 요청을 보낼 때마다 그제야 서버에서 동적으로 렌더링을 시작해야 합니다. (당연히 매번 렌더링하느라 느려지겠죠!)

블로그 포스트처럼 이미 작성된 글 목록이 정해져 있다면, generateStaticParams를 추가해서 빌드 타임(build time)에 미리 싹 다 정적 페이지로 생성되도록 보장해 주세요. 이렇게 하면 수백, 수천 명의 사용자가 들어와도 눈 깜짝할 새에 페이지가 열립니다.

export async function generateStaticParams() {
  // 전체 게시글 데이터를 API에서 불러옵니다.
  const posts = await fetch('https://.../posts').then((res) => res.json())

  // 어떤 slug들이 존재하는지 배열 형태로 반환해 주면, 
  // Next.js가 빌드할 때 이 배열의 개수만큼 HTML을 미리 구워냅니다!
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  // ... 게시글 본문 렌더링
}
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 }) {
  const { slug } = await params
  // ...
}

3. 느린 네트워크 환경 (Slow networks)

사용자의 인터넷 환경이 느리거나 불안정하다면(지하철에서 스마트폰을 한다거나), 사용자가 링크를 클릭하기 전까지 프리패칭이 채 끝나지 않을 수 있습니다.
이 문제는 정적 라우트와 동적 라우트 모두에서 발생할 수 있는데요. 이런 최악의 상황에서는 심지어 loading.js 폴백 컴포넌트조차 아직 다운로드 받지 못해서 즉각적으로 로딩 화면이 뜨지 않을 수도 있습니다.

이럴 때 사용자가 답답함을 느끼지 않게(인지적 성능, perceived performance 개선) 하려면, Next.js에서 제공하는 useLinkStatus 훅(hook) 을 사용하여 화면이 넘어가는 중이라는 즉각적인 피드백(예: 버튼에 스피너 돌기)을 보여주면 됩니다.

'use client'

import { useLinkStatus } from 'next/link'

export default function LoadingIndicator() {
  // 현재 내비게이션이 진행 중(pending)인지 여부를 알 수 있습니다.
  const { pending } = useLinkStatus()
  return (
    <span aria-hidden className={`link-hint ${pending ? 'is-pending' : ''}`} />
  )
}
'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) 시작하도록 '디바운스(debounce)' 처리를 해보세요.
그러면 화면 전환이 0.1초 안에 순식간에 일어나면 로딩 표시가 아예 뜨지 않고, 전환이 예상보다 오래 걸릴 때만 스무스하게 로딩 표시가 나타납니다. 훨씬 고급스러운 UX를 만들 수 있죠! CSS 코드가 궁금하시다면 useLinkStatus 레퍼런스(빠른 내비게이션 우아하게 처리하기) 를 꼭 확인해 보세요.

알아두면 좋은 정보(Good to know): 상태창 외에도 화면 맨 위에 프로그레스 바(진행률 표시줄)를 띄우는 시각적 피드백 패턴도 아주 훌륭합니다. 실무에서 정말 많이 쓰는 방식인데요, 이곳(GitHub 링크)에서 예제 코드를 확인해 볼 수 있습니다.

4. 프리패칭 기능 비활성화 (Disabling prefetching)

만약 화면에 수백 개의 링크가 나열된 긴 리스트(예: 무한 스크롤이 적용된 쇼핑몰 상품 목록 테이블)가 있다고 생각해 봅시다. 이 수백 개의 링크를 Next.js가 화면에 보일 때마다 백그라운드에서 전부 프리패칭 해버리면 사용자 데이터 요금도 낭비되고 서버도 터져나갈 수 있습니다.

이럴 때는 불필요한 자원 낭비를 막기 위해 <Link> 컴포넌트의 prefetch 속성을 false 로 설정하여 프리패칭을 강제로 끌 수(opt out) 있습니다.

{/* 명시적으로 prefetch={false}를 주면 미리 가져오지 않습니다 */}
<Link prefetch={false} href="/blog">
  Blog
</Link>

하지만 세상에 공짜는 없죠. 프리패칭을 꺼버렸기 때문에 얻는 트레이드오프(단점)도 분명히 존재합니다.

  • 정적 라우트: 사용자가 링크를 실제로 '클릭'하는 순간에만 데이터를 가져옵니다. (체감 속도 저하)
  • 동적 라우트: 클라이언트가 화면을 넘기기 전에, 먼저 서버에 데이터를 요청하고 응답을 다 렌더링할 때까지 꼼짝없이 기다려야 합니다.

"자원 낭비는 막고 싶은데, 그렇다고 프리패칭을 아예 끄면 너무 느려지잖아요? 어떡하죠?"
이럴 때는 절충안으로 마우스 호버(Hover) 시에만 프리패칭하도록 구현할 수 있습니다. 화면에 보이는(viewport) 모든 링크를 무지성으로 다 긁어오는 대신, 사용자가 마우스를 올려서 실제로 클릭할 가능성이 매우 높은 링크만 선택적으로 프리패칭하는 것이죠.

'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)

  // 처음에는 prefetch={false}로 꺼두었다가, 
  // 마우스가 올라가는 순간(onMouseEnter) active를 true로 바꿔서 프리패칭을 시작합니다.
  return (
    <Link
      href={href}
      prefetch={active ? null : false}
      onMouseEnter={() => setActive(true)}
    >
      {children}
    </Link>
  )
}
'use client'

import Link from 'next/link'
import { useState } from 'react'

function HoverPrefetchLink({ href, children }) {
  const [active, setActive] = useState(false)

  return (
    <Link
      href={href}
      prefetch={active ? null : false}
      onMouseEnter={() => setActive(true)}
    >
      {children}
    </Link>
  )
}

5. 하이드레이션(Hydration)이 완료되지 않은 상태 (Hydration not completed)

이 개념은 프론트엔드 면접에서도 자주 나오는 핵심입니다!
Next.js의 <Link> 컴포넌트는 사실 클라이언트 컴포넌트(Client Component) 입니다. 무슨 말이냐면, 작동하기 위해서는 단순히 HTML만 있어서 되는 게 아니라 React의 자바스크립트 코드들이 해당 HTML 요소에 생명(이벤트 리스너 등)을 불어넣어 주는 하이드레이션(Hydration) 과정을 마쳐야만 비로소 라우트를 프리패칭할 준비가 완료된다는 뜻입니다.

그런데 초기 접속 시 사용자가 다운로드해야 할 자바스크립트 번들 파일(bundle)의 크기가 너무 방대하다면? 다운로드 받고 실행하느라 하이드레이션 과정 자체가 지연되고, 결국 프리패칭 시작 타이밍도 늦어지게 됩니다.

React는 이 문제를 완화하기 위해 '선택적 하이드레이션(Selective Hydration)' 기술을 사용하지만, 여러분이 직접 앱을 더 최적화하려면 다음과 같은 방법을 써야 합니다.

  • @next/bundle-analyzer 플러그인을 사용하여 어떤 거대한 라이브러리가 자바스크립트 용량을 차지하고 있는지 분석하고, 불필요한 의존성을 제거하여 번들 사이즈를 줄이세요.
  • 가능하다면 클라이언트 측에서 처리하던 로직을 서버 측(Server Component)으로 옮기세요. 자바스크립트가 브라우저로 전송될 필요가 없어져 번들이 훨씬 가벼워집니다! 가이드가 필요하다면 서버와 클라이언트 컴포넌트(Server and Client Components) 문서를 꼭 정독해 보세요.

예제 (Examples)

네이티브 History API 활용 (Native History API)

최신 Next.js는 화면을 새로고침하지 않고도 브라우저의 뒤로가기/앞으로가기 히스토리 스택을 업데이트할 수 있는 브라우저 기본 웹 API인 window.history.pushStatewindow.history.replaceState 메서드를 완벽하게 지원합니다.

Next.js의 라우터는 이 pushStatereplaceState 호출과 완벽하게 연동(integrate)되도록 설계되어 있습니다. 이 말은 즉, 여러분이 이 API들을 직접 호출해도 Next.js의 상태 훅인 usePathnameuseSearchParams 값이 문제없이 동기화(sync)된다는 뜻입니다!

💡 강사의 팁:
버튼을 눌러서 window.history.pushState로 URL을 바꾸면, Next.js의 useSearchParams 훅은 그 변화를 감지해서 컴포넌트를 다시 렌더링합니다.

window.history.pushState

이 메서드는 브라우저의 히스토리 스택(방문 기록)에 새로운 항목을 추가할 때 사용합니다. 항목이 쌓이기 때문에 사용자가 브라우저의 '뒤로 가기' 버튼을 누르면 이전 상태로 돌아갈 수 있습니다.
예를 들어, 상품 목록을 오름차순/내림차순으로 정렬(sort)하는 기능을 만들 때 사용하면 찰떡입니다.

'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder: string) {
    // 기존 URL의 검색 파라미터를 복사해서 새로운 객체를 만듭니다.
    const params = new URLSearchParams(searchParams.toString())
    // 'sort' 라는 키에 전달받은 값('asc' 또는 'desc')을 세팅합니다.
    params.set('sort', sortOrder)
    // 브라우저의 URL 창을 업데이트하고 방문 기록을 하나 남깁니다. (새로고침 안 됨!)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>오름차순 정렬</button>
      <button onClick={() => updateSorting('desc')}>내림차순 정렬</button>
    </>
  )
}
'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder) {
    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>
    </>
  )
}

window.history.replaceState

이 메서드는 브라우저 히스토리 스택에 항목을 추가하지 않고, 현재 항목을 덮어씌워 버립니다(replace). 즉, 이 액션을 취한 후에는 사용자가 '뒤로 가기' 버튼을 눌러도 방금 전의 상태로 돌아갈 수 없습니다.
이 방식은 앱의 다국어(locale) 설정을 전환할 때처럼, 언어를 바꾼 후 "뒤로 가기를 눌러서 다시 다른 언어로 가는 게 말이 안 되는" 논리적 상황에서 아주 유용하게 쓰입니다.

'use client'

import { usePathname } from 'next/navigation'

export function LocaleSwitcher() {
  const pathname = usePathname()

  function switchLocale(locale: string) {
    // 현재 경로 앞에 언어 코드(locale)를 강제로 끼워 넣습니다.
    // 예: '/about' -> '/en/about' 또는 '/fr/about'
    const newPath = `/${locale}${pathname}`
    // 방문 기록을 새로 남기지 않고, 현재 주소창의 상태만 스르륵 교체합니다.
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>English (영어)</button>
      <button onClick={() => switchLocale('fr')}>French (프랑스어)</button>
    </>
  )
}
'use client'

import { usePathname } from 'next/navigation'

export function LocaleSwitcher() {
  const pathname = usePathname()

  function switchLocale(locale) {
    // e.g. '/en/about' or '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>French</button>
    </>
  )
}


모든 문서에 대한 의미론적인 전체 개요(semantic overview)를 보시려면 /docs/sitemap.md 를 확인하세요.

사용 가능한 모든 문서의 전체 색인(Index) 목록을 찾으신다면 /docs/llms.txt 에서 확인하실 수 있습니다.


수고하셨습니다! 공식문서의 내용을 하나도 빠짐없이, 거기에 실무적인 팁까지 더해서 학습해보았는데요.
어떠셨나요? 프리패칭과 스트리밍의 원리, 그리고 왜 Next.js가 화면 전환에 강점을 가지는지 감이 팍 오셨을 거라 믿습니다! 더 궁금한 부분이나 이해 안 되는 코드가 있다면 언제든 강사에게 질문해주세요! 😊

profile
프론트에_가까운_풀스택_개발자

0개의 댓글