React & Next.js용 타입 URL 상태 — useState처럼
useUrlState 는 자신을 쿼리 문자열에 기록하는 React 상태입니다. 객체, 배열, 날짜는 타입을 유지하고, 모든 상태는 공유 가능한 링크가 되며, 새로고침 후에도 유지되고 뒤로 가기 버튼도 동작합니다 — 프로바이더도, Suspense 경계도, 보일러플레이트도 필요 없습니다.
- ~2 KB gzip 압축
- 의존성 없음
- TypeScript 우선
- Next.js / react-router / Remix / Astro
- MIT
npm i state-in-urluseUrlState — 라이브로 시연: next.js
아래에 입력하세요 — URL이 빛나는 것을 보세요
URL에서 읽습니다 — props도 context도 없이, 타입과 구조가 유지됩니다
{name: stringage: undefinedundefinedagree_to_terms: falsebooleantags: []}
Next.js, React Router, Remix, Astro의 URL 상태 관리 — 같은 API
빠른 시작
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
// use `Type` not `Interface`!
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};'use client';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
// One hook per feature - the whole API for this state
// "searchParams" only needed to pass params from Server Components
export const useFormState = (searchParams?: object) =>
useUrlState(form, { searchParams });'use client';
import { useFormState } from './useFormState';
export const ComponentA = () => {
// see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
const { urlState, setState, setUrl } = useFormState();
return <>
<input
id="name"
value={urlState.name}
onChange={(ev) => setUrl({ name: ev.target.value })}
/>
// OR can update state immediately but sync change to url as needed
<input
value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
onBlur={() => setUrl()}
/>
<button onClick={() => setUrl((curr, initial) => initial)}>
Reset
</button>
</>
};'use client';
import { useFormState } from './useFormState';
// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
// same state as ComponentA - no props, no context
const { urlState } = useFormState(searchParams);
// will be defaultValue from `form` if not in url, no need to check
return <div>name: {urlState.name}</div>
};'use client';
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
searchParams,
});
// first navigation will push new history entry
// all following will just replace that entry
// this way will have history with only 2 entries - ['/url', '/url?key=param']
const replace = React.useRef(false);
const setUrl = React.useCallback((
state: Parameters<typeof setUrlBase>[0],
opts?: Parameters<typeof setUrlBase>[1]
) => {
setUrlBase(state, { replace: replace.current, ...opts });
replace.current = true;
}, [setUrlBase]);
return { urlState, setUrl, resetUrl: reset };
};AI 코딩 에이전트를 사용하시나요?
에이전트는 여기서 매번 같은 두 가지를 틀립니다. 상태 형태를 interface 로 입력하는데, 이는 JSONCompatible 제약이 즉시 거부합니다. 그리고 컴포넌트 안에서 기본 상태 객체를 만들어 공유를 조용히 깨뜨립니다. 객체 동일성으로 키가 지정되므로 오류가 발생하지 않고, 두 컴포넌트가 서로를 보지 못하게 될 뿐입니다.
그래서 이 패키지는 7개의 SKILL.md 파일을 제공합니다. 에이전트는 TanStack Intent 를 통해 필요할 때 하나를 로드합니다. 이 파일들은 이 페이지가 아니라 라이브러리와 함께 버전이 관리됩니다.
npx @tanstack/intent@latest install이미 state-in-url 이(가) 설치된 프로젝트에서 한 번 실행하세요. 그러면 에이전트가 node_modules/state-in-url/skills/ 에서 스킬을 찾습니다.
feature-state-hook상태 정의와 useUrlState를 기능 범위 훅으로 감싸기input-handling텍스트 입력, 슬라이더 등 빠르게 변하는 모든 것nextjs-ssrApp Router: searchParams 전달, 레이아웃용 Proxyreact-router-remix-setupReact Router v6/v7 또는 Remix v2 설정astro-setupAstro 아일랜드(React 또는 Preact), 혹은 클라이언트 프레임워크가 없는 페이지form-library-integrationreact-hook-form(또는 formik)과 함께 사용shared-state-no-urluseSharedState — URL을 건드리지 않고 공유
소스는 GitHub에 있습니다. Intent 스킬을 로드할 수 없는 에이전트는 대신 llms.txt 을 읽어야 합니다 — 같은 규칙이 한 파일에 압축되어 있습니다.
state-in-url vs nuqs
nuqs 대안을 찾고 계신가요? 둘 다 타입이 유지되는 상태를 쿼리 문자열에 저장하지만, 필요한 설정량과 값으로 담을 수 있는 것이 다릅니다.
| 항목 | state-in-url | nuqs |
|---|---|---|
| 설정 | Next.js, React Router v6/v7, Remix, Astro, 순수 JS 헬퍼 | 어댑터 컴포넌트로 앱을 감싸야 함 |
| 상태 형태 | 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을 선택하세요.
React의 URL 상태 — 자주 묻는 질문
- React 상태를 왜 URL에 저장하나요?
- 상태를 담은 URL은 공유 가능한 링크입니다: 새로고침하든, 북마크하든, 보내든 같은 필터, 탭, 페이지가 열립니다. 뒤로 가기와 앞으로 가기는 저절로 동작하고, 서로 관련 없는 컴포넌트가 프로바이더 없이 같은 값을 읽을 수 있습니다. state-in-url은 직접 파싱한 문자열 대신 타입 객체 하나로 이를 해냅니다.
- 어떤 상태를 URL에 넣어야 하나요?
- 사용자가 북마크하거나 공유할 만한 것 전부: 필터, 정렬, 페이지네이션, 활성 탭, 날짜 범위, 검색어. 비공개이거나, 너무 크거나, 순전히 일시적인 것은 넣지 마세요 — 인증 토큰, 다이얼로그 열림 여부, 마우스 위치 같은 것들입니다. 간단한 기준: 이 값이 들어간 링크를 공유해도 여전히 말이 되는가?
- state-in-url로 React에서 URL 파라미터를 읽고 쓰려면 어떻게 하나요?
- 기본 상태 객체를 넘겨 useUrlState를 호출하세요. urlState에는 이미 타입이 지정된 현재 값이 들어 있고, setUrl은 partial 객체를 쿼리 문자열에 기록하며, setState는 직접 flush하기 전까지 URL을 건드리지 않고 상태만 갱신합니다. 숫자, 불리언, 배열, 중첩 객체, Date는 들어간 타입 그대로 돌아옵니다.
- URL 상태는 페이지 새로고침 후에도 유지되나요?
- 네. 상태가 곧 쿼리 문자열이므로 새로고침, 북마크, 다른 곳에 붙여넣은 링크 모두 상태를 복원합니다. Next.js App Router에서는 페이지의 searchParams prop을 훅에 넘기세요. 그러면 첫 서버 렌더링부터 기본값 대신 올바른 값이 표시됩니다.
- Suspense 경계 없이 Next.js Server Components에서도 동작하나요?
- 네. 이 훅은 useSearchParams를 호출하지 않으므로, 사용하는 컴포넌트에 Suspense 경계가 필요 없고 페이지가 사전 렌더링에서 제외되지도 않습니다 — PPR 포함. Server Component는 searchParams prop으로 같은 상태를 읽고, 레이아웃은 proxy.ts에서 설정한 헤더에서 디코딩할 수 있습니다.
- react-hook-form이나 테이블 라이브러리를 URL과 동기화할 수 있나요?
- 네. 폼 라이브러리를 기준(source of truth)으로 두고, urlState를 기본값으로 넣어 초기화한 뒤, change 핸들러나 effect에서 setUrl로 변경 사항을 반영하세요. 같은 패턴이 TanStack Table 상태, 필터 패널, 그리고 값과 setter를 노출하는 모든 것에 통합니다.
- state-in-url은 어떤 프레임워크를 지원하나요?
- Next.js 14-16 App Router, React Router v6 및 v7, Remix v2, Astro 아일랜드(React 또는 Preact)를 각각 전용 진입점으로 지원합니다. 순수 JavaScript와 다른 모든 프레임워크는 encodeState, decodeState 헬퍼를 직접 사용할 수 있습니다. 의존성 없이 gzip 기준 ~2 KB입니다.
왜 state-in-url인가?
URL 상태 라이브러리는 존재하지만, 대부분은 설정이 번거롭거나 저장할 수 있는 것에 제한이 있습니다. state-in-url 은(는) 그냥 동작하는 것을 목표로 합니다. React.useState 을(를) 그대로 반영한 API로, URL을 저장소로 사용합니다.
보일러플레이트 없이 상태를 저장하고, 딥 링크를 만들고, 서로 관련 없는 클라이언트 컴포넌트 간에 데이터를 공유하세요. 프로바이더가 필요 없습니다. 구조와 타입은 처음부터 끝까지 유지됩니다. Date 이 들어가면 Date 이 나옵니다.
테스트 우선으로 구축되었으며, 유닛 테스트와 크로스 브라우저 e2e 스위트가 모든 커밋에서 실행됩니다.
Next.js: Suspense 경계 불필요
이 훅은 useSearchParams 을(를) 호출하지 않으므로, 이를 사용하는 컴포넌트는 Suspense 로 감쌀 필요가 없고, 페이지가 사전 렌더링에서 제외되지도 않습니다. PPR과 cacheComponents 도 포함됩니다. URL을 직접 읽고 이후의 모든 변경을 따라갑니다. 예를 들어 history.pushState 을(를), 그 존재조차 모르는 코드에서 호출한 경우도 따라갑니다.
Next.js나 react-router를 사용하지 않으시나요?
이 encodeState / decodeState 헬퍼는 어떤 프레임워크나 순수 JS에서도 동작합니다. 훅은 그 위의 편의 레이어입니다.
다음 GitHub 페이지 를 확인해 보세요. 스타 하나가 큰 힘이 됩니다.
다른 개발자와 공유하기

