본문으로 건너뛰기

기본 검색 매개변수 설정 방법

스키마 검증을 사용해 TanStack Router 라우트에 타입 안전하고 프로덕션에 적합한 검색 매개변수를 추가하는 방법을 알아봅니다. 이 가이드에서는 검색 매개변수 검증의 기본 사항, 값 읽기, 표준 스키마 호환 검증 라이브러리를 사용한 다양한 데이터 타입 처리를 다룹니다.

빠른 시작

스키마 검증으로 검색 매개변수를 설정합니다(프로덕션에 권장).

import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const productSearchSchema = z.object({
page: z.number().default(1).catch(1),
category: z.string().default('all').catch('all'),
showSale: z.boolean().default(false).catch(false),
})

export const Route = createFileRoute('/products')({
validateSearch: productSearchSchema,
component: ProductsPage,
})

function ProductsPage() {
const { page, category, showSale } = Route.useSearch()

return (
<div>
<h1>Products</h1>
<p>Page: {page}</p>
<p>Category: {category}</p>
<p>Show Sale Items: {showSale ? 'Yes' : 'No'}</p>
</div>
)
}

검색 매개변수에 스키마 검증을 사용하는 이유

프로덕션에서의 이점:

  • 타입 안전성: TypeScript 자동 추론
  • 런타임 검증: 유효하지 않은 URL 매개변수를 안전하게 감지
  • 기본값: 누락된 매개변수에 대한 대체값 처리
  • 오류 처리: 내장 검증 오류 관리
  • 유지 보수성: 명확하고 선언적인 스키마 정의

검증 라이브러리 설정

TanStack Router는 모든 표준 스키마 호환 검증 라이브러리를 지원합니다. 이 가이드에서는 예시에 Zod를 사용하지만 어떤 검증 라이브러리든 사용할 수 있습니다.

Zod v4 사용:

import { z } from 'zod'

const searchSchema = z.object({
page: z.number().default(1),
category: z.string().default('all').catch('all'),
})

export const Route = createFileRoute('/products')({
validateSearch: searchSchema,
component: ProductsPage,
})

Zod v3 사용:

npm install zod @tanstack/zod-adapter
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'

const searchSchema = z.object({
page: fallback(z.number(), 1).default(1),
category: fallback(z.string(), 'all').default('all'),
})

export const Route = createFileRoute('/products')({
validateSearch: zodValidator(searchSchema),
component: ProductsPage,
})

검증 라이브러리의 자세한 비교와 고급 검증 패턴은 다음을 참고하세요: 스키마로 검색 매개변수 검증

Zod를 사용한 단계별 설정

이 가이드의 나머지 예시에서는 Zod v4를 사용하지만 패턴은 모든 검증 라이브러리에 적용됩니다.

1단계: 검색 스키마 정의

먼저 라우트에 필요한 검색 매개변수를 확인합니다.

import { z } from 'zod'

const shopSearchSchema = z.object({
// Pagination
page: z.number().default(1),
limit: z.number().default(20),

// Filtering
category: z.string().default('all'),
minPrice: z.number().default(0),
maxPrice: z.number().default(1000),

// Settings
sort: z.enum(['name', 'price', 'date']).default('name'),
ascending: z.boolean().default(true),

// Optional parameters
searchTerm: z.string().optional(),
showOnlyInStock: z.boolean().default(false),
})

type ShopSearch = z.infer<typeof shopSearchSchema>

2단계: 라우트에 스키마 검증 추가

스키마를 라우트에 연결합니다.

export const Route = createFileRoute('/shop')({
validateSearch: shopSearchSchema,
component: ShopPage,
})

3단계: 컴포넌트에서 검색 매개변수 읽기

라우트의 useSearch() 훅을 사용해 검증되고 타입 지정된 검색 매개변수에 액세스합니다.

function ShopPage() {
const searchParams = Route.useSearch()

// All properties are fully type-safe and validated
const {
page,
limit,
category,
sort,
ascending,
searchTerm,
showOnlyInStock,
} = searchParams

return (
<div>
<h1>Shop - Page {page}</h1>
<div>Category: {category}</div>
<div>
Sort: {sort} ({ascending ? 'ascending' : 'descending'})
</div>
<div>Items per page: {limit}</div>
<div>In stock only: {showOnlyInStock ? 'Yes' : 'No'}</div>
{searchTerm && <div>Search: "{searchTerm}"</div>}
</div>
)
}

일반적인 검색 매개변수 패턴

제약 조건이 있는 페이지네이션

const paginationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(10).max(100).default(20),
})

export const Route = createFileRoute('/posts')({
validateSearch: paginationSchema,
component: PostsPage,
})

function PostsPage() {
const { page, limit } = Route.useSearch()

// Calculate offset for API calls
const offset = (page - 1) * limit

return (
<div>
<h1>Posts (Page {page})</h1>
<p>Showing {limit} posts per page</p>
<p>Offset: {offset}</p>
{/* Render posts... */}
</div>
)
}

기본값을 사용하는 열거형 검증

const catalogSchema = z.object({
sort: z.enum(['name', 'date', 'price']).default('name'),
category: z.enum(['electronics', 'clothing', 'books', 'all']).default('all'),
ascending: z.boolean().default(true),
})

