본문 바로가기
카테고리 없음

nuqs 라이브러리로 React URL 상태 관리하기 - 타입 안전한 쿼리 파라미터 핸들링 방법

by tricks 2025. 6. 1.

 

React애플리케이션에서 상태 관리는 개발자들이 항상 고민하는 핵심 과제입니다. 컴포넌트 내부 상태부터 애플리케이션 전역 상태, 그리고 서버와의 데이터 동기화까지 다양한 레벨의 상태를 효과적으로 다루는 것은 서비스 규모가 커질수록 더욱 중요해집니다.

useState, useReducer, Context API, Zustand 같은 도구들이 널리 사용되고 있지만, 개발자들이 종종 간과하는 강력한 상태 저장 공간이 하나 있습니다. 바로 URL입니다.

🌐 URL을 활용한 상태 관리의 숨겨진 장점들

웹 브라우저의 주소창에 표시되는 쿼리 파라미터는 서버 통신 목적뿐만 아니라 클라이언트 애플리케이션의 특정 상태를 표현하고 전달하는 데 매우 효과적입니다. 페이지 필터링 조건, 데이터 정렬 방식, 현재 페이지 번호, 사용자 검색어 등을 URL에 포함시키면 다음과 같은 핵심적인 이점들을 얻을 수 있습니다.

 

상태 공유와 북마킹의 편리함
URL 링크 하나만 전달하면 다른 사용자도 정확히 동일한 화면 상태를 확인할 수 있습니다.

 

브라우저 네비게이션의 자연스러운 활용
페이지 새로고침, 뒤로가기, 앞으로가기 등 브라우저 기본 기능만으로도 이전 상태 복원이나 현재 상태 유지가 자동으로 처리됩니다.

 

서버 사이드 렌더링과의 완벽한 호환성
SSR이나 SSG 환경에서 URL 쿼리 파라미터를 읽어 초기 렌더링 상태를 결정하는 과정이 매우 직관적입니다.

하지만 순수한 URLSearchParams API나 라우팅 라이브러리의 기본 기능만으로는 여러 제약사항이 존재합니다. 복잡한 구현 과정, 타입 안전성 부족, 반복적인 파싱/직렬화 코드 작성, 다중 상태 동시 관리의 어려움 등이 대표적입니다.

⚡ nuqs 라이브러리 소개와 핵심 기능

이러한 문제점들을 해결하기 위해 개발된 nuqs는 특히 Next.js 생태계(Pages Router와 App Router 모두 지원)에 최적화된 라이브러리입니다. React Hook 패턴을 기반으로 URL 쿼리 파라미터를 일반적인 React 상태처럼 직관적이고 타입 안전하게 관리할 수 있도록 설계되었습니다.

 

nuqs의 핵심은 URL 쿼리 파라미터와 React 상태 간의 실시간 양방향 동기화를 자동화한다는 점입니다. useQueryState 훅을 통해 URL에 저장할 상태를 정의하면, 라이브러리가 자동으로 상태 값을 URL 쿼리 파라미터로 변환하고, URL 변경 시 해당 상태를 다시 파싱하여 컴포넌트를 업데이트합니다.

"use client";
import { parseAsInteger, useQueryState } from "nuqs";

export function SearchDemo() {
  const [searchTerm, setSearchTerm] = useQueryState("search", { defaultValue: "" });
  const [pageNumber, setPageNumber] = useQueryState(
    "page",
    parseAsInteger.withDefault(1),
  );

  return (
    <>
       setPageNumber((current) => current + 1)}>
        페이지: {pageNumber}

       setSearchTerm(e.target.value || null)}
      />
      검색 결과: {searchTerm || "전체 목록"}

  );
}

🤔 nuqs가 해결하는 문제

기존에 URL 쿼리 파라미터를 다룰 때 겪었던 문제들을 생각해보세요:

  • 검색어를 입력했는데 새로고침하면 사라짐
  • 필터 설정을 URL로 공유하고 싶은데 복잡함
  • 브라우저 뒤로가기 버튼이 제대로 작동하지 않음
  • 타입 안정성이 보장되지 않음

