프론트엔드 상태 관리와 URL의 재발견
프론트엔드 개발에서 상태 관리는 언제나 뜨거운 감자와 같고, 컴포넌트 로컬 상태부터 전역 상태, 서버 상태까지 다양한 종류의 상태를 어떻게 효율적이고 예측 가능하게 관리할 것인가는 서비스의 복잡성이 증가함에 따라 더욱 중요한 문제가 된다.
그리고 이를 위한 useState
, useReducer
, Context API
, Zustand
등 수많은 도구가 있지만, 사람들이 잠시 잊고 있었던 강력한 상태 저장소가 있는데, 바로 URL이다.
특히 웹 애플리케이션에서 URL의 쿼리 파라미터는 서버와의 통신뿐만 아니라, 클라이언트 측의 특정 상태를 표현하고 공유하는 데 매우 유용하게 사용될 수 있다. 사용자가 보고 있는 페이지의 필터, 정렬 기준, 현재 페이지 번호, 검색어 등을 URL에 담으면 다음과 같은 강력한 이점을 얻는다.
상태 공유 및 북마크 가능: URL만 공유하면 다른 사용자도 동일한 상태의 화면을 볼 수 있음
새로고침/뒤로가기/앞으로가기 동작: 브라우저의 기본 동작만으로도 이전 상태로 쉽게 돌아가거나 현재 상태를 유지할 수 있음
SSR/SSG 환경과의 자연스러운 통합: 서버 렌더링 시 URL의 쿼리 파라미터를 읽어 초기 상태를 결정하기 용이하다.
하지만 순수하게 URLSearchParams
API나 라우터 라이브러리의 기본 기능만으로 URL 상태를 관리하는 것은 번거롭고, 타입 안정성을 보장하기 어렵고, 상태 파싱/직렬화 로직이 반복적으로 작성되며, 여러 상태를 동시에 관리하기 복잡하다는 단점이 존재한다.
Type-Safe한 URL 상태 관리 솔루션 nuqs

nuqs
는 바로 이러한 문제를 해결하기 위해 등장한 라이브러리다.
특히 Next.js 환경(Pages Router 및 App Router 모두 지원)에 최적화되어 있으며, React의 hook 기반으로 URL 쿼리 파라미터를 마치 일반적인 React 상태처럼 쉽고 Type-Safe하게 다룰 수 있게 해준다.
nuqs는 URL 쿼리 파라미터와 React 상태 간의 양방향 동기화를 추상화한다. useQueryState
훅을 사용하여 URL에 저장하고 싶은 상태를 선언하고, nuqs는 이 상태 값을 자동으로 URL 쿼리 파라미터로 직렬화하고, URL 변경 시 해당 상태를 다시 파싱하여 업데이트해준다.
"use client";
import { parseAsInteger, useQueryState } from "nuqs";
// nuqs 공식 사이트의 예제
export function Demo() {
const [hello, setHello] = useQueryState("hello", { defaultValue: "" });
const [count, setCount] = useQueryState(
"count",
parseAsInteger.withDefault(0),
);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<input
value={hello}
placeholder="Enter your name"
onChange={(e) => setHello(e.target.value || null)}
/>
<p>Hello, {hello || "anonymous visitor"}!</p>
</>
);
}
nuqs 사용의 이점
압도적인 개발 경험 (DX) 향상
타입 안정성
TypeScript와의 완벽한 통합으로 URL 파라미터 파싱/직렬화 과정에서의 런타임 오류를 컴파일 타임에 잡을 수 있다. 복잡한 객체나 배열도 커스텀 파서를 통해 타입 안전하게 관리 가능하다.
간결한 API
useQueryState
, useQueryStates
훅은 React 개발자에게 매우 친숙하며, 상태 선언 및 업데이트 로직이 매우 간결해서 보일러플레이트 코드가 현저히 줄어듬
자동 동기화
URL과 상태 간의 동기화 로직을 직접 구현할 필요가 없어 개발자는 비즈니스 로직에 더 집중할 수 있다
SSR/SSG 환경에서의 강력함
Search Params를 서버사이드에서도 사용할 수 있다. loader
함수를 사용해야 하는데, createLoader
함수를 이용하면 search params descriptor object
를 만들어서 넘겨줄 수 있다고 한다.
import { parseAsFloat, createLoader } from 'nuqs/server'
// Describe your search params, and reuse this in useQueryStates / createSerializer:
export const coordinatesSearchParams = {
latitude: parseAsFloat.withDefault(0),
longitude: parseAsFloat.withDefault(0)
}
export const loadSearchParams = createLoader(coordinatesSearchParams)
import { loadSearchParams } from './search-params'
import type { SearchParams } from 'nuqs/server'
type PageProps = {
searchParams: Promise<SearchParams>
}
export default async function Page({ searchParams }: PageProps) {
const { latitude, longitude } = await loadSearchParams(searchParams)
return <Map
lat={latitude}
lng={longitude}
/>
// Pro tip: you don't *have* to await the result.
// Pass the Promise object to children components wrapped in <Suspense>
// to benefit from PPR / dynamicIO and serve a static outer shell
// immediately, while streaming in the dynamic parts that depend on
// the search params when they become available.
}
서버 렌더링 시 URL에서 초기 상태를 읽어와 올바른 UI를 렌더링하고, 클라이언트 측에서 하이드레이션 후에도 상태를 자연스럽게 이어받아 상호작용할 수 있고, 초기 로딩 성능(LCP, FCP) 개선 및 SEO에 긍정적인 영향을 미칠 수 있다.

