Docs
Guide
Search Params

Search Params

TanStack Query가 React 애플리케이션에서 서버 상태를 쉽게 처리할 수 있도록 만든 것처럼, TanStack Router는 애플리케이션에서 URL 검색 파라미터의 기능을 활용할 수 있도록 돕습니다.

Why not just use URLSearchParams?

"플랫폼을 사용하라"는 말을 자주 들으셨겠지만, 대부분 동의하면서도 고급 사용 사례에서는 플랫폼의 한계를 인정하는 것이 중요하다고 생각합니다. URLSearchParams가 바로 그런 한계를 보여주는 예라고 믿습니다.

전통적인 검색 파라미터 API는 보통 다음과 같은 가정을 합니다:

  • 검색 파라미터는 항상 문자열이다.
  • 검색 파라미터는 대체로 플랫하다.
  • URLSearchParams를 사용한 직렬화 및 역직렬화가 충분하다. (스포일러: 충분하지 않습니다.)
  • 검색 파라미터 수정은 URL의 경로명과 밀접하게 연결되어 있으며, 경로명이 변경되지 않아도 함께 업데이트되어야 한다.

그러나 실제로는 이러한 가정과는 다릅니다.

  • 검색 파라미터는 애플리케이션 상태를 나타내므로, 다른 상태 관리 도구와 동일한 개발자 경험(DX)을 기대하게 됩니다. 이는 원시 값 유형을 구분하고 중첩 배열 및 객체와 같은 복잡한 데이터 구조를 효율적으로 저장하고 조작할 수 있는 기능을 포함합니다.
  • 상태를 직렬화하고 역직렬화하는 방법은 여러 가지가 있으며, 각각 다른 트레이드오프가 있습니다. 애플리케이션에 적합한 방법을 선택하거나 최소한 URLSearchParams보다 나은 기본값을 선택할 수 있어야 합니다.
  • 불변성과 구조적 공유. URL 검색 파라미터를 문자열화하거나 파싱할 때마다 참조 무결성과 객체 동일성이 손실됩니다. 이는 React와 같은 프레임워크에서 예기치 않은 성능 문제를 야기할 수 있습니다.
  • 검색 파라미터는 URL 경로명과 독립적으로 자주 변경됩니다. 예를 들어, 페이지네이션된 목록에서 페이지 번호를 변경할 때 URL 경로명은 변경되지 않을 수 있습니다.

Search Params, the "OG" State Manager

검색 파라미터는 URL에서 ?page=3 또는 ?filter-name=tanner와 같이 표시됩니다. 이는 URL 내에서 글로벌 상태의 한 형태라는 점을 부인할 수 없습니다. URL에 특정 상태를 저장하는 것은 유용합니다. 다음과 같은 이유 때문입니다:

  • 사용자들은 다음을 할 수 있어야 합니다:
    • Cmd/Ctrl + 클릭으로 링크를 새 탭에서 열고 기대한 상태를 신뢰할 수 있어야 합니다.
    • 애플리케이션의 링크를 북마크하거나 공유했을 때, 복사한 당시의 상태가 유지되어야 합니다.
    • 애플리케이션을 새로고침하거나 페이지 간 이동 시 상태를 잃지 않아야 합니다.
  • 개발자들은 다음을 쉽게 할 수 있어야 합니다:
    • URL에서 상태를 추가, 제거 또는 수정할 수 있어야 합니다.
    • 애플리케이션에 안전한 형식과 타입으로 URL에서 검색 파라미터를 검증할 수 있어야 합니다.
    • 직렬화 형식을 걱정하지 않고 검색 파라미터를 읽고 쓸 수 있어야 합니다.

JSON-first Search Params

위 목표를 달성하기 위해 TanStack Router에는 URL 검색 문자열을 구조화된 JSON으로 자동 변환하는 강력한 검색 파라미터 파서가 내장되어 있습니다. 이를 통해 JSON-직렬화 가능한 모든 데이터 구조를 검색 파라미터에 저장할 수 있으며, JSON으로 파싱 및 직렬화됩니다. 이는 배열 및 중첩 데이터에 대한 지원이 제한적인 URLSearchParams보다 큰 개선점입니다.

