History API란 무엇이고 언제 어떻게 써야 할까

Frontend

프론트엔드 개발을 하다 보면 브라우저의 뒤로 가기와 앞으로 가기가 단순한 브라우저 기능이 아니라는 것을 점점 체감하게 됩니다.

  • SPA에서 페이지 전환처럼 보이게 만들고 싶을 때
  • URL은 바꾸되 전체 새로고침은 피하고 싶을 때
  • 필터나 탭 상태를 히스토리에 남기고 싶을 때
  • 뒤로 가기 동작이 어색하지 않게 맞추고 싶을 때
  • 어떤 이동은 히스토리에 남기고, 어떤 이동은 교체하고 싶을 때

이때 핵심이 되는 것이 History API입니다.

겉보기에는 history.back()이나 history.go(-1) 같은 단순한 기능처럼 보일 수 있습니다. 하지만 실무에서는 여기서 한 단계 더 들어가 pushState, replaceState, popstate를 이해해야 URL 상태, 브라우저 히스토리, SPA 라우팅 흐름을 제대로 다룰 수 있습니다.

이 글에서는 History API를 아래 흐름으로 정리해보겠습니다.

  1. 무엇인지
  2. location과는 무엇이 다른지
  3. 주요 메서드와 이벤트는 무엇인지
  4. pushState, replaceState, popstate는 어떻게 연결되는지
  5. 실무에서 언제 쓰고 언제 조심해야 하는지

한눈에 보면

먼저 짧게 정리하면 이렇습니다.

  • History API는 브라우저 세션 히스토리를 읽고 조작하는 API입니다.
  • back(), forward(), go()는 이미 있는 히스토리를 이동합니다.
  • pushState()는 새 히스토리 엔트리를 추가하고, replaceState()는 현재 엔트리를 교체합니다.
  • popstate는 사용자가 뒤로 가기/앞으로 가기 등으로 히스토리 엔트리가 바뀔 때 반응할 때 중요합니다.
  • SPA 라우터는 내부적으로 이 API를 활용해 "새로고침 없는 이동"을 구성합니다.

즉, History API의 핵심은 단순 이동이 아니라 주소 변화와 히스토리 흐름을 브라우저 기본 동작과 맞추는 것입니다.

History API는 정확히 무엇일까?

가장 단순하게 말하면 History API현재 탭의 브라우저 방문 기록을 다루는 브라우저 기본 API입니다.

브라우저에서는 보통 이렇게 접근합니다.

console.log(window.history);
console.log(history);

이 객체를 통해 브라우저는:

  • 이전 페이지로 이동하고
  • 다음 페이지로 이동하고
  • 특정 상대 위치로 점프하고
  • 현재 URL을 새 히스토리로 추가하거나 교체할 수 있습니다

즉, history는 주소 문자열 자체보다 주소 이동의 흐름과 세션 기록에 더 가까운 객체입니다.

location과는 무엇이 다를까?

locationhistory는 같이 자주 나오기 때문에 구분이 중요합니다.

  • location: 현재 주소가 무엇인지 읽고, 필요하면 그 주소로 이동시키는 쪽
  • history: 그 주소들이 어떤 순서로 쌓이고 이동되는지 다루는 쪽

예를 들어:

  • location.pathname은 지금 경로가 무엇인지 읽습니다
  • history.back()은 이전 히스토리 엔트리로 이동합니다
  • history.pushState()는 새 엔트리를 추가하면서 URL을 바꿉니다

즉, location이 "현재 주소"에 더 가깝다면, history는 "주소 이동의 시간축"에 더 가깝습니다.

실무에서는 둘을 함께 보게 됩니다.

  • 주소를 어떻게 읽을지: location
  • 주소 변경을 히스토리에 어떻게 반영할지: history

가장 기본적인 메서드들

처음에는 back, forward, go부터 이해하면 흐름이 잡힙니다.

1. history.back()

브라우저 뒤로 가기와 같은 의미입니다.

history.back();

즉, 사용자가 브라우저 뒤로 가기 버튼을 누른 것과 비슷하게 동작합니다.

2. history.forward()

브라우저 앞으로 가기와 같은 의미입니다.

history.forward();

뒤로 갔던 흐름이 있을 때 다시 앞으로 이동합니다.

