이번에 스프린트 미션으로 마켓 상품 정렬 및 검색 기능을 다뤘는데,
useState를 주로 써서 useState를 활용해서 작성했다.

그러던 와중에 궁금한 점이 하나 생겼다.

...
const [page, setPage] = useState(1);
const [orderBy, setOrderBy] = useState("recent");
const [keyword, setKeyword] = useState("");

useEffect(() => {
  async function loadProducts() {
    const res = await fetch(
      `https://.../products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&keyword=${keyword}`
    );
  }

  loadProducts();
}, [page, pageSize, orderBy, keyword]);
...

위 코드는 useState를 활용한 코드다.
근데 이제 위 코드를
useSearchParams를 활용한 코드로 바꿔보면

...
const [searchParams, setSearchParams] = useSearchParams();

const page = Number(searchParams.get("page")) || 1;
const orderBy = searchParams.get("orderBy") || "recent";
const keyword = searchParams.get("keyword") || "";

useEffect(() => {
  async function loadProducts() {
    const res = await fetch(
      `/products?page=${page}&orderBy=${orderBy}&keyword=${keyword}`
    );
  }

  loadProducts();
}, [page, orderBy, keyword]);
...

이런 식으로 쓰이고,
fetch 요청 형식이 똑같다.
그래서 의문이 들었던 게 있었다.

이러면 useSearchParams는 굳이 왜 있는거지?

그래서 옳거니 하고 바로 블로그 주제로 잡았다.


1. 요청 코드가 아니라 상태의 위치 차이

useState를 사용하든,
useSearchParams를 사용하든,
결국 fetch 요청을 보면 이렇게 된다.

/products?page=${page}&orderBy=${orderBy}&keyword=${keyword}

서버에서 보내는 요청 형식은 똑같다.
그래서 처음에는 이렇게 생각했다.

둘 다 결국 같은 요청을 보내는 거면
그냥 useState만 써도 되는 거 아닌가?

그런데 여기서 중요한 건
요청 형식이 아니라 값이 어디에서 관리되고 있느냐였다.

useState로 관리하는 경우

const [page, setPage] = useState(1);
const [orderBy, setOrderBy] = useState("recent");
const [keyword, setKeyword] = useState("");

이 값들은 어디에 저장되어 있을까?

정답은 컴포넌트 내부 상태다.

즉 이 값들은 이 컴포넌트가 살아있는 동안만 존재하고
페이지를 새로고침하면 초기화되고
URL 주소에는 아무 정보도 남지 않는다.

이 상태는 컴포넌트 내부에서만 유지되는 상태다.

useSearchParams로 관리하는 경우

const [searchParams, setSearchParams] = useSearchParams();

const page = Number(searchParams.get("page")) || 1;
const orderBy = searchParams.get("orderBy") || "recent";
const keyword = searchParams.get("keyword") || "";

이번에는 값이 어디에 있을까?

이 값들은 컴포넌트 내부 상태가 아니라 URL 쿼리스트링에 저장되어 있다.

주소창을 보면 이렇게 바뀐다.

/products?page=2&orderBy=price&keyword=phone

즉 상태가 컴포넌트가 아니라 URL에 존재하게 된다.

정리하면 차이는 fetch 요청 코드가 아니라 상태가 어디에 저장되는지에 있다.

  • useState → 상태가 컴포넌트 안에서 관리됨
  • useSearchParams → 상태가 URL 쿼리스트링에서 관리됨

이 차이 때문에 페이지 동작 방식이 완전히 달라진다.

  • 새로고침
  • 링크 공유
  • 뒤로가기
  • 앞으로가기
  • 북마크
  • 탭 복사

이 모든 동작이 URL을 기준으로 동작하기 때문이다.

그래서 검색, 정렬, 페이지 번호 같은 값들은
컴포넌트 상태보다 URL 쿼리스트링으로 관리하는 것이 더 자연스럽다.


2. 컴포넌트 안에만 있으면 생기는 문제

앞에서 정리한 것처럼

  • useState → 상태가 컴포넌트 안에서 관리됨
  • useSearchParams → 상태가 URL 쿼리스트링에서 관리됨

여기까지 보면 단순히 저장 위치만 다른 것처럼 보일 수 있다.
그런데 이 저장 위치 차이가 실제 동작에서 큰 차이를 만든다.

특히 검색, 정렬, 페이지 번호 같은 값들은
컴포넌트 안에만 있으면 여러 문제가 생긴다.

새로고침하면 사라지는 상태

예를 들어

  1. 검색어 입력
  2. 정렬 선택
  3. 3페이지까지 이동
  4. 새로고침

useState로 관리하고 있었다면 상태는 전부 초기화된다.

  • 검색어 → 초기화
  • 정렬 → 초기화
  • 페이지 → 1페이지로 돌아감

왜냐하면 useState는 컴포넌트 내부 메모리에 있는 상태라서
페이지가 새로 로드되면 다시 처음부터 시작하기 때문이다.

반대로 URL에 이렇게 남아 있다면

/products?page=3&orderBy=price&keyword=phone

새로고침을 해도
브라우저는 같은 URL을 다시 요청하고,
컴포넌트는 URL에서 값을 다시 읽어오면 된다.

즉 상태가 유지된다.

링크 공유 불가

이건 실제 서비스에서 중요한 부분이다.

예를 들어 어떤 상품을 검색해서
필터 + 정렬 + 페이지 이동까지 한 상태라고 해보자.

검색어: phone
정렬: 가격순
페이지: 3

