브라우저 주소창은 단순히 페이지 주소를 보여주는 공간처럼 보인다. 우리는 보통 그곳에 주소를 입력하고, 페이지가 이동하면 다시 크게 신경 쓰지 않는다.
하지만 URL은 생각보다 많은 정보를 담고 있다. 어떤 페이지를 보고 있는지, 어떤 조건으로 목록을 정렬했는지, 문서의 어느 위치를 보고 있는지까지 URL 안에 남길 수 있다. 다시 말해 URL은 단순한 주소가 아니라, 사용자가 웹에서 보고 있는 상태를 표현하는 또 다른 저장소가 될 수 있다.
URL은 웹이나 네트워크상의 특정 리소스가 어디에 있는지 나타내는 주소다. 문서, 이미지, API처럼 인터넷에 존재하는 리소스의 위치를 문자열로 표현한 것이 URL, 즉 Uniform Resource Locator다. 우리가 브라우저 주소창에 입력하는 값도 결국 어떤 리소스를 찾아갈 것인가를 나타내는 URL이다.
그런데 프론트엔드에서 URL을 다룰 때 중요한 지점은 여기서 한 걸음 더 나아간다. URL은 리소스의 위치만 가리키는 것이 아니라, 사용자가 현재 어떤 화면 상태에 도달했는지도 함께 설명할 수 있다.

웹의 초창기부터 사용되어 온 URL은 웹의 기본 골격에 가깝다. 브라우저는 URL을 기준으로 리소스를 요청하고, 사용자의 탐색 기록을 남기며, 현재 페이지를 공유하거나 다시 방문할 수 있게 만든다.
이 흐름은 웹 플랫폼 곳곳에 녹아 있다. 쿼리 스트링을 읽고 조작하기 위한 URLSearchParams가 표준으로 제공되고, 브라우저 히스토리는 사용자의 뒤로 가기와 앞으로 가기 흐름에 직접 연결된다. 사용자가 어떤 페이지에 도달했는지, 어떤 조건으로 보고 있는지, 이전에는 어디에 있었는지가 모두 URL과 함께 움직인다.
이런 점에서 URL에 담긴 상태는 React의 local state나 Zustand 같은 애플리케이션 내부 상태보다 더 바깥 계층에 있다. 특정 컴포넌트 안에서만 유지되는 값이 아니라, 브라우저와 서버, 사용자 간 공유 흐름에 걸쳐 유지되는 상태에 가깝다. 그래서 URL에 담긴 상태는 새로고침, 공유, 북마크, 서버 렌더링과 자연스럽게 이어진다.

그래서 정렬 기준인 ?sort=top, 검색어인 ?q=react, 특정 초안을 가리키는 ?draft=... 같은 상태는 URL에 두기 좋은 후보가 된다. 이 값들은 단순히 컴포넌트 내부의 UI 상태라기보다, 사용자가 현재 어떤 조건으로 화면을 보고 있는지를 설명한다.
같은 URL을 열었을 때 같은 조건의 화면이 복원되어야 하고, 다른 사람에게 공유했을 때도 가능한 한 같은 맥락의 화면이 보여야 한다면, 그 상태는 URL에 담을 만하다. URL은 단순히 어디로 이동할 것인가만 나타내는 것이 아니라, 어떤 조건으로 그 리소스를 바라볼 것인가까지 함께 표현할 수 있기 때문이다.
URL 상태
URL을 상태로 본다는 건 어떤 의미일까? Sprinters의 URL을 예시로 들어보자.
https://www.sprinters.run/?sort=latest#record-sort-tabs
여기서 중요한 건 사용자가 지금 어떤 화면에 있고, 그 화면을 어떤 조건과 위치에서 보고 있는가이다. /는 사용자가 Sprinters의 메인 화면에 있다는 것을 나타낸다. ?sort=latest는 글 목록을 최신순으로 보고 있다는 조건을 나타내며, #record-sort-tabs는 문서의 특정 섹션이나 위치를 가리킨다.
다른 예시로 /new-record라는 경로가 있다면, 이 URL만 보고도 사용자가 새 기록을 작성하는 화면에 도달했다는 것을 알 수 있다. 이는 URL이 단순한 주소를 넘어 현재 화면의 목적과 맥락까지 함께 표현할 수 있음을 보여준다.
이때의 상태는 isOpen, inputValue 같은 일시적인 UI 상태가 아니다. 경로 세그먼트는 사용자가 어떤 페이지나 리소스에 도달했는지를 나타내는 라우팅 상태 또는 리소스 식별 상태로 볼 수 있다. 쿼리 스트링은 그 리소스를 어떤 조건으로 바라볼 것인지를 나타내는 조건 상태에 해당한다. 해시(앵커)는 서버로 전달되는 값이라기보다 문서 안의 특정 위치나 섹션을 가리키는 클라이언트 측 위치 상태에 가깝다.