export const Route = createFileRoute('/catalog')({
validateSearch: catalogSchema,
component: CatalogPage,
})

복잡한 데이터 타입

const dashboardSchema = z.object({
// Numbers with validation
userId: z.number().positive().default(1),
refreshInterval: z.number().min(1000).max(60000).default(5000),

// Strings with validation
theme: z.enum(['light', 'dark']).default('light'),
timezone: z.string().optional(),

// Arrays with validation
selectedIds: z.number().array().default([]),
tags: z.string().array().default([]),

// Objects with validation
filters: z
.object({
status: z.enum(['active', 'inactive']).optional(),
type: z.string().optional(),
})
.prefault({}),
})

날짜 및 고급 타입

const reportSchema = z.object({
startDate: z.string().pipe(z.coerce.date()).optional(),
endDate: z.string().pipe(z.coerce.date()).optional(),
format: z.enum(['pdf', 'csv', 'excel']).default('pdf').catch('pdf'),
includeCharts: z.boolean().default(true),
})

컴포넌트 외부에서 검색 매개변수 읽기

getRouteApi 사용

코드 분할된 컴포넌트나 별도 파일에서는 다음과 같이 합니다.

// components/ProductFilters.tsx
import { getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/products')

export function ProductFilters() {
const { category, sort, showSale } = routeApi.useSearch()

return (
<div>
<select value={category}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
{/* More filters... */}
</div>
)
}

from과 함께 useSearch 사용

import { useSearch } from '@tanstack/react-router'

function GenericSearchDisplay() {
const search = useSearch({ from: '/products' })

return <div>Current filters: {JSON.stringify(search, null, 2)}</div>
}

수동 검증(프리미티브 이해)

프로덕션에는 스키마 검증을 권장하지만, 수동 검증을 이해하면 검색 매개변수가 내부적으로 작동하는 방식을 파악하는 데 도움이 됩니다.

// Educational example - use schema validation for production
export const Route = createFileRoute('/example')({
validateSearch: (search: Record<string, unknown>) => ({
// Numbers need coercion from URL strings
page: Number(search.page) || 1,

// Strings can be cast with defaults
category: (search.category as string) || 'all',

// Booleans: TanStack Router auto-converts "true"/"false" to booleans
showSale: Boolean(search.showSale),

// Arrays need JSON parsing validation
selectedIds: Array.isArray(search.selectedIds)
? search.selectedIds.map(Number).filter(Boolean)
: [],
}),
component: ExamplePage,
})

프로덕션 체크리스트

  • 타입 안전성과 런타임 검증을 위해 검증 라이브러리로 스키마 검증 사용
  • 안전한 오류 처리를 위해 대체값 추가
  • 선택적 매개변수에 기본값 설정
  • 검증 라이브러리의 내장 검증기로 제약 조건 검증
  • 선택적 매개변수 적절히 처리
  • 올바르게 스키마를 설정하면 타입 추론이 자동으로 작동
  • 검증 실패를 처리하도록 오류 경계 구성

일반적인 문제

문제: 검색 매개변수로 TypeScript 오류 발생

원인: 스키마 정의가 없거나 올바르지 않습니다.

해결 방법: 스키마가 모든 검색 매개변수를 포함하고 올바른 타입을 사용하는지 확인합니다.

// ❌ Missing schema or incorrect types
export const Route = createFileRoute('/page')({
component: MyPage,
})

// ✅ Complete schema with proper validation
const searchSchema = z.object({
page: z.number().default(1).catch(1),
category: z.string().default('all').catch('all'),
})

export const Route = createFileRoute('/page')({
validateSearch: searchSchema,
component: MyPage,
})

문제: 유효하지 않은 URL 매개변수로 앱이 중단됨

원인: 오류 상황에 대한 대체값 처리를 사용하지 않았습니다.

해결 방법: 안전한 기본값을 제공하도록 대체값을 사용합니다.

// ❌ No fallback handling
const schema = z.object({
page: z.number().default(1), // Will throw on invalid input
})

// ✅ Graceful fallback handling
const schema = z.object({
page: z.number().default(1).catch(1), // Safe fallback to 1
})

문제: TypeScript에서 선택적 매개변수가 필수로 처리됨

원인: .default()를 사용하면 탐색에서 매개변수가 필수가 됩니다.

해결 방법: 실제로 선택적인 매개변수에는 .optional()을 사용합니다.

const schema = z.object({
// Required with default (navigation can omit, but always present in component)
page: z.number().default(1).catch(1),

// Truly optional (can be undefined in component)
searchTerm: z.string().optional(),
})

문제: 복잡한 객체가 검증되지 않음

원인: 중첩 객체에는 명시적인 스키마 정의가 필요합니다.

해결 방법: 완전한 중첩 스키마를 정의합니다.

const schema = z.object({
filters: z
.object({
status: z.enum(['active', 'inactive']).optional(),
tags: z.string().array().optional(),
dateRange: z
.object({
start: z.string().pipe(z.coerce.date()),
end: z.string().pipe(z.coerce.date()),
})
.optional(),
})
.prefault({})
.catch({}),
})

일반적인 다음 단계

기본 검색 매개변수를 설정한 후 다음 작업을 고려할 수 있습니다.