URL은 곧 상태다 — URL을 상태 컨테이너로 활용하기
핵심 개념
섹션 제목: “핵심 개념”URL은 단순히 페이지 주소가 아니라 상태 컨테이너다. URL에 상태를 담으면 별도의 DB, 쿠키, localStorage 없이도 아래 기능이 자동으로 생긴다.
- 공유 가능성: 링크를 보내면 상대방이 동일한 화면/상태를 본다.
- 북마크: 특정 시점의 UI 상태를 URL로 저장할 수 있다.
- 브라우저 뒤로/앞으로 가기: 히스토리가 자연스럽게 작동한다.
- 딥 링크: 앱의 특정 상태로 바로 진입 가능하다.
URL 구성 요소별 용도
섹션 제목: “URL 구성 요소별 용도”| 구성 요소 | 적합한 상태 예시 |
|---|---|
Path Segment (/users/123/posts) | 계층적 리소스 네비게이션 |
Query Parameter (?page=2&sort=date) | 필터, 정렬, 페이지네이션, UI 설정 |
Fragment (#features) | 페이지 내 섹션 이동, 클라이언트 라우팅 |
URL에 담기 좋은 상태 vs 나쁜 상태
섹션 제목: “URL에 담기 좋은 상태 vs 나쁜 상태”URL에 담아야 하는 것
섹션 제목: “URL에 담아야 하는 것”- 검색어, 필터, 정렬
- 페이지네이션
- 뷰 모드 (리스트/그리드, 다크/라이트 모드)
- 날짜 범위, 선택된 탭/항목
- A/B 테스트 변형, 기능 플래그
URL에 담으면 안 되는 것
섹션 제목: “URL에 담으면 안 되는 것”- 비밀번호, 토큰, 개인정보 (URL은 로그·히스토리에 남음)
- 모달 열림/닫힘 같은 일시적 UI 상태
- 저장 안 된 폼 입력 중간값
- 마우스 위치, 스크롤 위치 같은 고빈도 상태
- 지나치게 크거나 복잡한 중첩 데이터
판단 기준: “이 URL을 다른 사람이 클릭했을 때 같은 화면을 봐야 하는가?” → Yes면 URL에 담아라.
쿼리 파라미터 패턴
섹션 제목: “쿼리 파라미터 패턴”# 다중 값 (구분자)?tags=frontend,react,hooks?languages=javascript+typescript
# 불리언 플래그?debug=true?mobile (존재 자체가 true)
# 배열 (괄호 표기법, PHP 유래)?tags[]=frontend&tags[]=react
# Base64 인코딩 (복잡한 객체)?config=eyJyaWNrIjoicm9sbCJ9==구현 (Vanilla JS)
섹션 제목: “구현 (Vanilla JS)”// 읽기const params = new URLSearchParams(window.location.search);const view = params.get('view') || 'grid';
// 업데이트 (페이지 새로고침 없이)function updateFilters(filters) { const params = new URLSearchParams(window.location.search); params.set('status', filters.status); params.set('sort', filters.sort); window.history.pushState({}, '', `${window.location.pathname}?${params}`); renderContent(filters);}
// 뒤로/앞으로 가기 대응window.addEventListener('popstate', () => { const params = new URLSearchParams(window.location.search); renderContent({ status: params.get('status') || 'all', sort: params.get('sort') || 'date', });});구현 (React)
섹션 제목: “구현 (React)”import { useSearchParams } from 'react-router-dom';// Next.js: import { useSearchParams } from 'next/navigation';
function ProductList() { const [searchParams, setSearchParams] = useSearchParams(); const color = searchParams.get('color') || 'all';
const handleColorChange = (newColor) => { setSearchParams(prev => { const params = new URLSearchParams(prev); params.set('color', newColor); return params; }); };}모범 사례
섹션 제목: “모범 사례”기본값은 URL에 넣지 않는다
섹션 제목: “기본값은 URL에 넣지 않는다”기본값이 아닐 때만 URL에 포함해 URL을 깔끔하게 유지한다.
// Bad: ?theme=light&lang=en&page=1// Good: ?theme=dark (light은 기본값이므로 생략)function getTheme(params) { return params.get('theme') || 'light';}검색어 등 빈번한 업데이트는 디바운싱
섹션 제목: “검색어 등 빈번한 업데이트는 디바운싱”const updateSearch = debounce((value) => { const params = new URLSearchParams(window.location.search); value ? params.set('q', value) : params.delete('q'); window.history.replaceState({}, '', `?${params}`); // replaceState로 히스토리 오염 방지}, 300);pushState vs replaceState 구분
섹션 제목: “pushState vs replaceState 구분”pushState: 필터 변경, 페이지 이동 → 뒤로 가기로 돌아올 수 있어야 할 때replaceState: 타이핑 중 실시간 업데이트 → 히스토리를 오염시키면 안 될 때
피해야 할 안티패턴
섹션 제목: “피해야 할 안티패턴”- 메모리 상태만 사용:
useState에만 필터를 저장하면 새로고침 시 상태가 사라짐 → 사용자 경험 최악 - 민감 정보를 URL에 포함: URL은 서버 로그, 브라우저 히스토리, Referrer 헤더에 모두 기록됨
- 파라미터명이 불명확:
?foo=true&x=dark→?mobile=true&theme=dark - 과도하게 복잡한 상태: Base64 JSON이 너무 길어진다면 URL에 두기 부적합하다는 신호
replaceState남용: 뒤로 가기가 동작해야 할 곳에서replaceState를 쓰면 히스토리가 망가짐
실제 사례
섹션 제목: “실제 사례”- PrismJS: 선택한 테마·언어·플러그인 전체를 URL 하나로 공유 가능
- GitHub:
#L108-L136으로 특정 코드 라인을 링크 - Google Maps: 좌표·줌 레벨·지도 타입이 URL에 포함
- Figma: 캔버스 위치·줌·선택 요소가 URL에 담겨 협업 링크 가능
- 이커머스:
?brand=dell+hp&price=500-1500&sort=price-asc형태로 필터 북마크
URL을 상태 관리 도구로 제대로 활용하면 Redux 같은 외부 라이브러리 없이도 공유·북마크·딥링크·히스토리 네비게이션이 모두 해결된다. 앱이 새로고침 시 상태를 잃는다면, 웹의 가장 오래된 기능을 활용하지 못하고 있는 것이다.