URL은 상태를 나타냅니다

I
Inkyu Oh

Front-End2025.11.24

Ahmad Alfy's Blog 블로그 포스트 번역
2025-10-31T02:00:00+02:00


몇 주 전에 URL 디자인의 숨겨진 비용을 발행할 때 SQL 구문 강조 표시를 추가해야 했습니다. PrismJS 웹사이트로 향했는데 플러그인으로 추가해야 하는지 기억하려고 했습니다. 다운로드 페이지의 옵션이 너무 많아서 코드로 돌아갔습니다. PrismJS 파일을 확인했을 때 파일 상단에 URL이 포함된 주석을 찾았습니다:
/* https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+css-extras+markdown+scss+sql&plugins=line-highlight+line-numbers+autolinker */
완전히 잊고 있었습니다. URL을 클릭했을 때 모든 체크박스, 드롭다운, 옵션이 제 정확한 구성과 일치하도록 미리 선택된 PrismJS 다운로드 페이지였습니다. 테마가 선택되었습니다. 언어가 선택되었습니다. 플러그인이 활성화되었습니다. 모든 것이 그 단 하나의 URL에서 완벽하게 재구성되었습니다.
한때 알고 있던 것이 갑자기 새로운 의미로 다시 클릭되는 순간이었습니다. 여기 URL은 단순히 페이지를 가리키는 것 이상을 하고 있었습니다. 상태를 저장하고, 의도를 인코딩하고, 제 전체 설정을 공유 가능하고 복구 가능하게 만들고 있었습니다. 데이터베이스 없음. 쿠키 없음. localStorage 없음. 단지 URL일 뿐입니다.
이것이 저를 생각하게 했습니다: 우리 프론트엔드 엔지니어들이 URL을 상태 관리 도구로 간과하는 경우가 얼마나 많을까요? 우리는 전역 저장소, 컨텍스트, 캐시 같은 모든 종류의 추상화에 손을 뻗으면서 웹의 가장 우아하고 오래된 기능 중 하나인 겸손한 URL을 무시합니다.
이전 기사에서 저는 나쁜 URL 디자인의 숨겨진 비용에 대해 썼습니다. 오늘은 그 관점을 뒤집고 좋은 URL 디자인의 엄청난 가치에 대해 이야기하고 싶습니다. 특히 URL을 현대 웹 애플리케이션의 일급 상태 컨테이너로 취급하는 방법에 대해 말입니다.

URL의 간과된 힘

Scott Hanselman은 유명하게 "URL은 UI입니다"라고 말했고 그는 완전히 맞습니다. URL은 단순히 브라우저가 리소스를 가져오는 데 사용하는 기술적 주소가 아닙니다. 그들은 인터페이스입니다. 그들은 사용자 경험의 일부입니다.
하지만 URL은 UI 이상입니다. 그들은 상태 컨테이너입니다. URL을 만들 때마다 어떤 정보를 보존할지, 무엇을 공유 가능하게 만들지, 무엇을 북마크 가능하게 만들지에 대한 결정을 내리고 있습니다.
URL이 무료로 제공하는 것들을 생각해 봅시다:
  • 공유 가능성: 누군가에게 링크를 보내면 그들은 정확히 당신이 보는 것을 봅니다
  • 북마크 가능성: URL을 저장하면 시간의 한 순간을 저장한 것입니다
  • 브라우저 히스토리: 뒤로 가기 버튼이 그냥 작동합니다
  • 딥 링킹: 특정 애플리케이션 상태로 직접 이동합니다
URL은 웹 애플리케이션을 탄력적이고 예측 가능하게 만듭니다. 그들은 웹의 원래 상태 관리 솔루션이며 1991년 이후로 안정적으로 작동해 왔습니다. 문제는 URL이 상태를 저장할 수 있는지가 아닙니다. 우리가 그들의 전체 잠재력을 사용하고 있는지입니다.
예제를 살펴보기 전에 URL이 상태를 어떻게 인코딩하는지 분석해 봅시다. 여기 전형적인 상태 저장 URL이 있습니다:
URL의 구조

