타입이 지정된 상태, 사는 곳은 URL
useUrlState 는 자신을 쿼리 문자열에 기록하는 React 상태입니다. 객체, 배열, 날짜는 타입을 유지하고, 모든 상태는 공유 가능한 링크가 되며, 새로고침 후에도 유지됩니다. 프로바이더도 보일러플레이트도 필요 없습니다.
- ~2 KB gzip 압축
- 의존성 없음
- TypeScript 우선
- Next.js / react-router / Remix
- MIT
npm i state-in-urlstate-in-url vs nuqs
nuqs 대안을 찾고 계신가요? 둘 다 타입이 유지되는 상태를 쿼리 문자열에 저장하지만, 필요한 설정량과 값으로 담을 수 있는 것이 다릅니다.
| 항목 | state-in-url | nuqs |
|---|---|---|
| 설정 | 불필요 — 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
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}'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 })}
/>
);
};'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, 의존성 0 | Next.js, React Router, Remix에서 설정 없는 타입 객체를 원할 때 |
| nuqs | 어댑터 컴포넌트, 키별 파서 | JSON 파서에 자체 검증 추가 | 약 6.7 KB gzip, 의존성 1 | 값마다 읽기 쉬운 쿼리 파라미터를 원할 때 |
| TanStack Router | 라우트마다 validateSearch | 객체·배열은 JSON-first, 날짜는 직접 직렬화 | 라우터에 내장 | TanStack Router 사용 중 — 내장 기능 사용 |
| use-query-params | Provider와 라우터 어댑터, 파라미터별 설정 | 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에서 의미가 있습니다.
