HooksuseQueryParams

useQueryParams

URL 쿼리 νŒŒλΌλ―Έν„°λ₯Ό μ„ μ–Έμ μœΌλ‘œ κ΄€λ¦¬ν•˜λŠ” React Hookμž…λ‹ˆλ‹€.

μ–Έμ œ μ‚¬μš©ν•˜λ‚˜μš”?

  • πŸ” 검색/필터링 (νŽ˜μ΄μ§€λ„€μ΄μ…˜, μ •λ ¬)
  • πŸ”— URL 곡유 (μƒνƒœ μœ μ§€)
  • ⬅️ λΈŒλΌμš°μ € λ’€λ‘œκ°€κΈ° 지원
  • πŸ“Š νƒ­/λ·° μƒνƒœ 관리

μ£Όμš” νŠΉμ§•

1. νƒ€μž… μ•ˆμ „μ„±

μŠ€ν‚€λ§ˆ 기반 νŒŒμ„œλ‘œ νƒ€μž… μžλ™ μΆ”λ‘ 

2. 선언적 API

Zod μŠ€νƒ€μΌμ˜ λΉŒλ” νŒ¨ν„΄

3. SSR 지원

Next.js λ“± SSR ν™˜κ²½μ—μ„œ μ•ˆμ „ν•˜κ²Œ λ™μž‘

4. ν”„λ ˆμž„μ›Œν¬ μ–΄λŒ‘ν„°

Browser, Next.js, React Router λ“± λ‹€μ–‘ν•œ ν™˜κ²½ 지원


κΈ°λ³Έ μ‚¬μš©λ²•

import {
  useQueryParams,
  parseAsInteger,
  parseAsString,
  parseAsBoolean,
  parseAsStringEnum,
} from '@frontend-toolkit-js/hooks';
 
const sortOptions = ['latest', 'oldest', 'popular'] as const;
 
function ProductList() {
  const { page, search, sort, showSoldOut, setParams } = useQueryParams({
    page: parseAsInteger.withDefault(1),
    search: parseAsString.withDefault(''),
    sort: parseAsStringEnum(sortOptions).withDefault('latest'),
    showSoldOut: parseAsBoolean.withDefault(false),
  });
 
  return (
    <div>
      {/* ν˜„μž¬ κ°’ μ‚¬μš© */}
      <p>ν˜„μž¬ νŽ˜μ΄μ§€: {page}</p>
      <p>검색어: {search}</p>
 
      {/* κ°’ μ—…λ°μ΄νŠΈ */}
      <button onClick={() => setParams({ page: page + 1 })}>
        λ‹€μŒ νŽ˜μ΄μ§€
      </button>
 
      {/* μ—¬λŸ¬ κ°’ λ™μ‹œ μ—…λ°μ΄νŠΈ */}
      <button onClick={() => setParams({ search: '', page: 1 })}>
        검색 μ΄ˆκΈ°ν™”
      </button>
    </div>
  );
}

Parsers

κΈ°λ³Έ νƒ€μž…

Parserνƒ€μž…μ˜ˆμ‹œ
parseAsStringstring?name=hello β†’ 'hello'
parseAsIntegernumber?page=5 β†’ 5
parseAsFloatnumber?price=19.99 β†’ 19.99
parseAsBooleanboolean?active=true β†’ true

λ‚ μ§œ

Parserνƒ€μž…μ˜ˆμ‹œ
parseAsIsoDateDate?date=2024-01-15 β†’ Date
parseAsIsoDateTimeDate?date=2024-01-15T09:30:00Z β†’ Date

볡합 νƒ€μž…

Parserνƒ€μž…μ˜ˆμ‹œ
parseAsStringEnum(values)T?sort=latest β†’ 'latest'
parseAsLiteral(values)TparseAsStringEnum의 별칭
parseAsJson<T>()T?data={"a":1} β†’ { a: 1 }

λ°°μ—΄

Parserνƒ€μž…μ˜ˆμ‹œ
parseAsArray(parser)T[]?tags=a,b,c β†’ ['a', 'b', 'c']
parseAsNativeArray(parser)T[]?id=1&id=2 β†’ [1, 2]

Default Values