URL의 구조 - 출처: URL이란 무엇인가 - MDN Web Docs
많은 해 동안 이것들이 URL의 유일한 구성 요소로 간주되었습니다. 이것은 텍스트 조각의 도입으로 변경되었습니다. 이것은 페이지 내의 특정 텍스트 부분으로 직접 링크할 수 있는 기능입니다. 제 기사 더 똑똑한 'Ctrl+F': 웹 페이지 콘텐츠로 직접 링크하기에서 더 자세히 읽을 수 있습니다.
URL의 다른 부분은 다른 유형의 상태를 인코딩합니다:
  1. 경로 세그먼트 (/path/to/myfile.html). 계층적 리소스 네비게이션에 가장 잘 사용됩니다:
  • /users/123/posts - 사용자 123의 게시물
  • /docs/api/authentication - 문서 구조
  • /dashboard/analytics - 애플리케이션 섹션
  1. 쿼리 매개변수 (?key1=value1&key2=value2). 필터, 옵션, 구성에 완벽합니다:
  • ?theme=dark&lang=en - UI 기본 설정
  • ?page=2&limit=20 - 페이지 매김
  • ?status=active&sort=date - 데이터 필터링
  • ?from=2025-01-01&to=2025-12-31 - 날짜 범위
  1. 앵커 프래그먼트 (#SomewhereInTheDocument). 클라이언트 측 네비게이션 및 페이지 섹션에 이상적입니다:
  • #L20-L35 - GitHub 라인 강조 표시
  • #features - 섹션으로 스크롤
  • #/dashboard - 단일 페이지 앱 라우팅 (요즘은 거의 사용되지 않음)

쿼리 매개변수에 적합한 일반적인 패턴

구분 기호가 있는 여러 값

때때로 쉼표나 더하기 기호 같은 구분 기호를 사용하여 여러 값이 단일 키에 압축되어 있는 것을 볼 수 있습니다. 컴팩트하고 인간이 읽을 수 있지만 서버 측에서 수동 파싱이 필요합니다.
?languages=javascript+typescript+python
?tags=frontend,react,hooks

중첩되거나 구조화된 데이터

개발자들은 종종 복잡한 필터나 구성 객체를 단일 쿼리 문자열로 인코딩합니다. 간단한 규칙은 쉼표로 구분된 키-값 쌍을 사용하고, 다른 것들은 JSON을 직렬화하거나 안전을 위해 Base64로 인코딩합니다.
?filters=status:active,owner:me,priority:high
?config=eyJyaWNrIjoicm9sbCJ9== (base64로 인코딩된 JSON)

부울 플래그

플래그나 토글의 경우 부울을 명시적으로 전달하거나 키의 존재를 참으로 사용하는 것이 일반적입니다. 이것은 URL을 더 짧게 유지하고 기능 토글을 쉽게 만듭니다.
?debug=true&analytics=false
?mobile (존재 = true)

배열 (괄호 표기법)

?tags[]=frontend&tags[]=react&tags[]=hooks
또 다른 오래된 패턴은 괄호 표기법으로, 쿼리 매개변수에서 배열을 나타냅니다. 이것은 PHP 같은 초기 웹 프레임워크에서 시작되었으며 매개변수 이름에 []를 추가하면 여러 값이 함께 그룹화되어야 함을 나타냅니다.
?tags[]=frontend&tags[]=react&tags[]=hooks
?ids[0]=42&ids[1]=73
많은 현대 프레임워크와 파서 (Node의 qs 라이브러리나 Express 미들웨어 같은)는 여전히 이 패턴을 자동으로 인식합니다. 그러나 URL 사양에서 공식적으로 표준화되지 않았으므로 서버 또는 클라이언트 구현에 따라 동작이 다를 수 있습니다. 제 웹사이트의 구문 강조 표시를 어떻게 깨뜨리는지 주목하세요.
핵심은 일관성입니다. 애플리케이션에 맞는 패턴을 선택하고 그것을 고수하세요.

URL 매개변수를 통한 상태

URL을 상태 컨테이너로 사용하는 실제 예제를 살펴봅시다:
PrismJS 구성
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers
전체 구문 강조 표시기 구성이 URL에 인코딩되어 있습니다. UI에서 무엇이든 변경하면 URL이 업데이트됩니다. URL을 공유하면 다른 사람이 정확한 설정을 얻습니다. 이것은 앵커를 사용하고 쿼리 매개변수가 아니지만 개념은 동일합니다.
GitHub 라인 강조 표시
https://github.com/zepouet/Xee-xCode-4.5/blob/master/XeePhotoshopLoader.m#L108-L136
특정 파일로 링크하면서 108~136줄을 강조 표시합니다. 어디서나 이 링크를 클릭하면 논의 중인 정확한 코드 섹션에 도착합니다.
Google 지도
https://www.google.com/maps/@22.443842,-74.220744,19z
좌표, 줌 레벨, 지도 유형이 모두 URL에 있습니다. 이 링크를 공유하면 누구나 지도의 정확히 같은 보기를 볼 수 있습니다.
Figma 및 디자인 도구
https://www.figma.com/file/abc123/MyDesign?node-id=123:456&viewport=100,200,0.5
공유 가능한 디자인 링크 이전에 큰 파일에서 업데이트된 화면이나 구성 요소를 찾는 것은 번거로웠습니다. 누군가가 말 그대로 당신에게 보여줘야 했고, 레이어 전체를 스크롤하고 확대/축소했습니다. 오늘날 Figma 링크는 캔버스 위치, 줌 레벨, 선택된 요소 같은 모든 컨텍스트를 전달합니다. 말 그대로 작업 공간에 바로 떨어뜨리는 데 필요한 모든 것입니다.
전자상거래 필터
https://store.com/laptops?brand=dell+hp&price=500-1500&rating=4&sort=price-asc
이것은 당신이 만날 가장 일반적인 실제 패턴 중 하나입니다. 모든 필터, 정렬 옵션, 가격 범위가 보존됩니다. 사용자는 정확한 검색 기준을 북마크하고 언제든지 돌아올 수 있습니다. 가장 중요한 것은 페이지를 벗어나거나 새로 고친 후에도 돌아올 수 있다는 것입니다.

프론트엔드 엔지니어링 패턴

구현 세부 사항을 논의하기 전에 URL에 무엇이 들어가야 하는지에 대한 명확한 지침을 설정해야 합니다. 모든 상태가 URL에 속하는 것은 아닙니다. 다음은 간단한 휴리스틱입니다:
URL 상태에 적합한 후보:
  • 검색 쿼리 및 필터
  • 페이지 매김 및 정렬
  • 보기 모드 (목록/그리드, 어두움/밝음)
  • 날짜 범위 및 기간
  • 선택된 항목 또는 활성 탭
  • 콘텐츠에 영향을 미치는 UI 구성
  • 기능 플래그 및 A/B 테스트 변형
URL 상태에 부적합한 후보:
  • 민감한 정보 (비밀번호, 토큰, 개인 식별 정보)
  • 임시 UI 상태 (모달 열림/닫힘, 드롭다운 확장됨)
  • 진행 중인 양식 입력 (저장되지 않은 변경 사항)
  • 매우 크거나 복잡한 중첩 데이터
  • 높은 빈도의 일시적 상태 (마우스 위치, 스크롤 위치)
URL이 상태에 속하는지 확실하지 않으면 자신에게 물어보세요: 다른 사람이 이 URL을 클릭하면 같은 상태를 봐야 할까요? 그렇다면 URL에 속합니다. 그렇지 않으면 다른 상태 관리 접근 방식을 사용하세요.

순수 JavaScript를 사용한 구현

현대 URLSearchParams API는 URL 상태 관리를 간단하게 만듭니다:
// URL 매개변수 읽기
const params = new URLSearchParams(window.location.search);
const view = params.get('view') || 'grid';
const page = params.get('page') || 1;

// URL 매개변수 업데이트
function updateFilters(filters) {
const params = new URLSearchParams(window.location.search);

// 개별 매개변수 업데이트
params.set('status', filters.status);
params.set('sort', filters.sort);

// 페이지 새로 고침 없이 URL 업데이트
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({}, '', newUrl);

// 이제 새 필터를 기반으로 UI 업데이트
renderContent(filters);
}

// 뒤로/앞으로 버튼 처리
window.addEventListener('popstate', () => {
const params = new URLSearchParams(window.location.search);
const filters = {
status: params.get('status') || 'all',
sort: params.get('sort') || 'date'
};
renderContent(filters);
});
popstate 이벤트는 사용자가 브라우저의 뒤로 또는 앞으로 버튼으로 네비게이션할 때 발생합니다. 이것은 URL과 일치하도록 UI를 복원할 수 있게 해주며, 이는 앱의 상태와 히스토리를 동기화 상태로 유지하는 데 필수적입니다. 보통 프레임워크의 라우터가 이것을 처리하지만 내부적으로 어떻게 작동하는지 아는 것이 좋습니다.

React를 사용한 구현

React Router와 Next.js는 이것을 훨씬 더 깔끔하게 만드는 훅을 제공합니다:
import { useSearchParams } from 'react-router-dom';
// 또는 Next.js 13+의 경우: import { useSearchParams } from 'next/navigation';

function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();

// URL에서 읽기 (기본값 포함)
const color = searchParams.get('color') || 'all';
const sort = searchParams.get('sort') || 'price';

// URL 업데이트
const handleColorChange = (newColor) => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
params.set('color', newColor);
return params;
});
};

