안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

자바스크립트 (JavaScript)

[JavaScript] HTML5 History API, history 패키지 (feat. react-router-dom)

피그브라더 2020. 11. 10. 15:03

React를 공부하면서 react-router-dom 패키지에서 제공하는 클라이언트 사이드 라우팅의 동작 원리를 알고 싶어졌다. 그러다 보니 react-router-dom 패키지에서 클라이언트 사이드 라우팅을 구현할 때 내부적으로 사용하는 history 패키지의 동작 원리를 공부해야 했고, 이를 이해하기 위해서는 다시 또 history 패키지에서 내부적으로 사용하는 HTML5 history API에 대해서도 공부해야 했다. 이번 포스팅에서는 그렇게 공부한 내용들을 깔끔하게 정리해보도록 할 것이다. 먼저 HTML5 history API부터 알아보도록 하자.

 

1. HTML5 history API

대부분의 현대 브라우저들은 HTML5 history API를 통해 현재 브라우저 탭의 세션 히스토리(방문 기록)를 조작할 수 있다. 기본적으로 브라우저는 현재 탭의 방문 기록을 메모리 상에서 스택의 형태로 관리하며, 이러한 히스토리 스택을 조작하는 HTML5 history API는 탭 단위로 존재하는 window 전역 객체의 프로퍼티인 history 객체의 프로퍼티들과 메소드들로서 제공이 된다. 하나씩 살펴보자.


먼저, history 객체의 프로퍼티들로는 다음과 같은 것들이 있다.

 

  • history.length : 현재 페이지를 포함한, 방문 기록의 총길이를 나타낸다.
  • history.scrollRestoration : 방문 기록 탐색 시 스크롤 위치를 원래 위치로 복원할지 말지의 여부를 나타낸다. 값으로는 auto (기본값) 혹은 manual을 지정할 수 있다.
  • history.state : 히스토리 스택의 최상단에 위치한 상태 값을 나타낸다. popstate 이벤트에 반응할 필요 없이 현재 방문 기록의 상태 값을 쉽게 확인할 수 있는 방법이다.

다음으로, history 객체의 메소드들로는 다음과 같은 것들이 있다. 아래 메소드들을 활용한 방문 기록 탐색이 완료되는 시점은 popstate 이벤트 핸들러를 통해 알 수 있다.

 

  • history.back() : 방문 기록의 바로 뒤 페이지로 이동하는 비동기 메소드이다. 브라우저의 뒤로 가기 버튼을 눌렀을 때, 그리고 history.go(-1)을 사용했을 때와 같다. (참고로, 방문 기록의 제일 첫 번째 페이지에서 호출해도 오류는 발생하지 않는다.) 이는 스택의 포인터만 한 칸 뒤로 보낼 뿐이기 때문에, 방문 기록들은 전부 그대로 남아 있다.
  • history.forward() : 방문 기록의 바로 앞 페이지로 이동하는 비동기 메소드이다. 브라우저의 앞으로 가기 버튼을 눌렀을 때, 그리고 history.go(1)을 사용했을 때와 같다. (참고로, 방문 기록의 제일 마지막 페이지에서 호출해도 오류는 발생하지 않는다.) 이는 스택의 포인터를 한 칸 앞으로 보낼 뿐이다.
  • history.go([delta]) : 현재 페이지를 기준으로, 상대적인 위치에 존재하는 방문 기록 내 페이지로 이동하는 비동기 메소드이다. 예를 들어, 매개변수로 -1을 제공하면 바로 뒤로, 1을 제공하면 바로 앞으로 이동한다. 방문 기록의 범위를 벗어나는 값을 제공하면 아무 일도 일어나지 않는다. 매개변수를 제공하지 않거나, 0을 제공하면 현재 페이지를 다시 불러온다(location.reload()와 동일).
참고로, 브라우저의 새로고침(= location.reload() 함수의 호출)은 추가적인 방문 기록을 Push 하지 않는다.

그리고 HTML5부터는 다음과 같은 메소드들도 추가가 되었다.

 

  • history.pushState(state, title[, url]) : 주어진 상태 값을 히스토리 스택에 추가한다. 상태 값으로는 직렬화가 가능한 어떠한 JavaScript 객체도 가능하다. 브라우저는 주어진 URL로 탐색하지 않는다. 새로운 URL은 현재 URL과 같은 출처(Origin)여야 하며, URL을 지정하지 않은 경우 자동으로 현재 URL을 사용한다. (참고로, Safari를 제외한 모든 브라우저는 title 매개변수를 무시한다.)
  • history.replaceState(state, title[, url]) : 히스토리 스택의 제일 최근 항목을 주어진 상태 값으로 대체한다. 상태 값으로는 직렬화가 가능한 어떠한 JavaScript 객체도 가능하다. 이는 특히 일부 유저의 동작에 대한 응답으로 저장된 객체의 상태나 현재의 URL을 바꾸고 싶은 경우에 유용하다. (참고로, Safari를 제외한 모든 브라우저는 title 매개변수를 무시한다.)
참고로, 앞의 방문 기록들이 스택에 남아 있는 상태에서 다른 페이지를 탐색하거나 history.pushState() 함수를 호출하면 앞의 방문 기록들이 전부 Pop 되고 새로운 방문 기록이 Push 된다.

 

