[프론트 스터디 1주차] 페이지네이션

강동욱·2024년 2월 25일
0

들어가기 앞서

이번 1주차 과제를 진행하면서 리액트에 대해서 다시 한번 많을 것을 느꼈다. 제일 크게 느낀것은 2가지가 있었다.

  • 한가지 방식에 집중을 하다보면 다른 방식을 생각하지 못한 나
  • 이미지 로딩을 다루면서 네트워크탭을 보고 data를 불러오는거랑 이미지 소스가 로딩되는 것은 다른 부분인 것을 새삼 느꼈다.

이번 블로그는 이 두가지에 대해서 얘기를 나눠보려고 한다.

Pagination 섹션을 보여주는 코드

기존 코드

import { usePageContext } from '@contexts/PageContext'
import { useEffect, useState } from 'react'
import { MAX_PAGE_COUNT, VISIBLE_PAGE_BUTTONS_NUMBER } from '@utils/Constants'

const startEndPageBtnHandler = (currentPage: number) => {
  const currentPageReminder = currentPage % VISIBLE_PAGE_BUTTONS_NUMBER
  let visiblePageStart: number
  let visiblePageEnd: number

  if (currentPageReminder === 0) {
    visiblePageStart = currentPage - VISIBLE_PAGE_BUTTONS_NUMBER
    visiblePageEnd = currentPage

    return { visiblePageStart, visiblePageEnd }
  }

  visiblePageStart = currentPage - currentPageReminder
  visiblePageEnd = currentPage + (VISIBLE_PAGE_BUTTONS_NUMBER - currentPageReminder)

  if (visiblePageEnd > MAX_PAGE_COUNT) {
    visiblePageEnd = MAX_PAGE_COUNT
  }

  return { visiblePageStart, visiblePageEnd }
}

export const useGetPageSection = () => {
  const { currentPage, nextPageSection, setNextPageSection, prevPageSection, setPrevPageSection } = usePageContext()
  const allPageBtns = Array.from({ length: MAX_PAGE_COUNT }, (_, i) => i + 1)
  const [visiblePageBtnSection, setVisiblePageBtnSection] = useState<number[]>(() => {
    const { visiblePageStart, visiblePageEnd } = startEndPageBtnHandler(currentPage)

    return [...allPageBtns.slice(visiblePageStart, visiblePageEnd)]
  })

  useEffect(() => {
    const { visiblePageStart, visiblePageEnd } = startEndPageBtnHandler(currentPage)

    if (nextPageSection) {
      setVisiblePageBtnSection(allPageBtns.slice(visiblePageStart, visiblePageEnd))
      setNextPageSection(false)
    }

    if (prevPageSection) {
      setVisiblePageBtnSection(allPageBtns.slice(visiblePageStart, visiblePageEnd))
      setPrevPageSection(false)
    }
  }, [currentPage, nextPageSection, allPageBtns, prevPageSection, setNextPageSection, setPrevPageSection])

  return visiblePageBtnSection
}

간단하게 설명을 하자면 총 페이지수가 20개라고 하면 5개씩 페이지를 끊어서 보여줄 때 5페이지에서 6페이지로 넘어갈때 6~10페이지를 전부 보여주는 로직이다. 일단 나는 5에서 6페이지로 넘어갈 때 5개의 페이지를 보여주고 싶어서 현재페이지인 6에서 나누기 5를 했을때 나머지가 1이다. 그런데 현재 페이지는 6이고 6번 페이지를 담고있는 배열의 인덱스는 5이기 때문에 6에서 나머지 1을 빼줌으로써 시작인덱스를 설정하고 있다. 그런데 문제는 어차피 보여줄 개수만 알면 마지막 인덱스를 따로 설정을 안해도 되는데 이미 이 방식에 생각이 꽂힌 나머지 마지막 인덱스도 나머지를 이용해서 구하는 코드를 짜버렸다. 그래도 이런것도 경험이고 내가 이런 생각을 한다는 것 자체가 이전보다 실력이 조금은 상승했구나 생각이 된 경험이었다.

리팩토링 코드

import { usePageContext } from '@contexts/PageContext'
import { useEffect, useState } from 'react'
import { MAX_PAGE_COUNT, VISIBLE_PAGE_BUTTONS_NUMBER } from '@utils/Constants'

