본문으로 건너뛰기

안동민 개발노트

본문 시작

페이지 간 링크 생성

Link의 클라이언트 전환과 프리페치를 이해하고 정적·동적 href와 useRouter를 상황에 맞게 사용합니다.

웹 애플리케이션의 본질은 정보와 기능을 제공하고 사용자가 자유롭게 이동하도록 만드는 데 있습니다.

그래서 페이지를 연결하는 링크(Link)는 필수 요소입니다.

Next.js App Router에서는 기본 <a> 태그보다 Next.js의 <Link> 컴포넌트 사용을 권장합니다.

이 절에서는 <Link> 컴포넌트의 사용법과 그 이점, 그리고 실제 애플리케이션에서 링크를 효율적으로 관리하는 방법에 대해 자세히 알아보겠습니다.

아래 표는 내부 링크를 만들 때 <Link>, href, 프리페치, 명령형 이동을 어떤 기준으로 나눠 볼지 먼저 정리합니다.

내부 이동은 Link, 이벤트 이후 이동은 useRouter, 외부 이동은 a로 나눈다

페이지 이동을 만들기 전에 목적지가 내부 라우트인지, 사용자 이벤트 뒤에 이동하는지, 브라우저 기본 동작이 필요한지부터 정한다.

상황기본 선택이유주의할 점
내부 페이지 이동<Link href="/about">클라이언트 탐색과 프리페치 활용Link 안에 a 태그를 다시 넣지 않음
동적 상세 페이지<Link href={`/products/${id}`}>실제 URL 문자열로 이동id 값이 비어 있지 않은지 확인
폼 제출 후 이동useRouter().push()검증·저장 이후 명령형 이동client component에서만 사용
외부 사이트<a href="https://...">브라우저 기본 링크가 자연스러움target/rel 보안 속성 확인
다운로드/파일<a download>파일 다운로드 기본 동작 사용앱 내부 라우팅으로 처리하지 않음

<a> 태그 대신 <Link>를 사용하는 이유

아래 표는 일반 <a>와 Next.js <Link>의 차이를 이동 방식, 프리페치, SEO 관점에서 비교한 것입니다.

Next.js Link는 내부 라우트 이동을 빠르게 만들고 a는 브라우저 기본 링크를 맡는다

둘 다 최종적으로 링크처럼 보이지만, 앱 내부 이동에서 Link는 새로고침을 줄이고 대상 페이지를 미리 준비할 수 있다.

비교 항목Linka 태그선택 기준
대상앱 내부 라우트외부 URL, 다운로드, 메일내부 경로면 Link 우선
렌더링자체가 a 요소로 렌더링브라우저 기본 aLink 안에 a를 중첩하지 않음
이동 방식클라이언트 사이드 탐색전체 문서 이동 가능내부 이동은 깜빡임을 줄임
프리페치뷰포트에 보이면 준비 가능없음많은 링크는 prefetch 조절
SEOa 기반 링크로 크롤링 가능표준 링크의미 있는 href 유지

일반적인 HTML의 <a> 태그를 사용하여 페이지를 이동할 경우, 브라우저는 해당 페이지를 처음부터 다시 로드합니다.

이는 전통적인 웹사이트에서는 일반적이었지만, React와 같은 SPA(Single Page Application) 프레임워크 기반의 애플리케이션에서는 전체 페이지가 깜빡이거나 새로고침되는 듯한 사용자 경험을 제공하게 됩니다.

반면, Next.js의 <Link> 컴포넌트를 사용하면 다음과 같은 중요한 이점을 얻을 수 있습니다.

  • 클라이언트 사이드 탐색 (Client-side Navigation): <Link> 컴포넌트는 페이지 전체를 새로 로드하는 대신, JavaScript를 사용하여 필요한 부분만 업데이트합니다.

    이는 마치 데스크톱 애플리케이션처럼 부드럽고 빠른 페이지 전환을 가능하게 합니다.

    브라우저의 새로고침 현상 없이 콘텐츠만 바뀌는 것을 경험할 수 있습니다.

  • 코드 스플리팅 및 프리페칭 (Code Splitting and Pre-fetching): Next.js는 <Link> 컴포넌트로 페이지를 연결할 때, 해당 페이지에 필요한 JavaScript 코드를 자동으로 분할(Code Splitting)합니다.

    더 나아가 사용자가 링크를 클릭하기도 전에 링크 대상 페이지의 코드를 미리 로드(Pre-fetching)합니다.

    덕분에 사용자가 링크를 클릭했을 때 페이지 로딩 시간을 거의 느끼지 못하고, 사용자 경험이 크게 향상됩니다.

  • 검색 엔진 최적화 (SEO) 이점: <Link> 컴포넌트는 여전히 내부적으로 <a> 태그로 렌더링되므로, 검색 엔진 크롤러가 웹사이트의 구조를 잘 파악하고 색인화하는 데 문제가 없습니다.


<Link> 컴포넌트를 사용하는 방법은 매우 간단합니다.

'next/link'에서 임포트: 링크를 사용하고자 하는 컴포넌트 파일 상단에 Link 컴포넌트를 임포트합니다.