return (
<div>
<select value={color} onChange={e => handleColorChange(e.target.value)}>
<option value="all">All Colors</option>
<option value="silver">Silver</option>
<option value="black">Black</option>
</select>

{/* 필터링된 제품이 여기에 렌더링됩니다 */}
</div>
);
}

URL 상태 관리를 위한 모범 사례

이제 URL이 애플리케이션 상태를 어떻게 보유할 수 있는지 살펴봤으므로 URL을 깔끔하고 예측 가능하며 사용자 친화적으로 유지하는 몇 가지 모범 사례를 살펴봅시다.

기본값을 우아하게 처리하기

기본값으로 URL을 오염시키지 마세요:
// 나쁨: URL이 기본값으로 복잡해짐
?theme=light&lang=en&page=1&sort=date

// 좋음: URL에는 기본값이 아닌 값만 있음
?theme=dark // light가 기본값이므로 생략
매개변수를 읽을 때 코드에서 기본값을 사용하세요:
function getTheme(params) {
return params.get('theme') || 'light'; // 코드에서 처리된 기본값
}

URL 업데이트 디바운싱

높은 빈도의 업데이트 (검색 입력 중처럼)의 경우 URL 변경을 디바운스하세요:
import { debounce } from 'lodash';

const updateSearchParam = debounce((value) => {
const params = new URLSearchParams(window.location.search);
if (value) {
params.set('q', value);
} else {
params.delete('q');
}
window.history.replaceState({}, '', `?${params.toString()}`);
}, 300);