.withDefault()λ₯Ό μ‚¬μš©ν•˜λ©΄:

  1. 쿼리 νŒŒλΌλ―Έν„°κ°€ 없을 λ•Œ κΈ°λ³Έκ°’ μ‚¬μš©
  2. νƒ€μž…μ΄ T | nullμ—μ„œ T둜 λ³€κ²½ (null 제거)
// withDefault 없이: string | null
const { name } = useQueryParams({
  name: parseAsString,
});
 
// withDefault μ‚¬μš©: string
const { name } = useQueryParams({
  name: parseAsString.withDefault(''),
});

History Options

// replace (κΈ°λ³Έκ°’): νžˆμŠ€ν† λ¦¬μ— μŒ“μ΄μ§€ μ•ŠμŒ
setParams({ page: 2 });
setParams({ page: 2 }, { history: 'replace' });
 
// push: νžˆμŠ€ν† λ¦¬μ— μΆ”κ°€ (λ’€λ‘œκ°€κΈ°λ‘œ 이전 μƒνƒœ 볡원 κ°€λŠ₯)
setParams({ page: 2 }, { history: 'push' });
μ˜΅μ…˜μ„€λͺ…μ‚¬μš© 사둀
replaceν˜„μž¬ νžˆμŠ€ν† λ¦¬ ν•­λͺ© ꡐ체필터 λ³€κ²½, νŽ˜μ΄μ§€λ„€μ΄μ…˜
pushμƒˆ νžˆμŠ€ν† λ¦¬ ν•­λͺ© μΆ”κ°€νƒ­ μ „ν™˜, μ€‘μš”ν•œ μƒνƒœ λ³€κ²½

Framework Adapters

ν”„λ ˆμž„μ›Œν¬λ³„ μ–΄λŒ‘ν„°λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. Provider νŒ¨ν„΄μœΌλ‘œ μ•± μ „μ—­μ—μ„œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Next.js App Router

// app/providers.tsx
'use client';
 
import { Suspense, type ReactNode } from 'react';
import { QueryParamsProvider } from '@frontend-toolkit-js/hooks';
import { useNextAppAdapter } from '@frontend-toolkit-js/hooks/useQueryParams/adapters/next-app';
 
function QueryParamsProviderInner({ children }: { children: ReactNode }) {
  const adapter = useNextAppAdapter();
  return (
    <QueryParamsProvider adapter={adapter}>
      {children}
    </QueryParamsProvider>
  );
}
 
export function Providers({ children }: { children: ReactNode }) {
  return (
    <Suspense fallback={null}>
      <QueryParamsProviderInner>{children}</QueryParamsProviderInner>
    </Suspense>
  );
}
// app/layout.tsx
import { Providers } from './providers';
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Note: useSearchParamsλ₯Ό μ‚¬μš©ν•˜λ―€λ‘œ Suspense둜 감싸야 ν•©λ‹ˆλ‹€.

Next.js Pages Router

// _app.tsx
import { QueryParamsProvider } from '@frontend-toolkit-js/hooks';
import { useNextPagesAdapter } from '@frontend-toolkit-js/hooks/useQueryParams/adapters/next-pages';
 
function MyApp({ Component, pageProps }) {
  const adapter = useNextPagesAdapter({ shallow: true });
  return (
    <QueryParamsProvider adapter={adapter}>
      <Component {...pageProps} />
    </QueryParamsProvider>
  );
}

React Router v6+

// App.tsx
import { BrowserRouter } from 'react-router-dom';
import { QueryParamsProvider } from '@frontend-toolkit-js/hooks';
import { useReactRouterAdapter } from '@frontend-toolkit-js/hooks/useQueryParams/adapters/react-router';
 
function Providers({ children }) {
  const adapter = useReactRouterAdapter();
  return (
    <QueryParamsProvider adapter={adapter}>
      {children}
    </QueryParamsProvider>
  );
}
 
function App() {
  return (
    <BrowserRouter>
      <Providers>
        {/* 라우트 */}
      </Providers>
    </BrowserRouter>
  );
}

μ–΄λŒ‘ν„° μ˜΅μ…˜

μ–΄λŒ‘ν„°μ˜΅μ…˜κΈ°λ³Έκ°’μ„€λͺ…
useNextAppAdapterscrollfalseURL λ³€κ²½ μ‹œ 슀크둀 μ΄ˆκΈ°ν™”
useNextPagesAdapterscrollfalseURL λ³€κ²½ μ‹œ 슀크둀 μ΄ˆκΈ°ν™”
shallowtruegetServerSideProps μž¬μ‹€ν–‰ λ°©μ§€
useReactRouterAdapterpreventScrollResetfalse슀크둀 μœ„μΉ˜ μœ μ§€

