useSearchParams는 현재 URL의 쿼리 스트링(query string)을 읽을 수 있게 해주는 클라이언트 컴포넌트(Client Component) 훅이에요.
👨🏫 강사의 부연 설명: > 쿼리 스트링이 뭐냐고요? 웹사이트 주소를 보면
https://example.com/products?search=shoes&color=black처럼?뒤에 붙는 값들을 보신 적 있죠? 여기서search=shoes&color=black부분이 바로 쿼리 스트링입니다. 사용자가 검색한 키워드나 필터링 옵션 같은 데이터를 URL을 통해 전달할 때 아주 많이 쓰인답니다.
useSearchParams는 브라우저에 내장된 URLSearchParams 인터페이스의 읽기 전용(read-only) 버전을 반환해요.
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
// URL -> `/dashboard?search=my-project`
// `search` -> 'my-project'
return <>Search: {search}</>
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
// URL -> `/dashboard?search=my-project`
// `search` -> 'my-project'
return <>Search: {search}</>
}
const searchParams = useSearchParams()
useSearchParams는 호출할 때 아무런 파라미터도 받지 않아요. 빈 괄호로 편하게 호출하시면 됩니다.
useSearchParams는 URLSearchParams 인터페이스의 읽기 전용 버전을 반환하는데요, 이 안에는 URL의 쿼리 스트링을 읽어올 수 있는 유용한 유틸리티 메서드들이 포함되어 있어요.
URLSearchParams.get(): 검색 파라미터와 연결된 첫 번째 값을 반환합니다. 예를 들어볼게요:
| URL | searchParams.get("a") |
|---|---|
/dashboard?a=1 | '1' |
/dashboard?a= | '' |
/dashboard?b=3 | null |
/dashboard?a=1&a=2 | '1' - 모든 값을 가져오고 싶다면 getAll()을 사용하세요 |
URLSearchParams.has(): 주어진 파라미터가 URL에 존재하는지를 불리언(boolean, true/false) 값으로 반환해 줘요. 예시를 볼까요?
| URL | searchParams.has("a") |
|---|---|
/dashboard?a=1 | true |
/dashboard?b=3 | false |
이 외에도 getAll(), keys(), values(), entries(), forEach(), toString()과 같은 URLSearchParams의 다른 읽기 전용 메서드들에 대해 더 알아보시는 것도 좋습니다.
알아두면 좋은 점 (Good to know):
useSearchParams는 클라이언트 컴포넌트(Client Component) 전용 훅이에요. 부분 렌더링(partial rendering) 과정에서 예전(stale) 값이 남는 것을 방지하기 위해 서버 컴포넌트(Server Components)에서는 지원되지 않습니다.- 만약 서버 컴포넌트에서 검색 파라미터(search params)를 바탕으로 데이터를 가져오고 싶다면, 해당 Page 컴포넌트의
searchParamsprop을 읽어오는 것이 훨씬 좋은 선택입니다. 그렇게 읽어온 값을 해당 페이지 내의 다른 컴포넌트들(서버든 클라이언트든 상관없이)에게 prop으로 넘겨주면 되니까요.- 어플리케이션에 예전 방식인
/pages디렉토리가 포함되어 있다면,useSearchParams는ReadonlyURLSearchParams | null을 반환할 수 있어요. 여기서null값은 마이그레이션(이전) 기간 동안의 호환성을 위한 것인데요,getServerSideProps를 사용하지 않는 페이지를 사전 렌더링(pre-rendering)할 때는 검색 파라미터를 알 수 없기 때문이랍니다.
이 부분이 정말 중요합니다! Next.js가 페이지를 정적으로 렌더링할지, 동적으로 렌더링할지에 따라 이 훅의 동작이 달라지거든요. 집중해서 봐주세요!
만약 라우트가 정적으로 렌더링(statically rendered) 된다면, useSearchParams를 호출할 때 해당 컴포넌트부터 가장 가까운 Suspense 경계(boundary)까지의 클라이언트 컴포넌트 트리가 클라이언트 측에서 렌더링(client-side rendered) 되도록 유도합니다.
이렇게 하면 라우트의 일부는 정적으로 렌더링하면서도, useSearchParams를 사용하는 동적인 부분만 클라이언트 렌더링으로 분리할 수 있게 되는 거죠.
그래서 useSearchParams를 사용하는 클라이언트 컴포넌트는 반드시 <Suspense/> 경계로 감싸주는 것을 권장해요. 이렇게 하면 그 상위에 있는 모든 클라이언트 컴포넌트들이 정상적으로 정적 렌더링되어 초기 HTML의 일부로 전송될 수 있답니다. 예제 보기.
예시 코드를 살펴볼게요:
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
// 이 부분은 정적 렌더링을 사용할 때 서버 측 콘솔에 찍히지 않아요.
console.log(search)
return <>Search: {search}</>
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
// 이 부분은 정적 렌더링을 사용할 때 서버 측 콘솔에 찍히지 않아요.
console.log(search)
return <>Search: {search}</>
}
import { Suspense } from 'react'
import SearchBar from './search-bar'
// Suspense의 fallback으로 전달된 이 컴포넌트는
// 초기 HTML에서 search bar를 대신해서 렌더링됩니다.
// React의 하이드레이션(hydration) 과정 중 값을 사용할 수 있게 되면,
// 이 fallback은 실제 `<SearchBar>` 컴포넌트로 교체됩니다.
function SearchBarFallback() {
return <>placeholder</>
}
export default function Page() {
return (
<>
<nav>
<Suspense fallback={<SearchBarFallback />}>
<SearchBar />
</Suspense>
</nav>
<h1>Dashboard</h1>
</>
)
}
import { Suspense } from 'react'
import SearchBar from './search-bar'
// Suspense의 fallback으로 전달된 이 컴포넌트는
// 초기 HTML에서 search bar를 대신해서 렌더링됩니다.
// React의 하이드레이션(hydration) 과정 중 값을 사용할 수 있게 되면,
// 이 fallback은 실제 `<SearchBar>` 컴포넌트로 교체됩니다.
function SearchBarFallback() {
return <>placeholder</>
}
export default function Page() {
return (
<>
<nav>
<Suspense fallback={<SearchBarFallback />}>
<SearchBar />
</Suspense>
</nav>
<h1>Dashboard</h1>
</>
)
}
💡 강사의 특급 꿀팁!
실무에서 가장 많이 에러를 내는 부분 중 하나예요!useSearchParams를 썼는데 상위에<Suspense>가 없다? 그러면 빌드할 때 에러가 나면서 전체 페이지가 최적화되지 못하는 참사가 발생합니다. "나는 쿼리 파라미터만 하나 읽었을 뿐인데 웹사이트가 왜 이렇게 느려졌지?" 하는 상황을 막으려면 꼭 Suspense로 감싸는 습관을 들이세요!
알아두면 좋은 점 (Good to know):
- 개발 모드(development)에서는 라우트들이 요청이 올 때마다 렌더링되기 때문에(on-demand),
useSearchParams가 Suspense를 발생시키지 않아서Suspense없이도 그냥 잘 동작하는 것처럼 보일 수 있어요. (이래서 배포할 때 깜짝 놀라는 분들이 많습니다!)- 프로덕션 빌드 과정에서는,
useSearchParams를 호출하는 클라이언트 컴포넌트가 포함된 정적 페이지(static page)의 경우 반드시Suspense경계로 감싸져 있어야 합니다. 그렇지 않으면 Missing Suspense boundary with useSearchParams 에러와 함께 빌드가 실패합니다.- 만약 이 라우트를 완전히 동적으로 렌더링하고 싶으시다면, 서버 컴포넌트에서 들어오는 요청을 기다리게 하는
connection함수를 먼저 사용하는 것을 권장해요. 이렇게 하면 그 아래에 있는 모든 것들이 사전 렌더링(prerendering)에서 제외됩니다. 라우트를 동적으로 만드는 원리에 대해서는 동적 렌더링 가이드(Dynamic Rendering guide)를 참고해보세요.- 이미 서버 컴포넌트 Page 내부에 있다면, 훅을 쓰기보단
searchParamsprop을 사용해서 그 값을 클라이언트 컴포넌트에 넘겨주는 방식을 고려해보세요. 이게 훨씬 깔끔할 수 있습니다.- Page의
searchParamsprop을 클라이언트 컴포넌트로 직접 넘긴 다음 React의use()훅으로 풀어낼(unwrap) 수도 있어요. 물론 이 방법도 Suspense를 발생시키기 때문에 클라이언트 컴포넌트를Suspense경계로 감싸주어야 합니다.
만약 라우트가 동적으로 렌더링(dynamically rendered) 된다면, 클라이언트 컴포넌트의 초기 서버 렌더링 과정 중 서버 측에서 useSearchParams를 사용할 수 있게 됩니다.
예시를 볼게요:
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
// 이 콘솔 로그는 초기 렌더링 시에는 서버에서 찍히고,
// 이후 페이지 이동(navigation) 시에는 클라이언트에서 찍히게 됩니다.
console.log(search)
return <>Search: {search}</>
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
// 이 콘솔 로그는 초기 렌더링 시에는 서버에서 찍히고,
// 이후 페이지 이동(navigation) 시에는 클라이언트에서 찍히게 됩니다.
console.log(search)
return <>Search: {search}</>
}
import { connection } from 'next/server'
import SearchBar from './search-bar'
export default async function Page() {
await connection()
return (
<>
<nav>
<SearchBar />
</nav>
<h1>Dashboard</h1>
</>
)
}
import { connection } from 'next/server'
import SearchBar from './search-bar'
export default async function Page() {
await connection()
return (
<>
<nav>
<SearchBar />
</nav>
<h1>Dashboard</h1>
</>
)
}
알아두면 좋은 점 (Good to know):
- 예전에는 페이지에
export const dynamic = 'force-dynamic'을 설정해서 강제로 동적 렌더링을 만들곤 했어요. 하지만 이제는connection()을 사용하는 것을 더 권장합니다. 들어오는 요청(request)과 동적 렌더링을 의미론적으로(semantically) 더 잘 연결해주기 때문이죠.
이 부분도 초보자분들이 정말 많이 헷갈려하시는 부분이에요. 서버 컴포넌트에서는 쿼리 스트링을 어떻게 다뤄야 할까요?
Pages (서버 컴포넌트)에서 검색 파라미터에 접근하려면, searchParams prop을 사용하면 됩니다. 서버 컴포넌트인 Page는 기본적으로 이 prop을 전달받거든요.
하지만 Pages와 달리, Layouts (서버 컴포넌트)는 searchParams prop을 받지 않습니다. 그 이유는 공통 레이아웃의 경우 라우팅 간에 리렌더링 되지 않기 때문에, 페이지 이동 간에 이전(stale) searchParams 값이 남아서 꼬일 수 있기 때문이에요. 자세한 설명 보기.
👨🏫 강사의 부연 설명:
"어? 저는 모든 페이지 상단에 있는 Header(레이아웃에 포함됨)에서 현재 검색어를 보여주고 싶은데요?" 라고 하신다면 방법이 있습니다! 레이아웃 자체에서 파라미터를 읽으려고 하지 마시고,
대신 검색어를 보여줄 자그마한 클라이언트 컴포넌트를 하나 만드세요. 그 안에서useSearchParams훅을 사용한 뒤, 그 컴포넌트를 Layout에 가져와서 렌더링하면 됩니다. 클라이언트 컴포넌트는 새로운searchParams가 들어올 때마다 최신 상태로 알아서 잘 리렌더링 되거든요!
대신에 Page의 searchParams prop을 사용하거나, 클라이언트 측에서 항상 최신 searchParams로 리렌더링되는 클라이언트 컴포넌트 안에서 useSearchParams 훅을 사용하세요.
searchParams (searchParams 업데이트하기)읽기만 하는 게 아니라 URL의 쿼리 스트링을 바꾸고 싶다면 어떻게 해야 할까요? useRouter나 Link를 사용해서 새로운 searchParams를 설정할 수 있습니다. 페이지 이동(navigation)이 실행되고 나면, 현재의 page.js는 업데이트된 searchParams prop을 새로 전달받게 됩니다.
👨🏫 강사의 코드 리딩 팁!
아래 코드를 보면createQueryString이라는 함수를 직접 만들어서 쓰고 있죠? 단순히 문자열 더하기("?sort=" + value)를 하지 않고URLSearchParams객체를 활용하면, 기존의 다른 파라미터들(예:?page=2)은 그대로 유지하면서 내가 원하는 파라미터만 쏙쏙 추가하거나 변경할 수 있어서 매우 안전하고 편리한 패턴입니다. 무조건 외워두세요!
'use client'
export default function ExampleClientComponent() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
// 현재의 searchParams와 새로 제공된 key/value 쌍을 병합해서
// 새로운 searchParams 문자열을 만들어내는 함수입니다.
const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams.toString())
params.set(name, value)
return params.toString()
},
[searchParams]
)
return (
<>
<p>Sort By</p>
{/* useRouter를 사용하는 방법 */}
<button
onClick={() => {
// <pathname>?sort=asc 형태로 이동
router.push(pathname + '?' + createQueryString('sort', 'asc'))
}}
>
ASC
</button>
{/* <Link> 컴포넌트를 사용하는 방법 */}
<Link
href={
// <pathname>?sort=desc 형태로 이동
pathname + '?' + createQueryString('sort', 'desc')
}
>
DESC
</Link>
</>
)
}
'use client'
export default function ExampleClientComponent() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
// 현재의 searchParams와 새로 제공된 key/value 쌍을 병합해서
// 새로운 searchParams 문자열을 만들어내는 함수입니다.
const createQueryString = useCallback(
(name, value) => {
const params = new URLSearchParams(searchParams)
params.set(name, value)
return params.toString()
},
[searchParams]
)
return (
<>
<p>Sort By</p>
{/* useRouter를 사용하는 방법 */}
<button
onClick={() => {
// <pathname>?sort=asc 형태로 이동
router.push(pathname + '?' + createQueryString('sort', 'asc'))
}}
>
ASC
</button>
{/* <Link> 컴포넌트를 사용하는 방법 */}
<Link
href={
// <pathname>?sort=desc 형태로 이동
pathname + '?' + createQueryString('sort', 'desc')
}
>
DESC
</Link>
</>
)
}
| Version | Changes (변경 사항) |
|---|---|
v13.0.0 | useSearchParams 훅이 처음 도입되었습니다. |
모든 문서의 구조적 개요를 보시려면 https://nextjs.org/docs/sitemap.md 를 확인해주세요.
사용 가능한 모든 문서의 전체 인덱스를 보시려면 https://nextjs.org/docs/llms.txt 를 확인해주세요.
자, 이렇게 useSearchParams에 대한 공식문서 번역과 부가 설명을 모두 마쳤습니다! 클라이언트 컴포넌트에서만 사용해야 한다는 점과 Suspense로 꼭 감싸줘야 한다는 사실만 잘 기억하셔도 큰 에러들을 피하실 수 있을 거예요.
혹시 이 문서를 읽고 나서 직접 코드를 작성해 보시다 막히는 부분이 생기면 언제든 질문해 주실래요? 제가 실무에서 어떻게 해결하는지 더 자세히 알려드릴게요!