검색 파라미터는 사실상 UI 상태를 직렬화한 표현이다.
예를 들어, 사용자가 상품 목록을 보고 있을 때
- 필터: filter=active
- 정렬: sort=asc
- 페이지네이션: page=2
이 값들이 URL에 들어가면 아래와 같은 장점이 생긴다.
- 브라우저 히스토리 지원: 뒤/앞으로 가기 시 동일한 상태를 복원할 수 있다.
- 딥링크 가능: 특정 상태를 URL로 공유하면 다른 사용자가 동일한 화면을 볼 수 있다.
- 공유 가능: URL을 복사하여 다른 사용자에게 보내면 동일한 필터와 정렬 상태가 유지된다.
왜 대부분의 앱은 문자열 파싱에 의존할까?
window.location.search나 URLSearchParams로 접근하는 방식이 대부분이다.
이 방식은 단순하지만 한계/문제가 있다.
타입 안전성 부족
모든 값이 문자열로 들어오기 때문에 숫자나 boolean은 직접 변환해야 한다. 때문에 변환 로직을 매번 작성해야 하고, 실수하면 런타임 오류가 발생한다.
중복 로직
각 컴포넌트에서 파라미터를 파싱하고 검증하는 코드가 반복된다. 예를 들면, 필터 컴포넌트, 정렬 컴포넌트, 페이지네이션 컴포넌트가 각각 URLSearchParams를 파싱해야 한다.
스키마 불일치
어떤 컴포넌트는 sort를 'asc' | 'desc'로 기대하지만, 다른 컴포넌트는 'asc' | 'desc' | 'random'을 허용할 수도 있다. 때문에 타입 드리프트 발생되면서 버그로 이어질 수 있다.
취약한 유틸리티
대부분의 앱은 간단한 헬퍼 함수로 파라미터를 직렬화/파싱한다. 하지만 배열, 중첩 객체, 복잡한 타입을 지원하지 못한다. 예: tags=["react","typescript"] → tags=react,typescript (단순 콤마 구분)
결과적으로 생기는 문제
- 런타임 오류: 잘못된 타입 변환, 누락된 기본값
- 예측 불가능한 버그: 서로 다른 컴포넌트가 다른 스키마를 사용
- 유지보수 어려움: 라우트 변경 시 관련된 모든 파싱 로직을 찾아 수정해야 함
- 테스트 복잡성 증가: 각 컴포넌트마다 파라미터 처리 로직이 분산되어 있음
해결책
검색 파라미터를 라우트 스코프에서 타입 안전하게 관리하기
TanStack Router의 접근
TanStack Router는 검색 파라미터 스키마를 라우트 정의에 내재화한다.
즉, 각 라우트가 자신이 허용하는 검색 파라미터를 명시적으로 선언하고, 타입 추론, 유효성 검증, 기본값 관리, 반응형 렌더링을 제공한다.
코드 예제 (React + TanStack Router + Zod)
1. 라우트 정의에서 검색 파라미터 스키마 선언
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
// /products 라우트 정의
export const Route = createFileRoute('/products')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']).default('asc'),
page: z.number().default(1),
filter: z.string().optional(),
}),
});
- 단일 진실 공급원 (SSOT) -> 스키마가 라우트에 포함되므로 중복 없음
- 기본값 (default)도 스키마에서 관리 -> 누락된 값 자동 처리
2. 타입 안전한 네비게이션
import { Link, useNavigate } from '@tanstack/react-router';
function ProductList() {
const navigate = useNavigate();
return (
<>
// ✅ 타입 안전
<Link to="/products" search={{ sort: 'desc', page: 2 }}>Sort Desc</Link>
<button onClick={() => navigate({
search: prev => ({ ...prev, page: prev.page + 1 }), // ✅ 리듀서 스타일
})
}>
Next Page
</button>
</>
);
}
- sort는 'asc' | 'desc' 외 값 불가 → 컴파일 타임에서 오류 방지
- page는 숫자 타입 보장
- 상태 업데이트는 리듀서 스타일로 안전하게 처리
3. 계층적 확장 (부모 -> 자식)
// 부모 라우트
export const Route = createFileRoute('/dashboard')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']).default('asc'),
}),
});
// 자식 라우트
export const Route = createFileRoute('/dashboard/$id')({
validateSearch: z.object({
filter: z.string().optional(),
// ✅ sort는 부모에서 상속됨
}),
});
- 부모 스키마를 자식이 확장 가능
- 잘못된 타입 재정의 시 → 컴파일 오류 발생
- 자식 라우트에서 상속받은 파라미터를 재정의하려고 하면 타입 오류 발생
TanStack Router의 핵심 장점
- 타입 안전성: 런타임 + 컴파일 타임 모두 보장
- 단일 진실 공급원: 스키마가 라우트 정의에 포함 → 중복 제거
- 계층적 확장: 부모-자식 라우트 간 일관성 유지
- 반응형 렌더링: 필요한 컴포넌트만 리렌더링 → 성능 최적화
이상 스텔라였습니다 ✍🏻

'TECH' 카테고리의 다른 글
| useOptimistic: React 19의 새로운 기능 🆕 (0) | 2025.12.17 |
|---|---|
| 5가지 노드 버전 관리자 비교 (0) | 2025.12.04 |
| 아이콘 현지화 (localization) (0) | 2025.12.02 |
| 논리 할당 연산자 (0) | 2025.11.20 |
| Vite+ (0) | 2025.11.19 |