import Link from 'next/link';

href prop 지정: <Link> 컴포넌트의 href prop에 이동하고자 하는 경로를 문자열로 지정합니다.

이 경로는 App Router의 파일 시스템 기반 라우트와 일치해야 합니다.

링크 텍스트나 요소 배치: Next.js 13 이상에서는 <Link> 자체가 <a>로 렌더링됩니다.

따라서 <Link> 안에 다시 <a> 태그를 넣지 않고, 링크 텍스트나 스타일 속성을 <Link>에 직접 둡니다.

src/app/page.tsx (예시)
import Link from 'next/link';

export default function HomePage() {
  return (
    <div>
      <h1>환영합니다!</h1>
      <p>이곳은 저희 웹사이트의 홈 페이지입니다.</p>
      <nav>
        <ul>
          <li>
            <Link href="/about">회사 소개 페이지로 이동</Link>
          </li>
          <li>
            <Link href="/dashboard" className="dashboard-link">
              대시보드 보러 가기
            </Link>
          </li>
          <li>
            <Link href="/blog/my-first-post">
              자세한 게시글 보기 (동적 라우트)
            </Link>
          </li>
        </ul>
      </nav>
    </div>
  );
}
코드 설명
  • 첫 번째 <li>에서는 <Link>에 이동할 경로와 텍스트를 직접 넣었습니다.
  • 두 번째 <li>에서는 <Link>className을 적용해 버튼처럼 보이게 만들 수 있습니다.
  • 세 번째 <li>는 동적 라우트의 예시입니다. 나중에 자세히 다루겠지만, href에 동적인 경로를 지정할 수 있습니다.

[slug] 또는 [id]와 같이 대괄호로 정의된 동적 라우트 세그먼트를 가진 페이지로 이동할 때도 <Link> 컴포넌트를 사용합니다.

이때 href 속성에는 실제 경로를 문자열로 전달합니다.

예를 들어 src/app/products/[id]/page.tsx라는 동적 라우트가 있다면, 특정 상품 페이지로 이동하는 링크는 다음과 같이 작성할 수 있습니다.

src/app/products/[id]/page.tsx
// 특정 상품 목록 페이지에서
import Link from 'next/link';

