API/Components/Link Component

김동현·2026년 3월 8일

next.js 공식문서 번역

목록 보기
69/79

<Link>는 기존 HTML의 <a> 요소를 확장해서 만든 React 컴포넌트예요. 라우트(route) 간에 이동할 때 프리패칭(prefetching) 기능과 클라이언트 사이드 네비게이션(client-side navigation)을 제공한답니다. Next.js 환경에서 페이지를 이동할 때 가장 기본적이고 최우선으로 사용해야 하는 방법이죠.

👨‍🏫 강사님의 팁: > 기존의 <a> 태그를 사용하면 페이지를 이동할 때마다 브라우저가 새로고침되면서 화면이 깜빡거리게 됩니다. 하지만 Next.js의 <Link> 컴포넌트를 사용하면 마치 Single Page Application(SPA)처럼 부드럽고 빠른 화면 전환을 경험할 수 있어요. 프론트엔드 개발자로서 사용자 경험(UX)을 끌어올리기 위한 첫걸음입니다!

기본적인 사용법을 먼저 살펴볼까요?

import Link from 'next/link'

export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}
import Link from 'next/link'

export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

Reference (레퍼런스)

<Link> 컴포넌트에는 다음과 같은 prop들을 전달할 수 있어요. 어떤 기능들이 있는지 표로 먼저 확인해 봅시다.

PropExample (예시)Type (타입)Required (필수 여부)
hrefhref="/dashboard"String or Object (문자열 또는 객체)Yes (필수)
replacereplace={false}Boolean (불리언)-
scrollscroll={false}Boolean (불리언)-
prefetchprefetch={false}Boolean or null (불리언 또는 null)-
onNavigateonNavigate={(e) => {}}Function (함수)-

💡 알아두면 좋은 점 (Good to know): className이나 target="_blank"와 같은 일반적인 <a> 태그의 속성들도 <Link>의 prop으로 추가할 수 있어요. 이 속성들은 내부적으로 렌더링되는 실제 <a> 요소에 그대로 전달된답니다.


href (필수 사항)

이동하고자 하는 경로(path)나 URL을 지정하는 prop입니다.

import Link from 'next/link'

// Navigate to /about?name=test (/about?name=test 로 이동합니다)
export default function Page() {
  return (
    <Link
      href={{
        pathname: '/about',
        query: { name: 'test' },
      }}
    >
      About
    </Link>
  )
}
import Link from 'next/link'

// Navigate to /about?name=test (/about?name=test 로 이동합니다)
export default function Page() {
  return (
    <Link
      href={{
        pathname: '/about',
        query: { name: 'test' },
      }}
    >
      About
    </Link>
  )
}

👨‍🏫 강사님의 부연 설명: > 문자열로 깔끔하게 href="/about" 처럼 적을 수도 있지만, 위 예제처럼 객체 형태를 사용할 수도 있어요. 특히 검색 필터나 복잡한 쿼리 파라미터(query parameters)를 다룰 때 객체 형태로 넘기면 코드를 훨씬 구조적이고 가독성 좋게 유지할 수 있답니다.


replace

기본값은 false입니다. 만약 true로 설정하면, next/link브라우저의 히스토리 스택(history stack)에 새로운 URL을 추가(push)하는 대신, 현재의 히스토리 상태를 덮어쓰게(replace) 됩니다.

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" replace>
      Dashboard
    </Link>
  )
}
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" replace>
      Dashboard
    </Link>
  )
}

👨‍🏫 강사님의 팁: > "뒤로 가기를 눌렀을 때, 방금 지나쳐 온 페이지로 다시 돌아가지 않게 하고 싶을 때" 이 replace가 아주 유용해요. 예를 들어 '로그인 페이지'에서 로그인에 성공해 '메인 페이지'로 넘어갔다고 해볼게요. 이때 push를 쓰면 사용자가 뒤로 가기를 눌렀을 때 이미 로그인했는데도 다시 '로그인 페이지'가 보이겠죠? 이럴 때 replace를 적용해주면 사용자 경험이 훨씬 매끄러워집니다!


scroll

