YKSS
검색 파라미터는 상태입니다
URL 검색 파라미터는 "문자열 부산물"이 아니라 공유 가능한 1급 상태다. TanStack Router 의 validateSearch 처럼 라우트가 스키마를 소유하면 타입·검증·기본값이 단일 소스가 된다.
핵심 요약
URL 검색 파라미터는 전역적·직렬화 가능·공유 가능한 상태이지만 대부분의 앱은 URLSearchParams + zod 수동 파싱처럼 임시방편으로 다룬다. 읽기는 Nuqs 등으로 어느 정도 풀리지만 쓰기/계층/단일 진실 공급원 문제는 라우트 자체가 스키마를 소유해야 풀린다. TanStack Router 는 createFileRoute({ validateSearch: ... }) 로 라우트에 스키마를 묶고, <Link search={...}> 와 navigate({ search: prev => ... }) 가 모두 타입 안전하게 동작하도록 만든다.
검색 파라미터는 useState 같은 "상태"의 한 종류로 보라. 차이는 단지 저장소가 URL이라는 점이다. URL은 공유·새로고침·딥링크가 되는 강력한 저장소이지만, 문자열만 담고 타입이 없어서 "라우트가 스키마를 소유"하는 형태로 강제하지 않으면 서로 다른 곳에서 다른 모양으로 쓰는 단편화가 일어난다.
면접에서 "상태 관리"를 물으면 대부분 Redux/Zustand로만 답한다. 그러나 실무 버그의 큰 비율은 "URL ↔ UI 상태 동기화"에서 나온다. 이 글은 검색 파라미터를 읽기/쓰기/검증/계층 상속의 4축으로 분해해서, "어디까지가 라이브러리의 책임이고 어디까지가 라우터의 책임인가"를 설명할 수 있게 해준다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
URL은 동기화가 필요한 "상태 저장소"다
필터·정렬·페이지·탭 같은 상태는 URL에 두면 새로고침 후에도, 다른 사람과 공유해도 같은 화면이 된다. 반대로 컴포넌트 useState 에만 두면 "링크 공유 시 다른 화면"이 되는 버그가 생긴다.
필터를 컴포넌트 상태로만 두고 사용자에게 "URL을 그대로 공유" 기능을 약속하기. 실제로는 기본값 화면이 열리고 사용자는 필터를 다시 눌러야 한다.
읽기보다 "쓰기"가 더 어려운 문제
URLSearchParams 는 문자열만 지원하고 중첩 JSON·배열·타입 강제를 못 한다. 게다가 navigate({ search }) 호출 측이 라우트의 스키마를 모르면 잘못된 키/타입을 쉽게 흘려넣는다. 호출자와 라우트 간 계약이 없으면 컴파일 타임 가드도 없다.
쓰기 전용 헬퍼 함수만 만들어 두고 "읽을 때 zod로 검증"하는 패턴. 호출자 쪽에서 잘못된 필드를 넣어도 막아주지 못한다.
라우트가 스키마를 소유하는 패턴
TanStack Router 의 validateSearch 는 단일 진실 공급원 역할을 한다. 부모 라우트가 sort 를 정의하면 자식 라우트는 자동 상속 + 확장만 가능. 부모와 호환되지 않는 재정의(sort: z.boolean() 등)는 타입 오류로 막힌다.
라우트별 스키마와 컴포넌트별 zod 스키마를 따로 둬서, 같은 sort 키가 두 군데에서 다르게 정의되는 "스키마 단편화".
리듀서 스타일 갱신과 부분 구독
navigate({ search: prev => ({ ...prev, page: prev.page + 1 }) }) 처럼 이전 검색 상태를 받아 일부만 갱신하는 패턴이 핵심. 라우터가 반응성 모델과 통합되어 있으면, 컴포넌트는 자기가 쓰는 키가 바뀔 때만 리렌더한다.
URL 전체를 새로 만들어 pushState 하기. 다른 키들이 의도치 않게 초기화되거나 모든 구독자가 같이 리렌더된다.
읽는 순서
- 1이론
URL을 "상태 저장소"로 보는 관점,
URLSearchParamsAPI 한계,pushState/replaceState차이를 정리. - 2구현
Next.js
useSearchParams(읽기) +useRouter().push(쓰기) 또는 TanStack Router 의validateSearch로 필터·정렬 화면을 직접 만들어 본다. 새로고침/공유/뒤로가기 동작을 모두 검증. - 3실무
기존 코드의 "필터 =
useState" 부분을 URL로 끌어올리는 마이그레이션을 1개 화면에 적용. 분석으로 "공유 가능 URL" 비율이 늘었는지 확인. - 4설명
면접에서 "상태를 어디에 둘지" 결정 트리(메모리 / URL / 서버 / 로컬스토리지)를 자기 언어로 설명할 수 있도록 정리.
면접 연결 질문
[감점 답변] "URL에 저장된다" 한 줄. [좋은 답변] 장점: 새로고침 보존, 공유 가능, 뒤로 가기 통합, 서버 렌더링과 자연스럽게 연결. 단점: 문자열뿐(타입 없음), 길이 제한, 민감 데이터 부적합, 너무 자주 바뀌는 값(예: 마우스 위치)은 history 스팸. 따라서 **"공유 가능성과 새로고침 후 보존이 가치 있는 상태"**만 URL로 올린다.
[감점 답변] "각자 URLSearchParams 로 읽으면 된다". [좋은 답변] (1) 단일 스키마를 라우트 또는 별도 모듈에 두고 모두가 import, (2) 쓰기 시 prev => next 함수형 업데이트로 다른 키 보존, (3) 검증은 진입 시점 1회(validateSearch)로 끝내고 컴포넌트는 타입 안전하게 사용, (4) 잘못된 값은 즉시 리다이렉트하거나 기본값으로 폴백.
[감점 답변] "길어져서 안 된다". [좋은 답변] (1) URL 길이 제한(브라우저·서버 양쪽), (2) 인코딩/디코딩 오류 가능성, (3) 사람이 읽을 수 없어 디버깅 어려움, (4) 보안: 사용자가 임의 JSON을 주입할 수 있어 검증이 필수. 실무에서는 평탄화하거나, nuqs 의 parser 같은 명시적 직렬화 계약을 쓰는 게 낫다.
자기 점검
"전역 상태는 다 URL"이 아니다. 폼 입력 중간값·드래그 좌표 같은 고빈도 상태는 URL이 아니라 메모리에 두어야 한다.
쓰기 측에서만 타입을 단언하는 것은 "안전"이 아니다. 라우트가 자체 스키마를 갖고 호출 측의 인자를 추론으로 강제해야 진짜 안전이다.