모든 포스트
All Posts

Browser Router와 Hash Router

Hash Router 너는 누구니?

회사 어드민 프로젝트에 필터 기능을 구현하기 위해 쿼리스트링을 열심히 달던 중이였다. 그런데 정말 귀신이 곡할 노릇으로 새로 고침만 하면 쿼리스트링 부분이 홀랑 날아가버렸다. 아니 이게 무슨 일이냐고. (헛짚은거 같긴 한데) 해시 라우터 니가 범인이니? 아니 그런데 잠깐(…) 이 프로젝트는 도대체 왜 해시 라우터로 되어 있는거지?
(깔깔 포인트: 너무나 당연하게도 해시 라우터는 범인이 아니였다. 그저 어떤 HOC에서 의도치 않게 쿼리스트링을 날려버리는 버그가 있었을 뿐 ^^ 결국 이 문제는 잘 해결했다.)

(… 부끄러운 이야기이지만) 나는 이제껏 경험한 React 프로젝트에서는 모두 Browser Router만을 사용했기 때문에 Hash Router는 친숙하지 않았다. https://reactrouter.com/en/main/router-components/hash-router 심지어 React Router의 공식 문서에도 강력하게 해시 라우터의 사용을 추천하지 않고 있었다. 그런데 왜 이 프로젝트는 무슨 이유로 브라우저 라우터를 사용하지 않고 해시 라우터를 사용한 것일까? 두 라우터 간의 차이점은 뭘까?


여러가지 글을 찾아본 결과 두 라우터가 다음과 같은 특징을 갖고 있음을 알 수 있었다.

Browser Router