기본값은 true입니다. Next.js에서 <Link>의 기본 스크롤 동작은 일반 브라우저에서 뒤로/앞으로 가기를 할 때처럼 스크롤 위치를 유지하는 것이에요. 여러분이 새로운 페이지(Page)로 이동할 때, 해당 페이지가 화면(viewport) 내에 보인다면 스크롤 위치는 그대로 유지됩니다. 하지만 해당 페이지가 화면 내에 보이지 않는다면, Next.js는 새로운 페이지 요소의 가장 최상단으로 스크롤을 이동시켜 줍니다.

만약 scroll={false}로 설정하면, Next.js는 페이지 요소의 최상단으로 스크롤하려는 시도를 하지 않습니다.

💡 알아두면 좋은 점 (Good to know):
Next.js는 스크롤 동작을 관리하기 전에 가장 먼저 scroll: false인지 확인합니다. 스크롤이 활성화되어 있다면, 이동할 관련 DOM 노드를 식별하고 각 최상위 요소들을 검사해요. 이때 스크롤이 불가능한 요소나 렌더링된 HTML이 없는 요소는 모두 건너뜁니다. 여기에는 stickyfixed 포지션이 적용된 요소, 그리고 getBoundingClientRect로 계산된 보이지 않는 요소 등이 포함돼요. Next.js는 뷰포트 내에 보이는 스크롤 가능한 요소를 찾을 때까지 형제 노드들을 계속해서 탐색합니다.

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" scroll={false}>
      Dashboard
    </Link>
  )
}
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" scroll={false}>
      Dashboard
    </Link>
  )
}

👨‍🏫 강사님의 팁: > 화면 한 쪽에 탭(Tab) 메뉴가 있고, 탭을 클릭할 때마다 URL은 바뀌지만 전체 화면이 맨 위로 휙 올라가 버리면 사용자가 당황하겠죠? 이럴 때 스크롤 위치를 현재 그대로 묶어두고 싶다면 scroll={false}를 꼭 사용해보세요.


prefetch

프리패칭(Prefetching)은 <Link /> 컴포넌트가 사용자의 뷰포트(화면)에 나타날 때(초기 렌더링 시점이나 스크롤을 통해 나타날 때 모두 포함) 발생합니다. Next.js는 클라이언트 사이드 네비게이션의 성능을 향상시키기 위해 백그라운드에서 링크된 라우트(href로 지정된 곳)와 그 데이터를 미리 가져와 로드해 둡니다. 만약 사용자가 <Link /> 위에 마우스를 올렸을 때(hover) 이전에 미리 가져온 데이터의 유효기간이 지났다면, Next.js는 데이터를 다시 프리패칭하려고 시도해요. 이 프리패칭 기능은 오직 프로덕션(Production) 환경에서만 활성화됩니다.

prefetch prop에는 다음과 같은 값들을 넘겨줄 수 있어요:

  • "auto" 또는 null (기본값): 프리패칭의 동작은 해당 라우트가 정적(static)인지 동적(dynamic)인지에 따라 달라집니다. 정적 라우트의 경우, 전체 라우트(모든 데이터 포함)가 프리패칭됩니다. 동적 라우트의 경우, loading.js 경계(boundary)가 있는 가장 가까운 세그먼트까지 부분적인 라우트만 프리패칭됩니다.
  • true: 정적 라우트와 동적 라우트 모두 전체 라우트가 프리패칭됩니다.
  • false: 뷰포트에 들어올 때나 마우스를 올릴(hover) 때 등 어떤 경우에도 프리패칭이 발생하지 않습니다.
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" prefetch={false}>
      Dashboard
    </Link>
  )
}
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" prefetch={false}>
      Dashboard
    </Link>
  )
}

👨‍🏫 강사님의 부연 설명: > 프리패칭은 Next.js가 자랑하는 최고의 성능 마법 중 하나입니다! 사용자가 링크를 클릭할지 말지 고민하고 있는 찰나의 순간에, 이미 뒤에서 페이지를 받아오고 있는 거죠. 클릭하는 순간 지연 없이 화면이 짠! 하고 나타납니다. 다만, 로컬 개발 모드(npm run dev)에서는 이 최적화가 동작하지 않으니 "왜 프리패칭 안 되지?" 하고 헷갈리시면 안 됩니다! (개발 모드에서는 항상 최신 상태를 보여주기 위해 매번 로드합니다)


