이 글에서는 SPA에서 페이지 새로고침 없이 URL을 변경하고 검색 기능을 구현하는 방법을 알아본다.
History API를 활용한 클라이언트 사이드 라우팅은 프레임워크 없이도 효과적인 SPA를 구현할 수 있는 핵심 기술이다.
클라이언트 사이드 라우팅 구현
History API 활용
history.pushState()를 사용하면 페이지 새로고침 없이 브라우저의 URL을 변경할 수 있다.
// App.js
handleClick: async () => {
// 브라우저 히스토리에 새 항목 추가 (URL을 홈으로 변경)
history.pushState(null, null, "/");
// 포켓몬 목록 데이터 다시 불러오기
const pokemonList = await getPokemonList();
// 앱 전체 상태 업데이트
this.setState({
...this.state,
pokemonList,
type: "", // 타입 필터 초기화
searchWord: getSearchWord(), // 검색어 초기화
currentPage: "/", // 현재 페이지 경로 업데이트
});
}
history.pushState()의 첫 번째 인자는 상태 객체, 두 번째는 제목(대부분의 브라우저에서 무시됨), 세 번째는 변경할 URL이다.
이 메서드를 호출하면 브라우저의 주소창이 변경되지만 페이지는 새로고침되지 않는다.
URL 파라미터를 활용한 검색 기능
검색어를 URL에 반영하기
// App.js
handleSearch: async (searchWord) => {
// 검색어를 URL에 반영 (/?search=검색어 형태)
history.pushState(null, null, `?search=${searchWord}`);
// 검색어와 현재 타입으로 필터링된 포켓몬 목록 불러오기
const searchPokemonList = await getPokemonList(
this.state.type,
searchWord
);
// 검색 결과로 앱 상태 업데이트
this.setState({
...this.state,
searchWord,
pokemonList: searchPokemonList,
currentPage: `?search=${searchWord}`,
});
}
검색어를 URL 쿼리 파라미터로 관리하면 (/?search=피카츄) 사용자가 URL을 공유하거나 북마크할 때 검색 상태가 유지된다.
한글 검색어는 자동으로 인코딩되며, decodeURIComponent를 사용하여 디코딩할 수 있다.
Header 컴포넌트 구현
조건부 렌더링
this.template = () => {
const { currentPage, searchWord } = this.state;
let temp = `<div class='header-content' id="title">
<img src='/src/img/ball.webp' width=40px height=40px></img>
포켓몬 도감</div>`;
// 상세 페이지가 아닐 때만 검색 바 표시
if (!currentPage.includes("/detail")) {
temp += `<div class="search">
<input type="text" placeholder="포켓몬을 검색하세요!" id="search" autocomplete="off" value="${decodeURIComponent(searchWord)}" />
<button id="search-button"><img src="src/img/search.png"></img></button>
</div>`;
}
return temp;
};
현재 페이지 경로를 확인하여 상세 페이지일 때는 검색 바를 숨긴다.
decodeURIComponent로 URL에 인코딩된 한글 검색어를 디코딩하여 input 필드에 표시한다.
이벤트 처리
this.render = () => {
this.$target.innerHTML = this.template();
// 타이틀 클릭 이벤트 - 홈으로 이동
const $title = document.getElementById("title");
$title.addEventListener("click", () => {
this.handleClick();
});
// 검색 기능 (상세 페이지가 아닐 때만)
if (!this.state.currentPage.includes("/detail")) {
const $searchInput = document.getElementById("search");
const $searchButton = document.getElementById("search-button");
// 검색 실행 함수
const performSearch = () => {
this.handleSearch($searchInput.value);
};
// 엔터 키 입력 시 검색 실행
$searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") performSearch();
});
// 검색 버튼 클릭 시 검색 실행
$searchButton.addEventListener("click", performSearch);
}
};
타이틀 클릭, 엔터 키 입력, 검색 버튼 클릭 등 다양한 방식으로 검색을 실행할 수 있도록 이벤트 리스너를 등록한다.
performSearch 함수를 별도로 정의하여 중복 코드를 줄였다.

popstate 이벤트 처리
브라우저의 뒤로가기/앞으로가기 버튼을 눌렀을 때 상태를 업데이트하려면 popstate 이벤트를 처리해야 한다.
window.addEventListener('popstate', () => {
// URL 변경에 따라 상태 업데이트
const currentPage = window.location.pathname + window.location.search;
const searchWord = new URLSearchParams(window.location.search).get('search') || '';
// 상태 업데이트 및 재렌더링
this.setState({
...this.state,
currentPage,
searchWord
});
});
사용자가 브라우저의 뒤로가기/앞으로가기 버튼을 사용해도 애플리케이션이 정상적으로 동작한다.
완성된 프로젝트

이러한 방식으로 구현하면 사용자가 페이지 전환 없이 포켓몬을 검색하고 결과를 확인할 수 있다. 같은 패턴을 적용하여 PokemonDetail 컴포넌트까지 구현하면 완전한 포켓몬 도감 SPA가 완성된다.
클라이언트 사이드 라우팅의 장점
- 빠른 페이지 전환: 페이지 새로고침 없이 화면을 변경할 수 있다
- 부드러운 사용자 경험: 네이티브 앱과 유사한 UX를 제공한다
- 상태 유지: 검색어나 필터 상태를 URL에 저장하여 공유 가능하다
- 브라우저 히스토리 활용: 뒤로가기/앞으로가기 버튼이 정상 작동한다
'Frontend > JavaScript' 카테고리의 다른 글
| Vanilla JS 컴포넌트 설계 패턴 (0) | 2025.11.10 |
|---|---|
| 화살표 함수와 객체 (0) | 2025.11.09 |
| API 호출 (0) | 2025.10.31 |
| async와 await (0) | 2025.10.31 |
| Promise 객체 (0) | 2025.10.31 |