// replaceState를 사용하여 히스토리를 넘치지 않도록 함

pushState vs. replaceState

pushStatereplaceState 중에서 선택할 때 브라우저 히스토리가 어떻게 동작하기를 원하는지 생각하세요. pushState는 새 히스토리 항목을 만들며, 이는 필터 변경, 페이지 매김, 새 보기로의 네비게이션 같은 뚜렷한 네비게이션 작업에 적합합니다 — 사용자는 뒤로 버튼을 사용하여 이전 상태로 돌아갈 수 있습니다. 반면 replaceState는 새 항목을 추가하지 않고 현재 항목을 업데이트하며, 검색 입력 중이나 사소한 UI 조정 같은 개선 사항에 이상적입니다. 여기서 모든 키 입력으로 히스토리를 넘치고 싶지 않습니다.

URL을 계약으로

신중하게 설계되면 URL은 단순한 상태 컨테이너 이상이 됩니다. 그들은 애플리케이션과 소비자 간의 계약이 됩니다. 좋은 URL은 인간, 개발자, 기계 모두에게 기대치를 정의합니다.

명확한 경계

잘 구조화된 URL은 공개와 비공개, 클라이언트와 서버, 공유 가능과 세션별 사이의 선을 그립니다. 상태가 어디에 있는지, 어떻게 동작해야 하는지를 명확히 합니다. 개발자는 무엇을 유지하기에 안전한지 알고, 사용자는 무엇을 북마크할 수 있는지 알고, 기계는 무엇이 인덱싱할 가치가 있는지 압니다.
그 의미에서 URL은 인터페이스로 작동합니다: 보이고, 예측 가능하고, 안정적입니다.