3. history.go()

상대적인 히스토리 위치로 이동합니다.

history.go(-1); // 뒤로 한 칸
history.go(1); // 앞으로 한 칸
history.go(0); // 현재 페이지 다시 로드와 유사

즉:

  • -1이면 뒤로
  • 1이면 앞으로
  • 0이면 현재 위치 재실행

입니다.

이 메서드들은 이해하기 쉽지만, SPA 실무에서는 보통 더 중요한 것이 pushState()replaceState()입니다.

pushState()는 왜 중요할까?

SPA에서 핵심은 "전체 새로고침 없이 URL과 화면을 함께 바꾸는 것"입니다. 이때 가장 중요한 도구 중 하나가 pushState()입니다.

형태는 이렇게 생깁니다.

history.pushState(state, '', url);

예를 들어:

history.pushState({ tab: 'comments' }, '', '/posts/123?tab=comments');

이 코드는:

  • 브라우저 히스토리에 새 엔트리를 추가하고
  • 주소창 URL을 바꾸고
  • 문서를 전체 새로고침하지는 않습니다

여기서 중요한 점은 URL은 바뀌지만 자동으로 화면 렌더링이 바뀌는 것은 아니라는 점입니다.

즉, pushState()는 라우터 전체를 대신하는 마법 메서드가 아니라:

  • URL을 바꾸고
  • 히스토리에 엔트리를 추가하고
  • 앱이 그 변화에 맞춰 화면을 업데이트할 수 있게 만드는 도구

에 가깝습니다.

이 특성 때문에 SPA 라우터는 pushState() 호출 뒤에:

  • 현재 경로를 읽고
  • 어떤 화면을 보여줄지 다시 계산하고
  • 필요한 데이터 패칭을 트리거합니다

즉, pushState()는 SPA 라우팅의 기반이지만, 라우팅 전체 그 자체는 아닙니다.

replaceState()는 언제 다를까?

형태는 비슷합니다.

history.replaceState(state, '', url);

예를 들어:

history.replaceState({ modal: 'login' }, '', '/login');

이 메서드는 현재 엔트리를 새로운 URL과 state로 교체합니다.

즉:

  • pushState(): 히스토리에 새 엔트리 추가
  • replaceState(): 현재 엔트리 교체

이 차이가 핵심입니다.

실무에서는 보통 아래 기준으로 나뉩니다.

pushState()가 잘 맞는 경우

  • 사용자가 "한 단계 이동했다"고 느끼는 변화
  • 뒤로 가기로 이전 상태로 돌아갈 가치가 있는 변화
  • 필터, 탭, 상세 열기처럼 탐색 흐름에 포함되는 상태

replaceState()가 잘 맞는 경우

  • 중간 상태를 히스토리에 남기고 싶지 않을 때
  • 잘못된 URL을 정규화할 때
  • 최초 진입 직후 추적 파라미터를 정리할 때
  • 뒤로 가기 흐름을 복잡하게 만들고 싶지 않을 때

예를 들어 사용자가 /posts?page=abc로 들어왔을 때 내부적으로 page=1로 보정하고 URL도 바꾸고 싶다면:

history.replaceState(null, '', '/posts?page=1');

처럼 현재 엔트리를 정리하는 선택이 더 자연스러울 수 있습니다.

state는 무엇을 담는 걸까?

pushState()replaceState()의 첫 번째 인자는 state 객체입니다.

history.pushState({ selectedTab: 'comments' }, '', '/posts/123?tab=comments');

이 값은 해당 히스토리 엔트리에 연결된 추가 데이터라고 보면 됩니다.

즉, URL만으로는 표현하지 않은 부가 정보를 엔트리에 붙일 수 있습니다.

하지만 여기서 중요한 점이 있습니다.

  • state는 브라우저 히스토리 엔트리와 함께 움직이고
  • 직렬화 가능한 얕은 데이터에 더 잘 맞고
  • 너무 크거나 복잡한 상태를 넣는 용도로는 적합하지 않습니다

즉, history.state는 편리하지만, 전역 상태 저장소처럼 쓰는 대상은 아닙니다.

popstate는 왜 중요한가?

History API를 이해할 때 가장 중요한 이벤트가 popstate입니다.