nuqs는 이런 문제들을 해결해주는 React용 URL 쿼리 파라미터 관리 라이브러리입니다. URL을 React 상태처럼 쉽게 다룰 수 있게 해주며, 특히 Next.js와의 호환성이 뛰어납니다.

⚡ 기본 사용법과 핵심 개념

useQueryState 훅 기본 사용법

가장 기본적인 사용법부터 살펴보겠습니다:

'use client'
import { useQueryState } from 'nuqs'

function SearchComponent() {
  const [search, setSearch] = useQueryState('search')

  return (
     setSearch(e.target.value)} 
      placeholder="검색어를 입력하세요"
    />
  )
}

이 코드는 useState와 거의 동일하게 작동하지만, 상태가 URL의 ?search=검색어 형태로 자동 저장됩니다.

타입 안전한 파서 사용하기

nuqs의 강력한 기능 중 하나는 타입 안전성입니다. 다양한 데이터 타입을 위한 내장 파서들을 제공합니다:

import { 
  useQueryState, 
  parseAsInteger, 
  parseAsBoolean,
  parseAsStringLiteral 
} from 'nuqs'

function FilterComponent() {
  // 숫자 타입 (페이지 번호)
  const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))

  // 불린 타입 (토글 상태)
  const [isActive, setIsActive] = useQueryState('active', parseAsBoolean.withDefault(false))

  // 특정 문자열만 허용
  const sortOptions = ['name', 'date', 'price'] as const
  const [sort, setSort] = useQueryState('sort', parseAsStringLiteral(sortOptions).withDefault('name'))

  return (

       setPage(page + 1)}>
        다음 페이지 ({page})

       setIsActive(!isActive)}>
        {isActive ? '활성' : '비활성'}

       setSort(e.target.value)}>
        {sortOptions.map(option => (
          {option}
        ))}


  )
}

🔧 자주 사용하는 실전 패턴들

1. 검색과 필터링 조합

가장 일반적인 사용 사례인 검색과 필터링을 함께 관리하는 패턴입니다:

import { useQueryStates, parseAsString, parseAsInteger } from 'nuqs'

function ProductList() {
  const [filters, setFilters] = useQueryStates({
    search: parseAsString.withDefault(''),
    category: parseAsString.withDefault('all'),
    page: parseAsInteger.withDefault(1),
    minPrice: parseAsInteger.withDefault(0),
    maxPrice: parseAsInteger.withDefault(10000)
  })

  // 검색어 변경 시 페이지를 1로 리셋
  const handleSearchChange = (newSearch) => {
    setFilters({ search: newSearch, page: 1 })
  }

  // 필터 초기화
  const resetFilters = () => {
    setFilters({
      search: '',
      category: 'all',
      page: 1,
      minPrice: 0,
      maxPrice: 10000
    })
  }

  return (

       handleSearchChange(e.target.value)}
        placeholder="상품 검색"
      />

       setFilters({ category: e.target.value, page: 1 })}
      >
        전체
        전자제품
        의류


      필터 초기화

      {/* 상품 목록 렌더링 */}
      현재 페이지: {filters.page}

  )
}

2. 커스텀 훅으로 로직 분리

복잡한 상태 관리 로직을 커스텀 훅으로 분리하면 재사용성과 가독성이 높아집니다:

// hooks/useProductFilters.js
import { useQueryStates, parseAsString, parseAsInteger, parseAsBoolean } from 'nuqs'

export function useProductFilters() {
  const [filters, setFilters] = useQueryStates({
    search: parseAsString.withDefault(''),
    category: parseAsString.withDefault('all'),
    page: parseAsInteger.withDefault(1),
    sortBy: parseAsString.withDefault('name'),
    isOnSale: parseAsBoolean.withDefault(false)
  })

  const updateSearch = (search) => {
    setFilters({ search, page: 1 }) // 검색 시 첫 페이지로
  }

  const updateCategory = (category) => {
    setFilters({ category, page: 1 }) // 카테고리 변경 시 첫 페이지로
  }

  const nextPage = () => {
    setFilters({ page: filters.page + 1 })
  }

  const prevPage = () => {
    if (filters.page > 1) {
      setFilters({ page: filters.page - 1 })
    }
  }

  const resetFilters = () => {
    setFilters({
      search: '',
      category: 'all',
      page: 1,
      sortBy: 'name',
      isOnSale: false
    })
  }

  return {
    filters,
    setFilters,
    updateSearch,
    updateCategory,
    nextPage,
    prevPage,
    resetFilters
  }
}

