URL 쿼리 스트링 연동 페이지네이션 상태 동기화 구현 절차
관리자 목록 화면에서 페이지 이동 후 새로고침이나 뒤로가기 시 1페이지로 초기화되는 문제를 해결하기 위해 URL 쿼리 스트링을 단일 상태 기준으로 설계하고 동기화하는 실무 구현법을 다룹니다.
가상 업무 사례: 새로고침 시 1페이지로 리셋되는 사내 주문 관리 도구
사내 전자상거래 운영팀에서 수만 건의 주문 내역을 처리하는 내부 백오피스 웹 도구를 개발하는 상황을 가정해 보겠습니다. 이 시스템은 서버에서 데이터를 한 번에 20건씩 불러오는 서버 사이드 페이지네이션 구조로 제작되었습니다. 운영 담당자는 8페이지에 위치한 특정 보류 주문을 확인한 뒤 상세 모달을 열거나 외부 결제 검증 페이지를 다녀오는 일이 빈번합니다.
하지만 컴포넌트 내부 메모리(예: 컴포넌트 로컬 state)에만 현재 페이지 번호를 보관하도록 작성된 경우 심각한 사용성 문제가 발생합니다. 사용자가 상세 페이지로 이동했다가 브라우저 뒤로가기 버튼을 누르면 목록 컴포넌트가 다시 마운트되면서 기본값인 1페이지로 강제 리셋되기 때문입니다. 또한 동료 검수자에게 '8페이지 세 번째 주문을 확인해 달라'며 링크를 복사해 메신저로 전달해도, 수신자는 1페이지 화면만 보게 되어 매번 페이지 번호를 수동으로 다시 찾아 눌러야 하는 비효율이 발생합니다.

상태 동기화 요구 조건과 단일 진실 공급원(SSOT) 설계
이러한 탐색 문제를 근본적으로 해결하려면 컴포넌트의 로컬 상태가 아니라 웹 브라우저의 'URL 쿼리 스트링'을 목록 위치의 단일 진실 공급원(Single Source of Truth)으로 삼아야 합니다. 화면의 페이지네이션 UI는 URL에 적힌 파라미터 값을 읽어와 보여주는 순수 뷰 컴포넌트로 동작하고, 사용자의 클릭 동작은 URL을 변경하는 트리거 역할만 수행하도록 구조를 분리하는 것이 핵심입니다.
이를 달성하기 위한 구체적인 시스템 요구 조건은 세 가지입니다. 첫째, 사용자가 URL의 `?page=4` 형태로 직접 진입하거나 새로고침하더라도 해당 페이지의 API 요청이 자동으로 실행되어야 합니다. 둘째, 페이지 버튼 클릭 시 전체 화면을 새로고침하지 않고 브라우저 히스토리 스택에 탐색 기록을 안전하게 누적(pushState)해야 합니다. 셋째, 브라우저 뒤로가기나 앞으로가기를 눌렀을 때 발생하는 `popstate` 이벤트를 감지하여 화면 목록과 활성 페이지 버튼 표시가 즉시 일치해야 합니다.

URLSearchParams와 히스토리 API를 결합한 접근 및 구현 절차
구현의 첫 단계는 브라우저 내장 객체인 `URLSearchParams`를 활용하여 주소창의 쿼리 스트링을 파싱하고 유효성을 검증하는 것입니다. 주소창의 `window.location.search`를 읽어 `page` 파라미터가 비어 있거나 음수 또는 숫자가 아닌 값이 들어왔을 때 안전하게 기본값 1로 폴백(fallback) 처리하는 방어 로직을 작성합니다. 이 값을 기준으로 API 호출 함수를 실행해 목록 데이터를 렌더링합니다.
페이지 이동 버튼을 클릭했을 때는 컴포넌트 내부 변수를 직접 수정하지 않습니다. 대신 현재 URL의 searchParams 인스턴스를 복사하여 `searchParams.set('page', targetPage)` 형태로 새 값을 주입하고, `window.history.pushState(null, '', newUrl)`를 실행하여 주소창만 부드럽게 갱신합니다. 최신 SPA 프레임워크(React Router, Next.js 등)를 사용하는 환경이라면 라우터가 제공하는 `useSearchParams`나 `router.push({ query: { page } })` 기능을 사용해 동일한 메커니즘을 완성할 수 있습니다.
마지막으로 네이티브 자바스크립트 환경에서는 `window.addEventListener('popstate', handler)`를 등록하여 사용자의 뒤로가기 동작 시 현재 URL 파라미터를 다시 읽어 API를 호출하는 갱신 루틴을 연결합니다. 프레임워크를 사용하는 경우에는 라우터의 쿼리 변경 훅(예: `useEffect`의 의존성 배열에 query 파라미터 등록)을 활용하여 URL 변화가 곧바로 목록 재조회로 이어지도록 설정합니다.

브라우저 탐색 및 엣지 케이스 동작 결과 검증
구현이 완료되었다면 브라우저 개발자 도구의 네트워크 탭과 콘솔을 열고 단계별로 동작을 검증해야 합니다. 1페이지에서 5페이지로 이동했을 때 주소창이 새로고침 없이 즉각 `?page=5`로 바뀌는지 확인하고, 해당 시점에 5페이지에 해당하는 API 요청 1건만 정확히 전송되는지 네트워크 탭을 통해 모니터링합니다. 불필요하게 API가 중복으로 2회 호출되는 레이스 컨디션이 없는지 점검하는 것이 필수적입니다.
이어서 브라우저의 뒤로가기 및 앞으로가기 버튼을 연속으로 눌러봅니다. 주소창의 숫자가 바뀌는 타이밍에 맞춰 페이지네이션 UI의 활성 버튼 스타일과 목록 렌더링이 오차 없이 맞춰지는지 확인합니다. 또한 주소창에 수동으로 `?page=abc`나 `?page=-10`, 또는 최대 페이지 수를 초과하는 `?page=999999`를 입력하고 엔터를 쳤을 때, 애플리케이션이 크래시되지 않고 안전하게 1페이지 또는 최대 페이지로 보정 처리되는지 엣지 케이스를 반드시 확인해야 합니다.

다중 필터 및 검색 조건 누적 응용 방향
페이지 번호 동기화 구조가 안정화되었다면, 동일한 패턴을 검색창의 검색어(`keyword`), 카테고리 셀렉트박스(`category`), 정렬 기준(`sort`) 등 다른 필터 상태로 손쉽게 확장할 수 있습니다. 예를 들어 검색어를 변경할 때는 페이지 번호를 다시 1페이지로 초기화해야 하므로, `searchParams.set('keyword', value)`와 함께 `searchParams.set('page', '1')`을 동시에 처리하는 쿼리 빌더 유틸리티 함수를 구축하면 일관된 상태 관리가 가능해집니다.
더 나아가 무한 스크롤(Infinite Scroll) 인터페이스를 구축할 때도 뷰포트에 도달한 섹션 번호를 `history.replaceState`로 URL에 주기적으로 반영해 두면, 사용자가 특정 아이템을 누르고 나갔다 돌아와도 이전 스크롤 지점을 완벽하게 복원할 수 있습니다. 이처럼 URL을 핵심 상태 저장소로 다루는 패턴은 컴포넌트 간 복잡한 Props 드릴링을 줄여줄 뿐 아니라 웹 본연의 공유성과 접근성을 극대화하는 표준적인 개발 접근법입니다.