pushState() 함수를 사용하는 것과 window.location = "#hash" 코드를 실행하는 것은 비슷한 동작을 하는 듯하다. 둘 다 새로운 방문 기록을 생성하여 히스토리 스택에 쌓기 때문이다. 그러나 pushState() 함수에는 다음과 같이 몇 가지 특징들이 존재한다.

 

  • 출처(Origin)만 같다면 어떠한 URL을 사용하더라도 페이지가 리로드 되지 않고 현재 웹 페이지에 머무른다.
  • 원하지 않는다면 URL을 안 바꿀 수도 있다.
  • 원하는 임의의 데이터를 히스토리 스택에 저장할 수 있다. 해시 기반 방식에서는 원하는 데이터를 문자열 형태로 인코딩해야 한다.
  • 다만 이전 URL과 신규 URL의 해시가 다르더라도 hashchange 이벤트를 발생시키지 않는다.

마지막으로, HTML5 history API에서 매우 중요한 popstate 이벤트에 대해서도 알아보자. 이는 사용자의 방문 기록 탐색으로 인해 현재 활성화되는 기록 항목이 바뀔 때마다 발생하는 이벤트이다. 만약 history.pushState() 함수나 history.replaceState() 메소드에 의해 생성된 기록 항목이라면 이벤트 객체의 state 프로퍼티는 해당 메소드가 저장한 상태 값의 사본을 가리킨다.

브라우저의 popstate 이벤트는 현재 활성화된 방문 기록이 변화할 때마다 발생하는 것이 기본인데, 이는 페이지를 새로 로드하지 않는 경우에만 해당한다는 것에 주의해야 한다. 또한, history.pushState() 및 history.replaceState() 함수의 호출은 이 이벤트를 발생시키지 않는다.

 

▼ 예시

window.onpopstate = function (event) {
    console.log('location: ' + document.location + ', state: ' + JSON.stringify(event.state));
};
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back();  // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back();  // Logs "location: http://example.com/example.html, state: null"
history.go(2);  // Logs "location: http://example.com/example.html?page=3, state: {"page":3}"

 

2. history 패키지 (Node.js 패키지)

history 패키지는 임의의 환경(브라우저까지 포함)에서 구동되는 JavaSciprt 애플리케이션에서 세션 히스토리(방문 기록)의 관리와 내비게이션 등을 쉽게 할 수 있도록 도와주는 라이브러리이다. 이는 세션 히스토리를 다루는 방법을 구동 환경을 기준으로 세 가지로 나눠서 제공하고 있다(createBrowserHistory, createHashHistory, createMemoryHistory).

 

필자는 react-router-dom 패키지에서 클라이언트 사이드 라우팅을 구현하는 원리를 알고 싶은 것이기 때문에, 이 셋 중에 HTML5 history API를 기반으로 구현되는 history 객체를 다루는 방식에만 주목할 것이다. 이 history 객체는 history 패키지가 제공하는 createBrowserHistory() 함수를 호출하면 생성할 수 있다. 이때 이 history 객체는 history 패키지의 API를 제공하기 위한 수단일 뿐, window.history 객체랑은 완전히 다른 객체임에 주의하자. 해당 history 객체가 제공하는 API들의 사용 예시는 다음과 같다. 자세한 API는 이곳에서 확인하기 바란다.

 

▼ history 패키지가 제공하는 API 사용 예시 (출처 : 공식 다큐먼트)

// Create your own history instance.
import { createBrowserHistory } from 'history';
let history = createBrowserHistory();

// ... or just import the browser history singleton instance.
import history from 'history/browser';

// Alternatively, if you're using hash history import
// the hash history singleton instance.
// import history from 'history/hash';

// Get the current location.
let location = history.location;

// Listen for changes to the current location.
let unlisten = history.listen(({ location, action }) => {
  console.log(action, location.pathname, location.state);
});

// Use push to push a new entry onto the history stack.
history.push('/home', { some: 'state' });

// Use replace to replace the current entry in the stack.
history.replace('/logged-in');

// Use back/forward to navigate one entry back or forward.
history.back();

// To stop listening, call the function returned from listen().
unlisten();

 

한편, 이렇게 생성된 history 객체를 react-router 패키지의 <Router> 컴포넌트에 history props로 넘겨서 래핑 한 컴포넌트가 바로 react-router-dom 패키지의 <BrowserRouter> 컴포넌트이다. react-router 패키지의 <Router> 컴포넌트는 여러 환경에서의 라우터 컴포넌트를 구현하기 위한 저수준의 인터페이스(토대)일 뿐, 실제로 사용할 때는 이것을 각 환경에서 사용할 수 있는 형태로 구현한 고수준의 라우터 컴포넌트(<BrowserRouter>, <HashRouter>, <MemoryRouter>, <NativeRouter>, 혹은 <StaticRouter>)를 사용한다.

 

결론적으로, react-router-dom 패키지는 history 패키지를 활용하여 클라이언트 사이드 라우팅을 구현한 것이고, history 패키지는 HTML5 history API를 이용하여 세션 히스토리 관리 및 내비게이션 기능을 구현한 것이다. (이번 포스팅은 여기서 마치고, react-router-dom 패키지가 클라이언트 사이드 라우팅을 어떤 식으로 구현했는지에 대한 더욱 자세한 내용은 이곳을 참고 바란다.)

 

 

 

 

 

 

본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.

https://developer.mozilla.org/ko/docs/Web/API/History/pushState

https://reactrouter.com/web/api/Router

https://github.com/ReactTraining/history/blob/28c89f4091ae9e1b0001341ea60c629674e83627/docs/getting-started.md