상태의 세 가지 세계
프론트엔드에서 다루는 웹의 상태는 크게 세 종류로 나눠볼 수 있다.
탐색 및 조회 조건 상태: 사용자가 어느 페이지에 있는지, 어떤 탭을 보고 있는지, 어떤 정렬 기준을 선택했는지, 어떤 검색어로 목록을 보고 있는지 같은 상태가 여기에 해당한다. 이 상태는 URL과 잘 맞는다. 브라우저의 뒤로 가기와 앞으로 가기, 새로고침, 공유, 북마크와 직접 연결되기 때문이다.
서버 상태: 서버에서 받아온 글 목록, 사용자 정보, 댓글 목록, 좋아요 수 같은 값이 여기에 해당한다. 이 값들은 URL 자체에 저장하는 상태가 아니다. URL은 '어떤 데이터를 가져와야 하는지'를 설명하고, 실제 데이터는 서버에서 가져오거나 클라이언트의 캐시 계층에서 관리된다.
상호작용 상태: 앞서 언급한
isOpen,inputValue처럼 사용자의 순간적인 조작에 따라 바뀌는 상태가 여기에 가깝다. 이런 상태는 대체로 URL에 담지 않는다. 공유하거나 새로고침했을 때 반드시 복원될 필요가 없고, 일시적인 상태에 가까운 경우가 많기 때문이다.
URL을 상태 컨테이너로 쓸 때 주의할 점
URL을 상태 컨테이너로 쓸 때 가장 중요한 건 검증과 정규화다. URL은 브라우저 주소창에 그대로 노출되고, 사용자가 직접 수정할 수도 있다. 그래서 URL에서 읽은 값은 애플리케이션 내부에서 만든 안전한 값이 아니라, 외부에서 들어온 입력처럼 다뤄야 한다.
예를 들어, 정렬을 ?sort=latest처럼 URL에 담는다고 해보자. 정상적인 값이라면 trending, top, latest 중 하나가 들어오겠지만, 실제로는 ?sort=abc처럼 모르는 값이 들어올 수도 있다. 이때 URL 값을 그대로 상태로 사용하면, 화면이 깨지거나 의도하지 않은 요청이 발생할 수 있다.
따라서 URL에서 값을 읽을 때는 먼저 허용된 값인지 확인하고, 허용되지 않은 값이라면 안전한 기본값으로 되돌려야 한다. 코드로 표현하면 다음과 같다.
const SORT_OPTIONS = ['trending', 'top', 'latest'] as const;
type SortOption = (typeof SORT_OPTIONS)[number]; // 'trending' | 'top' | 'latest'
const DEFAULT_SORT: SortOption = 'trending';
const isSortOption = (value: string): value is SortOption => {
for (const option of SORT_OPTIONS) {
if (option === value) {
return true;
}
}
return false;
};
export const parseSort = (value: string | null): SortOption => {
if (!value) {
return DEFAULT_SORT;
}
return isSortOption(value) ? value : DEFAULT_SORT;
};이렇게 하면 URL에는 어떤 문자열이 들어오더라도, 애플리케이션 내부에서는 항상 SortOption 타입의 안전한 값만 사용하게 된다. URL은 상태를 담을 수 있지만, 그 상태를 그대로 신뢰해서는 안 된다. URL에서 읽은 값은 검증을 거쳐 애플리케이션이 이해할 수 있는 형태로 바꾼 뒤 사용해야 한다.
복원 관점에서, 쿠키를 사용해도 되지 않나?
복원 가능한 모든 상태를 URL에 담아야 하는 것은 아니다. 화면을 다시 복원한다는 관점에서 보면, URL과 쿠키 중 무엇을 쓸지는 링크만으로 복원되어야 하는 화면 상태인가, 아니면 사용자별로 유지되어야 하는 기본 환경인가로 나눠 생각할 수 있다.
URL은 화면의 의미를 복원하는 데 적합하다. 사용자가 어떤 페이지에 있었는지, 어떤 탭을 보고 있었는지, 어떤 정렬 기준과 검색 조건으로 목록을 보고 있었는지를 표현하는 데 잘 맞는다. 이런 상태는 새로고침하거나 북마크에서 다시 열었을 때 같은 조건으로 복원되어야 하고, 주소를 복사해서 다른 사람에게 공유했을 때도 가능한 한 같은 맥락의 화면이 보여야 한다.
반면 쿠키는 사용자별 환경이나 선호를 복원하는 데 더 잘 맞는다. 예를 들어 사용자가 마지막으로 열어둔 사이드바 상태, 접어둔 패널, 다크 모드 설정처럼 개인의 작업 환경에 가까운 값들이 여기에 해당한다. 이런 값들은 다른 사람에게 공유할 화면의 의미라기보다, 같은 사용자가 다시 방문했을 때 이어서 사용하고 싶은 환경에 가깝다.
특히 서버 렌더링을 고려하면 쿠키의 장점이 더 분명해진다. 브라우저는 요청을 보낼 때 쿠키를 함께 전송하므로, 서버는 초기 HTML을 만들기 전에 사용자의 기본 환경을 알 수 있다. 예를 들어 사이드바가 열린 상태인지 닫힌 상태인지 쿠키로 저장해두면, 서버는 처음부터 그 상태에 맞는 화면을 렌더링할 수 있다. 이렇게 하면 클라이언트에서 뒤늦게 상태를 읽고 화면을 바꾸면서 생기는 깜빡임을 줄일 수 있다.
결국 URL과 쿠키의 기준은 상태의 성격에 있다. 주소가 바뀌는 것이 자연스럽고, 그 주소를 공유했을 때 같은 화면 맥락이 전달되어야 한다면 URL이 적합하다. 반대로 주소의 의미와는 관련이 없고, 사용자 개인의 환경으로 유지되어야 한다면 쿠키가 더 적합하다. ?sort=trending은 URL에 어울리지만, ?theme=dark로 테마를 표현하는 것은 대부분의 경우 어색한 이유도 여기에 있다.
제대로 이해하고, 사용하기
그동안 부트캠프의 여러 프로젝트를 리뷰하면서 URL을 상태로 적극적으로 활용하는 경우를 거의 못 본 것 같다. 대부분의 상태는 컴포넌트 내부 state나 전역 상태 관리 도구로 먼저 옮겨졌고, URL은 페이지 주소 정도로만 다뤄지는 경우가 많았다.
AI를 활용할 때도 크게 다르지 않았다. AI가 쿼리 스트링이나 경로를 이용해 기능을 구현해주더라도, 그 코드가 왜 그렇게 작성되었는지 이해하지 못하면 결국 내 선택이 되지 않는다. 구현은 되어 있지만, 설명할 수 없다면 제대로 사용하고 있다고 말하기 어렵다.
그래서 나도 다시 물어보게 되었다. 나는 URL을 상태의 관점에서 설명할 수 있을까? 단순히 쿼리 스트링을 읽고 값을 바꾸는 방법이 아니라, 어떤 상태를 URL에 두고, 어떤 상태는 두지 않아야 하는지 판단할 수 있을까?
URL은 또 다른 상태 저장소가 될 수 있다. 다만 모든 상태를 담는 저장소가 아니라, 공유되고 복원되어야 하는 화면의 의미를 담는 저장소에 가깝다. 이 기준을 이해하고 나면, URL은 더 이상 주소창에 남는 문자열이 아니라 웹이 원래부터 제공하던 상태 관리의 한 축으로 보이기 시작한다.

댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!