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> 요소로 렌더링된다.
그래서 새 탭으로 열기, 링크 주소 복사, 키보드 탐색, target과 download 같은 링크 동작을 유지할 수 있다.
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의 replace와 scroll={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에 맞추고 세부 옵션은 설치된 버전의 문서를 확인하는 것이 안전하다.
참고 자료







