브라우저에 내장된 Web History API를 활용해 URL을 전역 상태 관리 도구로 사용하는 방법과 그 이점, 그리고 주의해야 할 점을 설명합니다.
Web History API는 개발자들이 사용할 수 있는 상태 관리 도구 중 가장 과소평가된 도구다.
브라우저에 내장되어 있고, 모든 브라우저에서 지원되는 유일한 상태 관리 도구다. 어떤 라이브러리나 프레임워크도 필요 없고, 성능 면에서 다른 모든 상태 관리 도구를 능가한다.
URL이 로드해야 할 데이터를 더 잘 반영할수록, 데이터 패칭 전략을 더 공격적으로 최적화할 수 있다.
클라이언트 측 상태 관리 도구는 자신이 가진 정보를 서버로 전달하지 못하기 때문에, 브라우저는 한 번 더 왕복 요청을 해야 한다.
서버 측 상태 관리 도구를 사용한다면, 이 첫 세 단계를 건너뛸 수 있다.
?query=swimming&sort=price&page=4 같은 URL은 서버에게 어떤 데이터를 미리 패칭해서 완전히 렌더링된 상태로 브라우저에 보내야 하는지 정확히 알려준다.
Airbnb의 URL을 한 번 보자.
Airbnb에 들어가서 아무 숙소나 연 다음, 브라우저 콘솔에서 new URL(window.location.href)를 실행해 보자.
그러면 대략 이런 객체를 얻게 될 것이다.
txthostname: www.airbnb.com href: https://www.airbnb.com/rooms/12345678?adults=1&children=0&infants=0&pets=0&check_in=2023-05-05&check_out=2023-05-10 pathname: /rooms/12345678 searchParams: adults: "1", children: "0", infants: "0", pets: "0", check_in: "2023-05-05", check_out: "2023-05-10"
pathname 부분은 자명하다 — 지금 어떤 페이지에 있는지를 알아야 하는 것은 너무 당연하다. 하지만 searchParams는 눈여겨볼 만하다.
각 페이지에는 작은 예약 폼이 있어서, 여기에 정보를 입력하면 가격 견적을 받을 수 있다.
사용자가 페이지를 새로고침하면 어떤 일이 일어나야 할까? 폼을 비워서 다시 입력할 수 있게 해야 할까? 이미 입력한 값으로 폼을 미리 채우고 싶다면, URL만이 방법인 것은 아니다. 로컬 스토리지, 쿠키, 데이터베이스에 저장할 수도 있다.
하지만 사람들은 잠재적인 예약 내용을 다른 사람과 공유하기도 한다. 친구와 여행을 계획 중이라면, 내가 보고 있는 숙소 링크를 친구에게 보내고, 친구는 내가 보는 것과 똑같은 날짜·인원·가격 견적을 볼 수 있어야 한다.
URL만큼 상태를 공유 가능하게 만들어 주는 상태 관리 도구는 없다.
히스토리 스택은 사용자가 방문한 URL의 목록이다. 뒤로 가기 버튼을 누르면, 스택에서 이전 URL로 돌아간다.
이는 사용성 측면에서 매우 중요하다. 어떤 페이지에 있다가 링크를 클릭해서 다른 페이지로 이동한 뒤, 뒤로 가기 버튼을 누르면, 방금 링크를 클릭하기 직전 페이지로 돌아가길 기대한다.
이걸 엉망으로 처리했던 대표적인 예가, 지금은 서비스가 종료된 Spectrum이라는 앱이다.
Spectrum은 포럼이었고, 토론 주제들이 수많은 쓰레드 페이지로 나뉘어 있었다. 검색 기능이 있었는데, 검색어를 URL에 저장하는 방식은 제대로 구현되어 있었다. 어떤 걸 검색한 뒤 새로고침을 해도, 여전히 그 검색어에 대한 결과를 보게 된다.
문제는 페이지 번호를 URL에 저장하지 않았다는 점이다. 검색 결과의 10페이지까지 넘어가서 괜찮아 보이는 글을 클릭했다가, 뒤로 가기를 누르면 검색 결과의 1페이지로 돌아가 버리곤 했다.
사용자는 자연스러운 브라우징 경험의 일부로 앞/뒤로 가기 버튼을 사용한다. 모바일 사용자는 좌우 스와이프로 앞으로/뒤로 이동하기도 한다.
모든 웹 애플리케이션은 히스토리 네비게이션이 사용자 인터페이스의 일부라는 전제를 깔고 설계되어야 한다.
모달 팝업 다이얼로그는 사용자를 다른 페이지로 보내지 않고 추가 정보를 보여 줄 수 있는 유용한 방법이다.
작은 화면에서는 모달과 페이지 콘텐츠를 동시에 보여 줄 공간이 없는 경우가 많아서, 많은 웹사이트가 모달을 전체 화면으로 띄운다. 사용자 입장에서는 이것이 사실상 새로운 페이지처럼 보인다.
이 때문에, 모달은 뒤로 가기 버튼으로 닫을 수 있어야 한다는 점이 중요하다. 모바일 사용자는 종종 왼쪽으로 스와이프해서 기기의 뒤로 가기를 트리거한다. 이때 모달을 닫는 대신 이전 페이지로 보내 버리면, 사용자는 혼란을 느끼고 마치 두 페이지나 뒤로 간 것처럼 느낄 것이다.
사용자가 모달 상태를 북마크하거나, 다른 사람과 공유할 수 있어야 한다면 모달은 자신만의 URL을 가져야 한다. 이게 /issues/edit 같은 pathname의 일부인지, /issues?modal=edit처럼 쿼리 파라미터인지 자체는 중요하지 않다. 중요한 건 URL에 노출되어 있어야 한다는 것이다.
하지만 모달은 각기 다른 용도로 사용되므로, 최선의 처리 방법은 컨텍스트에 따라 달라질 수 있다.
모달이 사용자에게는 중요하지만, 다른 사람에게는 중요하지 않은 경우라면, 모달 상태가 공유되지 않는 URL을 원할 수 있다.
이럴 땐 쿼리 파라미터 대신 Location State를 사용해, 모달이 열려 있는지/닫혀 있는지를 URL 문자열과 분리해서 저장할 수 있다.
이 상태는 여전히 브라우저 히스토리에 저장된다. 앞으로 가기, 뒤로 가기, "닫은 탭 다시 열기" 같은 기능 모두 기대한 대로 동작한다.
React Router의 Link 컴포넌트에는 임의의 데이터를 브라우저 히스토리에 저장할 수 있는 state prop이 있다.
tsxexport default function Example() { const location = useLocation() const shouldShowModal = location.state?.modal === "edit" return shouldShowModal ? ( <Modal /> ) : ( <Link to="/issues" state={{ modal: "edit" }}> Edit Issue </Link> ) }
모달에는 보통 닫기 버튼이 있다. 모서리에 있는 X 아이콘일 수도 있고, "취소" 버튼일 수도 있다.
겉으로 보기에는 단순히 링크를 사용해 모달이 닫힌 페이지로 네비게이션하는 것이 가장 직관적으로 보일 수 있다. 화면 상으로는 모달이 닫힌 상태로 이동하는 것과 모달을 닫는 것이 같기 때문이다. 하지만 이는 흔한 실수다.
뒤로 가기 버튼은 항상 이전의 논리적 상태로 사용자를 돌려보내야 한다. 뒤로 가기를 눌러 모달이 닫히는 것은 자연스럽고, 그다음에 한 번 더 뒤로 가기를 누르면 이전 페이지로 가야 한다.
물리적인 "취소" 버튼을 누르는 것 역시 사용자를 이전의 논리적 상태로 돌려보내는 동작이다. 따라서 히스토리 스택에도 이 사실을 반영해야 한다.
이를 구현하는 가장 좋은 방법은 replaceState 메서드를 사용해, 현재 히스토리 엔트리를 모달이 닫힌 상태를 나타내는 새 엔트리로 교체하는 것이다.
Remix와 React Router에서는 Link 컴포넌트의 replace prop을 사용해, 새로운 히스토리를 추가하는 대신 현재 히스토리 엔트리를 교체할 수 있다.
tsxfunction Modal() { return ( <div> <h1>Edit Issue</h1> <Link to="/issues" replace> Cancel </Link> </div> ) }