Next.js Link와 useRouter 차이는 무엇일까?
화면에 보이는 링크는 Link, 로그인 성공 뒤처럼 코드 흐름 안에서 이동해야 할 때는 useRouter를 쓴다. 둘 다 새로고침 없는 클라이언트 사이드 내비게이션을 하지만, Link는 <a>로 렌더링되어 prefetch·검색 엔진 인식, 새 탭 열기가 되고 서버 컴포넌트에서도 그대로 쓸 수 있다.
두 도구가 해결하는 공통 문제
- 일반
<a>로 다른 문서를 탐색하면 보통 전체 문서 탐색이 일어나 현재 React 트리의 메모리 상태가 유지되지 않는다. (쉽게 말해, 깜박인다.) - Next.js는 이 문제를 줄이기 위해 새로고침 없이 바뀐 부분만 교체하는 클라이언트 사이드 내비게이션을 제공한다.
- 이 내비게이션을 실행하는 방법으로
Link컴포넌트와useRouter훅 두 가지가 있다. - 둘은 같은 라우터를 사용하지만, 이동을 언제 어떻게 시작하는가에서 차이가 생긴다.
Link 컴포넌트: 선언적으로 이동한다
-
Link는 HTML<a>태그를 확장한 컴포넌트로,next/link에서 가져와href만 넘기면 된다.import Link from 'next/link' export default function Nav() { return <Link href="/dashboard">대시보드</Link> } -
실제로는
<a>태그로 렌더링되므로, 검색 엔진이 링크를 인식할 수 있고 새 탭에서 열기 같은 브라우저 기본 기능도 동작한다. -
또한
Link는 화면에 보일 때 prefetch를 기본으로 시도한다. 전체 경로를 받는지 일부만 받는지는 정적-동적 렌더링,loading.js경계,prefetchprop과 설정에 따라 달라진다. -
이렇게 미리 받아둔 덕분에 사용자가 클릭했을 때 이동이 빠르게 느껴질 수 있다.
- ⭐️
Link의 자동 prefetch는 production 환경에서 활성화되며 개발 모드에서는 비활성화된다.prefetch={false}로 자동 prefetch를 끌 수도 있다.
- ⭐️
⭐️Link 하나 쓴다고 페이지 전체를 클라이언트 컴포넌트로 만들 필요는 없다.
- Next.js App Router에서 모든 컴포넌트는 기본적으로 서버 컴포넌트이며,
'use client'는 해당 파일 안에서 직접onClick,useState같은 브라우저 기능을 사용할 때만 필요하다. Link는 페이지 이동이라는 클라이언트 동작을 수행하지만, 이미 내부적으로 클라이언트 컴포넌트로 구현되어 있다.- 서버 컴포넌트는 클라이언트 컴포넌트를 import해서 렌더링할 수 있으므로, 서버 컴포넌트인 페이지에서
Link를 그대로 사용해도 문제가 없다. - 만약
Link때문에 페이지 전체에'use client'를 붙이면, 서버에서 데이터를 직접 가져오는 이점을 잃고 불필요한 JS까지 브라우저로 전송하게 된다. - 따라서 직접 작성한 이벤트 로직이 필요할 때만 그 부분을 별도 파일로 분리해
'use client'를 붙이고, 클라이언트 경계는 최대한 작게 유지하는 것이 바람직하다.
useRouter 훅: 코드로 이동한다
-
useRouter는next/navigation에서 가져오는 훅으로, 이벤트 핸들러 같은 코드 안에서 이동을 실행할 때 사용한다. -
useRouter는 클라이언트 컴포넌트에서 사용하는 훅이다. 서버에서 import하는 진입 파일에는'use client'를 붙이지만, 이미 그 파일의 클라이언트 모듈 그래프에 포함된 모든 하위 파일에 지시문을 반복할 필요는 없다.'use client' import { useRouter } from 'next/navigation' export default function LoginButton() { const router = useRouter() async function handleClick() { const ok = await login() if (ok) router.push('/dashboard') // 로그인에 성공했을 때만 이동한다 } return <button onClick={handleClick}>로그인</button> } -
위 예시처럼 어떤 작업이 끝난 뒤나 조건을 만족할 때만 이동해야 하는 상황에서 유용하다.
-
주요 메서드는 아래처럼 역할이 나뉜다.
router.push('/a') // 이동하고, 방문 기록을 남긴다 router.replace('/a') // 현재 기록을 교체한다 (그보다 이전 기록까지 지우지는 않는다) router.refresh() // 현재 경로를 다시 요청해 RSC 결과를 병합한다 (최신 데이터 보장은 아님) router.back() // 뒤로가기 router.forward() // 앞으로가기 router.prefetch('/a') // 이동할 경로를 직접 미리 받아둔다 -
다만
router.push를 호출하는 버튼은<a>태그가 아니므로, 검색 엔진이 링크로 인식하기 어렵고 새 탭에서 열기도 동작하지 않는다. -
router.push를 쓴다는 이유만으로 이동 전에 자동 prefetch가 예약되지는 않는다. 필요하면router.prefetch를 호출할 수 있으며, 다른Link등이 미리 채운 캐시가 있으면 이동 시 재사용할 수 있다. -
router.refresh()는 서버 측 데이터 캐시를 무효화하지 않으므로 같은 결과가 돌아올 수 있다. 변경된 Payload를 병합할 때 영향을 받지 않은 클라이언트 상태는 유지하며, 서버 캐시 갱신이 필요하면 해당 캐시 정책에 맞는revalidatePath·revalidateTag등의 처리가 별도로 필요하다.
언제 무엇을 쓰는가
- 공식 문서는
useRouter를 써야 하는 특별한 이유가 없다면Link를 사용하도록 권장한다. - 사용자가 눌러서 이동하는 메뉴, 목록의 항목, 카드 같은 일반적인 링크에는
Link가 어울린다. - 클라이언트에서 작업 완료나 조건 판단 후 이동하는 경우에는
useRouter가 어울린다. 서버 컴포넌트나 Server Action에서 처리하는 리다이렉트에는 서버에서 호출할 수 있는redirect()등을 사용한다. - 방문 기록을 남기지 않아야 한다면
router.replace를 쓰거나Link에replaceprop을 줄 수 있다. - 판단 기준을 한 문장으로 줄이면, 화면에 보이는 링크라면
Link, 코드 흐름 안에서 이동해야 한다면useRouter라고 볼 수 있다.