페이지 간 링크 생성
Link의 클라이언트 전환과 프리페치를 이해하고 정적·동적 href와 useRouter를 상황에 맞게 사용합니다.
웹 애플리케이션의 본질은 정보와 기능을 제공하고 사용자가 자유롭게 이동하도록 만드는 데 있습니다.
그래서 페이지를 연결하는 링크(Link)는 필수 요소입니다.
Next.js App Router에서는
기본 <a> 태그보다 Next.js의 <Link> 컴포넌트 사용을 권장합니다.
이 절에서는 <Link> 컴포넌트의 사용법과 그 이점, 그리고 실제 애플리케이션에서 링크를 효율적으로 관리하는 방법에 대해 자세히 알아보겠습니다.
<a> 태그 대신 <Link>를 사용하는 이유
일반적인 HTML의 <a> 태그를 사용하여 페이지를 이동할 경우, 브라우저는 해당 페이지를 처음부터 다시 로드합니다.
이는 전통적인 웹사이트에서는 일반적이었지만, React와 같은 SPA(Single Page Application) 프레임워크 기반의 애플리케이션에서는 전체 페이지가 깜빡이거나 새로고침되는 듯한 사용자 경험을 제공하게 됩니다.
반면, Next.js의 <Link> 컴포넌트를 사용하면 다음과 같은 중요한 이점을 얻을 수 있습니다.
-
클라이언트 사이드 탐색 (Client-side Navigation):
<Link>컴포넌트는 페이지 전체를 새로 로드하는 대신, JavaScript를 사용하여 필요한 부분만 업데이트합니다.이는 마치 데스크톱 애플리케이션처럼 부드럽고 빠른 페이지 전환을 가능하게 합니다.
브라우저의 새로고침 현상 없이 콘텐츠만 바뀌는 것을 경험할 수 있습니다.
-
코드 스플리팅 및 프리페칭 (Code Splitting and Pre-fetching): Next.js는 라우트별로 필요한 코드를 나눠 전달합니다.
<Link>를 추가하는 행위 자체가 코드 분할의 기준은 아닙니다.production의 자동 프리페치는 링크가 뷰포트에 들어오면 대상 라우트와 데이터를 정책에 따라 준비합니다. 네트워크와 동적 데이터 대기가 남을 수 있어 즉시 완료되는 전환을 보장하지 않습니다.
-
검색 엔진 최적화 (SEO) 이점:
<Link>컴포넌트는 여전히 내부적으로<a>태그로 렌더링되므로, 검색 엔진 크롤러가 웹사이트의 구조를 잘 파악하고 색인화하는 데 문제가 없습니다.
<Link> 컴포넌트 사용법
<Link> 컴포넌트를 사용하는 방법은 매우 간단합니다.
'next/link'에서 임포트:
링크를 사용하고자 하는 컴포넌트 파일 상단에 Link 컴포넌트를 임포트합니다.
import Link from 'next/link';href prop 지정:
<Link> 컴포넌트의 href prop에 이동하고자 하는 경로를 문자열로 지정합니다.
앱 내부 페이지로 이동할 때는 해당 App Router 경로와 일치하는 URL을 사용합니다. href에는 해시나 외부 URL도 쓸 수 있으며, 외부 사이트와 다운로드에는 일반 <a>도 적합합니다.
링크 텍스트나 요소 배치:
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에 동적인 경로를 지정할 수 있습니다.
이 절의 첫 클릭 실습은 앞서 생성한 /about, /dashboard, /dashboard/settings 페이지에서 확인합니다. /blog/my-first-post, 뒤의 상품 URL과 /heavy-page는 대상 페이지를 작성했다는 조건의 예시입니다. href만 추가해 없는 페이지가 만들어지지는 않으므로, 해당 파일이 없는 프로젝트에서의 404를 Link 문법 오류와 혼동하지 않습니다.
동적 경로(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는 Next.js의 이동 후 스크롤 관리를 끕니다. 기본값true도 무조건 문서 맨 위로 이동하는 것은 아니며, 새 페이지 영역이 현재 화면에 보이는지 등을 확인해 위치를 조정합니다.<Link href="/products#section-a" scroll={false}> 상품 페이지 특정 섹션으로 이동 (스크롤 유지) </Link> -
prefetch: 기본값은"auto"또는null이며, 정적·동적 라우트와 설정에 따라 준비하는 범위가 달라집니다.false는 뷰포트 진입과 hover 프리페치를 모두 끕니다. 자동 프리페치는 production에서만 실행됩니다.<Link href="/heavy-page" prefetch={false}> 무거운 페이지로 이동 (미리 로딩 안 함) </Link>
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>
);
}예시
client-button.tsx를 저장하는 것만으로 화면에 버튼이 나타나지는 않습니다. 기존 /dashboard 페이지를 아래 별도 실습 버전으로 교체해 컴포넌트를 가져오고 렌더링합니다. 위 ClientButton 파일은 그대로 사용하며, 이전 DashboardHomePage와 아래 페이지를 한 파일에 함께 두지 않습니다.
import ClientButton from './client-button';
export default function DashboardNavigationPage() {
return (
<section>
<h1>대시보드 이동 실습</h1>
<ClientButton />
</section>
);
}/dashboard에서 버튼을 누르면 onClick이 router.push('/dashboard/settings')를 호출합니다. 대상은 앞서 만든 settings 페이지이므로 URL이 /dashboard/settings로 바뀌고 Dashboard Settings 제목이 보입니다. 버튼 파일은 Client Component, 이를 가져오는 위 Page는 기본 Server Component입니다. 단순한 이동 링크에는 Link를 쓰고, 이처럼 이벤트 처리에서 이동을 호출해야 할 때 useRouter를 사용합니다.
히스토리도 URL과 함께 추적합니다. 현재 이력이 / → /about이고 소개가 활성 항목인 상황을 따로 가정합니다. settings로 기본 push하면 / → /about → /dashboard/settings가 되어 한 번 뒤로 갈 대상은 /about입니다. 같은 출발 이력에서 replace로 이동하면 현재 소개만 교체하여 / → /dashboard/settings가 되고 뒤로 갈 대상은 /입니다. 이는 Link의 replace 규칙이며, 스크롤 설정이나 프리페치 여부와는 별개입니다.
독립 연습: replace 다음에 push를 더한다면?
예측 범위의 최초 이력이 / 한 항목이라고 가정합니다. 기본 Link로 /about에 간 뒤 replace로 /dashboard에 가고, 마지막으로 ClientButton의 push로 /dashboard/settings에 갑니다. 각 이동은 정상 완료되며 그 사이 뒤로 가기나 새로고침은 없습니다. 마지막 이력 순서와 한 번 뒤로 갈 때의 URL을 말해 보세요. replace를 한 번 썼다는 이유로 마지막 push도 이력을 교체하나요?
풀이: /about 이동 뒤는 / → /about, replace 뒤는 / → /dashboard, 마지막 push 뒤는 / → /dashboard → /dashboard/settings입니다. 한 번 뒤로 갈 대상은 /dashboard입니다. replace는 그 이동에서 현재 항목을 교체하는 선택이며 이후 push 동작을 바꾸지 않습니다. 프리페치를 꺼도 클릭 이동 자체나 이력 규칙이 사라지는 것은 아닙니다.
useRouter 훅은 push, replace, refresh, back 등 다양한 메서드를 제공하여 라우팅을 세밀하게 제어할 수 있게 해줍니다.
이 훅에 대한 더 자세한 내용은 뒤에서 다룰 예정입니다.
탐색 옵션이 바꾸는 결과
같은 초기 history에서 push는 항목을 추가하고 replace는 현재 항목을 바꾼 뒤 back 결과가 달라집니다. 고정 history 역할의 전후 상태를 두 독립 분기로 추적하면 브라우저 기록 변화가 드러납니다.
NEXT · CH3-4
replace는 현재 항목만 교체합니다
홈→소개에서 설정으로 이동하는 예입니다. 이력, 스크롤, 미리 가져오기는 서로 다른 선택이며, replace가 모든 뒤로 가기를 막지는 않습니다.
| 선택 | 구체적인 결과 | 남는 경계 |
|---|---|---|
| 기본 push | 이력이 홈 → 소개일 때 설정으로 가면 홈 → 소개 → 설정 | 뒤로 가면 소개로 돌아갈 수 있음 |
| replace | 같은 출발 이력에서 설정으로 가면 홈 → 설정 | 현재 소개 항목만 교체; 홈 등 더 앞선 이력은 남음 |
| scroll={false} | Next.js가 이동 후 스크롤 위치를 자동 조정하지 않음 | URL에 #id를 넣는 것과 그 요소로 스크롤하는 것은 구분 |
| prefetch={false} | 뷰포트 진입·hover에서 대상 준비를 하지 않음 | 클릭 탐색은 가능하며, 그때 필요한 데이터를 기다릴 수 있음 |
- 기본 push
- 구체적인 결과: 이력이 홈 → 소개일 때 설정으로 가면 홈 → 소개 → 설정남는 경계: 뒤로 가면 소개로 돌아갈 수 있음
- replace
- 구체적인 결과: 같은 출발 이력에서 설정으로 가면 홈 → 설정남는 경계: 현재 소개 항목만 교체; 홈 등 더 앞선 이력은 남음
- scroll={false}
- 구체적인 결과: Next.js가 이동 후 스크롤 위치를 자동 조정하지 않음남는 경계: URL에 #id를 넣는 것과 그 요소로 스크롤하는 것은 구분
- prefetch={false}
- 구체적인 결과: 뷰포트 진입·hover에서 대상 준비를 하지 않음남는 경계: 클릭 탐색은 가능하며, 그때 필요한 데이터를 기다릴 수 있음
이력은 동작을 설명하는 예시이며 실행 기록이 아닙니다. 자동 프리페치를 확인하려면 개발 서버와 production을 구분합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 출발 이력은 홈과 현재 소개 두 항목입니다. 설정으로 push하면 세 항목이 되어 뒤로 갈 때 소개입니다. replace하면 현재 소개만 설정으로 교체해 두 항목이며 뒤로 갈 때 홈입니다. 두 결과는 같은 출발점의 독립 비교입니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 같은 출발 history 두 독립 실험 모두 홈·현재 소개의 두 항목에서 출발합니다. | history: 홈→소개 · 현재: 소개 · 항목 수: 2 | 비교할 출발 상태를 동일하게 고정합니다. |
| push와 replace의 전후 push는 설정을 추가하고 replace는 소개 자리를 설정으로 교체합니다. | push: 홈→소개→설정 · replace: 홈→설정 · 실험: 독립 분기 | 항목 수와 이전 항목이 다르게 바뀝니다. |
| back 결과 push 실험의 back은 소개이고 replace 실험의 back은 홈입니다. | push back: 소개 · replace back: 홈 | 변경한 history가 서로 다른 되돌아가기 결과를 만듭니다. |
탐색 옵션이 바꾸는 결과
같은 초기 history에서 push는 항목을 추가하고 replace는 현재 항목을 바꾼 뒤 back 결과가 달라집니다. 고정 history 역할의 전후 상태를 두 독립 분기로 추적하면 브라우저 기록 변화가 드러납니다.
NEXT · CH3-4
replace는 현재 항목만 교체합니다
홈→소개에서 설정으로 이동하는 예입니다. 이력, 스크롤, 미리 가져오기는 서로 다른 선택이며, replace가 모든 뒤로 가기를 막지는 않습니다.
| 선택 | 구체적인 결과 | 남는 경계 |
|---|---|---|
| 기본 push | 이력이 홈 → 소개일 때 설정으로 가면 홈 → 소개 → 설정 | 뒤로 가면 소개로 돌아갈 수 있음 |
| replace | 같은 출발 이력에서 설정으로 가면 홈 → 설정 | 현재 소개 항목만 교체; 홈 등 더 앞선 이력은 남음 |
| scroll={false} | Next.js가 이동 후 스크롤 위치를 자동 조정하지 않음 | URL에 #id를 넣는 것과 그 요소로 스크롤하는 것은 구분 |
| prefetch={false} | 뷰포트 진입·hover에서 대상 준비를 하지 않음 | 클릭 탐색은 가능하며, 그때 필요한 데이터를 기다릴 수 있음 |
- 기본 push
- 구체적인 결과: 이력이 홈 → 소개일 때 설정으로 가면 홈 → 소개 → 설정남는 경계: 뒤로 가면 소개로 돌아갈 수 있음
- replace
- 구체적인 결과: 같은 출발 이력에서 설정으로 가면 홈 → 설정남는 경계: 현재 소개 항목만 교체; 홈 등 더 앞선 이력은 남음
- scroll={false}
- 구체적인 결과: Next.js가 이동 후 스크롤 위치를 자동 조정하지 않음남는 경계: URL에 #id를 넣는 것과 그 요소로 스크롤하는 것은 구분
- prefetch={false}
- 구체적인 결과: 뷰포트 진입·hover에서 대상 준비를 하지 않음남는 경계: 클릭 탐색은 가능하며, 그때 필요한 데이터를 기다릴 수 있음
이력은 동작을 설명하는 예시이며 실행 기록이 아닙니다. 자동 프리페치를 확인하려면 개발 서버와 production을 구분합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 출발 이력은 홈과 현재 소개 두 항목입니다. 설정으로 push하면 세 항목이 되어 뒤로 갈 때 소개입니다. replace하면 현재 소개만 설정으로 교체해 두 항목이며 뒤로 갈 때 홈입니다. 두 결과는 같은 출발점의 독립 비교입니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 같은 출발 history 두 독립 실험 모두 홈·현재 소개의 두 항목에서 출발합니다. | history: 홈→소개 · 현재: 소개 · 항목 수: 2 | 비교할 출발 상태를 동일하게 고정합니다. |
| push와 replace의 전후 push는 설정을 추가하고 replace는 소개 자리를 설정으로 교체합니다. | push: 홈→소개→설정 · replace: 홈→설정 · 실험: 독립 분기 | 항목 수와 이전 항목이 다르게 바뀝니다. |
| back 결과 push 실험의 back은 소개이고 replace 실험의 back은 홈입니다. | push back: 소개 · replace back: 홈 | 변경한 history가 서로 다른 되돌아가기 결과를 만듭니다. |