onNavigate

클라이언트 사이드 네비게이션 중에 호출되는 이벤트 핸들러입니다. 이 핸들러는 preventDefault() 메서드가 포함된 이벤트 객체를 전달받기 때문에, 필요하다면 페이지 이동 자체를 취소(cancel)할 수 있는 권한을 제공합니다.

import Link from 'next/link'

export default function Page() {
  return (
    <Link
      href="/dashboard"
      onNavigate={(e) => {
        // Only executes during SPA navigation (SPA 네비게이션 중에만 실행됩니다)
        console.log('Navigating...')

        // Optionally prevent navigation (선택적으로 네비게이션을 취소할 수 있습니다)
        // e.preventDefault()
      }}
    >
      Dashboard
    </Link>
  )
}
import Link from 'next/link'

export default function Page() {
  return (
    <Link
      href="/dashboard"
      onNavigate={(e) => {
        // Only executes during SPA navigation (SPA 네비게이션 중에만 실행됩니다)
        console.log('Navigating...')

        // Optionally prevent navigation (선택적으로 네비게이션을 취소할 수 있습니다)
        // e.preventDefault()
      }}
    >
      Dashboard
    </Link>
  )
}

💡 알아두면 좋은 점 (Good to know):
onClickonNavigate는 비슷해 보일 수 있지만, 목적이 전혀 달라요. onClick은 모든 클릭 이벤트에 대해 실행되지만, onNavigate는 오직 클라이언트 사이드 네비게이션이 발생할 때만 실행됩니다. 주요 차이점을 살펴볼까요:

  • 단축키(Ctrl/Cmd + 클릭)와 함께 클릭할 때, onClick은 실행되지만 onNavigate는 실행되지 않아요. 왜냐하면 Next.js가 새 탭을 띄울 때는 기본 네비게이션 동작을 막기 때문이죠.
  • 외부 URL(External URLs)로 이동할 때는 onNavigate가 트리거되지 않습니다. 이 이벤트는 오직 같은 도메인 내의 클라이언트 사이드 이동만을 위해 존재하거든요.
  • download 속성이 있는 링크의 경우 브라우저가 이동이 아닌 파일 다운로드로 처리하기 때문에, onClick은 작동해도 onNavigate는 작동하지 않습니다.

Examples (사용 예시)

다음 예제들을 통해 <Link> 컴포넌트를 다양한 상황에서 어떻게 활용할 수 있는지 확인해 볼게요.

Linking to dynamic route segments (동적 라우트 세그먼트로 연결하기)

동적 세그먼트(dynamic segments)로 링크를 걸 때, 템플릿 리터럴(template literals)과 보간법(interpolation)을 사용하면 링크 목록을 쉽게 생성할 수 있어요. 예를 들어, 블로그 포스트 목록을 생성한다고 해봅시다.

import Link from 'next/link'

interface Post {
  id: number
  title: string
  slug: string
}

export default function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}
import Link from 'next/link'

export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