이 화면을 다른 사람에게 보여주고 싶어서
주소를 복사해서 보내면 어떻게 될까?

  • useState → 상대방은 기본 화면이 뜬다
  • useSearchParams → 같은 검색 결과 화면이 뜬다

왜냐하면 상태가 컴포넌트 안에 있느냐,
URL에 있느냐의 차이이기 때문이다.

그래서 실제 쇼핑몰, 게시판, 검색 페이지 같은 곳에서는
검색 조건, 정렬, 페이지 번호를 URL 쿼리스트링으로 관리하는 경우가 많다.

뒤로가기 / 앞으로가기 동작 불가

페이지 이동 흐름을 생각해보자

  • 1페이지 + 2페이지 → 3페이지

이 상태에서 뒤로가기를 누르면
사용자는 보통 이렇게 기대한다.

  • 3페이지 → 뒤로가기 → 2페이지

그런데 상태가 useState에만 있으면
브라우저는 상태 변화를 기억하지 못한다.

왜냐하면 브라우저는 URL 이동 기록만 기억하기 때문이다.

반대로 URL이 이렇게 바뀌고 있었다면

/products?page=1
/products?page=2
/products?page=3

뒤로가기를 누르면 URL이 바뀌고,
우리는 URL에서 page 값을 다시 읽으면 된다.

그래서 뒤로가기와 앞으로가기 동작이 자연스럽게 동작한다.

검색, 정렬, 페이지 번호 같은 값들은
단순히 화면을 위한 상태가 아니라
현재 어떤 페이지 상태를 보고 있는지를 설명하는 상태다.

그래서 이 값들은 컴포넌트 상태가 아니라 페이지 상태라고 볼 수 있다.

그리고 페이지 상태는
컴포넌트 안이 아니라
URL과 함께 움직이는 것이 더 자연스럽다.

그래서 여기서 useSearchParams를 사용하게 되는 거다.

즉 useSearchParams는 단순히 값을 저장하는 도구가 아니라,
페이지 상태를 URL과 동기화하기 위한 도구라고 볼 수 있다.


3. useSearchParams의 존재 이유

앞에서 정리한 내용을 한 번 이어서 생각해보자.

useState로 관리해도 fetch 요청은 만들 수 있다.
그래서 처음에는 useSearchParams가 왜 필요한지 잘 느껴지지 않을 수 있다.
그런데 검색, 정렬, 페이지 번호는 화면 상태가 아니라 페이지 상태다.
페이지 상태는 URL과 함께 관리되는 것이 더 자연스럽다.
그래서 이런 페이지 상태를 관리할 때 useSearchParams를 사용한다.

여기서 중요한 건

useSearchParams는 fetch 요청을 쉽게 만들기 위한 훅이 아니다.
URL에 상태를 저장하기 위한 훅이다.

이게 거의 이 글의 핵심이다.

useState vs useSearchParams

구분useStateuseSearchParams
상태 저장 위치컴포넌트 내부 상태URL 쿼리스트링
새로고침상태 사라짐상태 유지
링크 공유불가능가능
뒤로가기/앞으로가기상태 이동 안 됨상태 이동
사용 용도UI 상태페이지 상태

여기서 가장 중요한 차이는
useState는 화면(UI)을 위한 상태를 저장하고,
useSearchParams는 페이지 상태를 저장한다는 점이다.

언제 무엇을 쓸까

useState를 쓰는 경우

  • 모달 열림 / 닫힘
  • 드롭다운 열림 / 닫힘
  • input 입력값
  • 탭 선택
  • 체크박스
  • 토글 버튼
  • hover 상태

→ 화면(UI) 안에서만 필요한 상태

useSearchParams를 쓰는 경우

  • 검색어
  • 정렬 방식
  • 페이지 번호
  • 필터 조건
  • 카테고리 선택
  • 보기 방식 (grid/list)

→ 현재 어떤 페이지 상태를 보고 있는지 설명하는 상태

여기서 기준 문장 하나로 정리하면 좋다.

이 값은 화면 안에서만 필요한 값인가?
아니면 현재 페이지를 설명하는 값인가?

이 질문으로 대부분의 경우 어떤 상태 관리 방법을 써야 할지 결정할 수 있다.

useState와 useSearchParams의 차이는 기능의 차이가 아니라 상태를 저장하는 위치의 차이다.

컴포넌트 내부 상태는 useState로 관리하고,
페이지 상태는 useSearchParams로 관리한다.


처음에는 useState로도 충분히 동작하는 것처럼 보여서 주제를 잡았다.
fetch 요청만 보면 useState와 useSearchParams는 큰 차이가 없어 보이기도 한다.

하지만 차이는 요청 방식이 아니라 상태가 어디에 저장되는지에 있었다.
검색, 정렬, 페이지 번호 같은 값들은 단순히 화면을 위한 상태가 아니라
현재 어떤 페이지 상태를 보고 있는지를 설명하는 상태였다.

그래서 이런 값들은 컴포넌트 안이 아니라 URL과 함께 관리되어야 했고,
그 역할을 하는 것이 useSearchParams였다.

결국 React에서 상태를 관리할 때 중요한 것은
어떤 훅을 사용할 것인가가 아니라
이 상태를 어디에 저장해야 하는지를 먼저 생각하는 것이었다.

1개의 댓글

comment-user-thumbnail
2026년 4월 1일

상태를 어디에 저장할지 먼저 고민하는 것이 중요하다는 걸 이해했습니다.
특히 useSearchParams를 사용하면 상태를 URL에 유지할 수 있어서 새로고침이나 공유 시에도 값이 유지된다는 점이 유용하다고 느꼈습니다.

답글 달기