사용자가:

  • 브라우저 뒤로 가기
  • 브라우저 앞으로 가기
  • history.back()
  • history.forward()
  • history.go()

같은 동작으로 히스토리를 이동하면, 앱은 그 변화를 알아야 합니다.

이때 쓰는 것이 popstate입니다.

window.addEventListener('popstate', (event) => {
  console.log(event.state);
  console.log(location.pathname);
});

이 이벤트를 통해 앱은:

  • 히스토리 이동이 일어났다는 사실을 알고
  • 현재 URL과 state를 다시 읽고
  • 그에 맞는 화면을 다시 그릴 수 있습니다

즉, SPA에서 뒤로 가기/앞으로 가기가 자연스럽게 동작하려면 popstate를 통해 히스토리 변화를 반영하는 흐름이 필요합니다.

여기서 자주 헷갈리는 점이 하나 있습니다. pushState()replaceState()를 호출한다고 해서 popstate가 자동으로 발생하는 것은 아닙니다. popstate는 보통 히스토리를 "이동"했을 때 중요합니다.

즉:

  • pushState()/replaceState(): 히스토리 엔트리 생성 또는 교체
  • popstate: 그 히스토리 엔트리 사이를 이동할 때 반응

으로 이해하면 정리가 쉽습니다.

실무 예제로 보면 더 잘 보인다

예를 들어 게시글 탭 UI가 있다고 해보겠습니다.

function openTab(tab: 'summary' | 'comments') {
  history.pushState({ tab }, '', `/posts/123?tab=${tab}`);
  renderTab(tab);
}
 
window.addEventListener('popstate', (event) => {
  const tab = (event.state?.tab as 'summary' | 'comments' | undefined) ?? 'summary';
  renderTab(tab);
});

이 코드의 의미는:

  • 사용자가 탭을 바꾸면 새 히스토리 엔트리를 추가하고
  • URL도 바꾸고
  • 화면도 바로 업데이트하고
  • 나중에 뒤로 가기를 누르면 popstate를 통해 이전 탭 상태를 복원한다

는 것입니다.

이 흐름이 바로 History API가 SPA에서 중요한 이유입니다.

hash 라우팅과는 무엇이 다를까?

예전 SPA에서는 # 기반 라우팅이 많이 쓰였습니다.

예를 들어:

/#/posts/123

이 방식은 구현이 단순하지만, 현대 SPA에서는 보통 pushState() 기반 라우팅이 더 자연스럽습니다.

이유는 아래와 같습니다.

  • URL이 더 깔끔하고
  • path 기반 라우팅과 더 잘 맞고
  • SEO나 서버 라우팅 설계와도 더 자연스럽게 연결되기 쉽습니다

즉:

  • hash 라우팅: location.hash 중심
  • history 라우팅: pushState, replaceState, popstate 중심

이라고 볼 수 있습니다.

실무에서는 언제 직접 History API를 쓸까?

프레임워크가 없는 순수 브라우저 앱이라면 직접 다루는 경우가 꽤 있습니다.

1. 가벼운 SPA 라우팅

간단한 라우터를 직접 만들 때:

history.pushState(null, '', '/dashboard');
renderRoute('/dashboard');

같은 흐름이 기본이 됩니다.

2. 필터/탭 상태를 URL과 히스토리에 반영

history.pushState({ sort: 'latest' }, '', '/posts?sort=latest');

뒤로 가기로 이전 정렬 상태를 복원하고 싶다면 이런 패턴이 자연스럽습니다.

3. URL 정규화

history.replaceState(null, '', '/posts?page=1');

잘못된 파라미터나 불필요한 추적 파라미터를 제거할 때 유용합니다.

4. 브라우저 히스토리와 맞는 사용자 경험 설계

단순히 화면만 바꾸는 것이 아니라, 그 변화가 "뒤로 가기 가능한 이동인가?"를 같이 설계해야 할 때 직접적으로 중요해집니다.

React나 Next.js에서는 무엇을 조심해야 할까?

실무에서는 여기서 판단이 갈립니다.

History API는 강력하지만, 프레임워크 위에서 무조건 직접 다루는 것이 항상 좋은 것은 아닙니다.

1. 내부 라우팅은 프레임워크 라우터가 더 적합한 경우가 많다