예를 들어, 다음 경로로 이동하면:

const link = (
  <Link
    to="/shop"
    search={{
      pageIndex: 3,
      includeCategories: ["electronics", "gifts"],
      sortBy: "price",
      desc: true,
    }}
  />
);

다음과 같은 URL이 생성됩니다:

/shop?pageIndex=3&includeCategories=%5B%22electronics%22%2C%22gifts%22%5D&sortBy=price&desc=true

이 URL이 파싱되면, 검색 파라미터는 다음 JSON으로 정확히 변환됩니다:

{
  "pageIndex": 3,
  "includeCategories": ["electronics", "gifts"],
  "sortBy": "price",
  "desc": true
}

여기에서 몇 가지 중요한 점을 확인할 수 있습니다:

  • 검색 파라미터의 첫 번째 수준은 URLSearchParams와 마찬가지로 플랫하고 문자열 기반입니다.
  • 문자열이 아닌 첫 번째 수준 값들은 실제 숫자 및 불리언 값으로 정확히 보존됩니다.
  • 중첩된 데이터 구조는 URL 안전 JSON 문자열로 자동 변환됩니다.

🧠 일반적으로 다른 도구들은 검색 파라미터가 항상 플랫하고 문자열 기반이라고 가정하기 때문에, TanStack Router는 첫 번째 수준에서 URLSearchParams와 호환되도록 유지합니다. 따라서 TanStack Router가 JSON으로 검색 파라미터를 관리하더라도 다른 도구들은 여전히 URL에서 첫 번째 수준의 파라미터를 정상적으로 읽고 쓸 수 있습니다.

Validating and Typing Search Params

TanStack Router는 검색 파라미터를 신뢰할 수 있는 JSON으로 파싱할 수 있지만, 이들은 결국 사용자 입력의 원시 텍스트에서 비롯됩니다. 따라서 검색 파라미터를 사용하기 전에 애플리케이션이 신뢰하고 의존할 수 있는 형식으로 검증해야 합니다.

Enter Validation + TypeScript!

TanStack Router는 검색 파라미터를 검증하고 타입화할 수 있는 편리한 API를 제공합니다. 이는 RoutevalidateSearch 옵션으로 시작됩니다:

// /routes/shop.products.tsx
 
type ProductSearchSortOptions = "newest" | "oldest" | "price";
 
type ProductSearch = {
  page: number;
  filter: string;
  sort: ProductSearchSortOptions;
};
 
export const Route = createFileRoute("/shop/products")({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    // validate and parse the search params into a typed state
    return {
      page: Number(search?.page ?? 1),
      filter: (search.filter as string) || "",
      sort: (search.sort as ProductSearchSortOptions) || "newest",
    };
  },
});

위 예제에서는 allProductsRoute의 검색 파라미터를 검증한 후, 타입화된 ProductSearch 객체를 반환합니다. 이 타입화된 객체는 해당 경로의 다른 옵션뿐만 아니라 모든 하위 경로에서도 사용할 수 있습니다!

Validating Search Params

validateSearch 옵션은 JSON으로 파싱된(하지만 검증되지 않은) 검색 파라미터를 Record<string, unknown>로 전달받아 원하는 타입의 객체를 반환하는 함수입니다. 일반적으로, 잘못된 형식이거나 예기치 않은 검색 파라미터에 대해 합리적인 기본값을 제공하여 사용자 경험이 중단되지 않도록 하는 것이 좋습니다.

다음은 예시입니다:

// /routes/shop.products.tsx
 
type ProductSearchSortOptions = "newest" | "oldest" | "price";
 
type ProductSearch = {
  page: number;
  filter: string;
  sort: ProductSearchSortOptions;
};
 
export const Route = createFileRoute("/shop/products")({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    // 검색 파라미터를 검증하고 타입화된 상태로 변환합니다.
    return {
      page: Number(search?.page ?? 1),
      filter: (search.filter as string) || "",
      sort: (search.sort as ProductSearchSortOptions) || "newest",
    };
  },
});