const visiblePageHandler = (currentPage: number): number[] => {
  const allPageBtns = Array.from({ length: MAX_PAGE_COUNT }, (_, i) => i + 1)
  const currentPageReminder = currentPage % VISIBLE_PAGE_BUTTONS_NUMBER
  const visiblePageStart: number =
    currentPageReminder === 0 ? currentPage - VISIBLE_PAGE_BUTTONS_NUMBER : currentPage - currentPageReminder
  const visiblePageEnd = visiblePageStart + VISIBLE_PAGE_BUTTONS_NUMBER

  if (visiblePageEnd > MAX_PAGE_COUNT) {
    allPageBtns.slice(visiblePageStart, MAX_PAGE_COUNT)
  }

  return allPageBtns.slice(visiblePageStart, visiblePageEnd)
}

export const useGetPageSection = () => {
  const { currentPage, nextPageSection, setNextPageSection, prevPageSection, setPrevPageSection } = usePageContext()
  const [visiblePageBtnSection, setVisiblePageBtnSection] = useState<number[]>(() => {
    const visiblePageArr = visiblePageHandler(currentPage)

    return visiblePageArr
  })

  useEffect(() => {
    if (nextPageSection) {
      const visiblePageArr = visiblePageHandler(currentPage)
      setVisiblePageBtnSection(visiblePageArr)
      setNextPageSection(false)
    }

    if (prevPageSection) {
      const visiblePageArr = visiblePageHandler(currentPage)
      setVisiblePageBtnSection(visiblePageArr)
      setPrevPageSection(false)
    }
  }, [currentPage, nextPageSection, prevPageSection, setNextPageSection, setPrevPageSection])

  return visiblePageBtnSection
}

1페이지에서 5페이지 안의 숫자들이 생길때 6페이지에서 10페이지로 넘어간다면 6페이지에 해당하는 인덱스만 나머지를 이용해서 구하고 그 다음은 한 페이지 섹션당 보여질 페이지 개수를 더하는 로직으로 코드를 리팩토링 했다. 확실히 startEndPageBtnHandler의 코드 비중이 줄어들었다

이미지 관련 로딩

문제

위와 같은 영상을 보면 suspense로 fallback을 스켈레톤 UI로 대체했지만 문제는 스켈레톤 로딩이 끝난 뒤에 이미지가 각각 따로 로드되는것을 볼 수 있다. 이것에 대해서 원인을 생각하다가 네크워크탭을 한번 보기로 결정했다.

위와 같이 네트워크탭을 잘보면 파란부분이 컨텐츠 다운로드 시간이고 초록색이 서버응답을 기다리는 시간이다. reaceSuspenseQuery는 초록색 그래프 시간까지 관리해주는거고 더 나은 사용자 경험을 위해서는 파란색 그래프가 다 끝날때 까지 skeleton UI가 계속 화면에 렌더링이 되어야한다.

해결방안

const fetchAnimalPost = async (currentPage: number) => {
  const { data } = await axiosAnimalApi.get<AnimalPostType[]>(`${currentPage}`)
  // 해결방안!
  const postsUrls = data.map(post => {
    return new Promise((res, rej) => {
      const image = new Image()
      image.src = post.url
      image.onload = () => res(post.url)
      image.onerror = () => rej(post.url)
    })
  })

  await Promise.all(postsUrls)

  return data
}

export const useGetAnimalPicture = () => {
  const { currentPage } = usePageContext()
  const { data } = useSuspenseQuery({
    queryKey: ['animalPicture', currentPage],
    queryFn: () => fetchAnimalPost(currentPage),
    staleTime: 4 * (1000 * 60),
    retry: 0,
  })

  return data
}

위에 코드는 동물사진을 가지고오는 훅인데 프로미스 형태를 가진 배열 postUrls라는 변수에 담아준다. 배열의 요소들은 새로운 이미지 인스턴스를 생성해서 image가 onload 되었을때만 이미지의 주소값을 resolve로 감싸줘서 반환한다. 그리고 나서 await Promise.all을 사용해서 postUrl안에 있는 요소들이 전부 onload되어서 resolve를 통해 반환되어질 때까지 기다리게 한다. 그리고 postUrls를 반환해도되지만 이지미 주소값 이외에 여러 데이터를 담고있는 기존에 불러온 data를 반환해도 되기때문에 data를 반환해줬다.

결과는 다음과 같이 한번에 이미지가 불러와졌다.

profile
차근차근 개발자

0개의 댓글