타입이 지정된 상태, 사는 곳은 URL

useUrlState 는 자신을 쿼리 문자열에 기록하는 React 상태입니다. 객체, 배열, 날짜는 타입을 유지하고, 모든 상태는 공유 가능한 링크가 되며, 새로고침 후에도 유지됩니다. 프로바이더도 보일러플레이트도 필요 없습니다.

  • ~2 KB gzip 압축
  • 의존성 없음
  • TypeScript 우선
  • Next.js / react-router / Remix
  • MIT
npm i state-in-url

state-in-url vs nuqs

nuqs 대안을 찾고 계신가요? 둘 다 타입이 유지되는 상태를 쿼리 문자열에 저장하지만, 필요한 설정량과 값으로 담을 수 있는 것이 다릅니다.

항목state-in-urlnuqs
설정불필요 — hook을 import하면 끝어댑터 컴포넌트로 앱을 감싸야 함
상태 형태React.useState 같은 타입 객체 하나키별 값, 각 키마다 파서 선언
컴포넌트 간 재사용hook을 한 번 감싸면 — 모든 컴포넌트가 상태 공유, props 불필요파서 맵을 감싸는 hook은 직접 추출
중첩 객체와 배열기본 지원 — 구조와 타입 유지JSON 파서에 자체 런타임 검증 추가 필요
날짜자동으로 유지내장 파서를 키마다 선언
크기, 전체 import약 2.9 KB gzip약 6.7 KB gzip
런타임 의존성없음1개
라우터Next.js, React Router v6/v7, Remix, 순수 JS 헬퍼Next.js, React Router, Remix, TanStack Router, 순수 React

크기: 라이브러리 전체 import, esbuild minify + gzip, 2026년 8월 nuqs 2.10.1 기준 측정.

nuqs도 훌륭한 라이브러리입니다. 값마다 읽기 쉬운 쿼리 파라미터를 원하거나 TanStack Router를 쓴다면 nuqs를, 타입 객체 전체를 설정 없이 URL에 넣고 싶다면 state-in-url을 선택하세요.

같은 기능, 두 가지 구현

필터 패널: 검색어, 페이지 번호, 태그 목록, 날짜. nuqs는 키마다 파서를 선언하고 루트에 어댑터를 연결하며, state-in-url은 객체를 받아 재사용 가능한 hook 하나로 감쌉니다.

app/layout.tsx (nuqs)
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}
filters.tsx (nuqs)
'use client';
import {
  useQueryStates,
  parseAsString,
  parseAsInteger,
  parseAsArrayOf,
  parseAsIsoDateTime,
} from 'nuqs';

export const Filters = () => {
  const [filters, setFilters] = useQueryStates({
    q: parseAsString.withDefault(''),
    page: parseAsInteger.withDefault(1),
    tags: parseAsArrayOf(parseAsString).withDefault([]),
    since: parseAsIsoDateTime,
  });

  return (
    <input
      value={filters.q}
      onChange={(ev) => setFilters({ q: ev.target.value, page: 1 })}
    />
  );
};
filters.tsx (state-in-url)
'use client';
import { useUrlState } from 'state-in-url/next';

export const filters = {
  q: '',
  page: 1,
  tags: [] as string[],
  since: undefined as Date | undefined,
};

// One reusable hook = the whole API for this feature
export const useFilters = () => useUrlState(filters);

export const SearchBox = () => {
  const { urlState, setUrl } = useFilters();

  return (
    <input
      value={urlState.q}
      onChange={(ev) => setUrl({ q: ev.target.value, page: 1 })}
    />
  );
};

export const ActiveTags = () => {
  // Same state, another component - no props, no context
  const { urlState } = useFilters();

  return <>{urlState.tags.join(', ')}</>; // tags is still string[]
};

이 커스텀 hook 하나가 그 기능의 API 전부입니다. 호출하는 모든 컴포넌트가 같은 타입 상태를 공유합니다 — 태그 목록은 배열 그대로, 날짜는 진짜 Date 객체로 돌아옵니다. props도, context도, 키별 배선도 필요 없습니다.

설정과 보일러플레이트

