
2025년 6월, Tanner Linsley는 검색 파라미터를 상태(state)로 다뤄야 한다는 명확한 관점을 담은 글을 공개했습니다. 많은 React 개발자들이 URL의 query string을 단순한 문자열 조작 대상으로 취급해온 현실에서, 이 글은 검색 파라미터를 정형화된 상태로 다루어야 할 이유와, 이를 해결하기 위한 TanStack Router의 접근을 정리한 것입니다.
전통적으로 검색 파라미터(search params)는:
하지만 대부분의 앱은 이를 여전히 string parsing이나 brittle utils에 의존하고 있고, 명시적 타입 유추나 구조화된 검증이 어렵다.
const schema = z.object({ sort: z.enum(["asc", "desc"]) });
const raw = Object.fromEntries(new URLSearchParams(location.href));
const result = schema.safeParse(raw);
이런 방식은 유효성 검증이 수동적이며 중복되고, 타입 추론이나 구조적 제약이 없기 때문에 깨지기 쉽고 유지보수 비용이 높다.
읽는 것은 어떻게든 해결 가능하지만, URL을 작성하는 건 더 어려운 문제다.
<Link to="/dashboards/overview" search={{ sort: 'asc' }} />
이 코드에서 sort가 실제로 유효한 파라미터인지, 올바른 enum 값인지, 누가 책임지고 검증해야 하는지 알 방법이 없다. 결국:
결국, 스키마에 기반한 제약(Constraint)이 있어야 조정과 협업이 가능하다.
Nuqs는 로컬 컴포넌트 레벨에서 Zod 스키마 기반의 파싱 및 작성 API를 제공하는 좋은 예시다.
하지만:
검색 파라미터는 라우터 수준에서 통합 관리되어야 진정한 안정성을 확보할 수 있다.
TanStack Router는 검색 파라미터를 라우트 정의에 통합함으로써 위 문제를 근본적으로 해결한다.
export const Route = createFileRoute('/dashboards/overview')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']),
filter: z.string().optional(),
}),
});
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
});
부모 라우트와 자식 라우트가 스키마를 안전하게 상속 및 확장할 수 있다.
// 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는 부모에서 자동 상속됨
}),
});
부모가 정의한 sort 스키마를 자식이 잘못 재정의하면 타입 에러 발생:
validateSearch: z.object({
sort: z.boolean(), // ❌ boolean은 상속된 enum에 부합하지 않음
});
즉, searchParams는 더 이상 문자열이 아닌 공식적인 상태의 일부로 취급되어야 한다.
많은 라우팅 시스템은 searchParams를 단지 보조 기능 정도로 취급한다. TanStack Router는 이를 1급 상태로 격상시켜:
URL을 더 이상 문자열로 다루지 말자. 애초에 상태(state)로 다뤘다면 생기지 않을 문제들이었다.