Browser Router는 HTML5의 History API를 사용하여 페이지 간의 전환을 처리한다. 이 방식은 실제 URL 경로를 사용하므로 http://example.com/about 과 같은 형태의 URL을 가진다. 주요 특징은 다음과 같다.

  • 브라우저 지원: HTML5의 History API를 지원하는 최신 브라우저에서 사용 가능하다.
  • 서버 요청: URL 경로가 변경되면 서버로 요청이 전송된다. 따라서 서버 측에서도 동일한 경로에 대한 응답을 제공해야 한다.
  • 깔끔한 URL: 해시(#) 없이 실제 경로를 사용하므로 URL이 깔끔하고 읽기 쉽다.
  • 새로 고침시에 에러가 발생하며 이를 해결하기 위해서는 서버에 추가적인 세팅이 필요하다.

Hash Router

Hash Router는 URL의 해시(#)를 사용하여 페이지 간의 전환을 처리한다. 예를 들어, http://example.com/#/about 과 같은 형태의 URL을 사용한다. 해시(#)는 URL의 일부로 간주되지만 클라이언트 측에서만 해석되고 서버로의 요청에는 포함되지 않는다. 이는 웹 브라우저의 내장된 기능으로 이러한 특징으로 인해 서버 측에서는 항상 동일한 URL(http://example.com/)의 요청만을 받게 되고 그 결과 동일한 페이지를 반환하게 된다. 주요 특징은 다음과 같다

  • 브라우저 지원: 모든 최신 브라우저에서 사용 가능하다.
  • 서버 요청: 해시가 변경되어도 서버로 요청이 전송되지 않는다. 즉, 클라이언트 측에서만 라우팅이 처리된다.
  • SEO에 취약: 해시 라우터를 사용하면 여러 페이지가 동일한 기본 URL을 공유하게 된다. 예를 들어, http://example.com/#/abouthttp://example.com/#/contact 라는 두 개의 페이지는 실제로는 동일한 기본 URL인 http://example.com/을 공유한다. 이로 인해 검색 엔진은 각각의 페이지를 별개의 인덱스로 처리하기 어려울 수 있다.
  • 호환성: 구형 브라우저와 호환되는 라우팅 방식이다.
  • 새로 고침해도 에러가 발생하지 않는다.

대부분의 항목에서 고개를 끄덕이며 읽어 내려오는데, 한 가지 특징에서 의문이 들었다. Browser Router를 사용하면 새로 고침시에 에러가 난다고? 왜?

이제까지 내가 경험했던 React 기반 프로젝트에서는 React Router의 Browser Router를 사용했으나 새로 고침시에 에러가 나는 현상을 만나지 못했었다. 그럼 내가 썼던 Browser Router는 무엇이었을까? (…) 나도 모르게 누군가가 추가적인 세팅을 해 주었던 것일까? (혹시 코드 우렁각시라도 있는 것일까? 🥺) 그 새로고침 에러를 해결하기 위해선 추가적인 세팅이 필요하다고 하는데, 그 추가적인 세팅이란 과연 무엇일까?


Browser Router를 사용하면 새로 고침시에 에러가 난다던데요? 🤔

우선 Browser Router 를 사용하면 왜 새로 고침시에 에러가 발생 하는지 근본적인 원인을 살펴보고자 했다.

MPA(Multi page Application)으로 구현된 웹사이트의 경우, 경로를 이동하면 다음과 같은 동작을 하게 된다.

  • 유저가 www.example.com/about 페이지로 앵커 태그를 통해 이동을 한다.
  • URL이 변경되면 서버에 GET 요청을 보내 about.html 을 받아온다.
  • 브라우저는 about.html 이용해 화면을 그린다.

즉, 이 MPA에서 새로운 화면을 보여준다는 것은 = 새로운 html 파일을 받아온다, 이며 새로운 페이지를 보기 위해서는 새로고침(과 같은 효과)가 일어나야만 했다. 따라서 유저들은 새로운 html 페이지를 받아올 때까지 하얗게 날아간 화면을 봐야만 했다.

그러나 SPA(Single Page Application)방식의 프레임 워크들이 대두되고 CSR을 하게 되면서 라우팅의 동작 방식이 변하게 된다. SPA는 html은 한번만 받아오고 나머지 화면을 전부 JS로 그리는 형태이다. 따라서 SPA / CSR 상황에서 페이지 이동(라우팅)이 일어날 때는 다음과 같이 동작한다 (+브라우저 라우터 기준이다).

  • React(SPA / CSR)로 구현된 웹 사이트의 홈페이지(인덱스 페이지)에 접속하면 웹 서버에 요청해 html, CSS, JS 파일을 받아온다.
  • 유저가 버튼을 눌러 다른 페이지로 이동하려고 하면, History API의 pushState를 이용하여 URL을 변경한다.
  • 이 때 pushState를 이용하면 URL만을 변경하고 웹서버에 요청을 보내지 않는다. (앵커 태그를 이용해 페이지를 이동하면 웹브라우저의 내장 기능에 따라 URL을 변경함과 동시에 자동으로 웹서버에 요청을 보낸다.)
  • 클라이언트 단에서는 URL의 변경을 감지하여 DOM API가 URL에 알맞는 화면을 그리게 된다.

URL의 변경과 라우팅은 이뤄지지만 모든 것이 클라이언트 사이드에서 행해지고 서버는 아무런 관계가 없다.

위와 같은 방식의 Browser Router를 이용한 라우팅은 해당 사이트를 사용하는 모든 유저가 무조건 홈페이지(인덱스 페이지 /)로 처음 접속하여 버튼을 눌러서만 이동 한다면, 문제 될 것이 없다. 인덱스 페이지에 접속하는 순간, URL의 요청을 받은 서버는 필요한 자원(Html, CSS, JS)을 제공할 것이고 그 다음 라우팅은 앞서 말한 CSR의 방식으로 동작할 것이기 때문이다. 하지만 유저가 언제나 이렇게 행동할 것이라고 누구도 장담 할 수 없다. 누군가는 F5를 눌러 의도적인 새로 고침을 유도할 것이고, 누군가는 인덱스 페이지가 아닌 다른 페이지(ex. /about) 주소를 주소창에 직접 입력하여 바로 접근하고 싶어할 것이다.

그럼 유저가 인덱스 페이지가 아닌 페이지에서 의도적으로 새로 고침을 하거나, 주소를 직접 쳐서 들어오려 되면 어떻게 될까?

  • 유저가 www.example.com/detail 페이지에서 새로 고침을 시도한다.
  • 웹브라우저의 내장 기능으로 인해 웹 서버에 파일을 요청하게 된다.
  • 이 때, 이미 URL은 React Router의 path로 얼룩져 있는 상태이기 때문에, 서버는 그것이 무엇을 반환해 달라는 말인지 알지 못한다. (서버는 인덱스 페이지에 대한 요청에만 무엇을 반환해야 할지 알고 있다.)
  • 따라서 서버는 404 not found 에러를 반환한다.

💡 바로 이 부분이 Browser Router를 이용하면, 새로 고침시에 에러가 난다! 는 것이다.


그렇다면 Browser Router에서 새로 고침시에 에러가 나는 현상을 해결하려면 어떻게 해야 할까?

이를 해결하는 방법은 크게 2가지 이다.

  1. 라우터를 Hash Router로 변경한다.
  2. 웹서버 세팅 시에 404 페이지에 대해서 작업한다.

1) Hash Router로 변경한다.

  • hash router의 경우 URL이 www.domain.com/#/path 과 같은 형태로 이루어지는데, URL의 해시 부분은 서버에 전송되지 않고 브라우저에서만 사용된다.
  • 서버는 언제나 해시 값을 제외한 www.domain.com(인덱스 페이지)에 대한 요청만 받아 들이게 되므로 무엇을 반환해야 하는지 알고 있다.
  • 따라서 서버 에러가 발생하지 않는다.
  • 클라이언트에서는 해시 값을 이용해서 라우팅 한다. (클라이언트에서 해시값에 해당하는 페이지가 없을 경우 index.html 만이 노출된다. 빈페이지 같은 것..)

2) 서버에서 404 페이지에 대한 작업을 한다.

/ 이외의 path로 서버에 요청할 때 무조건 서버가 무조건 index.html을 리턴하도록 설정해준다. 이 방법이 글 초반에 언급한 서버의 추가적인 세팅이다.


생각해보니

브라우저 라우터와 해시 라우터의 근본적인 동작 원리를 알아보고 나니, 왜 어드민에 해시 라우터를 사용했는지 이해되었다. 사내에서만 사용하는 작은 서비스이고 SEO를 고려할 필요도 없기 때문에 따로 추가적인 세팅을 하며 웹서버를 구축하기 보다는 간편하게 해시 라우터를 사용하여 새로 고침 에러를 방지한 것이었다. 사실 이제서야 이런 부분에 대해 이해하게 되었다는 것이 조금 부끄럽기도 하다. 익숙하고 자연스럽다고 생각한 것들에 좀 더 ‘왜’라는 물음표를 자주 띄워야 겠다는 생각이 든다.