👨‍🏫 강사님의 팁: > 백틱(`)을 사용한 템플릿 리터럴은 프론트엔드 개발의 빛과 소금 같은 존재죠! 데이터베이스에서 받아온 게시글 리스트나 상품 리스트를 렌더링할 때, 저렇게 .map() 함수와 조합해서 동적인 링크를 만드는 패턴은 포트폴리오 프로젝트하실 때 수백 번은 쓰시게 될 거예요.


현재 링크가 활성화된 상태인지(사용자가 해당 경로에 머물고 있는지) 확인하려면 usePathname() 훅을 사용할 수 있습니다. 예를 들어, 현재 pathname이 링크의 href와 일치하는지 확인해서 활성화된 링크에 특정 CSS 클래스를 추가할 수 있죠.

'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

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

  return (
    <nav>
      <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
        Home
      </Link>

      <Link
        className={`link ${pathname === '/about' ? 'active' : ''}`}
        href="/about"
      >
        About
      </Link>
    </nav>
  )
}
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

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

  return (
    <nav>
      <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
        Home
      </Link>

      <Link
        className={`link ${pathname === '/about' ? 'active' : ''}`}
        href="/about"
      >
        About
      </Link>
    </nav>
  )
}

👨‍🏫 강사님의 팁: > 웹사이트의 글로벌 네비게이션 바(GNB)를 떠올려보세요. 내가 지금 'About' 메뉴를 누르면 그 메뉴에만 진하게 하이라이트 처리가 되어 있죠? 바로 그 UI를 구현할 때 사용하는 정석적인 코드입니다. usePathname()과 삼항 연산자의 조합, 아주 찰떡궁합이에요!


Scrolling to an id (id로 스크롤하기)

네비게이션 시 특정 id가 있는 곳으로 스크롤을 이동시키고 싶다면, URL 뒤에 # 해시 링크를 덧붙이거나 href prop에 해시 링크 자체를 전달하면 됩니다. <Link> 컴포넌트가 결국 <a> 요소로 렌더링되기 때문에 이런 기존 웹 표준 방식이 모두 가능한 것이죠.

<Link href="/dashboard#settings">Settings</Link>

// Output (결과)
<a href="/dashboard#settings">Settings</a>

💡 알아두면 좋은 점 (Good to know):

  • 사용자가 이동했을 때 만약 해당 페이지(Page)가 화면에 보이지 않는 위치라면, Next.js가 알맞게 그 페이지 위치로 스크롤을 이동시켜 줍니다.

Replace the URL instead of push (새로운 기록을 추가하는 대신 URL 대체하기)

Link 컴포넌트의 기본 동작은 브라우저의 history 스택에 새로운 URL 기록을 밀어 넣는(push) 것입니다. 하지만 아래 예시처럼 replace prop을 사용하면 새로운 기록을 추가하는 것을 방지할 수 있습니다.

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/about" replace>
      About us
    </Link>
  )
}
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/about" replace>
      About us
    </Link>
  )
}

Disable scrolling to the top of the page (페이지 상단으로 스크롤되는 기능 끄기)

Next.js에서 <Link>의 기본 스크롤 동작은 일반 브라우저에서 뒤로 가기/앞으로 가기를 할 때처럼 스크롤 위치를 유지하는 것입니다. 새로운 페이지(Page)로 이동했을 때, 그 페이지가 뷰포트에 잘 보인다면 스크롤은 그대로 멈춰 있습니다.

하지만, 새로운 페이지가 뷰포트 바깥에 있어서 안 보인다면, Next.js는 새로운 페이지 요소의 맨 위로 스크롤을 쫙 끌어올려줍니다. 만약 이 자동 스크롤 동작을 끄고 싶다면, <Link> 컴포넌트에 scroll={false}를 전달하거나, router.push() 또는 router.replace()를 사용할 때 scroll: false 옵션을 주면 됩니다.

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/#hashid" scroll={false}>
      Disables scrolling to the top (상단 스크롤 비활성화)
    </Link>
  )
}
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/#hashid" scroll={false}>
      Disables scrolling to the top (상단 스크롤 비활성화)
    </Link>
  )
}

router.push() 또는 router.replace()를 사용할 경우:

// useRouter
import { useRouter } from 'next/navigation'

const router = useRouter()

router.push('/dashboard', { scroll: false })

Scroll offset with sticky headers (고정된 헤더가 있을 때 스크롤 위치 조정하기)

Next.js는 스크롤 타겟을 찾을 때 stickyfixed가 적용된 요소들은 건너뜁니다. 그렇기 때문에 네비게이션을 하고 났을 때 본문 내용이 상단에 고정된 헤더(sticky header) 뒤로 숨어버리는 곤란한 상황이 생길 수 있어요. 예를 들어, 여러분의 레이아웃 코드가 고정된 헤더를 가지고 있다면:

import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <header className="sticky top-0 h-16 bg-white">
          {/* Navigation (네비게이션 바) */}
        </header>
        {children}
      </body>
    </html>
  )
}
import './globals.css'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <header className="sticky top-0 h-16 bg-white">
          {/* Navigation (네비게이션 바) */}
        </header>
        {children}
      </body>
    </html>
  )
}

이럴 때는 스크롤을 감싸고 있는 최상위 요소에 CSS의 scroll-padding-top 속성을 적용해서 헤더의 높이만큼 여백을 계산해 줄 수 있습니다.

html {
  scroll-padding-top: 64px; /* 헤더의 높이와 똑같이 맞춰주세요 */
}

이것은 스크롤에 기반한 요소 위치를 오프셋(보정) 해주는 아주 고마운 브라우저 CSS 속성이에요. Next.js가 해시 조각(#id)으로 이동하는 경우를 포함해 브라우저 네이티브의 scrollIntoView() API를 사용할 때마다 동일하게 적용된답니다. 글로벌하게 전체 여백을 설정하는 대신, 특정 타겟 요소들에 개별적으로 scroll-margin-top 속성을 사용하는 방법도 있어요.

👨‍🏫 강사님의 부연 설명: > 웹 개발하다 보면 앵커 링크(#id) 클릭해서 넘어갔는데 하필 페이지 상단 메뉴바에 제목이 딱 가려지는 "킹받는" 경험, 다들 한 번쯤 있으시죠? 자바스크립트로 스크롤 높이를 매번 계산할 필요 없이, 이 CSS 한 줄이면 정말 우아하게 해결됩니다. 꼭 메모해 두세요!


프록시(Proxy)를 인증(authentication) 용도나 사용자를 다른 페이지로 재작성(rewrite)하기 위해 사용하는 경우가 흔히 있죠. 이처럼 프록시를 통해 rewrite가 일어나는 환경에서 <Link /> 컴포넌트가 제대로 프리패칭을 수행하게 하려면, 화면에 보여질 URL과 실제로 프리패칭해야 할 URL을 Next.js에게 정확히 알려주어야 합니다. 이렇게 해야 프록시가 올바른 라우트를 알아내기 위해 불필요하게 통신을 남발하는 것을 막을 수 있거든요.

예를 들어, 방문자용 화면과 로그인한 사용자용 화면이 분리되어 있는 /dashboard 라우트를 제공한다고 가정해 봅시다. 사용자를 알맞은 페이지로 리다이렉트 시켜주기 위해 프록시에 다음과 같은 코드를 추가할 수 있습니다.

import { NextResponse } from 'next/server'

export function proxy(request: Request) {
  const nextUrl = request.nextUrl
  if (nextUrl.pathname === '/dashboard') {
    if (request.cookies.authToken) {
      return NextResponse.rewrite(new URL('/auth/dashboard', request.url))
    } else {
      return NextResponse.rewrite(new URL('/public/dashboard', request.url))
    }
  }
}
import { NextResponse } from 'next/server'

export function proxy(request) {
  const nextUrl = request.nextUrl
  if (nextUrl.pathname === '/dashboard') {
    if (request.cookies.authToken) {
      return NextResponse.rewrite(new URL('/auth/dashboard', request.url))
    } else {
      return NextResponse.rewrite(new URL('/public/dashboard', request.url))
    }
  }
}

이런 환경이라면, <Link /> 컴포넌트 쪽 코드는 이렇게 작성해 주는 것이 좋습니다.

'use client'

import Link from 'next/link'
import useIsAuthed from './hooks/useIsAuthed' // 사용자가 만든 auth 훅

export default function Page() {
  const isAuthed = useIsAuthed()
  const path = isAuthed ? '/auth/dashboard' : '/public/dashboard'
  return (
    <Link as="/dashboard" href={path}>
      Dashboard
    </Link>
  )
}
'use client'

import Link from 'next/link'
import useIsAuthed from './hooks/useIsAuthed' // 사용자가 만든 auth 훅

export default function Page() {
  const isAuthed = useIsAuthed()
  const path = isAuthed ? '/auth/dashboard' : '/public/dashboard'
  return (
    <Link as="/dashboard" href={path}>
      Dashboard
    </Link>
  )
}

Blocking navigation (페이지 이동 막기)

폼(Form) 요소에서 아직 저장하지 않은 변경사항이 있는 경우 등 특정 조건이 충족될 때, onNavigate prop을 활용하면 페이지가 넘어가는 것을 꽉 막아줄 수 있습니다. 이 폼이 작성 중일 때 앱의 어디에 있는 링크를 누르더라도 다른 페이지로 못 넘어가게 앱 전역에서 상태를 관리해야 한다면, React의 Context(컨텍스트) API를 사용해 막기(blocking) 상태를 공유하는 것이 아주 깔끔한 해결책입니다.

먼저, 네비게이션 차단 상태를 추적할 컨텍스트를 만들어 볼까요?

'use client'

import { createContext, useState, useContext } from 'react'

interface NavigationBlockerContextType {
  isBlocked: boolean
  setIsBlocked: (isBlocked: boolean) => void
}

export const NavigationBlockerContext =
  createContext<NavigationBlockerContextType>({
    isBlocked: false,
    setIsBlocked: () => {},
  })

export function NavigationBlockerProvider({
  children,
}: {
  children: React.ReactNode
}) {
  const [isBlocked, setIsBlocked] = useState(false)

  return (
    <NavigationBlockerContext.Provider value={{ isBlocked, setIsBlocked }}>
      {children}
    </NavigationBlockerContext.Provider>
  )
}

export function useNavigationBlocker() {
  return useContext(NavigationBlockerContext)
}
'use client'

import { createContext, useState, useContext } from 'react'

export const NavigationBlockerContext = createContext({
  isBlocked: false,
  setIsBlocked: () => {},
})

export function NavigationBlockerProvider({ children }) {
  const [isBlocked, setIsBlocked] = useState(false)

  return (
    <NavigationBlockerContext.Provider value={{ isBlocked, setIsBlocked }}>
      {children}
    </NavigationBlockerContext.Provider>
  )
}

export function useNavigationBlocker() {
  return useContext(NavigationBlockerContext)
}

이제 이 컨텍스트를 사용하는 폼 컴포넌트를 만들어 줍니다.

'use client'

import { useNavigationBlocker } from '../contexts/navigation-blocker'

export default function Form() {
  const { setIsBlocked } = useNavigationBlocker()

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        setIsBlocked(false) // 저장하면 차단 해제!
      }}
      onChange={() => setIsBlocked(true)} // 값이 바뀌면 차단 시작!
    >
      <input type="text" name="name" />
      <button type="submit">Save (저장)</button>
    </form>
  )
}
'use client'

import { useNavigationBlocker } from '../contexts/navigation-blocker'

export default function Form() {
  const { setIsBlocked } = useNavigationBlocker()

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        setIsBlocked(false) // 저장하면 차단 해제!
      }}
      onChange={() => setIsBlocked(true)} // 값이 바뀌면 차단 시작!
    >
      <input type="text" name="name" />
      <button type="submit">Save (저장)</button>
    </form>
  )
}

그리고 네비게이션을 통제해 줄 커스텀 링크 컴포넌트를 작성해 볼게요.

'use client'

import Link from 'next/link'
import { useNavigationBlocker } from '../contexts/navigation-blocker'

interface CustomLinkProps extends React.ComponentProps<typeof Link> {
  children: React.ReactNode
}

export function CustomLink({ children, ...props }: CustomLinkProps) {
  const { isBlocked } = useNavigationBlocker()

  return (
    <Link
      onNavigate={(e) => {
        // 차단 상태인데 사용자가 '그래도 나갈래' 라고 하지 않았다면
        if (
          isBlocked &&
          !window.confirm('저장되지 않은 변경사항이 있습니다. 그래도 떠나시겠습니까?')
        ) {
          e.preventDefault() // 이동을 막아버립니다!
        }
      }}
      {...props}
    >
      {children}
    </Link>
  )
}
'use client'

import Link from 'next/link'
import { useNavigationBlocker } from '../contexts/navigation-blocker'

export function CustomLink({ children, ...props }) {
  const { isBlocked } = useNavigationBlocker()

  return (
    <Link
      onNavigate={(e) => {
        if (
          isBlocked &&
          !window.confirm('저장되지 않은 변경사항이 있습니다. 그래도 떠나시겠습니까?')
        ) {
          e.preventDefault()
        }
      }}
      {...props}
    >
      {children}
    </Link>
  )
}

이 커스텀 링크를 활용해서 네비게이션 바 컴포넌트를 만듭니다.

'use client'

import { CustomLink as Link } from './custom-link'

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
    </nav>
  )
}
'use client'

import { CustomLink as Link } from './custom-link'

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
    </nav>
  )
}

마지막으로, 이 모든 것이 제대로 동작할 수 있도록 앱의 최상위 레이아웃(root layout)을 NavigationBlockerProvider로 감싸주세요.

import { NavigationBlockerProvider } from './contexts/navigation-blocker'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <NavigationBlockerProvider>{children}</NavigationBlockerProvider>
      </body>
    </html>
  )
}
import { NavigationBlockerProvider } from './contexts/navigation-blocker'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <NavigationBlockerProvider>{children}</NavigationBlockerProvider>
      </body>
    </html>
  )
}

이제 여러분의 페이지에서 NavForm 컴포넌트를 렌더링해주시면 됩니다.

import Nav from './components/nav'
import Form from './components/form'

export default function Page() {
  return (
    <div>
      <Nav />
      <main>
        <h1>Welcome to the Dashboard</h1>
        <Form />
      </main>
    </div>
  )
}
import Nav from './components/nav'
import Form from './components/form'

export default function Page() {
  return (
    <div>
      <Nav />
      <main>
        <h1>Welcome to the Dashboard</h1>
        <Form />
      </main>
    </div>
  )
}

완성입니다! 이제 사용자가 폼에 무언가 입력해 놓고 저장하지 않은 상태에서 CustomLink를 눌러 딴 곳으로 가려고 하면, "진짜 떠나실 건가요?" 하고 묻는 경고창이 뜨면서 사용자의 실수를 막아줄 거예요.

👨‍🏫 강사님의 부연 설명: > 코드가 꽤 길어졌지만, 아주아주 중요한 실무 패턴입니다! 프론트엔드 취업 준비하시면서 작성하고 계신 포트폴리오 프로젝트 (예를 들어 블로그 글쓰기나 정보 수정 페이지)에 이 기능을 추가해보세요. "아, 이 개발자는 사용자의 실수까지도 꼼꼼하게 배려해서 UX를 설계할 줄 아는구나" 하고 면접관의 눈도장을 팍팍 찍을 수 있는 훌륭한 포인트가 될 겁니다! 😉


Version history (버전 업데이트 내역)

Next.js 버전에 따라 Link 컴포넌트에 어떤 변화가 있었는지 알아볼게요.

VersionChanges (변경 사항)
v15.4.0기본 prefetch 동작의 별칭(alias)으로 auto 옵션이 추가되었습니다.
v15.3.0클라이언트 이동을 가로챌 수 있는 onNavigate API가 새롭게 추가되었습니다.
v13.0.0더 이상 하위 요소로 <a> 태그를 필수로 요구하지 않습니다. 기존 코드를 자동으로 업데이트해 주는 codemod(코드모드) 도구가 제공되니 쉽게 마이그레이션 할 수 있어요.
v10.0.0동적 라우트(dynamic route)를 가리키는 href prop들이 이제 자동으로 해석되어서, 더 이상 번거롭게 as prop을 명시할 필요가 없어졌습니다.
v8.0.0프리패칭 성능이 한층 더 쾌적하게 개선되었습니다.
v1.0.0위대한 next/link가 세상에 처음 도입된 역사적인 순간입니다!

전체 문서의 논리적인 구조와 개요가 궁금하다면 /docs/sitemap.md를 참고해 주세요.

사용 가능한 모든 문서의 전체 인덱스가 필요하다면 /docs/llms.txt를 확인해 보세요!

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

0개의 댓글