다음은 Zod (opens in a new tab) 라이브러리를 사용하여 검색 파라미터를 검증하고 동시에 타입을 지정하는 예제입니다(다른 검증 라이브러리를 사용해도 괜찮습니다):

// /routes/shop.products.tsx
 
import { z } from "zod";
 
const productSearchSchema = z.object({
  page: z.number().catch(1),
  filter: z.string().catch(""),
  sort: z.enum(["newest", "oldest", "price"]).catch("newest"),
});
 
type ProductSearch = z.infer<typeof productSearchSchema>;
 
export const Route = createFileRoute("/shop/products")({
  validateSearch: (search) => productSearchSchema.parse(search),
});

validateSearchparse 속성을 가진 객체도 허용하기 때문에 다음과 같이 축약할 수 있습니다:

validateSearch: productSearchSchema;

위 예제에서, 우리는 Zod의 .catch() 수식을 사용하여 사용자에게 오류를 표시하는 대신 잘못된 검색 파라미터를 처리했습니다. 이는 잘못된 검색 파라미터로 인해 사용자 경험이 중단되지 않도록 하기 위한 것입니다. 그러나 때로는 오류 메시지를 표시하고 싶을 때가 있을 수 있습니다. 이 경우 .catch() 대신 .default()를 사용할 수 있습니다.

이 방식이 작동하는 이유는 validateSearch 함수가 오류를 던지기 때문입니다. 오류가 발생하면 해당 경로의 onError 옵션이 실행되고(error.routerCodeVALIDATE_SEARCH로 설정됨), 경로의 component 대신 errorComponent가 렌더링됩니다. 이를 통해 검색 파라미터 오류를 원하는 방식으로 처리할 수 있습니다.

Adapters

Zod (opens in a new tab)와 같은 라이브러리를 사용하여 검색 파라미터를 검증할 때, 검색 파라미터를 URL에 커밋하기 전에 transform을 사용하고 싶을 수 있습니다. 예를 들어, Zod의 일반적인 transformdefault입니다.

import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
 
const productSearchSchema = z.object({
  page: z.number().default(1),
  filter: z.string().default(""),
  sort: z.enum(["newest", "oldest", "price"]).default("newest"),
});
 
export const Route = createFileRoute("/shop/products/")({
  validateSearch: productSearchSchema,
});

위 코드에서 이 경로로 탐색할 때 search가 필수적이라는 점이 놀라울 수 있습니다. 다음과 같은 Linksearch가 누락되었기 때문에 타입 오류가 발생합니다.

<Link to="/shop/products" />

검증 라이브러리를 사용할 때, 올바른 inputoutput 타입을 추론하는 어댑터를 사용하는 것을 권장합니다.

Zod

Zod (opens in a new tab)를 위한 어댑터가 제공되며, 이를 통해 올바른 input 타입과 output 타입을 연결할 수 있습니다.

import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
 
const productSearchSchema = z.object({
  page: z.number().default(1),
  filter: z.string().default(""),
  sort: z.enum(["newest", "oldest", "price"]).default("newest"),
});
 
export const Route = createFileRoute("/shop/products/")({
  validateSearch: zodValidator(productSearchSchema),
});

여기에서 중요한 점은 Link 사용 시 더 이상 search 파라미터가 필요하지 않다는 것입니다.

<Link to="/shop/products" />

그러나 여기서 .catch를 사용하는 것은 타입을 덮어쓰며, page, filter, sortunknown으로 만들어 타입 손실을 야기합니다. 이를 해결하기 위해 검증 실패 시 fallback 값을 제공하면서 타입을 유지하는 fallback 제너릭 함수를 제공합니다.

import { createFileRoute } from "@tanstack/react-router";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
 
const productSearchSchema = z.object({
  page: fallback(z.number(), 1).default(1),
  filter: fallback(z.string(), "").default(""),
  sort: fallback(z.enum(["newest", "oldest", "price"]), "newest").default(
    "newest"
  ),
});
 
