페이지 간 링크 생성
Link의 클라이언트 전환과 프리페치를 이해하고 정적·동적 href와 useRouter를 상황에 맞게 사용합니다.
웹 애플리케이션의 본질은 정보와 기능을 제공하고 사용자가 자유롭게 이동하도록 만드는 데 있습니다.
그래서 페이지를 연결하는 링크(Link)는 필수 요소입니다.
Next.js App Router에서는
기본 <a> 태그보다 Next.js의 <Link> 컴포넌트 사용을 권장합니다.
이 절에서는 <Link> 컴포넌트의 사용법과 그 이점, 그리고 실제 애플리케이션에서 링크를 효율적으로 관리하는 방법에 대해 자세히 알아보겠습니다.
아래 표는 내부 링크를 만들 때 <Link>, href, 프리페치, 명령형 이동을 어떤 기준으로 나눠 볼지 먼저 정리합니다.
페이지 이동을 만들기 전에 목적지가 내부 라우트인지, 사용자 이벤트 뒤에 이동하는지, 브라우저 기본 동작이 필요한지부터 정한다.
| 상황 | 기본 선택 | 이유 | 주의할 점 |
|---|---|---|---|
| 내부 페이지 이동 | <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 관점에서 비교한 것입니다.
둘 다 최종적으로 링크처럼 보이지만, 앱 내부 이동에서 Link는 새로고침을 줄이고 대상 페이지를 미리 준비할 수 있다.
| 비교 항목 | Link | a 태그 | 선택 기준 |
|---|---|---|---|
| 대상 | 앱 내부 라우트 | 외부 URL, 다운로드, 메일 | 내부 경로면 Link 우선 |
| 렌더링 | 자체가 a 요소로 렌더링 | 브라우저 기본 a | Link 안에 a를 중첩하지 않음 |
| 이동 방식 | 클라이언트 사이드 탐색 | 전체 문서 이동 가능 | 내부 이동은 깜빡임을 줄임 |
| 프리페치 | 뷰포트에 보이면 준비 가능 | 없음 | 많은 링크는 prefetch 조절 |
| SEO | a 기반 링크로 크롤링 가능 | 표준 링크 | 의미 있는 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> 컴포넌트 사용법
<Link> 컴포넌트를 사용하는 방법은 매우 간단합니다.
'next/link'에서 임포트:
링크를 사용하고자 하는 컴포넌트 파일 상단에 Link 컴포넌트를 임포트합니다.
import Link from 'next/link';href prop 지정:
<Link> 컴포넌트의 href prop에 이동하고자 하는 경로를 문자열로 지정합니다.
이 경로는 App Router의 파일 시스템 기반 라우트와 일치해야 합니다.
링크 텍스트나 요소 배치:
Next.js 13 이상에서는 <Link> 자체가 <a>로 렌더링됩니다.
따라서 <Link> 안에 다시 <a> 태그를 넣지 않고, 링크 텍스트나 스타일 속성을 <Link>에 직접 둡니다.
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에 동적인 경로를 지정할 수 있습니다.
동적 경로(Dynamic Paths)와 <Link>
[slug] 또는 [id]와 같이 대괄호로 정의된 동적 라우트 세그먼트를 가진 페이지로 이동할 때도 <Link> 컴포넌트를 사용합니다.
이때 href 속성에는 실제 경로를 문자열로 전달합니다.
예를 들어 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 컴포넌트의 추가적인 prop들 (선택)
<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 | 서버 또는 클라이언트 컴포넌트 | 목적지가 화면에 드러남 |
| 카드 목록 상세 이동 | Link | 목록 렌더링 위치 | href를 데이터 id로 조립 |
| 로그인 성공 후 이동 | useRouter.push | client component | 검증 완료 뒤 이동 |
| 저장 후 뒤로가기 | router.replace 또는 back | client component | 히스토리 제어 필요 |
| 데이터 다시 읽기 | router.refresh | client component | 현재 경로에서 서버 데이터 재요청 |
useRouter 훅 (클라이언트 컴포넌트에서)
<Link> 컴포넌트는 선언적으로 페이지 이동을 처리하기에 가장 좋은 방법입니다.
하지만 특정 이벤트(예: 폼 제출 후)에 따라 프로그래밍 방식으로 페이지를 이동해야 할 때는 Next.js가 제공하는 useRouter 훅을 사용할 수 있습니다.
useRouter는 클라이언트 컴포넌트에서만 사용할 수 있습니다.
"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 등 다양한 메서드를 제공하여 라우팅을 세밀하게 제어할 수 있게 해줍니다.
이 훅에 대한 더 자세한 내용은 뒤에서 다룰 예정입니다.
링크 작성 시에는 목적지 노출 여부와 히스토리/스크롤 처리 기준을 먼저 정하면 선택이 단순해집니다.
Link의 prop은 이동 결과를 바꾸므로 화면 이동 자체와 브라우저 히스토리 경험을 함께 본다.
| prop | 하는 일 | 사용 예 | 주의 신호 |
|---|---|---|---|
| href | 이동할 내부 경로 | href="/dashboard" | 파일 시스템 라우트와 불일치 |
| replace | 현재 히스토리 항목 교체 | 로그인 후 이전 페이지로 돌아가지 않게 함 | 뒤로 가기 UX가 어색해짐 |
| scroll | 이동 후 스크롤 상단 이동 제어 | 앵커 섹션 이동에서 false 검토 | 사용자가 위치를 잃음 |
| prefetch | 대상 페이지 사전 준비 | 링크가 많은 화면에서 false 검토 | 무거운 페이지를 과하게 미리 불러옴 |
| className | 링크 자체 스타일 적용 | 버튼처럼 보이는 링크 | button을 링크 안에 중첩하지 않음 |
작성한 링크가 실제 App Router 구조와 맞는지는 href, 렌더링 방식, 이동 후 상태를 함께 점검하면 좋습니다.
링크가 보인다고 끝이 아니다. 직접 URL, 클릭 이동, 새로고침, 뒤로 가기 경험까지 함께 확인한다.
| 점검 항목 | 정상 상태 | 문제 신호 | 확인 위치 |
|---|---|---|---|
| href | 실제 라우트와 일치 | 404 또는 다른 화면 | src/app 폴더 구조 |
| Link 자식 | 텍스트나 요소를 직접 배치 | Link 안에 a 중첩 오류 | 브라우저 콘솔과 코드 |
| 동적 값 | id/slug가 URL에 채워짐 | /products/undefined | map 데이터와 href 조립 |
| 프리페치 | 필요한 링크만 준비 | 네트워크 요청이 과도함 | DevTools Network |
| 이동 후 상태 | 필요한 스크롤/히스토리 유지 | 뒤로 가기나 스크롤이 어색함 | replace/scroll prop |
페이지 간 링크는 사용자 경험과 애플리케이션 성능에 직접적인 영향을 줍니다.
<Link>는 클라이언트 내비게이션과 프리패칭을 제공하므로 내부 페이지 이동의 기본 선택지로 사용할 수 있습니다.
페이지 간 링크 생성의 판단 흐름을 내부 이동, 동적 href, 프리페치, 명령형 이동 기준으로 다시 묶었습니다.
무조건 Link 하나로 끝내지 말고 목적지와 이동 계기를 분리하면 선택이 단순해진다.
| 질문 | 예 | 선택 | 확인할 것 |
|---|---|---|---|
| 앱 내부 경로인가 | /about, /dashboard | Link | 해당 page.tsx 존재 |
| URL 값이 데이터로 바뀌는가 | /products/p001 | Link href 조립 | id/slug 값 존재 |
| 링크가 너무 많은가 | 목록 수백 개 | prefetch 조절 | 네트워크 부하 |
| 이벤트 결과 뒤 이동인가 | 저장 성공 후 이동 | useRouter.push/replace | client component |
| 브라우저 기본 동작인가 | 외부 URL, 다운로드 | a 태그 | target, rel, download |
마지막으로 Link와 useRouter를 언제 선택해야 하는지 이동 시나리오별로 정리합니다.
대부분의 내부 페이지 이동은 Link가 기본이고, 이벤트 처리가 끝난 뒤 이동해야 할 때만 useRouter를 꺼낸다.
| 시나리오 | 권장 방식 | 이유 | 대표 실수 |
|---|---|---|---|
| 상단 메뉴 | Link | 목적지가 명확히 보임 | onClick으로 단순 이동 처리 |
| 상품 목록 → 상세 | Link | 크롤링 가능한 href 유지 | id 없는 href 생성 |
| 검색 폼 제출 후 결과 | useRouter.push | 입력값 검증 뒤 이동 | 서버 컴포넌트에서 훅 사용 |
| 로그인 후 대시보드 | router.replace | 로그인 페이지로 뒤로가기 방지 | push로 히스토리 누적 |
| 외부 문서 링크 | a 태그 | 브라우저 기본 링크가 적합 | Link로 외부 이동까지 처리 |