nuqs를 사용해서 쿼리 파라미터 관리하기
nuqs란?
nuqs는 React 애플리케이션에서 URL 쿼리 파라미터를 타입 안전하게 관리할 수 있는 상태 관리 라이브러리입니다.
기본적으로 useState처럼 동작하지만, 상태를 URL의 쿼리 문자열에 저장해서 페이지를 새로고침하거나 공유해도 유지할 수 있도록 해줍니다.
따라서 필터, 검색, 정렬, 페이지네이션 같은 기능을 구현할 때 더 직관적이고 확장성 있게 코드를 작성할 수 있습니다.
nuqs가 필요한 이유
React에서 URL 쿼리 파라미터를 검색어나 필터링, 페이지네이션 등의 기능에 사용합니다. 이를 위해서는 아래와 같은 기능이 구현되어야 합니다.
- 새로고침해도 상태 유지
- 뒤로 가기 / 앞으로 가기 가능
- 현재 상태를 URL로 공유 가능
하지만 useState나 useSearchParams을 사용하면 다음과 같은 한계가 있습니다.
useState로 상태 관리하기
useState를 사용하여 상태를 관리하게 되면 다음과 같은 단점이 있습니다.
- 새로고침 시 상태가 사라짐
- 뒤로 가기/앞으로 가기 기능이 동작하지 않음
- 현재 상태를 공유할 수 없음 (URL을 통해 상태 공유 불가)
아래는 useState를 사용한 검색 기능의 예시입니다:
"use client";
import { useState } from "react";
export function SearchBar() {
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
// 페이지를 새로고침하면 검색어와 카테고리가 초기화됩니다
// URL에 상태가 반영되지 않아 현재 검색 결과를 공유할 수 없습니다
return (
<div className="flex gap-4">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="검색어를 입력하세요..."
className="border p-2 rounded"
/>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="border p-2 rounded"
>
<option value="all">전체</option>
<option value="electronics">전자기기</option>
<option value="clothing">의류</option>
<option value="books">도서</option>
</select>
<div className="mt-4">
검색어: {search || "없음"} / 카테고리: {category}
</div>
</div>
);
}이 코드의 주요 문제점들은 다음과 같습니다:
-
상태 초기화: 페이지를 새로고침하면 검색어와 카테고리가 초기값으로 돌아갑니다.
-
URL 공유 불가능: 특정 검색 조건을 다른 사용자와 공유하고 싶을 때, URL만으로는 불가능합니다.
-
브라우저 네비게이션 문제: 사용자가 브라우저의 뒤로 가기를 눌렀을 때, 이전 검색 상태로 돌아가지 않습니다.
useSearchParams로 상태 관리하기
useSearchParams로 상태를 관리하게 되면 useState를 사용했을 때의 단점을 해결할 수 있지만 다음과 같은 불편함이 여전히 존재합니다.
그리고 useSearchParams hook은 client component에서만 사용할 수 있습니다.
- 타입 안전성이 부족함 (searchParams.get("q")는 string | null 반환)
- 직렬화/파싱이 필요함 (숫자, 배열, 객체 등을 관리하기 어려움)
"use client";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
export function SearchComponent() {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const search = searchParams.get("q") || "";
const updateSearch = (value: string) => {
const params = new URLSearchParams(searchParams);
if (value) {
params.set("q", value);
}
router.replace(`${pathname}?${params.toString()}`); // Update URL
};
return (
<div>
<input
type="text"
value={search}
placeholder="Search..."
onChange={(e) => updateSearch(e.target.value)}
/>
</div>
);
}nuqs 사용하기
nuqs는 다음과 같은 기능을 제공해서 기존의 상태 관리 방식의 문제점을 해결합니다.
useQueryState hook를 사용하여 string, number, boolean, object 등 다양한 타입을 안전하게 관리 가능- 페이지 새로고침 후에도 상태 유지
- parseAsInteger, parseAsBoolean, parseAsJson 등의 parser를 지원
useQueryState
아래는 nuqs의 useQueryState를 사용한 예제입니다. 사용자가 입력하면 hello 값이 setHello()를 통해 URL에 반영됩니다 (?hello=value).
페이지 새로고침 후에도 hello 값이 유지되고, URL을 복사해서 공유하면 같은 상태가 유지됩니다.
"use client";
import { useQueryState } from "nuqs";
export function Demo() {
const [hello, setHello] = useQueryState("hello", { defaultValue: "" });
return (
<>
<input
value={hello}
placeholder="Enter your name"
onChange={(e) => setHello(e.target.value || null)}
/>
<p>Hello, {hello || "anonymous visitor"}!</p>
</>
);
}아래는 parser를 통해서 query state를 관리하는 예제입니다. count 값이 parseAsInteger를 사용해 숫자로 변환되므로, 문자열이 아닌 정수로 관리할 수 있습니다.
import { parseAsInteger, useQueryState } from "nuqs";
export function Counter() {
const [count, setCount] = useQueryState(
"count",
parseAsInteger.withDefault(0)
);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
</>
);
}useQueryStates
useQueryStates를 사용하면 여러 개의 상태를 한 번에 관리할 수 있습니다. 아래 코드는 q, page, sort 쿼리 상태를 관리하는 예제입니다. setQuery hook을 통해서 쿼리를 업데이트 할 수 있고, 사용자가 뒤로가기를 누르면 이전 상태로 복원됩니다.
import { parseAsInteger, useQueryStates } from "nuqs";
export function MultiFilter() {
const [query, setQuery] = useQueryStates({
q: { defaultValue: "" },
page: parseAsInteger.withDefault(1),
sort: { defaultValue: "asc" },
});
return (
<div>
<input
value={query.q}
placeholder="Search"
onChange={(e) => setQuery({ q: e.target.value || null })}
/>
<button onClick={() => setQuery({ page: query.page + 1 })}>
Next Page
</button>
<button
onClick={() =>
setQuery({ sort: query.sort === "asc" ? "desc" : "asc" })
}
>
Toggle Sort ({query.sort})
</button>
</div>
);
}서버 컴포넌트에서 nuqs 사용하기
nuqs는 서버 컴포넌트에서도 사용할 수 있습니다. 아래 코드와 같이 searchParams props를 통해 q 값을 가져와서 API 요청을 보내고 결과를 렌더링할 수 있습니다.
import { parseAsString } from "nuqs";
import { cookies } from "next/headers";
export async function SearchResults({
searchParams,
}: {
searchParams: { q?: string };
}) {
const q = parseAsString.withDefault("")(searchParams.q);
const data = q
? await fetch(`https://api.example.com/search?q=${q}`).then((res) =>
res.json()
)
: [];
return (
<div>
<h1>Search Results for: {q}</h1>
<ul>
{data.map((item: { id: string; title: string }) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}결론
nuqs는 기존 useState나 useSearchParams의 단점을 보완하여 타입 안전하고 직관적인 방식으로 URL 상태를 관리할 수 있는 강력한 도구입니다.
nuqs에서 제공하는 hook을 통해 검색, 필터링, 페이지네이션과 같은 기능들을 더 수월하게 구현할 수 있습니다.
그리고 Next.js의 클라이언트 컴포넌트와 서버 컴포넌트 모두에서 활용할 수 있다는 장점도 있습니다.