현록

Next.js App Router Link와 router.push 차이

Next.js App Router에서 다른 페이지로 이동할 때 <Link>router.push()를 사용할 수 있다.
둘 다 client-side navigation을 만들지만 출발점이 다르다.

화면에 링크가 존재한다면 <Link>가 기본이다.
저장 성공이나 조건 분기처럼 코드 실행 결과로 이동해야 한다면 router.push()가 맞다.

이 글은 저장소에서 사용 중인 Next.js 16 App Router를 기준으로 한다.
Pages Router의 next/router 예제와 섞지 않는다.

Link의 기본 역할

next/link<Link>는 HTML anchor를 확장해 client-side navigation과 prefetch를 제공한다.
Next.js 공식 문서도 route 사이를 이동하는 기본 방법으로 Link를 권장한다.

import Link from 'next/link'

type Post = {
  slug: string
  title: string
}

export function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/posts/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

Link는 실제 <a> 요소로 렌더링된다.
그래서 새 탭으로 열기, 링크 주소 복사, 키보드 탐색, targetdownload 같은 링크 동작을 유지할 수 있다.

Link를 렌더링하려고 페이지 전체에 'use client'를 붙일 필요는 없다.
Server Component에서도 Link를 렌더링할 수 있으며 필요한 클라이언트 경계만 hydrate된다.

router.push의 실행 조건

useRouter는 Client Component 안에서 코드로 route를 바꿀 때 사용한다.
App Router에서는 next/navigation에서 가져와야 한다.

'use client'

import { useRouter } from 'next/navigation'

export function CreatePostForm() {
  const router = useRouter()

  async function handleSubmit(formData: FormData) {
    const response = await fetch('/api/posts', {
      method: 'POST',
      body: formData,
    })

    if (!response.ok) {
      return
    }

    const post: { slug: string } = await response.json()
    router.push(`/posts/${post.slug}`)
  }

  return <form action={handleSubmit}>{/* fields */}</form>
}

이동할 URL이 렌더링 시점에 정해지는 것이 아니라 제출 결과에서 생기므로 Link보다 router가 자연스럽다.
router.push()는 브라우저 history에 항목을 추가하고 router.replace()는 현재 항목을 바꾼다.

로그인 성공 후 로그인 화면으로 뒤로 가지 않게 하려면 router.replace('/dashboard')가 더 알맞을 수 있다.
단순히 dashboard로 이동하는 버튼을 만들 목적이라면 router를 쓰기보다 Link를 사용해야 링크 의미가 유지된다.

prefetch와 history 동작

Link는 production에서 viewport에 들어온 route를 자동으로 prefetch한다.
정적 route는 전체를 가져올 수 있고 동적 route는 loading.tsx 경계까지 일부만 가져오거나 prefetch를 건너뛸 수 있다.

많은 링크가 있는 목록에서 요청 비용을 줄여야 한다면 prefetch={false}를 지정할 수 있다.
그만큼 클릭 뒤 서버 응답을 기다리는 시간이 길어질 수 있다.

router로 이동할 route도 미리 준비하고 싶다면 직접 prefetch한다.

'use client'

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

export function DashboardButton() {
  const router = useRouter()

  useEffect(() => {
    router.prefetch('/dashboard')
  }, [router])

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      대시보드 열기
    </button>
  )
}

Link의 replacescroll={false}는 각각 router의 replace(){ scroll: false } 옵션에 대응한다.
어느 API를 쓰든 새 history 항목이 필요한지와 이동 뒤 스크롤 위치를 함께 결정해야 한다.

동적 route에서 Link를 눌렀을 때 표시할 loading 경계는 Next.js App Router loading.tsx와 Suspense 차이에서 이어서 볼 수 있다.

서버 이동과 URL 보안

Server Component, Server Function, Route Handler에서 조건에 따라 바로 이동해야 한다면 redirect()를 사용한다.
Client Component의 event handler에서 프로그램 이동이 필요할 때 useRouter를 선택하면 server와 client 경계를 분명하게 유지할 수 있다.

import { redirect } from 'next/navigation'

export default async function AccountPage() {
  const user = await getCurrentUser()

  if (!user) {
    redirect('/login')
  }

  return <h2>{user.name}</h2>
}

router.push()router.replace()에 신뢰할 수 없는 문자열을 그대로 넘기면 안 된다.
Next.js 문서는 javascript: URL이 페이지 문맥에서 실행될 수 있다고 경고한다.

