콘텐츠로 이동

URL은 곧 상태다 — URL을 상태 컨테이너로 활용하기

URL은 단순히 페이지 주소가 아니라 상태 컨테이너다. URL에 상태를 담으면 별도의 DB, 쿠키, localStorage 없이도 아래 기능이 자동으로 생긴다.

  • 공유 가능성: 링크를 보내면 상대방이 동일한 화면/상태를 본다.
  • 북마크: 특정 시점의 UI 상태를 URL로 저장할 수 있다.
  • 브라우저 뒤로/앞으로 가기: 히스토리가 자연스럽게 작동한다.
  • 딥 링크: 앱의 특정 상태로 바로 진입 가능하다.

구성 요소적합한 상태 예시
Path Segment (/users/123/posts)계층적 리소스 네비게이션
Query Parameter (?page=2&sort=date)필터, 정렬, 페이지네이션, UI 설정
Fragment (#features)페이지 내 섹션 이동, 클라이언트 라우팅

URL에 담기 좋은 상태 vs 나쁜 상태

섹션 제목: “URL에 담기 좋은 상태 vs 나쁜 상태”
  • 검색어, 필터, 정렬
  • 페이지네이션
  • 뷰 모드 (리스트/그리드, 다크/라이트 모드)
  • 날짜 범위, 선택된 탭/항목
  • A/B 테스트 변형, 기능 플래그
  • 비밀번호, 토큰, 개인정보 (URL은 로그·히스토리에 남음)
  • 모달 열림/닫힘 같은 일시적 UI 상태
  • 저장 안 된 폼 입력 중간값
  • 마우스 위치, 스크롤 위치 같은 고빈도 상태
  • 지나치게 크거나 복잡한 중첩 데이터

판단 기준: “이 URL을 다른 사람이 클릭했을 때 같은 화면을 봐야 하는가?” → Yes면 URL에 담아라.


# 다중 값 (구분자)
?tags=frontend,react,hooks
?languages=javascript+typescript
# 불리언 플래그
?debug=true
?mobile (존재 자체가 true)
# 배열 (괄호 표기법, PHP 유래)
?tags[]=frontend&tags[]=react
# Base64 인코딩 (복잡한 객체)
?config=eyJyaWNrIjoicm9sbCJ9==

// 읽기
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',
});
});

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을 깔끔하게 유지한다.

// 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: 필터 변경, 페이지 이동 → 뒤로 가기로 돌아올 수 있어야 할 때
  • 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 같은 외부 라이브러리 없이도 공유·북마크·딥링크·히스토리 네비게이션이 모두 해결된다. 앱이 새로고침 시 상태를 잃는다면, 웹의 가장 오래된 기능을 활용하지 못하고 있는 것이다.