nuqs는 앱을 감싸는 어댑터 컴포넌트로 라우터에 연결되고, 상태 조각마다 파서를 선언합니다. state-in-url은 라우터별 hook을 제공합니다 — 맞는 것을 import하고 기본 상태 객체를 넘기면 끝. 아무것도 감싸지 않습니다.

Next.js, SSR, 프리렌더링

App Router에서 state-in-url은 useSearchParams를 호출하지 않으므로, 사용하는 컴포넌트에 Suspense 경계가 필요 없고 페이지는 계속 프리렌더링됩니다 — PPR 포함. 서버 컴포넌트는 그대로 전달된 searchParams prop으로 같은 상태를 읽습니다.

nuqs에서 마이그레이션

대부분 기계적입니다: 한 기능의 키들을 하나의 기본 상태 객체로 모으고, 파서 선언을 제거하고 — 타입 있는 값이 같은 정보를 담습니다 — 키별 setter를 partial을 받는 setter 하나로 바꿉니다. 최상위 필드는 여전히 각자의 쿼리 파라미터에 대응합니다.

다른 선택지 비교

nuqs만이 대안은 아닙니다. 같은 일 — 쿼리 문자열에 타입 상태 넣기 — 은 라우터 내장 기능과 더 오래된 라이브러리로도 가능하며, 각자 트레이드오프가 있습니다.

라이브러리설정중첩 객체와 날짜크기이럴 때 선택
state-in-url불필요 — hook만 import타입까지 자동 유지약 2.9 KB gzip, 의존성 0Next.js, React Router, Remix에서 설정 없는 타입 객체를 원할 때
nuqs어댑터 컴포넌트, 키별 파서JSON 파서에 자체 검증 추가약 6.7 KB gzip, 의존성 1값마다 읽기 쉬운 쿼리 파라미터를 원할 때
TanStack Router라우트마다 validateSearch객체·배열은 JSON-first, 날짜는 직접 직렬화라우터에 내장TanStack Router 사용 중 — 내장 기능 사용
use-query-paramsProvider와 라우터 어댑터, 파라미터별 설정JSON 파라미터 타입 경유, 느슨한 타입약 4.4 KB gzip + serialize-query-params이미 이 라이브러리로 지어진 코드베이스
useSearchParams불필요 — 라우터 내장문자열만 — 파싱·타입·기본값 모두 직접0 KB평평한 문자열 파라미터 한두 개뿐일 때

자주 묻는 질문

state-in-url은 좋은 nuqs 대안인가요?
네, 타입 객체 전체를 설정 없이 URL에 넣고 싶다면요: 어댑터 컴포넌트도, 키별 파서도 없고, 중첩 객체와 날짜는 자동으로 유지됩니다. 값마다 읽기 쉬운 쿼리 파라미터를 원하거나 TanStack Router를 쓴다면 여전히 nuqs가 낫습니다.
state-in-url과 nuqs 중 무엇이 더 작나요?
2026년 8월 esbuild(minify + gzip, 라이브러리 전체 import)로 측정: state-in-url은 약 2.9 KB에 런타임 의존성 0개, nuqs 2.10.1은 약 6.7 KB에 의존성 1개. 일부만 import하면 둘 다 줄어듭니다.
state-in-url에 어댑터나 프로바이더가 필요한가요?
아니요. 라우터마다 전용 진입점이 있습니다 — 맞는 hook을 import하고 기본 상태 객체를 넘기면 동작합니다. 앱을 감싸는 어댑터 컴포넌트도, 설정할 context 프로바이더도 없습니다.
nuqs에서 state-in-url로 옮기기 어렵나요?
대개 아닙니다: 한 기능의 키들을 하나의 기본 상태 객체로 모으고, 파서 선언을 제거하고, 키별 setter를 partial을 받는 setter 하나로 바꿉니다. 최상위 필드는 여전히 각자의 쿼리 파라미터에 대응합니다.
TanStack Router의 search params는요?
TanStack Router를 쓴다면 내장 기능을 쓰세요: 라우트마다 validateSearch로 검증하는 JSON-first search params입니다. state-in-url과 nuqs는 타입 search params가 내장되지 않은 Next.js, React Router, Remix에서 의미가 있습니다.