3. 배열 데이터 처리

선택된 태그나 카테고리 목록 같은 배열 데이터도 쉽게 처리할 수 있습니다:

import { useQueryState, parseAsArrayOf, parseAsString } from 'nuqs'

function TagFilter() {
  const [selectedTags, setSelectedTags] = useQueryState(
    'tags',
    parseAsArrayOf(parseAsString).withDefault([])
  )

  const availableTags = ['React', 'Next.js', 'TypeScript', 'JavaScript', 'CSS']

  const toggleTag = (tag) => {
    if (selectedTags.includes(tag)) {
      setSelectedTags(selectedTags.filter(t => t !== tag))
    } else {
      setSelectedTags([...selectedTags, tag])
    }
  }

  return (

      태그 선택 (선택된 태그: {selectedTags.length}개)
      {availableTags.map(tag => (
         toggleTag(tag)}
          style={{
            backgroundColor: selectedTags.includes(tag) ? '#007bff' : '#f8f9fa',
            color: selectedTags.includes(tag) ? 'white' : 'black'
          }}
        >
          {tag}

      ))}

  )
}

⚙️ 유용한 옵션들

히스토리 관리

// 기본: 현재 히스토리 항목을 대체 (뒤로가기 시 이전 상태로)
const [search, setSearch] = useQueryState('search')

// 새로운 히스토리 항목 생성 (뒤로가기로 각 변경사항 탐색 가능)
const [search, setSearch] = useQueryState('search', { history: 'push' })

서버 상태 동기화

// 클라이언트에서만 상태 업데이트 (기본값)
const [filter, setFilter] = useQueryState('filter')

// 서버에도 상태 변경 알림 (SSR 환경에서 유용)
const [filter, setFilter] = useQueryState('filter', { shallow: false })

업데이트 제한 (Throttling)

// 1초에 한 번만 URL 업데이트 (성능 최적화)
const [search, setSearch] = useQueryState('search', { 
  throttleMs: 1000,
  shallow: false 
})

🚀 Next.js에서 설정하기

Next.js App Router에서 사용하려면 루트 레이아웃에 어댑터를 설정해야 합니다:

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

export default function RootLayout({ children }) {
  return (


        {children}


  )
}

💡 실무에서 유용한 팁들

1. URL 키를 짧게 유지하기

const [coordinates, setCoordinates] = useQueryStates(
  {
    latitude: parseAsFloat.withDefault(37.5665),
    longitude: parseAsFloat.withDefault(126.9780)
  },
  {
    urlKeys: {
      latitude: 'lat',   // URL에서는 짧게
      longitude: 'lng'   // 코드에서는 명확하게
    }
  }
)

2. 조건부 렌더링과 함께 사용

function ConditionalContent() {
  const [activeTab, setActiveTab] = useQueryState('tab', parseAsString.withDefault('home'))

  return (


        {['home', 'about', 'contact'].map(tab => (
           setActiveTab(tab)}
            className={activeTab === tab ? 'active' : ''}
          >
            {tab}

        ))}


      {activeTab === 'home' && }
      {activeTab === 'about' && }
      {activeTab === 'contact' && }

  )
}

🚀 nuqs 도입으로 얻는 개발 경험 향상

강력한 타입 안전성 보장
TypeScript와의 완벽한 통합을 통해 URL 파라미터 처리 과정에서 발생할 수 있는 런타임 에러를 컴파일 단계에서 미리 방지할 수 있습니다. 복잡한 객체나 배열 구조도 커스텀 파서를 활용하여 타입 안전하게 처리 가능합니다.

 

직관적이고 간결한 API 설계
useQueryState, useQueryStates 훅은 React 개발자들에게 매우 친숙한 패턴을 따르며, 상태 선언과 업데이트 로직이 간결해서 불필요한 보일러플레이트 코드를 대폭 줄여줍니다.

 

자동화된 동기화 메커니즘
URL과 상태 간의 복잡한 동기화 로직을 직접 구현할 필요가 없어져 개발자는 핵심 비즈니스 로직 구현에 더 많은 시간을 투자할 수 있습니다.

 