의미 전달

읽을 수 있는 URL은 자신을 설명합니다. 아래 두 URL의 차이를 고려하세요.
https://example.com/p?id=x7f2k&v=3
https://example.com/products/laptop?color=silver&sort=price
첫 번째는 의도를 숨깁니다. 두 번째는 이야기를 말합니다. 인간은 그것을 읽고 무엇을 보고 있는지 이해할 수 있습니다. 기계는 그것을 파싱하고 의미 있는 구조를 추출할 수 있습니다.
Jim Nielsen은 이것들을 "훌륭한 URL의 예"라고 부릅니다. 자신을 설명하는 URL입니다.

캐싱 및 성능

URL은 캐시 키입니다. 잘 설계된 URL은 더 나은 캐싱 전략을 가능하게 합니다:
  • 같은 URL = 같은 리소스 = 캐시 히트
  • 쿼리 매개변수는 캐시 변형을 정의합니다
  • CDN은 URL 패턴을 기반으로 지능적으로 캐시할 수 있습니다
추가 추적 코드 없이도 사용자의 여정을 시각화할 수 있습니다:
/products → /products?category=laptops → /products?category=laptops&price=500-1000
(카테고리 선택) (가격 필터 추가)
분석 도구는 추가 계측 없이 이 흐름을 추적할 수 있습니다. 모든 URL 매개변수는 분석할 수 있는 차원이 됩니다.

버전 관리 및 진화

URL은 API 버전, 기능 플래그, 실험을 전달할 수 있습니다:
?v=2 // API 버전
?beta=true // 베타 기능
?experiment=new-ui // A/B 테스트 변형
이것은 점진적 롤아웃과 하위 호환성을 훨씬 더 관리하기 쉽게 만듭니다.

피해야 할 안티패턴

좋은 의도에도 불구하고 URL 상태를 오용하기는 쉽습니다. 다음은 일반적인 함정입니다:

"메모리 전용" SPA

고전적인 단일 페이지 앱 실수:
// 사용자가 새로 고침을 누르면 모든 것을 잃음
const [filters, setFilters] = useState({});
앱이 새로 고침 시 상태를 잊으면 웹의 기본 기능 중 하나를 깨뜨리고 있습니다. 사용자는 URL이 컨텍스트를 보존할 것으로 예상합니다. 몇 년 전 Reddit 사용자가 전자상거래 사이트에 대해 화낸 바이럴 비디오를 기억합니다: 뒤로 가기를 누를 때마다 모든 필터가 사라졌습니다. 그녀의 좌절감이 완벽하게 요약했습니다. 사용자가 컨텍스트를 잃으면 인내심을 잃습니다.

URL의 민감한 데이터

