icon
9장 : 웹 API와 브라우저 기능

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);

pushStatereplaceState 호출만으로는 popstate 이벤트가 발생하지 않습니다. 이 이벤트는 오직 사용자가 브라우저의 뒤로가기/앞으로가기 버튼을 클릭하거나, history.back(), history.forward(), history.go() 메서드를 통해 히스토리 스택을 탐색할 때만 발생합니다. 따라서 pushState를 호출한 후에는 해당 URL에 맞는 콘텐츠를 수동으로 렌더링해야 합니다.


클라이언트-사이드 라우팅 구현의 원리

History API를 활용한 클라이언트-사이드 라우팅의 일반적인 원리는 다음과 같습니다.

  1. 링크 클릭 가로채기: 모든 <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에 맞는 콘텐츠 렌더링
        });
    });
  2. popstate 이벤트 처리: popstate 이벤트가 발생하면 (사용자가 뒤로가기/앞으로가기 버튼 클릭 시) 현재 URL(window.location.pathname)을 확인하고, 그에 맞는 컴포넌트나 콘텐츠를 렌더링합니다.

  3. 초기 로드 시 처리: 페이지가 처음 로드될 때도 현재 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의 동작 원리를 아는 것은 여러분의 디버깅 능력과 프레임워크 활용 능력을 한 단계 높여줄 것입니다.