예를 들어 Next.js나 React Router는 내부적으로 히스토리 흐름을 관리합니다.

그래서 앱 내부 이동을 직접 history.pushState()로 처리하면:

  • 프레임워크 상태와 엇갈릴 수 있고
  • 라우터가 기대하는 라이프사이클을 건너뛸 수 있고
  • 데이터 패칭이나 전환 처리와 어긋날 수 있습니다

즉, 프레임워크 안에서는 보통:

  • Next.js: router.push(), router.replace()
  • React Router: navigate()

같은 라우터 API가 먼저입니다.

이 라우터 API들은 내부적으로 History API 개념을 감싸서 더 안전하게 제공합니다.

2. popstate만 믿고 상태를 복원하면 부족할 수 있다

히스토리 state는 도움이 되지만, 화면 복원의 진실의 원천을 전부 event.state에만 두면 꼬일 수 있습니다.

실무에서는 보통:

  • URL
  • 라우터 상태
  • 필요한 최소한의 history state

를 역할 분리해서 가져가는 편이 더 안정적입니다.

즉, 공유 가능해야 하는 값은 URL에, 부가적인 복원 정보만 history state에 두는 편이 낫습니다.

3. SSR 환경에서는 브라우저 객체 접근 시점을 조심해야 한다

history 역시 브라우저 객체이므로 서버에서는 바로 쓸 수 없습니다.

즉:

  • 이벤트 핸들러 안에서 사용하거나
  • 클라이언트 전용 코드에서 사용하거나
  • effect 안에서 구독/해제하는 식으로

실행 시점을 분리해야 합니다.

자주 하는 실수

정리하면 아래 실수가 정말 자주 나옵니다.

  • pushState()만 하면 화면도 자동으로 바뀔 것이라고 생각한다
  • pushState()replaceState()를 구분하지 않아 뒤로 가기 흐름이 어색해진다
  • popstate 없이 직접 이동만 처리해 뒤로 가기 복원이 깨진다
  • URL에 들어가야 할 값과 history.state에 들어가야 할 값을 섞어 넣는다
  • 프레임워크 라우터가 있는데도 무조건 History API를 직접 다룬다
  • 서버 환경에서 window.history를 바로 사용한다

즉, History API는 URL 조작 도구라기보다 브라우저 탐색 흐름을 설계하는 도구로 이해해야 실수가 줄어듭니다.

실무 체크리스트

실제로 적용할 때는 아래 질문으로 빠르게 점검하면 도움이 됩니다.

  1. 지금 필요한 것이 새 히스토리 추가인가, 현재 엔트리 교체인가?
  2. 이 변화는 뒤로 가기로 복원되어야 하는가?
  3. URL에 드러내야 할 상태와 history.state에만 둘 상태를 구분했는가?
  4. 뒤로 가기/앞으로 가기 시 popstate 흐름을 반영하고 있는가?
  5. 프레임워크 라우터가 이미 더 적절한 추상화를 제공하고 있지는 않은가?

이 기준을 먼저 세우면 pushState(), replaceState(), popstate를 언제 써야 하는지도 훨씬 선명해집니다.

정리하면

History API를 한 줄로 줄이면, 브라우저 세션 히스토리를 이동하고, 현재 URL 엔트리를 추가하거나 교체하며, 그 변화에 맞춰 앱을 동작하게 만드는 브라우저 기본 API입니다.

실무 기준으로 기억할 핵심은 이렇습니다.

  • back(), forward(), go()는 기존 히스토리 이동이고
  • pushState()는 새 엔트리 추가, replaceState()는 현재 엔트리 교체이며
  • popstate는 뒤로 가기/앞으로 가기 같은 히스토리 이동을 앱이 감지하는 데 중요하고
  • SPA에서는 결국 URL, 히스토리, 화면 상태를 함께 설계해야 자연스러운 UX가 나옵니다

History API를 이해하면 브라우저 뒤로 가기가 왜 어떤 화면에서는 자연스럽고, 어떤 화면에서는 어색한지도 더 잘 보이게 됩니다. 그리고 그 순간부터 라우팅은 단순한 URL 이동이 아니라, 사용자의 탐색 흐름을 설계하는 일에 더 가까워집니다.

같이 보면 좋은 글