이것은 명백해 보이지만 반복할 가치가 있습니다:
// 절대 이렇게 하지 마세요
?password=secret123
URL은 모든 곳에 기록됩니다: 브라우저 히스토리, 서버 로그, 분석, 레퍼러 헤더. 그들을 공개로 취급하세요.

일관성 없거나 불명확한 이름 지정

// 불명확하고 일관성 없음
?foo=true&bar=2&x=dark

// 자체 문서화되고 일관성 있음
?mobile=true&page=2&theme=dark
의미 있는 매개변수 이름을 선택하세요. 미래의 당신 (그리고 팀)이 감사할 것입니다.

URL을 복잡한 상태로 오버로드

?config=eyJtZXNzYWdlIjoiZGlkIHlvdSByZWFsbHkgdHJpZWQgdG8gZGVjb2RlIHRoYXQ_IiwiZmlsdGVycyI6eyJzdGF0dXMiOlsiYWN0aXZlIiwicGVuZGluZyJdLCJwcmlvcml0eSI6WyJoaWdoIiwibWVkaXVtIl0sInRhZ3MiOlsiZnJvbnRlbmQiLCJyZWFjdCIsImhvb2tzIl0sInJhbmdlIjp7ImZyb20iOiIyMDI0LTAxLTAxIiwidG8iOiIyMDI0LTEyLTMxIn19LCJzb3J0Ijp7ImZpZWxkIjoiY3JlYXRlZEF0Iiwib3JkZXIiOiJkZXNjIn0sInBhZ2luYXRpb24iOnsicGFnZSI6MSwibGltaXQiOjIwfX0==
거대한 JSON 객체를 base64로 인코딩해야 한다면 URL이 그 상태를 위한 올바른 장소가 아닐 가능성이 높습니다.

URL 길이 제한

브라우저와 서버는 URL 길이에 실질적인 제한을 부과합니다 (보통 2,000~8,000자 사이) 하지만 현실은 더 미묘합니다. 이 상세한 Stack Overflow 답변에서 설명하듯이 제한은 브라우저 동작, 서버 구성, CDN, 심지어 검색 엔진 제약의 혼합에서 나옵니다. 제한에 부딪히면 접근 방식을 다시 생각해야 한다는 신호입니다.

뒤로 가기 버튼 깨뜨리기

// 잘못된 상태 교체
history.replaceState({}, '', newUrl); // pushState가 필요했을 때 사용됨
브라우저 히스토리를 존중하세요. 사용자 작업이 뒤로 가기 버튼을 통해 "실행 취소"할 수 있어야 한다면 pushState를 사용하세요. 개선 사항이면 replaceState를 사용하세요.

마무리 생각

그 PrismJS URL은 저에게 중요한 것을 상기시켰습니다: 좋은 URL은 단순히 콘텐츠를 가리키지 않습니다. 그들은 사용자와 애플리케이션 간의 대화를 설명합니다. 그들은 의도를 포착하고, 컨텍스트를 보존하고, 다른 상태 관리 솔루션이 일치할 수 없는 방식으로 공유를 가능하게 합니다.
우리는 Redux, MobX, Zustand, Recoil 같은 점점 더 정교한 상태 관리 라이브러리를 구축했습니다. 그들 모두 자신의 자리가 있지만 때때로 최고의 솔루션은 항상 그곳에 있던 것입니다.
이전 기사에서 저는 나쁜 URL 디자인의 숨겨진 비용에 대해 썼습니다. 오늘 우리는 반대편을 탐색했습니다: 좋은 URL 디자인의 엄청난 가치입니다. URL은 단순한 주소가 아닙니다. 그들은 상태 컨테이너, 사용자 인터페이스, 계약이 모두 하나로 합쳐진 것입니다.
앱이 새로 고침 시 상태를 잊으면 웹의 가장 오래되고 가장 우아한 기능 중 하나를 놓치고 있는 것입니다.

0
12

댓글

?

아직 댓글이 없습니다.

첫 번째 댓글을 작성해보세요!

Inkyu Oh님의 다른 글

더보기

유사한 내용의 글