들어가며
오늘은 싱글 페이지 어플리케이션(SPA)를 만들기 위해서 꼭 알아둬야 할 history api 에 대해 공부했다.
리액트로 개발할 때 필수적으로 react-router-dom 이라는 라이브러리를 사용해 라우팅 기능을 구현했었다. react-router-dom 내부적으로 브라우저의 history api 를 사용한다고 해서 순수 자바스크립트로 튜토리얼을 따라 라우팅을 구현한 적이 있었다. 이 때는 혼자 공부하다보니 뭔가 더 어려운 것 같고 이해도 안되는 느낌이었는데, 강의를 들으니까 갑자기 이해가 쏙쏙 되는 느낌이다.
History Api
History Api는 브라우저의 세션 기록, 즉 현재 페이지를 불러온 탭 또는 프레임의 방문 기록을 조작하는 방법을 제공한다. 브라우저에서 페이지 로딩을 하면 세션 히스토리를 가지게 되는데, 세션 히스토리는 페이지 이동을 할 때마다 쌓이게 되고, 이를 통해 뒤로 가기 또는 앞으로 가기 기능을 사용할 수 있다.
History 객체는 세션 히스토리를 조작할 수 있는 다양한 메서드를 가지고 있다.
- History.go() 현재 페이지를 기준으로, 상대적인 위치에 존재하는 세션 히스토리 내 페이지로 이동하는 비동기 메서드이다. 예를 들어, 매개변수로 -1을 넣으면 바로 뒤로, 1을 넣으면 바로 앞으로 이동한다. 세션 히스토리를 벗어나는 값을 제공하면 아무 일도 일어나지 않고, 매개변수를 제공하지 않거나 0을 제공하면 현재 페이지를 다시 불러온다.
- History.back() 세션 히스토리의 바로 뒤 페이지로 이동하는 비동기 메서드이다. 브라우저의 뒤로 가기 버튼을 눌렀을 때, history.go(-1)과 같은 동작을 한다.
- History.forward() 세션 히스토리의 바로 앞 베이지로 이동하는 비동기 메서드이다. 브라우저의 앞으로 가기 버튼을 눌렀을 때, history.go(1)과 같은 동작을 한다.
앞에서 소개한 메서드들은 이미 저장되어 있는 세션 히스토리 내에서 이동할 수 있는 메서드들이다. 싱글 페이지 어플리케이션을 만들기 위해 중요한 것은 세션 히스토리에 새로운 url 상태를 쌓는 것이다. 이를 위해 History의 pushState(), replaceState() 메서드를 사용한다.
- History.pushState()
브라우저의 세션 히스토리 스택에 상태를 추가한다.
총 세 개의 매개변수를 받을 수 있다.1 history.pushState(state, title[, url]);- state: 새로운 세션 히스토리 항목에 연결할 상태 객체이다. 사용자가 새로운 상태로 이동하면 popstate 이벤트가 발생하는데 이 때 이벤트 객체의 state 속성에 해당 상태가 담기게 된다.
- title: 새로 추가될 url의 제목을 설정한다. 현재는 사파리를 제외한 모든 브라우저에서 지원하지 않는다.
- url: 세션 히스토리에 추가할 url이다. 만약 값을 입력하지 않으면 현재 url로 세션 히스토리에 추가된다. url이 변경된다고 해서 화면이 리로드 되진 않는다.
- History.replaceState()
전체적인 동작은 pushState() 와 같다. 다른 점은 세션 히스토리에 새로운 url을 추가하지 않고, 현재 url을 대체한다. 만약 게시물을 작성하는 페이지가 있을 때, 작성 완료 버튼을 눌러 작성된 게시물 페이지로 넘어간다고 하자. 이 때 pushState() 를 사용하게 되면 뒤로 가기 버튼을 눌렀을 때 다시 게시물 작성 페이지로 돌아가게 된다. 이런 상황에서 replaceState() 를 사용하게 되면 작성된 게시물 페이지에서 뒤로 가기를 눌렀을 때 다시 게시물 작성 페이지로 돌아가지 않게 된다.1 history.replaceState(state, title[, url]);
주의할 점
pushState() 나 replaceState() 를 사용해서 url을 추가하고 새로고침을 하면 404 에러가 뜬다. 만약 / root에서 /product 라는 url을 추가한 상황이라면 새로고침을 했을 때 /product.html 또는 /product/index.html 파일을 요청하게 된다. 지금은 세션 히스토리에 url 추가만 하고 실제 파일은 없기 때문에 에러가 발생하는 것이다.
SPA를 만들 때 이러한 문제가 발생하지 않도록 404 에러가 났을 경우 index.html로 요청을 돌려주는 처리가 필요하다.
React-Router
리액트를 맛 본 사람이라면 한 번쯤 써봤을 법한 라우팅 라이브러리이다. 내부적으로 History api 를 사용한다는 건 알고 있었는데 도대체 어떻게 쓰고 있을까 궁금해서 큰 맘 먹고 코드를 살펴보기로 했다. 그런데 pushState() 나 replaceState() 는 보이지 않았고, 최상위에서 history 라는 라이브러리를 가져오고 있었다.
1 // react-router/packages/react-router-dom/index.tsx2 import * as React from "react";3 import type { BrowserHistory, HashHistory, History } from "history";4 import { createBrowserHistory, createHashHistory } from "history";
그럼 history 라이브러리는 뭘까? 깃허브의 리드미를 읽어보면 세션 히스토리를 쉽게 관리하기 위한 히스토리 라이브러리라고 한다. 여기서 pushState() 와 replaceState() 를 찾아보자.
1 // history/packages/history/index.ts2 ...3 export function createBrowserHistory(...){4 ...5 let globalHistory = window.history;6 ...7 function push(to: To, state?: any) {8 let nextAction = Action.Push;9 let nextLocation = getNextLocation(to, state);10 function retry() {11 push(to, state);12 }13 if (allowTx(nextAction, nextLocation, retry)) {14 let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);15 try {16 globlHistory.pushState(historyState, "", url); // 여기!17 } catch (error) {18 window.location.assign(url);19 }20 }21 applyTx(nextAction);22 }23 }
push 함수에서 pushState() 메서드를 사용하는 걸 확인할 수 있었다. push 함수 뿐 아니라 replace, go 등의 함수를 정의해두고 history 라는 객체를 만들어 객체를 반환하고 있었다.
1 let history = {2 ...3 push,4 replace,5 go,6 back() {7 go(-1);8 },9 forward() {10 go(1);11 }12 ...13 }1415 return history;
아마 여기서 반환하는 history 객체가 react-router-dom 에서 사용하는 history 객체인 것 같다.
정리
예전부터 흥미가 있었던 history api를 사용해 간단한 라우팅 기능을 구현해서 재미있었다. 이미 만들어 둔 라이브러리에서 history api를 어떻게 사용하는지 확인하는 것도 재미있었다. 물론 코드 이해는 정말 못하지만... 결국 기초를 공부하는 게 나중에 프레임워크나 라이브러리를 사용할 때 필요하다는 걸 느꼈다.
참고자료
mdn History https://developer.mozilla.org/ko/docs/Web/API/History
react-router https://github.com/remix-run/react-router
history https://github.com/remix-run/history/tree/3e9dab413f4eda8d6bce565388c5ddb7aeff9f7e