FEInterview Prep

GitHub Pages

URL은 곧 상태입니다

URL을 1급 상태 컨테이너로 다루면 공유, 북마크, 뒤로가기가 공짜다. 어디에 넣고 어디에 두지 말지, React에서 어떻게 동기화할지 판단 기준을 정리한다.

2026-02-20·8분 읽기
React아키텍처
원문 보기 ↗

핵심 요약

Scott Hanselman 의 "URL은 UI다" 를 넘어, URL 을 상태 컨테이너로 취급하자는 주장이다. Path 는 계층적 리소스, Query Parameter 는 필터/정렬/페이지네이션, Fragment 는 페이지 내 위치를 담는다. React 에서는 useSearchParamsuseState 를 대체해 URL 과 UI 를 단방향 동기화한다. 모든 상태를 URL 에 넣는 건 아니며, 공유 가능성/새로고침 지속성/민감도 3가지로 걸러낸다. pushState vs replaceState, 기본값 생략, 디바운싱 같은 세부 규칙이 URL 을 깨끗하게 유지한다.

URL을 애플리케이션 상태의 진실 공급원(source of truth) 으로 보는 프레임. useState세션 안에서만 의미 있는 상태, URL 은 공유·복구 가능한 상태 를 담는다. 새 상태를 만들 때마다 "이 링크를 남이 클릭해도 같은 화면이 나와야 하는가?" 한 문장으로 저장 위치를 결정한다.

URL 상태는 실무와 면접 모두에서 자주 걸리는 판단 지점이다. 정리하면:

  • 공짜 기능이 많다: 북마크, 링크 공유, 뒤로가기/앞으로가기, 딥 링크, 새로고침 복구가 라이브러리 없이 된다.
  • SSR/캐시 친화적: URL 은 곧 캐시 키라 CDN·SWR·React Query 와 궁합이 좋다.
  • 분석 무료: 모든 쿼리 파라미터가 하나의 분석 차원(dimension)이 된다.
  • 안티 패턴 비용이 크다: 필터를 useState 에만 두면 뒤로가기로 다 날아가고 사용자 이탈로 이어진다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

URL에 넣을지 가르는 한 문장

"이 URL 을 남이 클릭했을 때 같은 상태를 봐야 하는가?" 하나로 결정한다. Yes 면 URL, No 면 useState/useReducer/localStorage.

기준URL (searchParams)useStatelocalStorage
공유 가능OXX
새로고침 생존OXO
뒤로가기 복원OXX
기기 간 공유OXX
민감 정보금지OK (메모리)주의
고빈도 업데이트비쌈최적최적

결과적으로 필터·정렬·페이지·탭·날짜 범위는 URL 로, 모달 open/close·드롭다운·입력 중인 폼·스크롤 위치는 로컬 상태로 간다.

source of truthsearchParamsshareabilityrefresh persistence
자주 하는 오해

"URL 에 넣으면 좋다더라" 수준으로 모달 open 여부까지 쿼리로 옮기는 것. 일시적 UI 상태를 URL 에 넣으면 브라우저 히스토리가 오염되고 뒤로가기가 "모달 닫기" 로 변해 사용자 기대와 어긋난다.

React에서 useSearchParams

react-router-dom 과 Next.js 13+ 의 next/navigation 모두 useSearchParams 훅을 제공한다. 로컬 상태처럼 읽고 쓰되, setter 가 URL 을 바꾸면서 자동 리렌더를 유발한다.

import { useSearchParams } from 'react-router-dom';
// Next.js App Router: import { useSearchParams } from 'next/navigation';

function ProductList() {
  const [params, setParams] = useSearchParams();
  const color = params.get('color') ?? 'all';

  const onChange = (next: string) => {
    setParams((prev) => {
      const p = new URLSearchParams(prev);
      p.set('color', next);
      return p;
    });
  };
  // ...
}

타입 안전·zod 검증·배열 직렬화까지 원하면 nuqs 또는 TanStack Router 의 search 옵션을 쓴다. 기본 API 로도 충분하지만 프로젝트 규모가 커지면 라이브러리 도입 = 타입 계약 추가 로 이해하면 된다.

