Post

[Day59]쇼핑몰 실습 - ShopPage,Pagination

[Day59]쇼핑몰 실습 - ShopPage,Pagination

URL과 params에 대한 핵심 내용은 따로 포스팅 해두었다 !
URL? URLSearchParams? useSearchParams?


Shop

기능

  • 상품 목록 페이지
  • 카테고리 필터링
  • 정렬 기능
  • 페이지네이션
  • URL Query 파라미터를 활용한 상태 관리 ?category=new&_sort=price&_page=2

Router 설정 router.jsx

1
{ path: '/shop', element: <ShopPage />, loader: shopPageLoader }
  • /shop 경로로 진입 시:
    • shopPageLoader로 데이터를 미리 불러오고
    • ShopPage 컴포넌트가 해당 데이터를 받아 렌더링

shopPageLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const shopPageLoader = async ({ request }) => {
  const url = new URL(request.url)
  const page = url.searchParams.get('_page') || 1
  const per_page = url.searchParams.get('_per_page') || 12
  const category = url.searchParams.get('category') || ''
  const sort = url.searchParams.get('_sort') || ''

  let queryString = `_page=${page}&_per_page=${per_page}`
  category ? (queryString += `&category=${category}`) : queryString
  sort ? (queryString += `&_sort=${sort}`) : queryString

  try {
    const products = await getProductsData(queryString)
    return { products, per_page }
  } catch (err) {
    console.log('err----', err)
    throw new Response('상품 데이터를 가져오는 중 오류 발생', {
      status: err.status || 500,
    })
  }
}
  1. request.url에서 query 파라미터 추출
  2. 해당 파라미터로 API 호출용 queryString 생성
  3. getProductsData(queryString)로 상품 데이터 요청
  4. 반환값은 { products, per_page } 형태로 전달됨

❓ loader({ request })의 request는 어디서 오는가?
✅ React Router가 자동으로 넣어준다 !
request는 브라우저가 현재 라우트를 요청할 때의 정보를 담은 Fetch API 스타일의 Request 객체

1
2
3
4
5
// 이 정보들이 포함되어 있음
request.url  // 현재 페이지의 전체 URL (query 포함)
request.method  // 'GET' 같은 HTTP 메서드
request.headers // 헤더 정보
request.signal  // AbortController용

ShopPage

🔹 카테고리 필터링

1
2
3
4
5
6
7
const handleCategoryFilter = category => {
    const params = new URLSearchParams(searchParams) // 현재 파라미터 정보 유지
    params.set('_page', 1) // 페이지를 1로 초기화
    params.set('_per_page', per_page) // 페이지당 상품 수를 설정
    category ? params.set('category', category) : params.delete('category') // 카테고리 필터링
    navigate(`/shop/?${params}`) // URL 변경
}
1
2
3
4
5
6
<button
  onClick={() => handleCategoryFilter('new')}
  className={currentCategory === 'new' ? css.active : ''}
>
  신상품(new)
</button>
  • 클릭 시 URL 파라미터 변경 → loader 재실행 → 상품 재조회

🔹 정렬 기능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const handleSort = sortOption => {
    const params = new URLSearchParams(searchParams)
    params.set('_page', 1)
    params.set('_sort', sortOption) // 선택한 정렬 기준을 _sort로 URL에 반영
    setIsDown(false)
    navigate(`/shop/?${params}`)
}

const sortTextMap = {
    id: '등록순',
    price: '낮은 가격순',
    '-price': '높은 가격순',
    discount: '낮은 할인순',
    '-discount': '높은 할인순',
}

const getSortText = () => {
    return sortTextMap[sortCase] || '등록순'
}
1
2
3
4
5
6
<div className={css.sortHeader} onClick={() => setIsDown(!isDown)}>
  <p>{getSortText()}</p>
</div>
<ul>
  <li onClick={() => handleSort('price')}>낮은 가격순</li>
</ul>
  • 토글로 정렬 옵션 노출
  • 정렬 선택 시 navigate()로 URL 변경 → loader 재호출

Pagination

주요 흐름

1
2
3
const Pagination = ({ initProductsData }) => {
  const [searchParams] = useSearchParams()
  const navigate = useNavigate()
  • initProductsData: loader에서 받아온 상품 데이터
  • useSearchParams: 현재 URL 쿼리 파라미터 정보 가져옴
  • navigate: 페이지 변경 시 URL 변경용 함수

🔹 페이지 이동 : handlePageChange

1
2
3
4
5
const handlePageChange = page => {
  const params = new URLSearchParams(searchParams)
  params.set('_page', page)
  navigate(`/shop/?${params}`)
}
  • _page 파라미터를 원하는 페이지로 변경한 후 URL 업데이트 → loader 재호출됨

🔹 페이지 번호 배열 생성: getPageNumbers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const getPageNumbers = () => {
    // 한번에 보여줄 최대 페이지 번호 수
    const maxPageNumbers = 10
    // 전체 페이지가가 최대 페이지보다 작으면 모든 페이지 번호 표시
    if (pages <= maxPageNumbers) {
      return Array.from({ length: maxPageNumbers }, (_, i) => i + 1)
    }
    // 페이지가 많을 경우 현재 페이지 번호를 기준으로 주변 번호 생성
    // 예) 현재 페이지 15 => 10 ~ 25까지 보여줌
    let startPage = Math.max(1, currentPage - Math.floor(maxPageNumbers / 2))
    let endPage = Math.min(pages, startPage + maxPageNumbers - 1)
    startPage = Math.max(1, endPage - maxPageNumbers + 1)

    return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
}
1
2
3
4
5
<button onClick={() => handlePageChange(first)}>처음으로 이동</button>
<button onClick={() => handlePageChange(prev)}><</button>
{pageNumbers.map(...)} // 페이지 번호 버튼
<button onClick={() => handlePageChange(next)}>{'>'}</button>
<button onClick={() => handlePageChange(last)}>마지막 페이지</button>
  • first, prev, next, last는 모두 initProductsData.products에서 전달받은 값
  • 현재 페이지일 경우 비활성화(disabled), 또는 스타일(className) 처리

상품 페이지 + 페이지네이션

day53 day53 day53


END

This post is licensed under CC BY 4.0 by the author.