Provider νŒ¨ν„΄

Providerλ₯Ό μ‚¬μš©ν•˜λ©΄ μ•± μ „μ—­μ—μ„œ μ–΄λŒ‘ν„°λ₯Ό κ³΅μœ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

// Provider μ„€μ • ν›„, μ»΄ν¬λ„ŒνŠΈμ—μ„œ adapter μƒλž΅ κ°€λŠ₯
const { page } = useQueryParams({
  page: parseAsInteger.withDefault(1),
});
// adapter μ˜΅μ…˜ λΆˆν•„μš”!

Provider 없이 직접 μ–΄λŒ‘ν„°λ₯Ό 전달할 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€:

import { createQueryParamsBrowserAdapter } from '@frontend-toolkit-js/hooks';
 
const { page } = useQueryParams(
  { page: parseAsInteger.withDefault(1) },
  { adapter: createQueryParamsBrowserAdapter() }
);

API Reference

useQueryParams(schema, options?)

Parameters

μ΄λ¦„νƒ€μž…μ„€λͺ…
schemaRecord<string, Parser>νŒŒμ„œ μŠ€ν‚€λ§ˆ 객체
options.adapterQueryParamsAdapterμ»€μŠ€ν…€ μ–΄λŒ‘ν„° (Provider μ‚¬μš© μ‹œ μƒλž΅ κ°€λŠ₯)

Returns

μ΄λ¦„νƒ€μž…μ„€λͺ…
[key]TμŠ€ν‚€λ§ˆμ— μ •μ˜λœ 각 νŒŒλΌλ―Έν„° κ°’
setParams(params, options?) => voidνŒŒλΌλ―Έν„° μ—…λ°μ΄νŠΈ ν•¨μˆ˜

setParams(params, options?)

μ˜΅μ…˜νƒ€μž…κΈ°λ³Έκ°’μ„€λͺ…
history'push' | 'replace''replace'νžˆμŠ€ν† λ¦¬ 처리 방식

μ£Όμ˜μ‚¬ν•­

⚠️ SSR ν™˜κ²½μ—μ„œ Hydration

SSR ν™˜κ²½μ—μ„œλŠ” μ„œλ²„μ™€ ν΄λΌμ΄μ–ΈνŠΈμ˜ 초기 값이 λ‹€λ₯Ό 수 μžˆμŠ΅λ‹ˆλ‹€. μ–΄λŒ‘ν„°κ°€ 이λ₯Ό μžλ™μœΌλ‘œ μ²˜λ¦¬ν•©λ‹ˆλ‹€.

⚠️ λ°°μ—΄ νŒŒμ„œ 선택

// 콀마 ꡬ뢄 (ꢌμž₯): ?tags=a,b,c
parseAsArray(parseAsString)
 
// λ„€μ΄ν‹°λΈŒ 방식: ?id=1&id=2&id=3
parseAsNativeArray(parseAsInteger)

⚠️ Provider μœ„μΉ˜

Next.js App Routerμ—μ„œλŠ” Suspense 내뢀에 Providerλ₯Ό λ°°μΉ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

// βœ… μ˜¬λ°”λ₯Έ 방법
<Suspense fallback={null}>
  <QueryParamsProviderInner>
    {children}
  </QueryParamsProviderInner>
</Suspense>

TypeScript

λͺ¨λ“  νƒ€μž…μ΄ μžλ™μœΌλ‘œ μΆ”λ‘ λ©λ‹ˆλ‹€.

const { page, search, tags } = useQueryParams({
  page: parseAsInteger.withDefault(1),
  search: parseAsString.withDefault(''),
  tags: parseAsArray(parseAsString).withDefault([]),
});
 
page;    // number (not null)
search;  // string (not null)
tags;    // string[] (not null)

withDefault 없이 μ‚¬μš©ν•˜λ©΄ null이 ν¬ν•¨λ©λ‹ˆλ‹€:

const { page } = useQueryParams({
  page: parseAsInteger,
});
 
page;  // number | null

κ΄€λ ¨ λ¬Έμ„œ

  • useFunnel - 단계별 UI 흐름 관리