서버 사이드 네비게이션
서버 사이드 네비게이션은 사용자가 링크를 클릭하거나 URL을 입력할 때 브라우저가 서버에 HTTP 요청(일반적으로 GET
방식)을
보내고, 서버는 해당 요청에 맞는 완전한 HTML 문서를 생성하여 응답합니다.
이 과정에서 브라우저는 새 페이지의 모든 리소스를 다시 불러오므로 전체 페이지가 새로고침됩니다.
또한 페이지 간 이동 시 전체 DOM
트리가 다시 렌더링 됩니다.
작동 원리
- HTTP 요청 발생: 사용자가 링크(a 태그)나 버튼을 클릭하면 브라우저는 해당 URL로 HTTP 요청을 보냅니다.
- 서버 응답: 서버는 요청에 따라 해당 페이지를 HTML, CSS, JavaScript 등의 리소스와 함께 응답합니다.
- 전체 페이지 새로고침: 브라우저는 응답받은 완성된 HTML을 이용해 페이지 전체를 다시 렌더링 합니다. 이 과정에서 브라우저는 기존에 있던
DOM
트리를 파기하고 새로운 트리를 생성합니다.
구현 방식
-
<a href="...">
태그를 이용한 페이지 이동 -
window.location
객체를 이용한 페이지 이동href
속성을 이용한 페이지 이동 ->assign()
메서드와 동일하게 동작합니다.assign()
: 페이지를 히스토리 스택에 추가합니다. 뒤로 가기 버튼을 누르면 이전 페이지로 이동합니다.replace()
: 페이지를 히스토리 스택에서 완전히 교체합니다. 뒤로 가기 버튼을 눌러도 이전 페이지로 이동하지 않습니다.
-
<form>
태그를 이용한 페이지 이동<form action="..." method="GET">
- form 요소를 이용해 GET 방식으로 페이지 이동을 할 수 있습니다.
-
meta
태그를 이용한 페이지 이동<meta http-equiv="refresh" content="0;url=...">
- content 속성에 지정된 시간(초) 후에 지정된 URL로 이동합니다.
장점
- SEO: 서버 사이드 렌더링은 검색 엔진 최적화(SEO)에 유리합니다. 검색 엔진은 페이지의 내용을 크롤링 할 때 HTML을 읽어야 하는데, 서버 사이드 네비게이션은 완성된 HTML을 제공하므로 검색 엔진이 쉽게 인덱싱할 수 있습니다.
- 단순한 구현: 클라이언트 사이드의 복잡한 로직 없이, 링크 클릭 시 기본 동작으로 동작하므로 상대적으로 구현이 단순합니다.
- 브라우저 호환성: 스크립트가 비활성 되어 있거나 오래된 브라우저에서도 동작하므로 기본 웹 환경에서 안정적입니다.
단점
- 느린 전환 속도: 전체 페이지를 새로고침하므로 필요한 리소스와 레이아웃 등 모든 것을 다시 불러와야 하므로 전환 속도가 느립니다. 사용자의 경우 페이지가 깜빡거리는 현상을 경험할 수 있습니다.
- 불필요한 리소스 다운로드: 전체 페이지를 새로 렌더링 하므로 이미 존재하는 리소스(공통 레이아웃, 공통 요소)를 다시 다운로드해야 합니다. 이는 불필요한 네트워크 트래픽을 발생시킵니다. -> 서버 부하 증가
- 상태 관리 어려움: 페이지 전환 시 클라이언트에 남아있던 상태(스크롤 위치, 입력값 등)가 초기화되므로 상태를 유지하기 어렵습니다.
이와 같은 서버 사이드 네비게이션의 단점을 보완하기 위해 클라이언트 사이드 네비게이션을 사용할 수 있습니다.
클라이언트 사이드 네비게이션
클라이언트 사이드 네비게이션은 사용자가 애플리케이션 내에서 이동할 때 전체 페이지 새로고침 없이, 필요한 부분만 동적으로 변경하는 방식입니다. 위의 서버 사이드 네비게이션과 달리 초기 로드 후 클라이언트 사이드에서 스크립트가 라우팅 및 렌더링을 담당합니다.
작동 원리
- 초기 페이지 로드: 브라우저는 처음에 최소한의 혹은 초기 상태의 HTML 문서를 서버에서 받아옵니다. 이때 서버는 클라이언트 사이드 라우팅을 위한 스크립트 번들을 함께 제공합니다.
- 라우팅 관리: 클라이언트 내에 존재하는 라우터가 현재 URL을 파악하고, 해당 URL에 맞는 컴포넌트 혹은 뷰를 렌더링 합니다. URL 변경 시 전체 페이지를 새로 고치는 대신 DOM의 일부분만 업데이트합니다.
History API
: 브라우저의History
객체를 이용해 URL을 변경하고, 뒤로/앞으로 가기 기능을 처리합니다.
장점
- 빠른 전환 속도: 전체 페이지를 새로고침하지 않고 필요한 부분만 업데이트하므로 전환 속도가 빠릅니다. 사용자는 페이지가 깜빡거리지 않고 부드럽게 전환되는 경험을 할 수 있습니다. -> 네이티브 앱과 유사한 사용자 경험 제공
- 상태 관리 용이: 페이지 전환 시 상태를 유지할 수 있습니다. 브라우저 히스토리를 이용해 뒤로/앞으로 가기 기능을 제공하면서 상태를 유지할 수 있습니다.
- 효율적인 리소스 관리: 전체 HTML을 매번 요청하는 대신, 변경되는 부분만 갱신하므로 불필요한 리소스 다운로드를 줄일 수 있습니다. -> 네트워크 트래픽 감소, 서버 부하 감소
단점
- 초기 로딩 비용 증가: SPA 등 클라이언트 사이드 네비게이션을 구현하는 경우, 초기 로딩 시 필요한 리소스(JavaScript, CSS, 이미지 등)를 모두 다운로드해야 하므로 초기 로딩 속도가 느릴 수 있습니다.
- SEO 이슈: 서버에서 생성된 HTML이 아닌 동적으로 구성된 컨텐츠를 제공하므로 검색 엔진 최적화(SEO)에 불리할 수 있습니다. -> SSR(Server-Side Rendering), SSG(Static Site Generation) 또는 Prerendering 등을 이용해 SEO 문제를 해결할 수 있습니다.
- 클라이언트 의존성: 클라이언트 성능에 의존하므로, 저사양 기기나 느린 네트워크 환경에서는 성능 이슈가 발생할 수 있습니다.
클라이언트 사이드 네비게이션은 사용자 상호작용 측면에서 부드러운 전환을 제공하지만, 초기 로딩 속도 및 SEO 최적화에는 한계가 있을 수 있습니다.
이런 문제를 해결하기 위해 SSR과 SSG 등을 이용해 SEO 문제를 해결하거나 초기 로딩 속도를 개선할 수 있습니다. -> Next.js
와 같은 프레임워크
클라이언트 사이드 네비게이션 구현 방식
History / Navigation API
History
브라우저의 세션 기록(History)을 제어하여 페이지 이동을 프로그래밍적으로 처리할 수 있도록 해줍니다.
주로 페이지 간 이동이나 SPA(Single Page Application) 구현 시 URL 변경을 처리할 때 사용됩니다.
popstate
이벤트를 통해 브라우저의 뒤로/앞으로 가기 동작에 대응할 수 있습니다.
- 주요 메서드
pushState(state, unused, url)
: 현재 페이지를 새로고침하지 않고 새로운 히스토리 항목을 추가합니다.- state: 해당 항목과 연관된 상태 객체
- unused: 대부분의 브라우저에서는 현재 무시됨 -> 빈 문자열 전달하는 것이 안전
- url: 주소 표시줄에 나타날 URL
replaceState(state, title, url)
: 현재 히스토리 항목을 새로운 데이터로 대체합니다. (새 항목 생성 x)back()
: 히스토리 스택을 한 단계 뒤로 이동하며, 브라우저의 뒤로 가기와 동일한 효과를 나타냅니다.forward()
: 히스토리 스택을 앞으로 이동합니다.go(n)
: 히스토리 스택에서 상대적인 위치(음수는 뒤로, 양수는 앞으로)로 이동하거나, 매개변수가 0이면 현재 페이지를 새로고침합니다.
history.pushState({ page: "home" }, "home", "/home");
history.replaceState({ page: "about" }, "about", "/about");
const goBack = () => {
window.history.back();
};
const refreshPage = () => {
window.history.go(0);
};
window.addEventListener("popstate", (event) => {
console.log("현재 상태:", event.state);
});
Navigation
보다 세밀한 네비게이션을 위해 등장한 API로, 프로그래밍적으로 코드 내에서 네비게이션을 트리거 하거나 (navigate()
)
navigate
이벤트를 가로채 추가 처리를 할 수 있도록 해줍니다. 기존 History API 기능을 보완하면서, 네비게이션의 시작, 커밋 및 완료 상태를 프로미스 객체로 관리합니다.
- 주요 메서드
navigate(url, options)
: 지정된 URL로 프로그램적으로 이동합니다. 옵션 객체에 상태 등 추가 정보를 전달할 수 있습니다. 프로미스를 반환하여 네비게이션 상태(커밋/완료)를 추적할 수 있습니다.reload(options)
: 현재 페이지를 새로고침하며, 새 상태를 지정할 수 있습니다.traverseTo(key)
: 저장해둔 특정 히스토리 key를 사용해 과거 특정 기록으로 이동합니다.
// programmatic navigation (특정 버튼 클릭 시 페이지 이동)
document.querySelector(".about").addEventListener("click", async () => {
try {
showLoadingIndicator();
const response = await fetch(url);
const prefetchedData = await response.text();
updatePageContent(prefetchedData);
await navigation.navigate(url).finished;
} catch (error) {
console.error("Error:", error);
} finally {
hideLoadingIndicator();
}
});
// intercept navigation (모든 네비게이션 이벤트를 가로채기)
navigation.addEventListener("navigate", (event) => {
event.intercept({
async handler() {
try {
showLoadingIndicator();
const response = await fetch(event.destination.url);
const prefetchedData = await response.text();
updatePageContent(prefetchedData);
} catch (error) {
console.error("Error:", error);
} finally {
hideLoadingIndicator();
}
},
});
});
위의 코드와 같이 네비게이션은 명시적으로 navigation.navigate()
메서드를 호출하여 프로그래밍적으로 페이지 이동을 처리할 수 있습니다.
네비게이션을 트리거 한 후 프로미스를 통해 네비게이션 상태를 추적할 수 있습니다. 또한 네비게이션 이벤트가 발생할 때 자동으로 가로채기 처리를 할 수 있습니다. event.intercept()
메서드를 통해 커스텀 로직(로딩 화면 표시, 데이터 프리페치 등)을 수행할 수 있습니다.
즉 History API를 통해 히스토리를 제어하고, Navigation API를 통해 네비게이션 과정을 좀 더 세밀하게 제어할 수 있습니다.
현재 Navigation API는 실험적인 기능으로, 브라우저 호환성 및 안정성에 주의해야 합니다.
Next.js
Next.js는 클라이언트 사이드 네비게이션을 보다 쉽게 구현할 수 있도록 Link
컴포넌트와 Router
객체를 제공합니다. 이를 통해 개발자는 저수준의 API를 직접 다루지 않고도 Link 컴포넌트를 사용하여 페이지 간 이동을 처리하거나, Router 객체로 프로그래밍 방식의 페이지 이동을 손쉽게 구현할 수 있습니다.
또한, 클라이언트 사이드 네비게이션의 한계점인 SEO 문제를 보완하기 위해 서버 사이드 렌더링(SSR)을 기본적으로 지원합니다.
Link
Next.js의 Link
컴포넌트는 기존 a 태그의 기능을 확장하여 클라이언트 사이드 네비게이션과 프리페칭 기능을 제공합니다.
- 해당 라우트에 필요한 부분만을 반영하여, 페이지 전환 시 전체 페이지 새로 고침 없이 이동합니다.
Link
컴포넌트가 브라우저의 뷰포트에 노출될 경우 자동으로 프리페칭을 수행하여 사용자 경험을 향상시킵니다.- 기본적으로
prefetch={null}
상태이며, 라우트가 정적일 경우 자동으로 프리페칭을 수행합니다. 동적 라우트의 경우loading.js
경계에 가장 가까운 라우트 세그먼트까지 부분적으로 프리페칭됩니다. prefetch={true}
로 설정하면 라우트에 관계없이 프리페칭됩니다.
- 기본적으로
- 프로그래밍적 네비게이션과 달리 간단하게 페이지 간 이동을 처리할 수 있습니다.
import Link from "next/link";
export default function Page() {
return (
<Link href="/post" prefetch={true}>
Dashboard
</Link>
);
}
Router
Next.js의 Router
객체는 프로그래밍적으로 페이지 이동을 처리할 수 있도록 해줍니다.
해당 방식을 사용하기 위해선 useRouter
훅을 사용하여 Router 인스턴스를 가져와야 합니다.
해당 인스턴스에서 제공되는 메서드를 사용하면 브라우저의 히스토리 스택을 조작하여, 클라이언트 사이드 네비게이션을 가능하게 해줍니다.
push(href, options)
: 새로운 URL로 이동하며, 브라우저 기록에 새 항목을 추가합니다. 전체 페이지 새로고침 없이 페이지 이동을 처리합니다.replace(href, options)
: 현재 라우트를 새로운 URL로 교체합니다. 새 항목을 추가하지 않고 현재 항목을 대체합니다.refresh()
: 현재 라우트를 새로고침합니다. 서버에 새로운 요청을 보내고 서버 컴포넌트를 리렌더링합니다. 이때, 클라이언트 상태에는 영향을 주지 않습니다.prefetch(href)
: 제공된 라우트를 프리페칭합니다.
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
const navigateToDashboard = () => {
router.push("/post");
};
const redirectToLogin = () => {
router.replace("/login");
};
const refreshCurrentPage = () => {
router.refresh();
};
useEffect(() => {
router.prefetch("/dashboard");
}, []);
return (
<div>
<button onClick={navigateToDashboard}>Go to Dashboard</button>
<button onClick={redirectToLogin}>Replace with Login</button>
<button onClick={refreshCurrentPage}>Refresh</button>
</div>
);
}
결론
서버 사이드 네비게이션과 클라이언트 사이드 네비게이션을 정리하면 다음과 같습니다:
특징 | 서버 사이드 네비게이션 | 클라이언트 사이드 네비게이션 |
---|---|---|
페이지 전환 속도 | 느림 (전체 새로고침 필요) | 빠름 (필요한 부분만 업데이트) |
SEO | 우수 (완전한 HTML 제공) | 제한적 (SSR 또는 SSG 필요) |
초기 로딩 속도 | 빠름 | 느림 (모든 리소스 초기 로딩 필요) |
상태 관리 | 어렵다 (페이지 전환 시 상태 초기화) | 용이하다 (상태 유지 가능) |
네트워크 부하 | 높음 (전체 페이지 재요청) | 낮음 (부분 업데이트로 효율적) |
사용자 경험 (UX) | 전환 시 깜빡임 및 지연 발생 가능 | 부드러운 전환, 애니메이션 등으로 몰입감 제공 |
단순한 구현과 SEO를 고려할 때는 서버 사이드 네비게이션을 사용하고, 빠른 전환 속도와 UX를 중요시하는 경우 클라이언트 사이드 네비게이션을 사용하는 것이 좋다고 생각합니다. 혹은, 두 가지 모두의 장점을 취합한 Next.js와 같은 프레임워크를 사용하여 SSR과 클라이언트 사이드 네비게이션을 조합하여 사용할 수도 있습니다.
출처