내부 route라면 허용된 경로를 코드에서 조합하고 외부 입력을 segment에 넣을 때는 인코딩과 허용 범위를 확인한다.
서버가 결정할 redirect 목적지도 같은 기준으로 검증해야 한다.

선택 기준

사용자가 보고 선택할 수 있는 일반적인 이동은 Link로 표현한다.
Link는 anchor 의미, 기본 브라우저 동작, 자동 prefetch를 함께 제공한다.

비동기 작업이 성공한 뒤 이동하거나 실행 중 조건에 따라 목적지가 정해질 때는 Client Component에서 router를 사용한다.
서버 렌더링 과정에서 이동이 결정되면 useRouter를 위한 Client Component를 만들지 않고 redirect()를 사용한다.

prefetch 세부 동작과 onNavigate, useLinkStatus 같은 API는 Next.js 버전에 따라 바뀔 수 있다.
이 글의 선택 기준은 Next.js 16 App Router에 맞추고 세부 옵션은 설치된 버전의 문서를 확인하는 것이 안전하다.

참고 자료

관련 포스트
Next.js App Router loading.tsx와 Suspense 차이 thumbnail
Next.js App Router loading.tsx와 Suspense 차이
Next.js App Router의 loading.tsx와 React Suspense가 만드는 로딩 경계의 차이를 정리합니다. route segment, 세밀한 streaming, await 위치, 상태 코드와 배포 조건까지 살펴봅니다.
Next.js App Router 서버 컴포넌트와 클라이언트 컴포넌트 차이 thumbnail
Next.js App Router 서버 컴포넌트와 클라이언트 컴포넌트 차이
Next.js App Router의 Server Component와 Client Component 차이를 기본 동작, use client 경계, 직렬화와 조합 규칙, 실무 선택 기준으로 정리합니다.
Next.js App Router에서 검색 날짜와 사이트맵 관리하기 thumbnail
Next.js App Router에서 검색 날짜와 사이트맵 관리하기
Next.js App Router 블로그에서 사용자에게 보이는 날짜, Open Graph modifiedTime, JSON-LD dateModified, sitemap lastModified를 같은 기준으로 연결하는 방법을 정리합니다.
Next.js 16 Cache Components와 use cache 정리 thumbnail
Next.js 16 Cache Components와 use cache 정리
Next.js 16에서 도입된 Cache Components와 use cache directive를 기준으로 App Router 캐싱 모델의 변화, cacheLife, cacheTag, revalidateTag 사용 흐름을 정리합니다.
Next.js App Router에서 레이아웃 사용하기 thumbnail
Next.js App Router에서 레이아웃 사용하기
Next.js App Router에서 app/layout.tsx, page.tsx, 중첩 layout을 사용해 공통 UI를 구성하는 방법을 정리합니다. Pages Router 시절의 수동 Layout 컴포넌트 패턴과 어떤 점이 다른지도 함께 봅니다.
next image blurDataURL 직접 부여하기 thumbnail
next image blurDataURL 직접 부여하기
Next.js의 Image 컴포넌트에서 blur placeholder를 사용할 때 자동 생성되는 경우와 직접 blurDataURL을 만들어야 하는 경우를 정리합니다. 정적 import, public 경로, 원격 이미지, 빌드 시점 생성 전략을 함께 다룹니다.
Next.js에서 path alias 설정하기 (feat. @/components) thumbnail
Next.js에서 path alias 설정하기 (feat. @/components)
오늘은 Next.js에서 import 시 복잡한 relative path 대신 absolute path 사용을 위한 설정법을 알아봅시다. file path를 상대경로로 지정하다보면 유지보수면에서도 복잡하고, path를 지정할 때마다 경로가 헷갈려서 발생하는 오류는 덤입니다. 이럴 때 path에 대한 alias를 설정하면 코드는 확 깔끔해질 것입니다.  아래 예제를 통해서 따라해봅시다.
Next.js에서 레이아웃 사용하기 thumbnail
Next.js에서 레이아웃 사용하기
홈페이지를 구성할 때, 우리는 대체로 네비게이션과 푸터, 플로팅 버튼 등을 포함합니다. 개발단계에서 이들 컴포넌트를 페이지마다 일일이 import하는 것은 매우 비효율적인 일입니다. 만약 이들처럼 대부분의 페이지에서 보여줘야될 내용이 있다면, 레이아웃 컴포넌트를 통해 손쉽게 유지보수할 수 있을 것입니다.