Next.js Link와 useRouter 차이는 무엇일까?

화면에 보이는 링크는 Link, 로그인 성공 뒤처럼 코드 흐름 안에서 이동해야 할 때는 useRouter를 쓴다. 둘 다 새로고침 없는 클라이언트 사이드 내비게이션을 하지만, Link는 <a>로 렌더링되어 prefetch·검색 엔진 인식, 새 탭 열기가 되고 서버 컴포넌트에서도 그대로 쓸 수 있다.

Next.js

두 도구가 해결하는 공통 문제

  1. 일반 <a>로 다른 문서를 탐색하면 보통 전체 문서 탐색이 일어나 현재 React 트리의 메모리 상태가 유지되지 않는다. (쉽게 말해, 깜박인다.)
  2. Next.js는 이 문제를 줄이기 위해 새로고침 없이 바뀐 부분만 교체하는 클라이언트 사이드 내비게이션을 제공한다.
  3. 이 내비게이션을 실행하는 방법으로 Link 컴포넌트와 useRouter 훅 두 가지가 있다.
  4. 둘은 같은 라우터를 사용하지만, 이동을 언제 어떻게 시작하는가에서 차이가 생긴다.

Next.js 공식 문서: Link Component

  1. Link는 HTML <a> 태그를 확장한 컴포넌트로, next/link에서 가져와 href만 넘기면 된다.

    import Link from 'next/link'
    export default function Nav() {
      return <Link href="/dashboard">대시보드</Link>
    }
  2. 실제로는 <a> 태그로 렌더링되므로, 검색 엔진이 링크를 인식할 수 있고 새 탭에서 열기 같은 브라우저 기본 기능도 동작한다.

  3. 또한 Link는 화면에 보일 때 prefetch를 기본으로 시도한다. 전체 경로를 받는지 일부만 받는지는 정적-동적 렌더링, loading.js 경계, prefetch prop과 설정에 따라 달라진다.

  4. 이렇게 미리 받아둔 덕분에 사용자가 클릭했을 때 이동이 빠르게 느껴질 수 있다.

    • ⭐️ Link의 자동 prefetch는 production 환경에서 활성화되며 개발 모드에서는 비활성화된다. prefetch={false}로 자동 prefetch를 끌 수도 있다.
  1. Next.js App Router에서 모든 컴포넌트는 기본적으로 서버 컴포넌트이며, 'use client'는 해당 파일 안에서 직접 onClick, useState 같은 브라우저 기능을 사용할 때만 필요하다.
  2. Link는 페이지 이동이라는 클라이언트 동작을 수행하지만, 이미 내부적으로 클라이언트 컴포넌트로 구현되어 있다.
  3. 서버 컴포넌트는 클라이언트 컴포넌트를 import해서 렌더링할 수 있으므로, 서버 컴포넌트인 페이지에서 Link를 그대로 사용해도 문제가 없다.
  4. 만약 Link 때문에 페이지 전체에 'use client'를 붙이면, 서버에서 데이터를 직접 가져오는 이점을 잃고 불필요한 JS까지 브라우저로 전송하게 된다.
  5. 따라서 직접 작성한 이벤트 로직이 필요할 때만 그 부분을 별도 파일로 분리해 'use client'를 붙이고, 클라이언트 경계는 최대한 작게 유지하는 것이 바람직하다.

useRouter 훅: 코드로 이동한다

Next.js 공식 문서: useRouter

  1. useRouter는 next/navigation에서 가져오는 훅으로, 이벤트 핸들러 같은 코드 안에서 이동을 실행할 때 사용한다.

  2. 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>
    }
  3. 위 예시처럼 어떤 작업이 끝난 뒤나 조건을 만족할 때만 이동해야 하는 상황에서 유용하다.

  4. 주요 메서드는 아래처럼 역할이 나뉜다.

    router.push('/a')     // 이동하고, 방문 기록을 남긴다
    router.replace('/a')  // 현재 기록을 교체한다 (그보다 이전 기록까지 지우지는 않는다)
    router.refresh()      // 현재 경로를 다시 요청해 RSC 결과를 병합한다 (최신 데이터 보장은 아님)
    router.back()         // 뒤로가기
    router.forward()      // 앞으로가기
    router.prefetch('/a') // 이동할 경로를 직접 미리 받아둔다
  5. 다만 router.push를 호출하는 버튼은 <a> 태그가 아니므로, 검색 엔진이 링크로 인식하기 어렵고 새 탭에서 열기도 동작하지 않는다.

  6. router.push를 쓴다는 이유만으로 이동 전에 자동 prefetch가 예약되지는 않는다. 필요하면 router.prefetch를 호출할 수 있으며, 다른 Link 등이 미리 채운 캐시가 있으면 이동 시 재사용할 수 있다.

  7. router.refresh()는 서버 측 데이터 캐시를 무효화하지 않으므로 같은 결과가 돌아올 수 있다. 변경된 Payload를 병합할 때 영향을 받지 않은 클라이언트 상태는 유지하며, 서버 캐시 갱신이 필요하면 해당 캐시 정책에 맞는 revalidatePath·revalidateTag 등의 처리가 별도로 필요하다.

언제 무엇을 쓰는가

  1. 공식 문서는 useRouter를 써야 하는 특별한 이유가 없다면 Link를 사용하도록 권장한다.
  2. 사용자가 눌러서 이동하는 메뉴, 목록의 항목, 카드 같은 일반적인 링크에는 Link가 어울린다.
  3. 클라이언트에서 작업 완료나 조건 판단 후 이동하는 경우에는 useRouter가 어울린다. 서버 컴포넌트나 Server Action에서 처리하는 리다이렉트에는 서버에서 호출할 수 있는 redirect() 등을 사용한다.
  4. 방문 기록을 남기지 않아야 한다면 router.replace를 쓰거나 Link에 replace prop을 줄 수 있다.
  5. 판단 기준을 한 문장으로 줄이면, 화면에 보이는 링크라면 Link, 코드 흐름 안에서 이동해야 한다면 useRouter라고 볼 수 있다.

댓글