export const Route = createFileRoute("/shop/products/")({
  validateSearch: zodValidator(productSearchSchema),
});

따라서 이 경로로 탐색할 때, search는 선택 사항이 되며 올바른 타입을 유지합니다. 권장되지는 않지만, output 타입이 input 타입보다 더 정확한 경우 inputoutput 타입을 구성할 수도 있습니다.

const productSearchSchema = z.object({
  page: fallback(z.number(), 1).default(1),
  filter: fallback(z.string(), "").default(""),
  sort: fallback(z.enum(["newest", "oldest", "price"]), "newest").default(
    "newest"
  ),
});
 
export const Route = createFileRoute("/shop/products/")({
  validateSearch: zodValidator({
    schema: productSearchSchema,
    input: "output",
    output: "input",
  }),
});

이 방식은 탐색 시 사용할 타입과 검색 파라미터를 읽을 때 사용할 타입을 유연하게 구성할 수 있도록 제공합니다.

Valibot

[!WARNING] Router는 Valibot 1.0 패키지가 설치되어 있어야 합니다.

Valibot (opens in a new tab)을 사용할 때, 검색 파라미터를 탐색 및 읽기에 적합한 올바른 inputoutput 타입을 보장하기 위해 별도의 어댑터가 필요하지 않습니다. 이는 valibotStandard Schema (opens in a new tab)를 구현하기 때문입니다.

import { createFileRoute } from "@tanstack/react-router";
import * as v from "valibot";
 
const productSearchSchema = v.object({
  page: v.optional(v.fallback(v.number(), 1), 1),
  filter: v.optional(v.fallback(v.string(), ""), ""),
  sort: v.optional(
    v.fallback(v.picklist(["newest", "oldest", "price"]), "newest"),
    "newest"
  ),
});
 
export const Route = createFileRoute("/shop/products/")({
  validateSearch: productSearchSchema,
});

Arktype

[!WARNING] Router는 Arktype 2.0-rc 패키지가 설치되어 있어야 합니다.

ArkType (opens in a new tab)을 사용할 때, 검색 파라미터를 탐색 및 읽기에 적합한 올바른 inputoutput 타입을 보장하기 위해 별도의 어댑터가 필요하지 않습니다. 이는 ArkType (opens in a new tab)Standard Schema (opens in a new tab)를 구현하기 때문입니다.

import { createFileRoute } from "@tanstack/react-router";
import { type } from "arktype";
 
const productSearchSchema = type({
  page: "number = 1",
  filter: 'string = ""',
  sort: '"newest" | "oldest" | "price" = "newest"',
});
 
export const Route = createFileRoute("/shop/products/")({
  validateSearch: productSearchSchema,
});

Reading Search Params

검색 파라미터가 검증되고 타입화된 후, 이를 읽고 쓰는 작업을 시작할 준비가 완료됩니다. TanStack Router에서는 이를 수행하는 여러 가지 방법이 있습니다.

Using Search Params in Loaders

loaderDeps 옵션을 사용하여 로더에서 검색 파라미터를 읽는 방법에 대한 자세한 내용은 Search Params in Loaders 섹션을 참조하세요.

Search Params are inherited from Parent Routes

상위 경로의 검색 파라미터와 타입은 경로 트리를 따라 내려가며 병합되므로, 하위 경로에서도 상위 경로의 검색 파라미터에 접근할 수 있습니다:

  • shop.products.tsx
const productSearchSchema = z.object({
  page: z.number().catch(1),
  filter: z.string().catch(""),
  sort: z.enum(["newest", "oldest", "price"]).catch("newest"),
});
 
type ProductSearch = z.infer<typeof productSearchSchema>;
 
export const Route = createFileRoute("/shop/products")({
  validateSearch: productSearchSchema,
});
  • shop.products.$productId.tsx
export const Route = createFileRoute("/shop/products/$productId")({
  beforeLoad: ({ search }) => {
    search;
    // ^? ProductSearch ✅
  },
});