function ProductList() {
  const products = [
    { id: 'p001', name: '노트북' },
    { id: 'p002', name: '마우스' },
  ];

  return (
    <div>
      <h1>상품 목록</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <Link href={`/products/${product.id}`}>
              {product.name} 상세 보기
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

보시는 것처럼, JavaScript의 템플릿 리터럴(Template Literal)을 사용하여 동적인 값을 href에 쉽게 삽입할 수 있습니다.


<Link> 컴포넌트는 href 외에도 몇 가지 유용한 prop을 제공합니다.

  • replace: true로 설정하면 현재 히스토리 스택의 항목을 새 항목으로 교체합니다.

    브라우저의 뒤로 가기 버튼을 눌렀을 때 해당 페이지로 돌아오지 않게 하고 싶을 때 유용합니다. (기본값: false)

    <Link href="/dashboard" replace>
      대시보드로 이동 (뒤로 가기 방지)
    </Link>
  • scroll: false로 설정하면 새 경로로 이동할 때 스크롤 위치를 맨 위로 올리지 않습니다. (기본값: true) 특정 섹션으로 이동하거나, 스크롤 위치를 유지하고 싶을 때 사용합니다.

    <Link href="/products#section-a" scroll={false}>
      상품 페이지 특정 섹션으로 이동 (스크롤 유지)
    </Link>
  • prefetch: false로 설정하면 해당 링크의 프리페칭 기능을 비활성화합니다. (기본값: true) 매우 많은 링크가 한 페이지에 존재하여 네트워크 부하가 우려될 때 고려해볼 수 있습니다.

    <Link href="/heavy-page" prefetch={false}>
      무거운 페이지로 이동 (미리 로딩 안 함)
    </Link>

링크 이동은 선언형 <Link>를 기본으로 두고, 이벤트 이후 이동처럼 명령형 제어가 필요할 때만 라우터 훅을 선택하면 좋습니다.

선언형 이동은 Link, 조건 처리 뒤 이동은 useRouter를 선택한다

사용자가 링크를 보고 직접 누르는 이동과 코드가 이벤트 결과에 따라 이동시키는 경우를 나눠야 한다.

이동 시나리오선택코드 위치판단 기준
메뉴/내비게이션Link서버 또는 클라이언트 컴포넌트목적지가 화면에 드러남
카드 목록 상세 이동Link목록 렌더링 위치href를 데이터 id로 조립
로그인 성공 후 이동useRouter.pushclient component검증 완료 뒤 이동
저장 후 뒤로가기router.replace 또는 backclient component히스토리 제어 필요
데이터 다시 읽기router.refreshclient component현재 경로에서 서버 데이터 재요청

useRouter 훅 (클라이언트 컴포넌트에서)

<Link> 컴포넌트는 선언적으로 페이지 이동을 처리하기에 가장 좋은 방법입니다.

하지만 특정 이벤트(예: 폼 제출 후)에 따라 프로그래밍 방식으로 페이지를 이동해야 할 때는 Next.js가 제공하는 useRouter 훅을 사용할 수 있습니다.

useRouter는 클라이언트 컴포넌트에서만 사용할 수 있습니다.

src/app/dashboard/client-button.tsx
"use client"; // 클라이언트 컴포넌트임을 명시

import { useRouter } from 'next/navigation'; // useRouter 임포트

export default function ClientButton() {
  const router = useRouter(); // useRouter 훅 사용

  const handleClick = () => {
    // 버튼 클릭 시 프로그래밍 방식으로 /dashboard/settings 페이지로 이동
    router.push('/dashboard/settings');
  };

  return (
    <button onClick={handleClick}>
      설정 페이지로 이동 (useRouter)
    </button>
  );
}

useRouter 훅은 push, replace, refresh, back 등 다양한 메서드를 제공하여 라우팅을 세밀하게 제어할 수 있게 해줍니다.

이 훅에 대한 더 자세한 내용은 뒤에서 다룰 예정입니다.

링크 작성 시에는 목적지 노출 여부와 히스토리/스크롤 처리 기준을 먼저 정하면 선택이 단순해집니다.

href는 목적지, replace는 히스토리, scroll은 위치, prefetch는 사전 준비를 조절한다

Link의 prop은 이동 결과를 바꾸므로 화면 이동 자체와 브라우저 히스토리 경험을 함께 본다.

prop하는 일사용 예주의 신호
href이동할 내부 경로href="/dashboard"파일 시스템 라우트와 불일치
replace현재 히스토리 항목 교체로그인 후 이전 페이지로 돌아가지 않게 함뒤로 가기 UX가 어색해짐
scroll이동 후 스크롤 상단 이동 제어앵커 섹션 이동에서 false 검토사용자가 위치를 잃음
prefetch대상 페이지 사전 준비링크가 많은 화면에서 false 검토무거운 페이지를 과하게 미리 불러옴
className링크 자체 스타일 적용버튼처럼 보이는 링크button을 링크 안에 중첩하지 않음

작성한 링크가 실제 App Router 구조와 맞는지는 href, 렌더링 방식, 이동 후 상태를 함께 점검하면 좋습니다.

작성한 링크는 href, 라우트 파일, 렌더링 결과, 이동 후 상태로 점검한다

링크가 보인다고 끝이 아니다. 직접 URL, 클릭 이동, 새로고침, 뒤로 가기 경험까지 함께 확인한다.

점검 항목정상 상태문제 신호확인 위치
href실제 라우트와 일치404 또는 다른 화면src/app 폴더 구조
Link 자식텍스트나 요소를 직접 배치Link 안에 a 중첩 오류브라우저 콘솔과 코드
동적 값id/slug가 URL에 채워짐/products/undefinedmap 데이터와 href 조립
프리페치필요한 링크만 준비네트워크 요청이 과도함DevTools Network
이동 후 상태필요한 스크롤/히스토리 유지뒤로 가기나 스크롤이 어색함replace/scroll prop

페이지 간 링크는 사용자 경험과 애플리케이션 성능에 직접적인 영향을 줍니다.

<Link>는 클라이언트 내비게이션과 프리패칭을 제공하므로 내부 페이지 이동의 기본 선택지로 사용할 수 있습니다.

페이지 간 링크 생성의 판단 흐름을 내부 이동, 동적 href, 프리페치, 명령형 이동 기준으로 다시 묶었습니다.

링크 판단은 내부 경로, 동적 값, 사전 로딩, 명령형 이동 순서로 좁힌다

무조건 Link 하나로 끝내지 말고 목적지와 이동 계기를 분리하면 선택이 단순해진다.

질문선택확인할 것
앱 내부 경로인가/about, /dashboardLink해당 page.tsx 존재
URL 값이 데이터로 바뀌는가/products/p001Link href 조립id/slug 값 존재
링크가 너무 많은가목록 수백 개prefetch 조절네트워크 부하
이벤트 결과 뒤 이동인가저장 성공 후 이동useRouter.push/replaceclient component
브라우저 기본 동작인가외부 URL, 다운로드a 태그target, rel, download

마지막으로 Link와 useRouter를 언제 선택해야 하는지 이동 시나리오별로 정리합니다.

Link와 useRouter는 사용자가 보는 이동인지 코드가 결정하는 이동인지로 나눈다

대부분의 내부 페이지 이동은 Link가 기본이고, 이벤트 처리가 끝난 뒤 이동해야 할 때만 useRouter를 꺼낸다.

시나리오권장 방식이유대표 실수
상단 메뉴Link목적지가 명확히 보임onClick으로 단순 이동 처리
상품 목록 → 상세Link크롤링 가능한 href 유지id 없는 href 생성
검색 폼 제출 후 결과useRouter.push입력값 검증 뒤 이동서버 컴포넌트에서 훅 사용
로그인 후 대시보드router.replace로그인 페이지로 뒤로가기 방지push로 히스토리 누적
외부 문서 링크a 태그브라우저 기본 링크가 적합Link로 외부 이동까지 처리