Baseline: Widely available (널리 사용 가능)
✅ Chrome ✅ Edge ✅ Firefox ✅ Safari
이 기능은 잘 확립되어 있고 많은 기기와 브라우저 버전에서 작동해요. 2015년 7월부터 브라우저 전반에서 사용할 수 있게 됐어요.
History API는 history 전역 객체를 통해 브라우저의 세션 히스토리에 접근할 수 있게 해줘요 (WebExtensions history와 혼동하지 마세요). 사용자의 히스토리를 앞뒤로 탐색하고, 히스토리 스택의 내용을 조작할 수 있는 유용한 메서드와 프로퍼티를 제공해요.
참고:
이 API는 메인 스레드(Window)에서만 사용 가능해요.Worker나Worklet컨텍스트에서는 접근할 수 없어요.
💡 강사 팁: History API는 SPA(Single Page Application)를 만들 때 정말 필수적인 API예요! React Router, Vue Router 같은 라우팅 라이브러리들이 내부적으로 이 API를 사용하고 있답니다.
특히
pushState()와replaceState()메서드가 핵심인데요, 페이지를 새로고침하지 않고도 URL을 변경할 수 있어서 SPA에서 "뒤로 가기" 버튼이 제대로 작동하게 만들 수 있어요.제 경험상 초보 개발자분들이 가장 많이 실수하는 부분이
popstate이벤트 처리예요. 사용자가 브라우저의 뒤로 가기/앞으로 가기 버튼을 클릭했을 때 이 이벤트가 발생하는데, 이걸 제대로 처리하지 않으면 UI와 URL이 불일치하는 버그가 생겨요. 꼭popstate이벤트 리스너를 등록해서 처리하세요!그리고 서버 설정도 중요해요. SPA에서
/about같은 URL로 직접 접근하면 서버가 404를 반환할 수 있거든요. 서버에서 모든 경로를index.html로 리다이렉트하는 설정을 해줘야 해요. 이 부분을 놓치면 배포 후에 문제가 생기는 경우가 많아요!
사용자의 브라우저 방문 기록(history)을 앞뒤로 이동하는 작업은 back(), forward(), 그리고 go() 메서드를 사용해서 수행합니다.
방문 기록에서 뒤로 이동하려면 아래와 같이 작성합니다.
history.back();
이 코드를 실행하면, 사용자가 브라우저 툴바에 있는 뒤로 가기(Back) 버튼을 직접 클릭한 것과 완벽하게 똑같이 동작합니다.
마찬가지로, 앞으로 이동하고 싶을 때(사용자가 앞으로 가기(Forward) 버튼을 클릭한 것처럼)는 이렇게 작성하면 됩니다.
history.forward();
💡 강사의 보충 설명:
뒤로 가기나 앞으로 가기를 실행했을 때, 브라우저는 이전/다음 페이지를 처음부터 다시 그리는 것이 아니라 캐시된 버전을 빠르게 불러오는 경우가 많습니다. 특히 뒤로 가기를 했을 때 화면 상태가 유지되어야 하는 경우가 많은데, 이때 이 메서드들이 유용하게 쓰입니다.
세션(session) 방문 기록에서 현재 페이지의 상대적인 위치를 기준으로, 특정 페이지를 로드하고 싶다면 go() 메서드를 사용할 수 있습니다. (참고로 현재 페이지의 상대적 위치 값은 0입니다.)
한 페이지 뒤로 이동하려면 다음과 같이 호출합니다. (이건 back()을 호출하는 것과 똑같아요.)
history.go(-1);
한 페이지 앞으로 이동하려면 아래처럼 하면 됩니다. 이건 forward()를 호출하는 것과 같습니다.
history.go(1);
같은 원리로, 인자로 2를 넘기면 앞으로 2페이지 이동할 수 있고, 다른 숫자들도 이런 식으로 사용할 수 있습니다.
go() 메서드의 또 다른 유용한 쓰임새는 바로 현재 페이지를 새로고침하는 것입니다. 숫자 0을 넘기거나, 아예 아무 인자도 넣지 않고 호출하면 됩니다.
// 아래의 두 구문은 모두
// 페이지를 새로고침하는
// 효과를 냅니다.
history.go(0);
history.go();
💡 강사의 팁: > 자바스크립트로 새로고침을 할 때
window.location.reload()를 많이 쓰시죠?history.go(0)도 거의 비슷한 역할을 합니다. 다만 라우팅 로직을 일관성 있게history객체 하나로 관리하고 싶을 때 종종 쓰이는 테크닉입니다.
length 속성의 값을 확인해 보면 현재 방문 기록 스택(history stack)에 총 몇 개의 페이지가 쌓여 있는지(개수)를 알아낼 수 있습니다.
const numberOfEntries = history.length;
History
브라우저의 세션 방문 기록(session history)을 조작할 수 있게 해줍니다. (세션 방문 기록이란, 현재 페이지가 로드되어 있는 브라우저 탭이나 프레임 안에서 방문했던 페이지들의 목록을 말해요.)
PopStateEvent
popstate 이벤트에 대한 인터페이스입니다.
다음 예제는 popstate 이벤트에 리스너를 등록하는 코드입니다. 그런 다음, history 객체의 여러 메서드들을 사용하여 현재 탭의 브라우저 방문 기록에 항목을 추가하거나, 대체하거나, 이동하는 방법을 보여줍니다.
window.addEventListener("popstate", (event) => {
alert(
`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(); // 알림창 표시: "location: [http://example.com/example.html?page=1](http://example.com/example.html?page=1), state: {"page":1}"
history.back(); // 알림창 표시: "location: [http://example.com/example.html](http://example.com/example.html), state: null"
history.go(2); // 알림창 표시: "location: [http://example.com/example.html?page=3](http://example.com/example.html?page=3), state: {"page":3}"
💡 강사의 핵심 보충 설명:
위 예제 코드는 원리 파악을 위해 아주아주 중요합니다! 특히 프론트엔드 라우팅의 핵심 기술이라 면접에서도 자주 물어보는 내용이에요. 한 줄씩 풀어서 설명해 드릴게요.
window.addEventListener("popstate", ...):popstate이벤트는 사용자가 브라우저의 뒤로 가기/앞으로 가기를 하거나history.back(),history.go()등을 자바스크립트로 호출해서 활성화된 기록 항목이 바뀔 때마다 발생합니다.pushState({ page: 1 }, "title 1", "?page=1"): URL을?page=1로 바꾸고 방문 기록에 새로 추가(push)합니다. 이때 화면이 새로고침되지는 않습니다! (SPA의 마법이죠)pushState({ page: 2 }, ...): URL을?page=2로 바꾸고 기록을 또 추가합니다.replaceState({ page: 3 }, ...): 이게 아주 중요합니다!replaceState는 새로운 기록을 쌓는 게 아니라, 현재 기록을 덮어씌웁니다(replace). 즉,?page=2였던 기록이?page=3으로 수정되어 버린 겁니다.history.back(): 방금 전 기록으로 돌아갑니다.?page=2는 아까 덮어씌워져서 사라졌으므로, 그 이전인?page=1로 이동하면서 알림창(alert)이 뜨게 됩니다.
안녕하세요! 프론트엔드 개발의 세계로 나아가고 계신 것을 환영합니다. 👋 오늘 우리가 함께 파헤쳐 볼 주제는 바로 History API입니다. 요즘처럼 React, Vue 같은 프레임워크로 웹을 만드는 시대에 이 API는 정말 필수적인 기초 지식이니, 꼼꼼하게 원본 내용을 번역해 드리고 실무에서 얻은 제 팁과 부연 설명도 팍팍 추가해 드릴게요! 자, 그럼 시작해 볼까요?
History API는 웹사이트가 브라우저의 '세션 기록(session history)'과 상호작용할 수 있게 해줍니다. 세션 기록이란, 쉽게 말해 사용자가 현재 열려있는 창(window)에서 방문했던 페이지들의 목록을 말해요. 사용자가 링크를 클릭해서 새로운 페이지를 방문할 때마다, 그 새로운 페이지들은 이 세션 기록에 차곡차곡 추가됩니다. 여러분도 잘 아시다시피, 사용자는 브라우저의 "뒤로 가기(Back)"와 "앞으로 가기(Forward)" 버튼을 사용해서 이 기록들을 왔다 갔다 할 수 있죠.
History API에서 정의하는 메인 인터페이스는 History 인터페이스인데요, 이 인터페이스는 크게 두 가지의 뚜렷하게 다른 메서드(method) 세트를 정의하고 있습니다.
세션 기록 내의 특정 페이지로 이동하기 위한 메서드들:
세션 기록을 직접 수정하기 위한 메서드들:
이 가이드 문서에서는 두 번째 세트인 세션 기록을 수정하는 메서드들에 대해서만 집중적으로 다룰 예정입니다.
pushState() 메서드는 세션 기록에 완전히 새로운 항목을 추가하는 반면, replaceState() 메서드는 현재 페이지에 대한 기존 세션 기록 항목을 업데이트(덮어쓰기) 합니다. 이 두 메서드는 모두 state라는 매개변수(parameter)를 받는데, 여기에는 어떤 직렬화 가능한 객체(serializable object)든 다 들어갈 수 있어요. 브라우저가 이 기록 항목으로 다시 이동하게 되면(예: 뒤로 가기를 눌렀을 때), 브라우저는 popstate 이벤트를 발생시키고, 이 이벤트 안에는 해당 기록 항목과 연결해 두었던 상태(state) 객체가 고스란히 담겨 있습니다.
이러한 API들의 가장 주된 목적은 바로 싱글 페이지 애플리케이션(Single-page applications, SPA) 같은 웹사이트들을 지원하기 위함입니다. SPA는 완전히 새로운 페이지를 처음부터 끝까지 새로 불러오는 대신, fetch() 같은 JavaScript API를 사용해서 새로운 콘텐츠로 페이지의 일부만 쏙 업데이트하거든요.
💡 강사의 팁: > 혹시 React Router나 Vue Router를 써보셨나요? 페이지가 새로고침 되지 않으면서 주소창의 URL만 싹 바뀌고 화면이 렌더링되죠! 그 마법 같은 프론트엔드 라우팅 기술의 핵심 원리가 바로 지금 우리가 배우고 있는
pushState,replaceState, 그리고popstate이벤트랍니다. 이 기초를 탄탄히 해두면 나중에 프레임워크를 다룰 때 내부 동작을 완벽하게 이해할 수 있어요!
전통적인 방식의 웹사이트들은 여러 개의 '페이지'들이 모인 형태로 구현됩니다. 사용자가 링크를 클릭해서 사이트의 다른 부분으로 이동할 때마다, 브라우저는 매번 전체 페이지를 새로 불러옵니다(새로고침 발생).
이 방식도 많은 사이트에서 훌륭하게 작동하지만, 몇 가지 단점이 있을 수 있어요:
이러한 이유로 현대 웹 앱에서 아주 인기 있는 패턴이 바로 싱글 페이지 애플리케이션 (SPA)입니다. 사용자가 링크를 클릭하면 SPA는 다음과 같은 단계들을 수행합니다.
event.preventDefault())예시 코드를 한번 볼까요?
document.addEventListener("click", async (event) => {
const creature = event.target.getAttribute("data-creature");
if (creature) {
// Prevent a new page from loading
event.preventDefault();
try {
// Fetch new content
const response = await fetch(`creatures/${creature}.json`);
const result = await response.json();
// Update the page with the new content
displayContent(result);
} catch (err) {
console.error(err);
}
}
});
이 클릭 핸들러 코드를 보면, 만약 클릭한 링크에 "data-creature"라는 데이터 속성(data attribute)이 있다면 그 속성값을 사용해 페이지를 업데이트할 새로운 콘텐츠가 담긴 JSON 파일을 가져옵니다.
그 JSON 파일은 대략 이런 모습일 거예요:
{
"description": "Bald eagles are not actually bald.",
"image": {
"src": "images/eagle.jpg",
"alt": "A bald eagle"
},
"name": "Eagle"
}
그리고 우리의 displayContent() 함수는 이 JSON 데이터를 받아서 화면을 업데이트합니다.
// Update the page with the new content
function displayContent(content) {
document.title = `Creatures: ${content.name}`;
const description = document.querySelector("#description");
description.textContent = content.description;
const photo = document.querySelector("#photo");
photo.setAttribute("src", content.image.src);
photo.setAttribute("alt", content.image.alt);
}
그런데 문제가 하나 있습니다. 이렇게 하면 브라우저의 "뒤로 가기"와 "앞으로 가기" 버튼이 우리가 기대하는 대로 동작하지 않게 됩니다!
사용자 입장에서는 링크를 클릭했고 페이지가 업데이트되었으니, 당연히 '새로운 페이지로 넘어왔다'라고 생각합니다. 그래서 브라우저의 "뒤로 가기" 버튼을 누르면 방금 링크를 클릭하기 이전의 상태로 돌아갈 것을 기대하죠.
하지만 브라우저 입장에서는 어떨까요? 마지막 링크를 클릭했을 때 완전히 새로운 페이지를 로드한 적이 없기 때문에, "뒤로 가기"를 누르면 사용자가 이 SPA 사이트를 처음 열기 직전에 보고 있던 완전히 다른 사이트(이전 방문 페이지)로 튕겨버리게 됩니다.
바로 이 문제를 해결해 주는 것이 pushState(), replaceState(), 그리고 popstate 이벤트입니다! 이들을 사용하면 우리가 가짜(synthesize) 기록 항목을 만들어서 추가할 수 있고, 사용자가 "뒤로 가기"나 "앞으로 가기" 버튼을 눌러서 현재 세션 기록이 우리가 만든 이 가짜 기록 항목으로 변경될 때 알림을 받을 수 있습니다.
pushState() 사용하기 (Using pushState())위에서 작성했던 클릭 핸들러에 pushState()를 추가해서 새로운 기록 항목을 남겨봅시다.
document.addEventListener("click", async (event) => {
const creature = event.target.getAttribute("data-creature");
if (creature) {
event.preventDefault();
try {
const response = await fetch(`creatures/${creature}.json`);
const result = await response.json();
displayContent(result);
// Add a new entry to the history.
// This simulates loading a new page.
history.pushState(result, "", creature);
} catch (err) {
console.error(err);
}
}
});
위 코드에서 우리는 pushState()를 호출하며 세 개의 인자를 넘겨주었습니다. 이 인자들이 각각 무엇을 의미하는지 자세히 뜯어볼까요?
result: 방금 우리가 서버에서 가져온 콘텐츠(상태 객체)입니다. 이 데이터는 기록 항목과 함께 저장되며, 나중에 popstate 이벤트가 발생할 때 핸들러로 전달되는 인자의 state 속성 안에 포함되어 돌아옵니다."": 이 빈 문자열은 과거의 레거시 사이트들과의 하위 호환성을 위해 필요한 인자(제목, title)입니다. 지금은 항상 빈 문자열을 넘겨주면 됩니다.creature: 이 값은 해당 기록 항목의 URL로 사용됩니다. 브라우저의 주소창(URL bar)에 표시되며, 페이지가 보내는 모든 HTTP 요청의 Referer 헤더 값으로도 사용됩니다. 주의할 점은 이 URL은 현재 페이지와 반드시 동일 출처(same-origin)를 가져야 한다는 것입니다.💡 강사의 팁:
두 번째 인자인 제목(title)은 사연이 참 많아요. 원래 스펙상으로는 이동할 페이지의<title>값을 넣으라고 만들었지만, 사파리, 크롬 등 대부분의 모던 브라우저들이 이 값을 무시하도록 구현해 버렸어요. 그래서 관습적으로 빈 문자열""을 넣습니다.
세 번째 인자인 URL이 정말 핵심인데요! 화면이 깜빡거리며 새로고침 되지 않는데도 브라우저 주소창이/creatures/eagle처럼 싹 바뀌는 마법이 바로 여기서 일어납니다.
popstate 이벤트 사용하기 (Using the popstate event)이제 사용자가 다음과 같은 행동을 한다고 상상해 봅시다.
pushState()를 통해 '기록 항목 A'가 추가됩니다.사용자가 "뒤로 가기"를 누르면, 현재 세션 기록은 '기록 B'에서 '기록 A'로 이동하게 됩니다. 이때 브라우저는 popstate 이벤트를 발생시킵니다! 그리고 놀랍게도 이 이벤트의 인자에는 우리가 예전에 A로 이동할 때 pushState()에 담아두었던 JSON 데이터(상태 객체)가 고스란히 담겨 있어요.
이 말은 즉, 다음과 같은 이벤트 핸들러를 작성하면 올바른 콘텐츠로 페이지를 다시 복구할 수 있다는 뜻입니다.
// Handle forward/back buttons
window.addEventListener("popstate", (event) => {
// If a state has been provided, we have a "simulated" page
// and we update the current page.
if (event.state) {
// Simulate the loading of the previous page
displayContent(event.state);
}
});
💡 강사의 부연 설명:
중요한 포인트 하나 짚고 갈게요!popstate이벤트는 오직 브라우저의 '뒤로 가기'나 '앞으로 가기' 버튼을 눌렀을 때(또는 자바스크립트로history.back()등을 호출했을 때)만 발생합니다. 여러분이 코드에서pushState()나replaceState()를 직접 실행할 때는 발생하지 않아요. 즉, 사용자의 탐색 액션에 반응하기 위한 리스너라고 생각하시면 됩니다.
replaceState() 사용하기 (Using replaceState())아직 퍼즐 조각이 하나 더 남았습니다. 사용자가 맨 처음 SPA 사이트에 접속(로드)할 때, 브라우저는 자연스럽게 첫 세션 기록 항목을 하나 만듭니다. 그런데 이건 실제 브라우저의 페이지 로드였기 때문에, 이 첫 항목에는 아무런 상태(state) 데이터가 연결되어 있지 않습니다. 그래서 사용자가 이렇게 행동했다고 가정해 봅시다.
pushState()로 새 기록 추가)이제 우리는 SPA의 맨 처음 초기 상태로 돌아가고 싶습니다. 하지만 이건 같은 문서 내에서의 탐색이기 때문에 브라우저가 페이지를 처음부터 다시 새로고침해주지 않습니다. 게다가 맨 처음 페이지 기록 항목에는 저장해 둔 state가 텅 비어있으니, popstate 이벤트가 발생해도 event.state 값이 없어서 아까 만든 코드로 화면을 복구할 수가 없죠.
이 문제에 대한 완벽한 해결책이 바로 초기 페이지의 상태 객체를 replaceState()를 사용해 설정(덮어쓰기)해 두는 것입니다! 예를 들면 이렇게요.
// Create state on page load and replace the current history with it
const image = document.querySelector("#photo");
const initialState = {
description: document.querySelector("#description").textContent,
image: {
src: image.getAttribute("src"),
alt: image.getAttribute("alt"),
},
name: "Home",
};
history.replaceState(initialState, "", document.location.href);
페이지가 맨 처음 로드될 때, 우리는 사용자가 나중에 이 출발점(SPA의 첫 화면)으로 되돌아왔을 때 화면을 복구하는 데 필요한 모든 요소의 정보를 싹 긁어모읍니다. 이 구조는 우리가 다른 링크를 클릭해서 탐색할 때 서버에서 받아오던 JSON 구조와 똑같이 맞춥니다. 그리고 이 initialState 객체를 replaceState() 메서드에 넘겨줍니다. 그러면 현재 텅 비어있던 첫 번째 기록 항목에 우리가 만든 알찬 상태 객체가 찰싹 달라붙게 됩니다.
이제 사용자가 이곳저곳을 돌아다니다가 다시 맨 처음 시작 지점까지 "뒤로 가기"를 연타해서 돌아오면, popstate 이벤트 안에는 우리가 예쁘게 세팅해 둔 초기 상태(initialState)가 들어있게 됩니다. 그럼 우리의 displayContent() 함수가 이걸 받아서 초기 화면을 완벽하게 다시 그려주는 거죠!
💡 강사의 팁:
pushState는 스택에 데이터를 새로 쌓아 올리면서 히스토리 길이를 늘리고,replaceState는 쌓지 않고 현재 머물고 있는 기록을 조용히 바꿔치기 합니다. 초기 화면 세팅뿐만 아니라, 의미 없는 URL 변경(예: 검색 필터 값 변경 등)이 뒤로 가기 목록을 너무 많이 지저분하게 채우는 걸 막고 싶을 때 실무에서replaceState를 엄청 자주 사용한답니다!
우리가 지금까지 다룬 전체 예제 코드는 GitHub 저장소(https://github.com/mdn/dom-examples/tree/main/history-api)에서 확인하실 수 있고, 실제 라이브 데모는 https://mdn.github.io/dom-examples/history-api/에서 직접 테스트해 보실 수 있습니다.
수고하셨습니다! 오늘은 브라우저의 역사(?)를 조작하는 아주 강력한 무기인 History API에 대해 배웠습니다. 웹 개발을 공부하시다가 또 막히는 영어 문서나 개념이 있다면 언제든 편하게 가져오세요. 제가 시원하게 풀어드리겠습니다! 파이팅입니다! 💻🚀