Search Params in Components

useSearch 훅을 통해 경로의 component에서 검증된 검색 파라미터에 접근할 수 있습니다.

// /routes/shop.products.tsx
 
export const Route = createFileRoute("/shop/products")({
  validateSearch: productSearchSchema,
});
 
const ProductList = () => {
  const { page, filter, sort } = Route.useSearch();
 
  return <div>...</div>;
};

[!TIP] 컴포넌트가 코드 분할되어 있는 경우, getRouteApi 함수를 사용하여 Route 설정을 가져오지 않고도 타입화된 useSearch() 훅에 접근할 수 있습니다.

Search Params outside of Route Components

useSearch 훅을 사용하여 애플리케이션의 어느 곳에서나 경로의 검증된 검색 파라미터에 접근할 수 있습니다. from ID 또는 경로를 전달하면 더욱 강력한 타입 안정성을 얻을 수 있습니다:

const allProductsRoute = createRoute({
  getParentRoute: () => shopRoute,
  path: "products",
  validateSearch: productSearchSchema,
});
 
// 다른 곳에서...
 
const routeApi = getRouteApi("/shop/products");
 
const ProductList = () => {
  const routeSearch = routeApi.useSearch();
 
  // OR
 
  const { page, filter, sort } = useSearch({
    from: allProductsRoute.fullPath,
  });
 
  return <div>...</div>;
};

혹은 strict: false를 전달하여 타입 안전성을 낮추고 선택적인 search 객체를 얻을 수도 있습니다:

function ProductList() {
  const search = useSearch({
    strict: false,
  });
  // {
  //   page: number | undefined
  //   filter: string | undefined
  //   sort: 'newest' | 'oldest' | 'price' | undefined
  // }
 
  return <div>...</div>;
}

Writing Search Params

이제 경로의 검색 파라미터를 읽는 방법을 배웠으니, 이를 수정하고 업데이트하는 기본 API에 대해 살펴보겠습니다.

<Link search />

검색 파라미터를 업데이트하는 가장 좋은 방법은 <Link /> 컴포넌트의 search 속성을 사용하는 것입니다.

현재 페이지의 검색을 업데이트하고 from 속성이 지정된 경우 to 속성을 생략할 수 있습니다.
예시는 다음과 같습니다:

// /routes/shop.products.tsx
export const Route = createFileRoute("/shop/products")({
  validateSearch: productSearchSchema,
});
 
const ProductList = () => {
  return (
    <div>
      <Link
        from={allProductsRoute.fullPath}
        search={(prev) => ({ page: prev.page + 1 })}
      >
        Next Page
      </Link>
    </div>
  );
};

여러 경로에서 렌더링되는 일반적인 컴포넌트에서 검색 파라미터를 업데이트하려면 from을 지정하기 어려울 수 있습니다.

이 경우 to="."를 설정하여 느슨하게 타입화된 검색 파라미터에 접근할 수 있습니다.
다음은 이를 설명하는 예제입니다:

// `page`는 __root 경로에 정의된 검색 파라미터이며 모든 경로에서 사용 가능합니다.
const PageSelector = () => {
  return (
    <div>
      <Link to="." search={(prev) => ({ ...prev, page: prev.page + 1 })}>
        Next Page
      </Link>
    </div>
  );
};

일반적인 컴포넌트가 경로 트리의 특정 하위 트리에서만 렌더링되는 경우, from을 사용하여 해당 하위 트리를 지정할 수 있습니다. 이 경우 to='.'를 생략할 수 있습니다.

