URL 객체는 URL을 파싱 및 조작하기 위한 JS의 내장 API이다. URL 객체를 사용하여 URL의 각 구성 요소로 접근이 가능하다. 접근하여 수정 및 삭제도 가능하다.
// URL 객체 생성
const url = new URL('https://www.example.com:8080/path/to/page?name=value&search=test#section');
// URL 구성 요소에 접근
console.log(url.protocol);// 'https:'
console.log(url.hostname);// 'www.example.com'
console.log(url.host);// 'www.example.com:8080'
console.log(url.port);// '8080'
console.log(url.pathname);// '/path/to/page'
console.log(url.search);// '?name=value&search=test'
console.log(url.hash);// '#section'
console.log(url.origin);// 'https://www.example.com:8080'
쿼리스트링은 검색 파라미터라고 불립니다. URL에서 경로다음 ? 다음에 나오는 것을 쿼리스트링 이라고 합니다. URL의 쿼리스트링을 분석 및 변경하면 조건에 맞게 특정 형태의 정보를 요청받을 수 있습니다. 위에 코드에서는
name=value&search=test가 쿼리 스트링이다.
URL API에서 제공하는 주소창의 경로를 다룰 수 있는 브라우저의 내장 객체입니다.
URLSearchParmas에는 12가지 메서드가 있으며 메서드를 이용해 수정.추가,삭제 등이 가능하다.
searchParams.append("mode", "dark");searchParams.set("mode", "dark");searchParams.delete("sort");두개의 차이점은 리액트 라우터 방식 VS 브라우저 기본 방식으로 나눌 수 있다.
useSearchParams를 이용하면 페이지를 전체 새로고침 하지않고 URL만 바귀며 컴포넌트는 다시 렌더링 된다. SPA에 맞게 작동하며 window.location.href를 사용하면 페이지가 새로고침 된다.
React의 상태 및 훅이 다 초기화 된다.
const [isActive, setIsActive] = useState(false)
const [searchParams] = useSearchParams()
const initProductData = useLoaderData()
const navigate = useNavigate()
const { products, per_page } = initProductData
const data = products.data
const sortCase = searchParams.get('_sort')
const currentCategory = searchParams.get('category')
const handleCategoryFilter = category => {
const params = new URLSearchParams(searchParams) // 현재 파라미터 정보 유지
console.log(params)
params.set('_page', 1)
params.set('_per_page', per_page)
category ? params.set('category', category) : params.delete('category')
navigate(`/shop/?${params}`)
}
여기서 useNavigate를 사용하지 않고 useSearchParams에서 setSearchParams를 이용해도 되지 않나 라는 생각을 하였고 GPT에게 물어보니 가능하다고 한다. 또한 setSearchParmas를 ㅣ이용하는 것이 useNavigate를 굳이 사용하지 않아도 되서 좋고 또한 불필요한 리렌더링을 방지한다고 한다.
여기서 navigate를 사용해서 하는 경우를 생각하면 동일한 경로인데 query값만 바뀐다면 setSearchParams를 사용해도 괜찮지만 경로가 다르고 query값도 달라진다면 useNavigate를 사용하여 좀 더 유연하게 이동을 하는것이 더 괜찮다.
const [isActive, setIsActive] = useState(false)
const [searchParams, setSearchParams] = useSearchParams()
const initProductData = useLoaderData()
const { products, per_page } = initProductData
const data = products.data
const sortCase = searchParams.get('_sort')
const currentCategory = searchParams.get('category')
const handleCategoryFilter = category => {
const params = new URLSearchParams(searchParams) // 현재 파라미터 정보 유지
console.log(params)
params.set('_page', 1)
params.set('_per_page', per_page)
category ? params.set('category', category) : params.delete('category')
setSearchParams(params)
}
불필요한 useNavigate를 사용하지 않았고 불필요한 리렌더링이나 페이지 이동을 최소화 할 수 있도록 setSearchParams에 params를 전달해 상태만 업데이트를 하였음.
오늘 페이지 네이션을 중점으로 배웠다. 또한 쿼리스트링을 이용하여 변화를 주는 방법도 오늘의 중요 포인트이다. 페이지 네이션 컴포넌트를 하나하나 뜯어보겠다.
const [searchParams, setSearchParmas] = useSearchParams()
const params = new URLSearchParams(searchParams)
const { last, first, prev, next, pages } = data.products
const currentPage = Number(params.get('_page') || 1)
const handlePageChange = page => {
const params = new URLSearchParams(searchParams)
params.set('_page', page)
setSearchParmas(params)
}
const getPageNumber = () => {
const maxPageNumber = 10
// 전체 페이지가 최대 페이지 보다 작으면 모든 페이지 보여줌
if (pages <= maxPageNumber) {
return Array.from({ length: pages }, (_, i) => i + 1)
}
// 페이지가 많을 경우 현재 페이지를 기준으로 하여 주변 번호 생성
// 예) 현재 페이지 15 => 10~20 페이지 보여줘야함.
let startPage = Math.max(1, currentPage - Math.floor(maxPageNumber / 2))
let endPage = Math.min(pages, startPage + maxPageNumber - 1)
startPage = Math.max(1, endPage - maxPageNumber + 1)
return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
}
pages가 maxPageNumber보다 작거나 같다면, 전체 페이지 수만큼의 번호만 보여주면 된다.
그 외의 경우엔, 현재 페이지(currentPage)를 중심으로 maxPageNumber개의 페이지 번호를 보여주어야 한다.
이때 startPage는 currentPage - (maxPageNumber / 2)로 계산하고, 값이 1보다 작아질 수 있으므로 Math.max(1, 계산값)으로 최소값을 1로 두었다.
이렇게 하면 현재 페이지가 가운데쯤 위치하도록 시작 페이지를 정할 수 있다.
endPage는 startPage + maxPageNumber - 1로 계산하고,
이 값이 전체 페이지 수(pages)보다 커질 수 있으므로
Math.min(pages, 계산값)으로 범위를 제한하였다.
만약 endPage가 제한되어 조정되었다면,
다시 startPage를 endPage - maxPageNumber + 1로 재계산해
항상 정확히 maxPageNumber 개수만큼의 페이지 번호가 표시되도록 맞춘다.
<div className={styles.paginationArea}>
<button
onClick={() => handlePageChange(first)}
disabled={currentPage === first}
className={currentPage === first ? styles.disabled : ''}
>
처음으로
</button>
<button
onClick={() => handlePageChange(prev)}
disabled={prev === null || currentPage === first}
className={currentPage === first ? styles.disabled : ''}
>
<i className="bi bi-chevron-left"></i>
</button>
{pageNumbers.map(num => (
<button
className={num === currentPage ? styles.active : ''}
onClick={() => handlePageChange(num)}
key={num}
>
{num}
</button>
))}
<button
onClick={() => handlePageChange(next)}
disabled={next === null || currentPage === last}
className={currentPage === last ? styles.disabled : ''}
>
<i className="bi bi-chevron-right"></i>
</button>
<button
onClick={() => handlePageChange(last)}
disabled={currentPage === last}
className={currentPage === last ? styles.disabled : ''}
>
마지막으로
</button>
</div>
버튼을 클릭하면 handlePageChange에 값을 주도록 하였고 currentPage와 num이 같으면 active 클래스를 주어 색깔이 변경 되게끔 하였다. 또한 앞 뒤 처음 마지막 버튼에는 조건을 추가하여 다음으로 또는 이전으로 갈 수 없는 상황(?)상태 라면 disabled를 true로 주었고 disabled 클래스도 추가해 주었다.