History API와 라우팅
우리는 9장를 통해 Fetch API
로 서버와 통신하고, 로컬 스토리지
와 세션 스토리지
로 클라이언트 측 데이터를 관리하는 방법을 학습했습니다. 이제 웹 애플리케이션의 중요한 사용자 경험 요소 중 하나인 내비게이션(Navigation) 에 대해 다룰 차례입니다.
전통적인 웹 페이지는 링크를 클릭하거나 폼을 전송할 때마다 서버로부터 새로운 HTML 문서를 받아와 전체 페이지를 새로고침(Full Page Reload)했습니다. 이는 사용자 경험 측면에서 화면 깜빡임이나 속도 저하를 야기할 수 있습니다.
현대 웹 개발에서는 이러한 단점을 극복하고 데스크톱 애플리케이션처럼 부드러운 사용자 경험을 제공하기 위해 Single Page Application (SPA) 이 대세가 되었습니다. SPA는 최초 한 번만 페이지를 로드하고, 이후에는 필요한 데이터만 비동기적으로 서버에서 받아와 페이지의 특정 부분만 업데이트합니다. 이때 중요한 문제가 발생하는데, 페이지 전체를 새로고침하지 않으면서도 URL을 변경하고, 브라우저의 뒤로가기/앞으로가기 버튼을 정상적으로 동작하게 하는 것입니다.
이러한 문제를 해결해 주는 것이 바로 History API입니다. 이번 장에서는 History API
의 주요 메서드들을 살펴보고, 이를 기반으로 SPA에서 어떻게 클라이언트-사이드 라우팅(Client-Side Routing) 을 구현하여 사용자에게 매끄러운 내비게이션 경험을 제공하는지 알아보겠습니다.
전통적인 웹 페이지 내비게이션의 한계
전통적인 웹 페이지는 <a>
태그를 클릭하거나, location.href
를 변경하거나, 폼을 제출할 때마다 브라우저가 새로운 HTTP 요청을 서버로 보내고, 서버는 해당 URL에 맞는 새로운 HTML 파일을 응답합니다. 이 과정에서 브라우저는 현재 페이지를 완전히 버리고 새로운 페이지를 처음부터 다시 그립니다.
단점
- 느린 속도: 매번 페이지 전체를 새로 로드해야 하므로, 네트워크 지연이 발생합니다.
- 화면 깜빡임: 페이지 전체가 다시 그려지면서 일시적인 화면 깜빡임이 발생하여 사용자 경험을 저해합니다.
- 불필요한 리소스 재로드: CSS, JavaScript 파일 등 변경되지 않은 리소스도 매번 다시 로드될 수 있습니다.
Single Page Application (SPA)
SPA는 이름 그대로 하나의 HTML 페이지 위에서 실행되는 웹 애플리케이션입니다. 초기 로드 시 필요한 모든 리소스(HTML, CSS, JS)를 한 번만 다운로드하고, 이후 사용자 상호작용에 따라 필요한 데이터만 서버와 비동기적으로 통신(Fetch API 사용)하여 페이지의 내용물을 동적으로 변경합니다.
SPA의 핵심 과제 중 하나는 페이지 전체를 새로고침하지 않으면서도, 사용자가 '다른 페이지'로 이동하는 것처럼 보이게 하고, URL을 변경하며, 브라우저의 뒤로가기/앞으로가기 버튼을 정상적으로 동작하도록 하는 것입니다. 이를 클라이언트-사이드 라우팅(Client-Side Routing) 이라고 합니다.
클라이언트-사이드 라우팅은 서버에 새로운 페이지를 요청하는 대신, 자바스크립트를 사용하여 현재 페이지의 URL을 변경하고, 그 URL에 맞는 컴포넌트를 동적으로 렌더링하는 방식으로 작동합니다. 이때 핵심적인 역할을 하는 것이 바로 History API입니다.
History API: 브라우저 히스토리 조작
History API는 웹 브라우저의 세션 히스토리(방문 기록)를 자바스크립트로 조작할 수 있도록 해주는 웹 API입니다. 이를 통해 페이지를 새로고침하지 않고도 URL을 변경하고, 사용자가 뒤로가기/앞으로가기 버튼을 눌렀을 때 적절한 동작을 수행하도록 만들 수 있습니다.
History API
는 전역 window.history
객체를 통해 접근할 수 있습니다.
history.pushState()
history.pushState(state, title, url)
메서드는 브라우저의 세션 히스토리에 새로운 항목을 추가합니다. 이는 마치 사용자가 새로운 페이지로 이동한 것처럼 보이지만, 실제로는 페이지 새로고침이 일어나지 않습니다.
state
: 새로운 히스토리 엔트리에 연결될 상태 객체입니다. 나중에popstate
이벤트가 발생했을 때 이 상태 객체를 통해 데이터를 전달받을 수 있습니다.title
: (대부분의 브라우저에서 무시됨) 변경될 페이지의 제목을 나타내는 문자열입니다. 현재는 잘 사용되지 않습니다.url
: (선택 사항) 변경될 새로운 URL입니다. 현재 출원(origin)과 동일해야 합니다. 이 URL은 브라우저의 주소 표시줄에 표시되지만, 서버에는 요청을 보내지 않습니다.
// 현재 URL: http://localhost:8080/
console.log("현재 URL:", window.location.href);
// 'about' 페이지로 이동하는 것처럼 보이게 URL 변경
history.pushState({ page: 'about' }, 'About Page', '/about');
console.log("변경 후 URL:", window.location.href); // 결과: http://localhost:8080/about
// 'products' 페이지로 이동하는 것처럼 보이게 URL 변경
history.pushState({ page: 'products', category: 'electronics' }, 'Products Page', '/products/electronics');
console.log("변경 후 URL:", window.location.href); // 결과: http://localhost:8080/products/electronics
// 실제 페이지 이동 없이 URL만 변경됨
pushState
는 URL을 변경하지만, popstate
이벤트는 발생시키지 않습니다.
history.replaceState()
history.replaceState(state, title, url)
메서드는 pushState()
와 유사하지만, 새로운 항목을 추가하는 대신 현재 히스토리 엔트리를 대체합니다. 이는 사용자가 현재 페이지에 머물면서 URL만 변경하고 싶을 때 유용합니다 (예: 검색 필터를 적용하여 URL만 업데이트).
// 현재 URL: http://localhost:8080/search?q=apple
console.log("현재 URL:", window.location.href);
// 검색어만 변경하고 싶을 때 (뒤로가기 시 이전 검색 결과로 돌아가기 위함이 아니라 현재 페이지 그대로 유지)
history.replaceState({ query: 'banana' }, 'Search for Banana', '/search?q=banana');
console.log("변경 후 URL:", window.location.href); // 결과: http://localhost:8080/search?q=banana
// replaceState도 popstate 이벤트를 발생시키지 않습니다.
그 외 메서드: 히스토리 이동
이 메서드들은 브라우저의 뒤로가기/앞으로가기 버튼과 동일한 기능을 수행합니다.
history.back()
: 뒤로가기history.forward()
: 앞으로가기history.go(delta)
:delta
값만큼 히스토리 스택을 이동합니다.history.go(0)
: 현재 페이지 새로고침history.go(-1)
:history.back()
과 동일history.go(1)
:history.forward()
와 동일
// 예시 시나리오:
// 1. 초기 로드: / (index.html)
// 2. history.pushState({}, '', '/about'); // URL: /about
// 3. history.pushState({}, '', '/contact'); // URL: /contact
// 현재 /contact
console.log("현재:", window.location.href); // 결과: /contact
history.back(); // 뒤로가기 (URL: /about으로 변경, popstate 이벤트 발생)
console.log("뒤로가기 후:", window.location.href);
history.forward(); // 앞으로가기 (URL: /contact으로 변경, popstate 이벤트 발생)
console.log("앞으로가기 후:", window.location.href);
// history.go(-2); // 두 단계 뒤로 (URL: /index.html으로 변경)
popstate
이벤트
사용자가 브라우저의 뒤로가기 또는 앞으로가기 버튼을 클릭하여 URL이 변경될 때 popstate
이벤트가 발생합니다. 이 이벤트는 window
객체에서 리스닝할 수 있으며, 이때 event.state
를 통해 pushState
또는 replaceState
로 저장했던 state
객체에 접근할 수 있습니다.
// 라우터 역할을 하는 함수
function handleRoute(event) {
const currentState = event.state; // pushState/replaceState로 전달했던 상태 객체
const currentPath = window.location.pathname;
console.log(`[popstate 이벤트 발생] URL: ${currentPath}, State:`, currentState);
// 여기에서 currentPath에 따라 적절한 UI 컴포넌트를 렌더링하는 로직 구현
if (currentPath === '/') {
renderHomePage();
} else if (currentPath === '/about') {
renderAboutPage();
} else if (currentPath.startsWith('/products')) {
renderProductsPage(currentPath);
} else {
renderNotFoundPage();
}
}
// popstate 이벤트 리스너 등록
window.addEventListener('popstate', handleRoute);
// 초기 페이지 로드 시에도 현재 URL에 맞는 콘텐츠를 렌더링
function initialRender() {
console.log("[초기 렌더링]", window.location.pathname);
handleRoute({ state: history.state }); // 초기 상태는 history.state로 접근
}
// 실제 렌더링 함수 (예시)
function renderHomePage() { document.getElementById('content').innerHTML = '<h1>환영합니다! 홈 페이지입니다.</h1>'; }
function renderAboutPage() { document.getElementById('content').innerHTML = '<h1>회사 소개 페이지입니다.</h1><p>우리는 이런 회사입니다.</p>'; }
function renderProductsPage(path) { document.getElementById('content').innerHTML = `<h1>제품 페이지입니다.</h1><p>현재 경로: ${path}</p>`; }
function renderNotFoundPage() { document.getElementById('content').innerHTML = '<h1>404 Not Found</h1><p>페이지를 찾을 수 없습니다.</p>'; }
// HTML 구조: <div id="content"></div>
// 초기 렌더링 호출
initialRender();
// 예시를 위한 수동 라우팅 (실제로는 <a> 태그 클릭 등을 통해 발생)
setTimeout(() => {
console.log("\n--- 'About'으로 pushState ---");
history.pushState({ page: 'about' }, 'About Page', '/about');
handleRoute({ state: history.state }); // 수동으로 콘텐츠 렌더링 트리거
}, 1000);
setTimeout(() => {
console.log("\n--- 'Products'로 pushState ---");
history.pushState({ page: 'products' }, 'Products Page', '/products/shoes');
handleRoute({ state: history.state }); // 수동으로 콘텐츠 렌더링 트리거
}, 2000);
setTimeout(() => {
console.log("\n--- 뒤로가기 버튼 클릭 (실제 브라우저 동작) ---");
// 브라우저의 뒤로가기 버튼을 누르는 것을 시뮬레이션
history.back(); // 이 호출이 popstate 이벤트를 발생시킵니다.
}, 3000);
setTimeout(() => {
console.log("\n--- 뒤로가기 버튼 클릭 (실제 브라우저 동작) ---");
history.back(); // 또 한 번 뒤로가기
}, 4000);
setTimeout(() => {
console.log("\n--- 앞으로가기 버튼 클릭 (실제 브라우저 동작) ---");
history.forward(); // 앞으로가기
}, 5000);
pushState
나 replaceState
호출만으로는 popstate
이벤트가 발생하지 않습니다. 이 이벤트는 오직 사용자가 브라우저의 뒤로가기/앞으로가기 버튼을 클릭하거나, history.back()
, history.forward()
, history.go()
메서드를 통해 히스토리 스택을 탐색할 때만 발생합니다. 따라서 pushState
를 호출한 후에는 해당 URL에 맞는 콘텐츠를 수동으로 렌더링해야 합니다.
클라이언트-사이드 라우팅 구현의 원리
History API를 활용한 클라이언트-사이드 라우팅의 일반적인 원리는 다음과 같습니다.
-
링크 클릭 가로채기: 모든
<a>
태그의 기본 동작(event.preventDefault()
)을 막고, 클릭 이벤트가 발생했을 때 History API를 사용하여 URL을 변경합니다.document.querySelectorAll('a').forEach(link => { link.addEventListener('click', function(event) { event.preventDefault(); // 기본 동작(페이지 새로고침) 방지 const path = this.getAttribute('href'); history.pushState({}, '', path); // URL 변경 handleRoute({ state: history.state }); // 변경된 URL에 맞는 콘텐츠 렌더링 }); });
-
popstate
이벤트 처리:popstate
이벤트가 발생하면 (사용자가 뒤로가기/앞으로가기 버튼 클릭 시) 현재 URL(window.location.pathname
)을 확인하고, 그에 맞는 컴포넌트나 콘텐츠를 렌더링합니다. -
초기 로드 시 처리: 페이지가 처음 로드될 때도 현재 URL에 맞는 콘텐츠를 렌더링해야 합니다.
현대 웹 개발에서는 React Router, Vue Router, Next.js 등의 프레임워크/라이브러리가 이 모든 라우팅 로직을 추상화하여 제공하므로 개발자가 직접 History API를 다룰 일이 많지는 않습니다. 하지만 이들이 내부적으로 어떻게 동작하는지 이해하는 것은 SPA의 동작 원리를 파악하는 데 매우 중요합니다.
마무리하며
이번 장에서는 Single Page Application (SPA)의 핵심 내비게이션 기술인 History API와 클라이언트-사이드 라우팅(Client-Side Routing) 에 대해 심도 있게 학습했습니다.
여러분은 전통적인 웹 페이지 내비게이션의 한계를 이해하고, SPA가 어떻게 페이지 새로고침 없이 동적인 콘텐츠를 제공하는지 알아보았습니다. 그리고 History API의 주요 메서드인 history.pushState()
(URL 추가), history.replaceState()
(URL 대체), history.back()/forward()/go()
(히스토리 이동)를 사용하여 브라우저의 방문 기록을 자바스크립트로 제어하는 방법을 익혔습니다. 또한, 사용자가 브라우저의 뒤로가기/앞으로가기 버튼을 눌렀을 때 발생하는 popstate
이벤트를 통해 상태 변화를 감지하고 적절한 콘텐츠를 렌더링하는 원리도 살펴보았습니다.
History API를 이해하는 것은 모던 웹 프레임워크의 라우팅 시스템이 어떻게 작동하는지 파악하는 데 필수적인 지식입니다. 비록 여러분이 직접 라우터를 구현할 일은 많지 않겠지만, 이 API의 동작 원리를 아는 것은 여러분의 디버깅 능력과 프레임워크 활용 능력을 한 단계 높여줄 것입니다.