August 21, 2025
검색 파라미터는 역사적으로 2급 상태처럼 취급되어 왔습니다. 전역적이고, 직렬화 가능하며, 공유 가능하지만, 대부분의 앱에서는 여전히 문자열 파싱, 느슨한 규칙, 취약한 유틸리티를 이용한 임시방편식 접근에 머물러 있습니다.
sort
파라미터를 검증하는 것처럼 단순한 일조차도 금방 장황해집니다.
const schema = z.object({
sort: z.enum(['asc', 'desc']),
});
const raw = Object.fromEntries(new URLSearchParams(location.href));
const result = schema.safeParse(raw);
if (!result.success) {
// 폴백을 표시하거나, 리다이렉션 하거나 에러를 표시합니다.
}
이 방식은 작동하지만 수동적이고 반복적입니다. 추론이 없고, 라우트 자체와 연결되지 않으며, 더 많은 타입, 기본값, 변환, 구조를 추가하려고 하는 순간부터 무너지고 맙니다.
더 나쁜 점은, URLSearchParams
는 문자열만 지원한다는 것입니다. 중첩 JSON, 배열(단순 콤마 분리 이상), 타입 강제 변환을 지원하지 않습니다. 따라서 상태가 평평하고 단순하지 않다면 금방 한계에 부딪히게 됩니다.
그래서 요즘 Nuqs, Next.js RFC, 그리고 사용자 정의 패턴과 같은 도구나 제안이 등장하고 있습니다. 이들은 검색 파라미터를 좀 더 타입 안전하고 사용하기 편하게 만들려는 시도들입니다. 대부분은 URL에서 읽어오는 동작을 개선하는 데 집중합니다.
하지만 더 깊고 어려운 문제, 즉 라우팅 컨텍스트를 완전히 인식한 상태에서 검색 파라미터를 안전하고 원자적으로 작성하는 문제를 거의 아무도 해결하지 못하고 있습니다.
URL에서 읽는 것과 유효한 URL을 코드로 구성하는 것은 전혀 다른 이야기입니다.
아래와 같은 시도를 하는 작업을 살펴보겠습니다.
<Link to="/dashboards/overview" search={{ sort: 'asc' }} />
이 라우트에 어떤 검색 파라미터가 유효한지, 그리고 그것을 올바르게 포맷하고 있는지 전혀 알 수 없다는 것을 깨닫게 됩니다. 파라미터를 문자열로 만드는 헬퍼가 있더라도, 호출자와 라우트 간의 계약을 강제하는 것은 아무것도 없습니다. 타입 추론도, 유효성 검증도, 가드레일도 없습니다.
여기서 제약 조건이 특징이 됩니다.
라우트 자체에서 검색 파라미터 스키마를 명시적으로 선언하지 않는다면, 결국 추측에 의존하게 됩니다. 한 곳에서 유효성 검증을 하더라도, 다른 컴포넌트가 유효하지 않거나 불완전하거나 충돌하는 상태로 탐색하는 것을 막을 방법이 없습니다.
제약 조건은 조정을 가능하게 합니다. 이를 통해 라우트 외부에서 호출하는 코드도 안전하게 동작할 수 있습니다.
Nuqs 같은 도구는 로컬 추상화가 검색 파라미터 처리의 사용성을 어떻게 향상시킬 수 있는지 보여주는 좋은 예입니다. Zod 기반 파싱, 타입 추론, 심지어 쓰기 가능한 API까지 모두 특정 컴포넌트나 훅 범위에 한정됩니다.
이것들은 개별적으로 검색 파라미터를 읽고 쓰는 것을 더 쉽게 만듭니다. 그 자체로 가치가 있습니다.
하지만 이러한 방식으로 더 광범위한 정합 문제는 해결하지 못합니다. 여전히 중복된 스키마, 서로 다른 기대치, 라우트나 컴포넌트 간 일관성을 강제할 방법이 없습니다. 기본값이 충돌하고, 타입이 어긋나고, 라우트가 변경될 때 호출자가 함께 업데이트된다는 보장도 없습니다.
이것이 진짜 단편화 문제이고, 이를 해결하려면 검색 파라미터 스키마를 라우팅 레이어 안으로 가져와야 합니다.
TanStack Router는 이 문제를 전반적으로 해결합니다.
스키마 로직을 앱 전체에 흩뿌리는 대신, 라우트 자체 내부에서 정의합니다.
export const Route = createFileRoute('/dashboards/overview')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']),
filter: z.string().optional(),
}),
});
이 스키마는 단일 진실 공급원(Single Source of Truth)이 됩니다. 전역적으로 완전한 추론, 유효성 검증, 자동 완성을 제공합니다.
<Link
to="/dashboards/overview"
search={{ sort: 'asc' }} // 완전한 타입 지정, 완전한 검증
/>
검색 상태의 일부만 업데이트하고 싶으신가요? 문제없습니다.
navigate({
search: prev => ({ ...prev, page: prev.page + 1 }),
});
리듀서 스타일의 트랜잭션 방식으로 동작하며, 라우터의 반응성 모델과 직접 연동됩니다. 컴포넌트는 자신이 사용하는 특정 검색 파라미터가 바뀔 때만 리렌더링되므로, URL이 변경될 때마다 매번 리렌더링되지 않습니다.
검색 파라미터 로직이 훅, 유틸, 헬퍼 등 사용자 영역에 흩어져 있으면, 결국 상충하는 스키마가 생기게 됩니다.
어떤 컴포넌트는 sort: 'asc' | 'desc'
를 기대합니다. 또 다른 컴포넌트는 filter
를 추가합니다. 세 번째는 sort: 'desc'
를 기본값으로 가정합니다. 하지만 이들은 모두 동일한 단일 진실 공급원을 공유하지 않습니다.
이로 인해 다음과 같은 문제가 발생합니다.
TanStack Router는 스키마를 라우트 정의에 직접 연결하고 계층적으로 관리함으로써 이를 방지합니다.
부모 라우트가 공유 검색 파라미터 검증을 정의하면, 자식 라우트는 해당 컨텍스트를 상속받아 타입 안전하게 추가하거나 확장할 수 있습니다. 이렇게 하면 앱의 다른 부분에서 겹치거나 호환되지 않는 스키마를 실수로 만들 수 없습니다.
실제로는 다음과 같이 작동합니다.
// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']).default('asc'),
}),
});
그다음, 자식 라우트는 안전하게 스키마를 확장할 수 있습니다.
// routes/dashboard/$dashboardId.tsx
export const Route = createFileRoute('/dashboard/$dashboardId')({
validateSearch: z.object({
filter: z.string().optional(),
// ✅ `sort`는 부모로부터 자동 상속됩니다
}),
});
/dashboard/123?sort=desc&filter=active
를 매칭하면, 부모는 sort
의 유효성을 검증하고, 자식은 filter
의 유효성을 검증하며, 모든 것이 매끄럽게 작동합니다.
부모의 필수 파라미터를 자식에서 완전히 다른 것으로 재정의하려고 시도한다면? 타입 오류가 발생합니다.
validateSearch: z.object({
// ❌ 타입 오류: boolean은 부모의 'asc' | 'desc'를 확장하지 않습니다
sort: z.boolean(),
filter: z.string().optional(),
});
이러한 강제는 중첩 라우트를 구성 가능하고 안전하게 만들어 주며, 이는 드문 조합입니다.
여기서의 마법 같은 점은 팀에게 규칙을 따르도록 가르칠 필요가 없다는 점입니다. 라우트가 스키마를 소유합니다. 모두가 이를 사용하기만 하면 됩니다. 중복도 없고, 타입 드리프트도 없으며, 조용한 버그도 없고, 추측도 없습니다.
유효성 검증, 타입 지정, 소유권을 라우터 자체로 가져오면, URL을 문자열이 아니라 진짜 상태처럼 다루게 됩니다. 본질적으로 그것이 상태이기 때문입니다.
대부분의 라우팅 시스템은 검색 파라미터를 사후적으로만 다룹니다. 읽고, 어쩌면 파싱 하고, 어쩌면 문자열 화하는 정도이며, 실제로 신뢰할 수 있는 상태로 취급하는 경우는 드뭅니다.
TanStack Router는 이를 완전히 뒤집습니다. 검색 파라미터를 라우팅 계약의 핵심 부분으로 만들어 검증 가능하고, 추론 가능하며, 쓰기 가능하고, 반응형이 뛰어나도록 해줍니다.
검색 파라미터를 상태처럼 다루지 않는다면, 결국 계속해서 정보를 유출하고, 깨뜨리고, 우회하게 될 것입니다.
처음부터 올바르게 다루는 것이 낫습니다.
검색 파라미터를 1급 상태로 취급하는 가능성에 흥미가 생기셨다면, TanStack Router를 사용해 보시기를 권합니다. 검증 가능하고, 추론 가능하며, 반응형 검색 파라미터를 라우팅 로직에서 직접 경험해 보세요.
🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!