서버 사이드 렌더링 환경에서의 우수한 성능
Search Params를 서버 측에서도 활용할 수 있으며, createLoader 함수를 이용하면 search params descriptor 객체를 생성하여 전달할 수 있습니다.

import { parseAsFloat, createLoader } from 'nuqs/server'

export const locationSearchParams = {
  latitude: parseAsFloat.withDefault(0),
  longitude: parseAsFloat.withDefault(0)
}

export const loadLocationParams = createLoader(locationSearchParams)

 

서버 렌더링 시 URL에서 초기 상태를 추출하여 적절한 UI를 렌더링하고, 클라이언트 측 하이드레이션 이후에도 상태를 자연스럽게 이어받아 상호작용할 수 있어 초기 로딩 성능(LCP, FCP) 개선과 SEO 최적화에 긍정적인 효과를 가져옵니다.

 

복잡한 상태 구조 관리의 용이성
다수의 URL 상태를 useQueryStates 훅 하나로 통합 관리하거나, 커스텀 훅으로 조합하여 연관된 상태들을 그룹화하기 쉽습니다. defaultValue, shallow 라우팅 옵션, history 옵션(push/replace) 등 다양한 설정을 통해 URL 업데이트 방식을 세밀하게 제어할 수 있습니다.

 

명확한 상태 관리 기준 제시
필터링, 정렬, 페이지네이션 등 공유 가능해야 하는 UI 상태의 단일 진실 공급원(Source of Truth)을 URL로 명확히 지정함으로써 상태 관리의 혼란을 줄일 수 있습니다.

⚠️ nuqs 사용 시 주의사항과 제한사항

URL 길이 제한 고려사항
과도하게 복잡하거나 대용량의 상태를 URL에 저장하려고 하면 브라우저의 URL 길이 제한에 부딪힐 수 있으므로 직렬화된 상태의 크기를 항상 고려해야 합니다. Chrome 브라우저의 경우 약 2000자 정도에서 문제가 발생할 수 있습니다.

 

URL 가독성 문제
상태가 증가할수록 URL이 길어지고 복잡해져 사용자 경험에 부정적인 영향을 줄 수 있으므로, 반드시 필요한 상태만 URL에 노출하는 것이 바람직합니다.

 

브라우저 히스토리 관리 이슈
상태 업데이트마다 URL이 변경되므로 history: 'push' 옵션(기본값)을 사용하면 브라우저 히스토리가 의도치 않게 과도하게 쌓일 수 있습니다. 사용자가 뒤로가기 버튼을 사용할 때 예상과 다른 경험을 할 수 있으므로, history: 'replace' 옵션이나 shallow 라우팅 옵션을 적절히 활용하는 전략이 필요합니다.

 

보안 취약점
민감한 개인정보나 사용자 식별 데이터는 절대 URL에 포함해서는 안 됩니다.

 

적용 범위의 한계
URL 상태는 본질적으로 "공유 가능성"과 "지속성"이 있는 상태에 적합하므로, 모달 창의 열림/닫힘 상태, 드롭다운 메뉴의 확장 여부 등 일시적이거나 특정 컴포넌트에 한정된 UI 상태는 useState나 다른 상태 관리 도구를 사용하는 것이 더 적절합니다.

 

nuqs는 모든 프론트엔드 상태 관리 문제를 해결하는 만능 해결책은 아니지만, URL을 활용한 상태 관리가 필요한 특정 요구사항에 대해서는 매우 우아하고 효율적인 솔루션을 제공합니다. 특히 Next.js 환경에서 타입 안전성과 개발 편의성을 중시하는 개발자라면 nuqs는 분명 매력적인 선택지가 될 것입니다.

 

nuqs는 URL을 통한 상태 관리를 매우 간단하게 만들어주는 강력한 도구입니다. 특히 사용자 경험을 향상시키고, 상태를 공유 가능하게 만들며, 브라우저 히스토리와 자연스럽게 통합되는 애플리케이션을 만들 때 매우 유용합니다. 위의 패턴들을 참고하여 프로젝트에 적용해보시기 바랍니다.

댓글