useSearchParamsURLSearchParamsnuqsTanStack Router
자주 하는 오해

setParams(next) 를 호출할 때마다 전체 파라미터를 덮어써서 다른 필터가 날아가는 버그. 항상 new URLSearchParams(prev) 로 이전 값을 복사한 뒤 set/delete부분 업데이트해야 한다.

pushState vs replaceState

URL 업데이트 수단 선택은 브라우저 히스토리가 어떻게 쌓여야 하는가로 결정한다.

상황선택이유
필터 토글, 페이지 이동, 탭 전환pushState뒤로가기로 이전 상태 복원이 기대됨
검색창 타이핑(키 입력마다)replaceState히스토리에 한 글자씩 쌓이는 것 방지
정렬 옵션 변경케이스 바이 케이스사용자가 되돌릴 만한 동작이면 push
const update = debounce((q: string) => {
  const p = new URLSearchParams(location.search);
  q ? p.set('q', q) : p.delete('q');
  history.replaceState({}, '', `?${p.toString()}`);
}, 300);

검색어처럼 고빈도 업데이트는 debounce 300ms + replaceState 조합이 정석. popstate 이벤트로 뒤로/앞으로 감지 시 UI 를 URL 과 재동기화해야 한다(프레임워크 라우터는 대개 자동).

pushStatereplaceStatepopstatedebounce
자주 하는 오해

모든 URL 업데이트를 pushState 로 처리해 타이핑 30글자에 뒤로가기 30번을 만들어버리는 것. 또는 반대로 필터 변경까지 replaceState 로 처리해 사용자가 뒤로가기로 이전 필터로 돌아갈 수 없게 만드는 것.

URL을 계약(Contract)으로 설계

잘 설계된 URL 은 상태 컨테이너를 넘어 앱과 사용자 간 계약이 된다. 지켜야 할 규칙:

  • 기본값은 URL 에서 생략: ?theme=light&page=1&sort=date? (기본값이면 쿼리 비움). 생략된 값은 읽는 쪽 params.get('theme') ?? 'light' 로 채운다.
  • 의미 있는 이름: ?p=x7f2k&v=3 대신 ?color=silver&sort=price. 사람과 기계 모두 의도를 읽을 수 있어야 한다.
  • 일관된 패턴 고수: 배열을 tags=a,b 로 쓸지 tags[]=a&tags[]=b 로 쓸지 프로젝트 내에서 하나로 고정.
  • 민감 정보 금지: URL 은 브라우저 히스토리, 서버 로그, Referer 헤더, 분석 도구로 누출된다. 토큰/비밀번호는 절대 안 된다.
  • Base64 JSON 주의: 상태가 너무 커서 인코딩해야 한다면 그건 URL 에 둘 상태가 아니라는 신호.
URL as contractcache key자기 설명적 URLURL 길이 제한
자주 하는 오해

파라미터 네이밍을 foo, x, v 처럼 짧게 줄여 URL 을 "깔끔하게" 만들려는 것. 결과적으로 자기 설명성을 잃고 팀원이 파라미터 의미를 코드에서 역추적해야 한다. 짧음보다 의미 전달이 우선.

읽는 순서

  1. 1이론

    아티클의 "URL 상태로 적합한 것 / 부적합한 것" 체크리스트를 외우고, 공유 가능성 + 새로고침 지속성 + 민감도 3축으로 판단 기준을 자기 문장으로 정리하세요.

  2. 2구현

    작은 상품 목록 페이지를 만들고 useSearchParamscolor, sort, page 3개 필터를 URL 에 동기화해 보세요. setter 에서 new URLSearchParams(prev) 를 사용해 부분 업데이트를 검증하고, 검색어 입력은 replaceState + debounce 300ms 로 구현해 보세요.

  3. 3실무

    현재 프로젝트에서 useState 로 관리 중인 상태 목록을 뽑아 "이 URL 을 남이 열어도 같은 화면이 나와야 하는가" 질문으로 하나씩 분류하세요. 공유 가능해야 할 상태 중 URL 밖에 있는 것이 있다면 마이그레이션 후보로 기록.

  4. 4설명

    동료에게 "왜 필터는 URL, 모달 open/close 는 useState 인가" 를 5분 안에 설명하세요. pushState vs replaceState, nuqs 같은 라이브러리 도입 근거, 민감 정보 예외까지 포함하면 합격선.