상태 복잡성 관리 용이성
여러 URL 상태를 useQueryStates
훅 하나로 관리하거나, 커스텀 훅으로 조합하여 관련 상태들을 그룹화하기 용이하다.
defaultValue
, shallow
라우팅 옵션, history
옵션 (push/replace) 등 다양한 옵션을 통해 URL 업데이트 방식을 세밀하게 제어할 수 있다.
Source of Truth
필터, 정렬, 페이지네이션 등 공유 가능해야 하는 UI 상태
의 Source of Truth를 URL로 명확히 지정함으로써, 상태 관리의 혼란을 줄일 수 있다. 전역 상태 저장소에 있어야 할 상태와 URL에 있어야 할 상태를 구분하기 좋은 기준이 된다.
nuqs 사용 시 고려사항 및 단점
URL 길이 제한
매우 복잡하거나 많은 양의 상태를 URL에 저장하려고 하면 URL의 길이 제한에 부딪힐 수 있으므로 직렬화된 상태의 크기를 항상 염두에 두어야 한다.
크롬은 2000자 정도에서 문제가 발생할 수 있다고 한다. 자세한 내용은 사이트 참고.

URL 가독성
상태가 많아질수록 URL이 길고 복잡해져 사용자가 보기에 지저분하게 느껴질 수 있으므로, 꼭 필요한 상태만 URL에 노출하는 것이 좋다
브라우저 히스토리 오염
상태 업데이트 시마다 URL이 변경되므로, history: 'push'
옵션(기본값)을 사용하면 브라우저 히스토리가 의도치 않게 많이 쌓일 수 있다.
사용자가 뒤로가기 버튼을 눌렀을 때 예상과 다른 경험을 할 수 있으므로, history: 'replace'
옵션이나 shallow
라우팅 옵션을 적절히 활용하는 전략이 필요하다. 디바운싱(Debouncing)이나 쓰로틀링(Throttling)을 적용하여 불필요한 히스토리 생성을 막는 것도 좋겠다.
디바운싱/쓰로틀링을 적용하면, 브라우저의 History API 속도 제한 등의 이슈로 문제가 생길 수 있는 여지를 막을 수 있는 것도 덤이다.
보안이 취약함
민감한 데이터나 사용자 식별 정보는 절대 URL에 포함해서는 안 된다.
모든 상태를 저장하기는 좋지 않다
URL 상태는 본질적으로 "공유 가능"하고 "지속성"이 있는 상태에 적합하므로, 모달의 열림/닫힘 상태, 드롭다운 메뉴의 확장 여부 등 일시적이거나 특정 컴포넌트에 국한된 UI 상태는 useState
나 다른 상태 관리 라이브러리를 사용하는 것이 더 적절하다.
nuqs의 주요 사용 사례
데이터 목록의 필터링, 정렬, 페이지네이션
사용자가 설정한 필터/정렬/페이지 상태를 URL에 저장하여 공유 및 복원을 용이하게 합니다. (가장 대표적인 사용 사례)
검색 결과 페이지
검색어 및 관련 옵션(날짜 범위, 카테고리 등)을 URL에 유지할 수 있다.
탭(Tab) 컴포넌트
현재 활성화된 탭 상태를 URL에 저장하여 특정 탭으로 바로 링크할 수 있게 한다.
지도 애플리케이션
현재 보이는 지도 영역(경위도, 줌 레벨) 상태를 URL에 저장할 수 있다.
단계별 폼(Multi-step form)
현재 진행 중인 단계를 URL에 표시하여 사용자가 중간에 이탈해도 해당 단계부터 다시 시작할 수 있도록 돕는다.
마무리
nuqs는 모든 프론트엔드 상태 관리 문제를 해결하는 은탄환(Silver Bullet)은 아니지만, URL을 활용하여 상태를 관리해야 하는 특정 요구사항에 대해서는 매우 우아하고 효율적인 솔루션을 제공한다.
특히 Next.js 환경에서 타입 안정성과 개발 편의성을 중시하는 시니어 개발자라면 NUQS는 분명 매력적인 선택지가 될 것 같다.