// `page`는 /posts 경로에 정의된 검색 파라미터이며 모든 하위 경로에서 사용 가능합니다.
const PageSelector = () => {
  return (
    <div>
      <Link
        from="/posts"
        to="."
        search={(prev) => ({ ...prev, page: prev.page + 1 })}
      >
        Next Page
      </Link>
    </div>
  )

useNavigate(), navigate({ search })

navigate 함수도 <Link />search 속성과 동일한 방식으로 작동하는 search 옵션을 허용합니다:

// /routes/shop.products.tsx
export const Route = createFileRoute("/shop/products/$productId")({
  validateSearch: productSearchSchema,
});
 
const ProductList = () => {
  const navigate = useNavigate({ from: Route.fullPath });
 
  return (
    <div>
      <button
        onClick={() => {
          navigate({
            search: (prev) => ({ page: prev.page + 1 }),
          });
        }}
      >
        Next Page
      </button>
    </div>
  );
};

router.navigate({ search })

router.navigate 함수는 위의 useNavigate/navigate 훅/함수와 동일한 방식으로 작동합니다.

<Navigate search />

<Navigate search /> 컴포넌트도 위의 useNavigate/navigate 훅/함수와 동일하게 작동하지만, 함수 인수 대신 속성으로 옵션을 전달합니다.

Transforming search with search middlewares

링크의 href가 생성될 때, 기본적으로 쿼리 문자열 부분에 중요한 것은 <Link>search 속성입니다.

TanStack Router는 search middlewares를 통해 href 생성 전에 검색 파라미터를 조작할 수 있는 방법을 제공합니다.
Search middlewares는 경로 또는 하위 경로에 대한 새 링크를 생성할 때 검색 파라미터를 변환하는 함수입니다. 또한, 검색 검증 후 탐색 시 쿼리 문자열을 조작할 수 있도록 실행됩니다.

다음은 모든 링크에 대해 rootValue 검색 파라미터가 현재 검색 파라미터에 포함된 경우 이를 추가하도록 설정하는 예제입니다. 링크가 search 내부에 rootValue를 지정하면 해당 값이 링크 생성에 사용됩니다.

import { z } from "zod";
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
 
const searchSchema = z.object({
  rootValue: z.string().optional(),
});
 
export const Route = createRootRoute({
  validateSearch: zodValidator(searchSchema),
  search: {
    middlewares: [
      ({ search, next }) => {
        const result = next(search);
        return {
          rootValue: search.rootValue,
          ...result,
        };
      },
    ],
  },
});

이 사용 사례는 매우 일반적이므로 TanStack Router는 검색 파라미터를 유지하는 retainSearchParams를 제공합니다:

import { z } from "zod";
import { createFileRoute, retainSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
 
const searchSchema = z.object({
  rootValue: z.string().optional(),
});
 
export const Route = createRootRoute({
  validateSearch: zodValidator(searchSchema),
  search: {
    middlewares: [retainSearchParams(["rootValue"])],
  },
});

또 다른 일반적인 사용 사례는 기본값이 설정된 경우 링크에서 검색 파라미터를 제거하는 것입니다. TanStack Router는 이 사용 사례를 처리하기 위해 stripSearchParams를 제공합니다:

import { z } from "zod";
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
 
const defaultValues = {
  one: "abc",
  two: "xyz",
};
 
const searchSchema = z.object({
  one: z.string().default(defaultValues.one),
  two: z.string().default(defaultValues.two),
});
 
export const Route = createFileRoute("/hello")({
  validateSearch: zodValidator(searchSchema),
  search: {
    // 기본값 제거
    middlewares: [stripSearchParams(defaultValues)],
  },
});

여러 개의 미들웨어를 체인으로 연결할 수 있습니다. 다음은 retainSearchParamsstripSearchParams를 결합하는 예제입니다.

import {
  Link,
  createFileRoute,
  retainSearchParams,
  stripSearchParams,
} from "@tanstack/react-router";
import { z } from "zod";
import { zodValidator } from "@tanstack/zod-adapter";
 
const defaultValues = ["foo", "bar"];
 
export const Route = createFileRoute("/search")({
  validateSearch: zodValidator(
    z.object({
      retainMe: z.string().optional(),
      arrayWithDefaults: z.string().array().default(defaultValues),
      required: z.string(),
    })
  ),
  search: {
    middlewares: [
      retainSearchParams(["retainMe"]),
      stripSearchParams({ arrayWithDefaults: defaultValues }),
    ],
  },
});