면접 연결 질문

mediumReact 애플리케이션에서 검색 필터 상태를 `useState` 로 관리할지, URL 쿼리 파라미터로 관리할지 어떤 기준으로 결정하시나요?
힌트

[감점 답변] "URL 이 좋다" 만 단언하거나 "라이브러리에서 그렇게 하더라" 수준. [좋은 답변] 공유 가능성/새로고침 지속성/뒤로가기 기대/민감도 4축으로 분류한다고 말한다. 필터·정렬·페이지처럼 링크로 공유 가능해야 할 상태는 URL, 모달 열림·드롭다운·입력 중인 폼처럼 세션 내 임시 상태useState. 추가로 URL 길이 제한, 고빈도 업데이트 시 replaceState + debounce 같은 트레이드오프까지 언급.

mediumNext.js 의 `useSearchParams` 로 필터를 구현했는데, 한 필터를 바꾸면 다른 필터가 초기화되는 버그가 발생했습니다. 원인과 해결책을 설명해 주세요.
힌트

[감점 답변] "리렌더 문제" 로 뭉뚱그림. [좋은 답변] 원인은 setter 에 URLSearchParams 인스턴스를 통째로 전달하면서 이전 값을 복사하지 않은 것. 해결은 setParams(prev => { const p = new URLSearchParams(prev); p.set(key, value); return p; }) 로 이전 값을 보존한 뒤 부분 업데이트. 더 나아가 nuqs 같은 타입 안전 라이브러리로 key 별 독립 업데이트를 강제하는 설계도 소개.

hardURL 을 상태로 쓸 때 발생할 수 있는 문제(안티 패턴)와 각각의 해결책을 설명해 주세요.
힌트

[감점 답변] "길이 제한" 만 언급. [좋은 답변] 최소 4가지 축으로 답한다. 1) 민감 정보 누출 — 브라우저 히스토리/서버 로그/Referer 로 새므로 토큰·비번 금지. 2) URL 길이 제한(2~8KB) — base64 JSON 까지 가면 구조를 다시 봐야 함. 3) 히스토리 오염 — 타이핑마다 pushState 하면 뒤로가기가 부서짐. replaceState + debounce. 4) 불명확한 네이밍?x=1&v=2 대신 자기 설명적 이름. 5) 기본값 노출 — 기본값은 생략해 URL 깨끗하게 유지.

자기 점검

어떤 상태를 URL 에 넣을지 말지, 3개 이하의 판단 기준으로 설명해 보세요.
공유 가능성새로고침뒤로가기민감 정보
자주 하는 오해

"URL 에 최대한 많이 넣는 게 좋다" 고 생각하는 것. 모달 열림, 스크롤 위치, 입력 중인 폼 같은 일시적 상태를 URL 에 넣으면 히스토리가 오염되고 뒤로가기 UX 가 망가진다.

`pushState` 와 `replaceState` 를 각각 어떤 상황에 쓰나요? 검색창 타이핑에는 왜 `replaceState` 를 쓰는지 설명해 보세요.
pushStatereplaceStatehistorydebounce
자주 하는 오해

두 API 의 차이를 "히스토리에 남냐 안 남냐" 정도로만 기억하는 것. 실제 판단 기준은 "사용자가 뒤로가기로 되돌릴 만한 동작인가" 이며, 타이핑처럼 고빈도 업데이트는 replaceState + debounce 조합이 기본값이다.

React 에서 여러 필터를 `useSearchParams` 로 관리할 때 다른 파라미터를 지우지 않고 한 키만 업데이트하는 패턴을 코드로 설명해 보세요.
useSearchParamsURLSearchParamsprevset
자주 하는 오해

setSearchParams({ color: 'red' }) 처럼 객체를 통째로 넘겨도 병합될 거라고 생각하는 것. 실제로는 덮어쓰기라 다른 필터가 날아간다. 항상 new URLSearchParams(prev) 로 복사 후 set/delete.