JavaScript History API 2편
pushState()와 popstate를 직접 만들어 보기
페이지를 새로 이동하지 않고 URL을 바꾸고, 브라우저 뒤로가기와 앞으로가기에 따라 화면 상태까지 복원해 봅니다.
1. 1편에서 무엇을 배웠을까?
1편에서는 아주 간단한 게시글 목록을 만들었습니다.
그리고 검색어를 URL에 넣었습니다.
list.html?keyword=JavaScript
검색할 때는 replaceState()를 사용했습니다.
history.replaceState(
null,
"",
url
);
이번 2편에서는 여기서 한 단계 더 나아가겠습니다.
2. 이번에는 이런 것을 만들어 봅니다
이번에는 게시글 목록 화면에서 페이지 번호를 클릭한다고 생각해 보겠습니다.
그런데 페이지를 클릭할 때마다 실제 HTML 페이지를 다시 로드하지 않는다고 가정합니다.
화면은 JavaScript가 변경하고 URL만 다음처럼 바꿉니다.
list.html?page=1
list.html?page=2
list.html?page=3
URL이 바뀌었을 때 브라우저의 뒤로가기와 앞으로가기도 정상적으로 동작하게 만들어 봅니다.
3. 먼저 아주 간단한 페이지를 만들어 봅니다
별도의 서버 없이 브라우저에서 실행할 수 있는 간단한 예제입니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>History API 실습</title>
</head>
<body>
<h1>게시글 목록</h1>
<div id="page"></div>
<button data-page="1">1페이지</button>
<button data-page="2">2페이지</button>
<button data-page="3">3페이지</button>
<script>
const pageElement =
document.querySelector("#page");
function renderPage(page) {
pageElement.textContent =
`${page}페이지를 보고 있습니다.`;
}
renderPage(1);
</script>
</body>
</html>
버튼을 눌러도 아직 아무 일도 일어나지 않습니다.
이제 버튼을 눌렀을 때 화면을 바꿔보겠습니다.
document
.querySelectorAll("button")
.forEach(button => {
button.addEventListener("click", () => {
const page =
button.dataset.page;
renderPage(page);
});
});
이제 버튼을 누르면 화면의 페이지 번호가 변경됩니다.
그런데 주소창은 그대로입니다.
4. 화면과 URL이 서로 따로 움직이고 있습니다
현재 상태를 생각해 봅시다.
3페이지를 보고 있음
여전히 list.html
이 상태에서는 브라우저 입장에서 1페이지에서 2페이지, 2페이지에서 3페이지로 이동했다는 사실을 알 수 없습니다.
따라서 브라우저의 뒤로가기 버튼을 눌러도 우리가 만든 페이지 상태를 되돌릴 방법이 없습니다.
5. pushState()를 사용해 봅니다
이번에는 버튼을 클릭할 때 History에 새로운 항목을 추가하겠습니다.
history.pushState(
null,
"",
"?page=2"
);
페이지를 새로 이동하지 않았는데 주소가 다음처럼 변경됩니다.
list.html?page=2
여기서 중요한 것은 새로운 History entry가 추가된다는 점입니다.
이제 브라우저 입장에서도 새로운 History 상태가 생겼습니다.
6. pushState()를 버튼에 연결합니다
이제 실제 버튼에 적용해 보겠습니다.
document
.querySelectorAll("button")
.forEach(button => {
button.addEventListener("click", () => {
const page =
button.dataset.page;
const url =
`?page=${page}`;
history.pushState(
null,
"",
url
);
renderPage(page);
});
});
이제 다음과 같은 흐름이 만들어집니다.
1페이지 클릭
↓
URL ?page=1
↓
2페이지 클릭
↓
URL ?page=2
↓
3페이지 클릭
↓
URL ?page=3
중요한 점은 페이지 전체가 새로 로드되지 않는다는 것입니다.
7. 그런데 뒤로가기를 눌러보면?
여기서 많은 주니어 개발자가 처음 헷갈립니다.
pushState()로 History를 만들었지만,
뒤로가기를 눌렀다고 해서 자동으로 화면이 2페이지로 변경되는 것은 아닙니다.
브라우저의 URL은 변경되지만 JavaScript가 화면을 다시 그려야 합니다.
8. popstate란?
브라우저의 History 이동으로 현재 문서의 History entry가 변경될 때
JavaScript에서 이를 처리할 수 있도록 사용하는 이벤트가 popstate입니다.
아주 간단하게 이벤트를 먼저 만들어 보겠습니다.
window.addEventListener(
"popstate",
() => {
console.log("History가 변경되었습니다.");
}
);
이제 브라우저의 뒤로가기나 앞으로가기로 History를 이동하면 이벤트에서 그 변화를 처리할 수 있습니다.
9. URL에서 page를 읽습니다
URL이 다음과 같다고 생각해 봅시다.
list.html?page=2
1편에서 배웠던 URLSearchParams를 그대로 사용할 수 있습니다.
function getPageFromUrl() {
const params =
new URLSearchParams(
window.location.search
);
return Number(
params.get("page") || 1
);
}
이제 URL이 현재 어떤 페이지를 가리키고 있는지 JavaScript가 알 수 있습니다.
10. popstate에서 화면을 다시 그립니다
이제 핵심 코드를 작성합니다.
window.addEventListener(
"popstate",
() => {
const page =
getPageFromUrl();
renderPage(page);
}
);
이제 흐름은 다음과 같습니다.
↓
History 변경
↓
popstate 발생
↓
URL에서 page 읽기
↓
화면 다시 그리기
11. 전체 코드를 합쳐봅니다
이제 지금까지 작성한 코드를 하나의 HTML 파일로 합쳐보겠습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>History API 실습</title>
</head>
<body>
<h1>게시글 목록</h1>
<div id="page"></div>
<button data-page="1">1페이지</button>
<button data-page="2">2페이지</button>
<button data-page="3">3페이지</button>
<script>
const pageElement =
document.querySelector("#page");
function renderPage(page) {
pageElement.textContent =
`${page}페이지를 보고 있습니다.`;
}
function getPageFromUrl() {
const params =
new URLSearchParams(
window.location.search
);
return Number(
params.get("page") || 1
);
}
document
.querySelectorAll("button")
.forEach(button => {
button.addEventListener(
"click",
() => {
const page =
button.dataset.page;
history.pushState(
null,
"",
`?page=${page}`
);
renderPage(page);
}
);
});
window.addEventListener(
"popstate",
() => {
const page =
getPageFromUrl();
renderPage(page);
}
);
renderPage(
getPageFromUrl()
);
</script>
</body>
</html>
12. 직접 테스트해 봅니다
이제 브라우저에서 실제로 테스트해 보세요.
① HTML 파일을 실행합니다.
② 1페이지 → 2페이지 → 3페이지를 클릭합니다.
③ 주소창의 URL이 변경되는지 확인합니다.
④ 브라우저 뒤로가기를 누릅니다.
⑤ 2페이지로 화면이 변경되는지 확인합니다.
⑥ 다시 뒤로가기를 눌러 1페이지로 이동합니다.
⑦ 앞으로가기로 2페이지, 3페이지도 이동해 봅니다.
13. pushState()와 replaceState()의 차이
이제 1편에서 사용했던 replaceState()와 비교해 보겠습니다.
| 구분 | pushState() | replaceState() |
|---|---|---|
| History | 새로운 entry 추가 | 현재 entry 변경 |
| 뒤로가기 | 이전 entry로 이동 가능 | 별도의 entry를 추가하지 않음 |
| 사용 예 | 페이지·화면 상태 이동 | 현재 URL 상태 갱신 |
A → B → C
History를 하나 더 만듭니다.
A → B → C'
현재 History entry를 바꿉니다.
14. 주니어 개발자가 꼭 기억할 것
pushState()는 URL만 바꾸는 함수라고 생각하면 부족합니다.
브라우저 History에 새로운 상태를 추가하고, 그 상태에 맞게 화면을 변경하는 것은 개발자가 처리해야 합니다.
그래서 pushState() + popstate + 화면 렌더링을 함께 이해해야 합니다.
15. 실무에서는 이렇게 연결됩니다
지금 만든 예제는 실제 서비스에서 훨씬 복잡한 형태로 발전할 수 있습니다.
| 실습 | 실무 |
|---|---|
| page | 페이지 번호 |
| keyword | 검색어 |
| URLSearchParams | 검색·필터·정렬 조건 |
| pushState() | 화면 상태를 History에 추가 |
| popstate | 뒤로가기·앞으로가기 상태 처리 |
| renderPage() | 실제 화면 렌더링 |
16. 1편과 2편을 연결해서 이해하기
검색어를 URL에 저장
↓
URLSearchParams
↓
replaceState()
2편
화면 상태를 History에 추가
↓
pushState()
↓
popstate
↓
화면 상태 복원
이렇게 연결해서 이해하면 History API가 단순히 "주소창의 URL을 바꾸는 기능"이 아니라는 것을 알 수 있습니다.
17. 다음 단계에서는 무엇을 할까?
여기까지 이해했다면 다음 단계에서는 검색과 페이지 이동을 하나로 묶어볼 수 있습니다.
list.html
↓
검색
↓
list.html?keyword=JavaScript
↓
페이지 2
↓
list.html?keyword=JavaScript&page=2
↓
상세 페이지
↓
뒤로가기
↓
검색어 + 페이지 상태 복원
그리고 이 구조가 조금 더 발전하면 Vue Router나 React Router 같은 SPA 라우팅 구조를 이해하는 데 필요한 기본 개념으로 연결됩니다.
18. 실습 체크리스트
□ 1페이지, 2페이지, 3페이지 버튼을 만들었다.
□ 버튼 클릭으로 화면을 변경했다.
□ pushState()로 URL을 변경했다.
□ URLSearchParams로 page를 읽었다.
□ popstate 이벤트를 등록했다.
□ 브라우저 뒤로가기를 테스트했다.
□ 브라우저 앞으로가기도 테스트했다.
□ pushState()와 replaceState()의 차이를 이해했다.
19. FAQ
pushState()를 호출하면 페이지가 새로고침되나요?
일반적인 페이지 이동처럼 현재 문서를 새로 로드하지 않고 History entry를 추가하면서 URL을 변경할 수 있습니다.
pushState()를 호출하면 화면도 자동으로 변경되나요?
아닙니다. URL과 History 상태를 변경한 뒤 실제 화면을 어떻게 변경할지는 JavaScript 코드에서 처리해야 합니다.
뒤로가기를 누르면 왜 popstate가 필요한가요?
History 이동에 따라 URL이 변경되었을 때 그 상태를 읽고 화면을 다시 렌더링하기 위해 사용할 수 있습니다.
모든 URL 변경에 pushState()를 사용하면 되나요?
그렇지는 않습니다. 새로운 History 단계로 남겨야 하는 상태인지, 현재 History entry만 변경하면 되는 상태인지에 따라 선택해야 합니다.
20. 마무리
2편에서는 복잡한 프레임워크 없이 순수 HTML과 JavaScript만으로 브라우저 History의 동작을 직접 확인했습니다.
새로운 History 상태 추가
↓
popstate
History 이동 감지
↓
URLSearchParams
현재 상태 확인
↓
render()
화면 복원
결국 중요한 것은 함수 이름을 외우는 것이 아닙니다.
"URL 상태와 화면 상태를 어떻게 연결하고, 브라우저 History 이동에 따라 화면을 어떻게 복원할 것인가?"
이 개념을 이해하면 이후 SPA 라우팅을 공부할 때도 단순히 라이브러리 사용법을 외우는 것이 아니라 그 내부에서 어떤 일이 일어나는지 이해할 수 있습니다.
참고 자료
- MDN Web Docs — History API
- MDN Web Docs — history.pushState()
- MDN Web Docs — history.replaceState()
- MDN Web Docs — popstate event
- MDN Web Docs — URLSearchParams
Web API의 세부 동작은 브라우저 및 웹 표준의 변경에 따라 달라질 수 있습니다. 실제 프로젝트에 적용하기 전 사용하는 브라우저의 최신 공식 문서를 확인하는 것을 권장합니다.
댓글
댓글 쓰기