[Routing] Linking and Navigating
Next.js 라우터는 클라이언트측 탐색과 함께 서버 중심의 라우팅을 사용한다. 이는 즉각적인 로딩 상태와 동시 렌더링을 지원하는데, 이는 곧 1) 클라이언트 측의 state를 유지하고 2) 비용이 많이드는 리렌더링을 피하며 3) race 컨디션을 유발하지 않는 동시에 4) 중단이 가능한 라우팅 기능을 제공함을 뜻한다.
※ '즉각적인 로딩 상태'란 페이지 이동과 같은 탐색시 (스피너 같은) UI가 즉각적으로 표출되는 것을 말함
이번 장에선 서로 다른 경로간의 탐색을 어떻게 구현할 수 있는지와 탐색이 동작하는 방식에 대해 알아보자.
1. Navigation 구현
Next.js에서 페이지 탐색을 위한 navigation 기능은 크게 두 가지로 구현할 수 있다. 하나는 Link 컴포넌트를 이용하는 것이고, 나머지 하나는 useRouter 훅을 이용하는 것이다.
1.1 Link 컴포넌트
HTML의 <a> 태그를 확장한 것으로, prefetching을 지원하고 경로간 client-side navigation을 지원하는 리액트 컴포넌트이다. Next.js에서 가장 흔하게 쓰이는 탐색 구현 방법.
사용법은 간단한데, next/link 모듈에서 Link를 가져와 href 속성만 잘 지정해주면 된다. Link 컴포넌트에서 지정 가능한 속성은 필수 속성인 href외에 replace, prefetch가 있는데, 자세한건 API 문서를 확인하자.
import Link from 'next/link';
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>;
}
1.2 Link 사용 예시
1) Linking to Dynamic Segments
아래는 dynamic segements간의 링크 지정에서 템플릿 리터럴과 string interpolation을 활용해 링크 배열을 새성하는 예제이다.
import Link from 'next/link';
export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</li>
))}
</ul>
);
}
2) Checking Active Link
usePathname을 쓰면 링크가 활성화되었는지 확인할 수 있다. 예를 들어, 현재 경로가 링크에서 지정한 href와 동일하다면 파란색 글씨로 나타내고 싶다면 아래와 같은 방식으로 구현이 가능하다.
'use client';
import { usePathname } from 'next/navigation';
import { Link } from 'next/link';
export function Navigation({ navLinks }) {
const pathname = usePathname();
return (
<>
{navLinks.map((link) => {
const isActive = pathname.startsWith(link.href);
return (
<Link
className={isActive ? 'text-blue' : 'text-black'}
href={link.href}
key={link.name}
>
{link.name}
</Link>
);
})}
</>
);
}
3) Scrolling to an id
<Link>의 기본 동작은 바뀌는 경로의 최상단 부분으로 스크롤하는 것인데, href에 특정 id가 할당돼있다면 <a>태그와 마찬가지로 해당 id의 상단으로 이동하게 된다. 만약 최상단으로 스크롤되는 동작이 싫다면 scroll속성을 false로 지정해주고, href에 hashed id를 할당하면 된다.
<Link href="/#hashid" scroll={false}>
Scroll to specific id.
</Link>
1.3 useRouter Hook
useRouter 훅은 클라이언트 컴포넌트 내부에서 경로를 바꿀 수 있게 해준다. 사용법은 next/navigation 모듈에서 import 한 뒤, 클라이언트 컴포넌트 내부에서 호출해주기만 하면 된다. useRouter 는 push, refresh같은 메서드를 지원하는데, 자세한건 API 문서를 참고하자.
'use client';
import { useRouter } from 'next/navigation';
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
);
}
사실 특별한 이유가 없는 이상 Link 컴포넌트를 쓰는게 더 좋다.
2. Navigation 동작 방식
Next.js에서 navigation이 동작하는 과정은 아래와 같은데, 일부 모르는 용어가 나와도 뒤에서 설명할 예정이니 전체 흐름을 살펴보자.
- <Link>나 router.push를 호출함으로써 경로 전환이 시작된다
- 라우터가 브라우저 주소창의 URL을 업데이트한다
- 라우터는 클라이언트 측 캐시를 뒤져 변경되지 않은 세그먼트는 그대로 재사용하고, 변경 사항이 발생한 부분만 다시 그려낸다. 이를 부분 렌더링이라고 하는데, 이를 통해 불필요한 작업량을 줄일 수 있다
- soft navigation 조건이 충족되면 라우터는 서버가 아닌 캐시에서 새로운 세그먼트를 fetch한다. 만약 그렇지 않은 경우라면 hard navigation을 수행하고, 서버로부터 서버 컴포넌트의 페이로드를 fetch해온다.
- 로딩 UI를 만들어놨다면 페이로드를 가져오는 동안 해당 UI를 표출한다.
- 라우터가 캐시를 쓰건 페이로드를 새로 가져오던 해서 준비한 데이터를 바탕으로 클라이언트 화면상에 새로운 세그먼트를 렌더링한다
2.1 Client-side Caching
13버전의 새 라우터는 인메모리 클라이언트 사이드 캐시를 가지고있는데, 렌더링된 서버 컴포넌트 결과물을 이 캐시에 저장해둔다. 이 캐시는 라우트 세그먼트별로 분할되어 관리되는데, 덕분에 어떤 수준에서건 캐시 무효화가 가능하고 동시 렌더링 전반에 걸쳐 데이터 일관성을 보장한다.
구체적으로는 유저가 앱을 탐색하는 과정에서 라우터는 이전에 fetch된 세그먼트와 prefetch된 세그먼트의 페이로드를 캐시에 저장해둔다. 즉, 서버에 request를 계속해서 새로 날릴 필요가 없으며, 캐시 내용물을 적당히 재사용함을 뜻한다. 이를 통해 불필요한 데이터 refetching과 컴포넌트 리렌더링을 줄일 수 있고, 결과적으로 앱의 성능을 향상시킬 수 있다.
2.2 캐시 무효화
router.refresh 함수를 쓰면 경로를 새로고침할 수 있다. 즉, 서버에 새로운 요청을 보내고 데이터 요청을 refetch하며 서버 컴포넌트의 리렌더링을 유발한다. 자세한건 API 문서를 참고하기 바라며, 향후 mutations은 자동으로 캐시를 무효화하게끔 만들 예정이다.
2.3 Prefetching
프리패칭이란 어떤 경로로 방문하기 전에 백그라운드에서 해당 경로의 페이지를 미리 로드시키는 방법이다. 프리패치된 경로의 렌더링 결과물은 라우터의 클라이언트 사이드 캐시에 저장되고, 유저가 해당 경로에 방문시 거의 즉각적인 페이지 전환이 일어나게끔 한다.
기본적으로 <Link> 컴포넌트를 쓰면 뷰포트안에 링크가 보이는 시점에 프리패치되는데, 이 동작은 페이지를 처음 로드하거나 스크롤할 때 발생한다. <Link>컴포넌트를 안쓴다면 useRouter훅의 prefetch 메서드를 써서 기계적으로 프리패치 시켜주는 방법도 있다. 기본적으로 프리패칭은 프로덕션 환경에서만 제공되며, <Link> 컴포넌트 기준 prefetch = { false } 속성으로 프리패칭을 막을 수도 있긴하다.
프리패칭은 정적/동적 라우트별로 약간 다르게 동작하는데, 각각 다음과 같다
- static routes : 라우트 세그먼트에 해당하는 모든 서버 컴포넌트의 페이로드가 프리패치된다
- dynamic routes : 첫 번째 shared layout부터 첫 번째 loading.js 파일까지 프리패치된다. 이 방법으로 전체 라우트를 동적으로 프리패칭하는 코스트를 줄이고, 다이나믹 라우트에 대한 로딩 상태를 즉각적으로 확인할 수 있다.
2.4 Hard/Soft Navigation
하드 네비게이션이란 탐색중 캐시를 무효화하고 서버에 데이터 refetch 및 changed segments의 리렌더링을 유발하는 탐색을 말한다. 반면, 소프트 네비게이션이란 변경되는 세그먼트의 캐시 내역이 존재하면 이를 재사용하고, 서버로의 새로운 데이터 요청을 막는 탐색을 말한다.
Next.js에선 특정 경로의 페이로드가 프리패치돼있고, dynamic segments를 포함하지 않거나 현재 경로와 동일한 dynamic parameter를 가지는 경우라면 소프트 네비게이션을 사용한다.
말이 좀 어려운데, 동적 경로인 [team] 세그먼트를 포함하는 경우를 생각해보자. 기본적인 경로는 /dashboard/[team]/* 로 구성될텐데, 이 상태에서 [team] 파라미터가 변할때만 하드 네비게이션이되고, 그게 아니라면 소프트 네비게이션이 된다 (당연한 소리지만 이 경로 이하의 세그먼트들이 캐시된 상태여야 한다. 그래야 재사용 하든가 말든가하지)
- 소프트 네비게이션 : /dashboard/team-red/A → /dashboard/team-red/B
- 하드 네비게이션 : /dashboard/team-red/A→ /dashboard/team-blue/A
2.5 Back/Forward Navigation (뒤로가기/앞으로가기)
브라우저의 뒤로가기/앞으로가기는 소프트 네비게이션으로 동작한다. 즉, 클라이언트측 캐시가 재사용되고 탐색이 거의 즉각적으로 일어난다는 뜻. (사실 이 부분은 웹의 History API중 일부인 popstate 이벤트와 연관이 깊은데 궁금하면 따로 찾아보자)
2.6 Focus & Scroll 관리
기본적으로 Next.js는 탐색에서 변경된 세그먼트로 스크롤 및 포커싱된다. 이 동작방식은 고급 라우팅 패턴에서 특히 더 쓸모있음.