React - 뭐? 페이지 네이션 기능이 없다고? 그럼 만들어야지

hee·2023년 10월 22일
1
post-thumbnail

서비스를 개발 중에 게시글이 많아지면 게시글을 일정 개수만 보여주는 페이지 네이션이 필요할것 같다는 생각이 들어 페이지 네이션을 구현 해보기로 했습니다.

페이지 네이션을 하면 사용자가 너무 많은 데이터를 한번에 보고 원하는 데이터를 계속 화면을 내려가며 찾아야 하는 문제를 해결 할 수 있습니다. 그리고 서버에서 화면에 보여 줄 데이터만 가지고 오기 때문에 성능적으로도 좋을 것 입니다.

페이지 네이션?

페이지 네이션이란 문서를 개별 페이지로 나누는 프로세스로 페이징이라고도 합니다.

구현 전 문제 인식

저는 백엔드를 파이어베이스로 구현하고 있었기 때문에 파이어베이스에 페이지 네이션 기능이 있는지 알아 보았습니다.

찾아본 결과 파이어베이스에서는 커서 기반의 페이지 네이션은 지원하지만 오프셋 기반의 페이지 네이션은 지원하지 않았습니다. (제가 자세히 찾아보지 않은 것 일수도 있습니다.)

저는 오프셋 기반의 페이지 네이션을 원했기 때문에 직접 구현 해보기로 했습니다.

아이디어 1

가장 먼저 생각나는 방법은 서버에서 전체 데이터를 가지고 와서 프론트에서 데이터를 나눠 화면에 보여주는 것 입니다.
근데 이 방법은 페이지 네이션을 구현하는 목적 중 하나인 화면에 보여 줄 데이터만 서버에서 가지고 와서 성능적인 이득을 얻는다에 부합하지 않다고 생각합니다. 서버에서 계속 전체 데이터를 가지고 오는 것도 좋지 않고 데이터가 많아지면 그 데이터를 프론트에서 나눠 화면에 보여주는것도 성능적으로 좋지 않다고 생각 합니다.

아이디어 2

두번째 생각 나는 방법은 파이어베이스에서 제공되는 쿼리 함수들을 이용하는 것 입니다. 이것이 주어진 환경에서 최선의 방법이라고 생각됩니다.

먼저 limit 함수를 이용해 데이터베이스에서 데이터를 조회 할 때 조회 할 데이터를 지정 합니다.
그리고 파이어베이스에서는 리스트 자료구조 같이 인덱스로 접근해서 내가 원하는 데이터만 가지고 오는것이 지원되지 않습니다.
그럼 어떻게 하냐?
불행중 다행일까요 파이어베이스에서 쿼리 함수로 범위를 지정할 수 있는 함수가 있습니다.
startAt, startAfter, endAt, endAfter 함수가 그 주인공 입니다. 인자에 넘겨준 값을 기준으로 start 는 오름차순, end 는 내림차순으로 데이터의 범위를 지정 할 수 있습니다.
At 으로 끝나는 함수들은 인자로 넘겨주는 값부터이고 After 로 끝나는 함수들은 인자로 넘겨주는 값 다음 데이터부터 입니다.

그럼 limit 과 위 4개의 함수를 잘 조합하면 원하는 범위의 데이터를 원하는 개수만큼 서버에서 가지고 올 수 있을 것 같습니다.

고려해야 할 점

페이지 네이션을 어떻게 구현할지 아이디어를 생각 해봤으니 이제 구현 중 고려해야 할 점을 생각 해봅시다.

  1. 어떤 방법으로 서버에게 요청하는 데이터의 범위를 정확하게 서버에게 요청하여 필요한 데이터만 가지고 올 것인가?

  2. 사용자가 어떤 페이지를 보다가 새로고침하게 되면 어떻게 사용자가 최근까지 보고 있던 페이지를 기억하여 다시 화면에 보여줄 것인가?

제가 생각한 페이지를 구현할 때 고려해야 할 점 이었습니다.

고려해야 할 점 1

제가 생각한 구현 방법? 또는 데이터를 서버에서 불러와야 하는 순서 입니다.

  1. 가장 먼저 화면에서 게시글들이 마운트 될 때 그때는 모든 데이터를 서버에게 요청 합니다. 그 이유는 처음 해당 데이터들의 개수가 몇개인지 알아야 화면에 몇 페이지까지 만들어 질지 UI 를 만들어 화면에 보여줘야 하기 때문입니다.

    위와 같이 사용자에게 현재 게시된 게시글이 몇 페이지까지 있는지 알려 줄 수 있습니다.

저는 useEffect 를 이용하여 처음 게시글들이 마운트 될 때 모든 데이터를 서버에서 불러 왔습니다.
그럼 코드로 보시죠.

  const [currentData, setCurrentData] = useState([]);
// 현재 화면에 보여줄 게시글 데이터 스테이트
  const [postData, setPostData] = useState(null);
// 전체 데이터를 저장, 페이지를 보여주는 UI 를 구현하는데 사용


// 처음 마운트 되었을 때 모든 데이터를 가지고 오는 함수
  const getPostData = useCallback(async () => {
    let q;
    try {
      q = query(
        collection(dbService, "test"),
        orderBy("createTime", "desc") // createTime 기준으로 내림차순으로 정렬
      );
      onSnapshot(q, (snapshot) => {
        const data = []; // 서버에서 받은 데이터를 임시로 저장 할 배열
        setCurrentData([]); // 새롭게 불러온 데이터를 저장하기 위해 현재 데이터를 초기화
        snapshot.forEach((doc) => data.push({ ...doc.data(), id: doc.id }));
        
        setPostData(data); 
        setCurrentData(data.slice(0, 8)); // 화면에 8개의 데이터만 보여줄 것이기 때문에 8개 데이터만 저장
      });
    } catch (e) {
      console.log(e);
    }
  }, []);

// 컴포넌트가 마운트되면 호출
 useEffect(() => {
    getPostData();
  }, []);

먼저 두개의 스테이스를 생성 합니다.

const [currentData, setCurrentData] = useState([]);
// 현재 화면에 보여줄 게시글 데이터 스테이트
const [postData, setPostData] = useState(null);
// 전체 데이터를 저장, 페이지를 보여주는 UI 를 구현하는데 사용

currentData 는 현재 화면에 보여줄 데이터를 가지는 스테이트 이고 postData 는 전체 데이터를 서버에서 불러올 때 전체 데이터를 받아 저장하는 스테이트 입니다.

 useEffect(() => {
    getPostData();
  }, []);

이제 컴포넌트가 마운트 되면 전체 데이터를 불러오는 함수를 호출 합니다.

가장 먼저 query 문을 작성해야 됩니다. 데이터를 어떻게 가지고 올지 서버에게 요청해야 하기 때문 입니다.

 const q = query(
        collection(dbService, "test"),
        orderBy("createTime", "desc") // createTime 기준으로 내림차순으로 정렬
      );

저는 test collection 안의 doc 들을 생성된 시간 기준으로 내림차순으로 데이터를 받겠다고 쿼리문을 생성하였습니다.
내림차순으로 해야 사용자는 최신순으로 게시글을 볼 수 있겠습니다.

      const querySnapshot = await getDocs(q);
      const data = [];
      querySnapshot.forEach((doc) => {
        data.push({ ...doc.data(), id: doc.id });
      });
      setPostData(data);
      setCurrentData(data.slice(0, 8));

이제 서버로부터 전체 데이터를 받아와 data 배열에 저장 합니다. data 배열에 먼저 저장 하는 이유는 data 배열로 두개의 스테이트에 값을 저장 할 것이기 때문입니다. 하지만 데이터를 다르게 저장 할 것 입니다.
postData 스테이트는 페이지 네이션 네비게이션 UI 를 만들어야 하기 때문에 전체 데이터가 필요합니다. data 를 모두 postData 로 업데이트 합니다.
currentData 는 화면에 8개의 게시글만 보여주기 위해서 data 의 데이터 중 8개의 데이터만 업데이트 합니다.

그럼 전체 코드를 보겠습니다.

  const getPostData = useCallback(async () => {
    try {
      const q = query(
        collection(dbService, "test"),
        orderBy("createTime", "desc") // createTime 기준으로 내림차순으로 정렬
      );
      onSnapshot(q, (snapshot) => {
        const data = []; // 서버에서 받은 데이터를 임시로 저장 할 배열
        setCurrentData([]); // 새롭게 불러온 데이터를 저장하기 위해 현재 데이터를 초기화
        snapshot.forEach((doc) => data.push({ ...doc.data(), id: doc.id }));
        
        setPostData(data); 
        setCurrentData(data.slice(0, 8)); // 화면에 8개의 데이터만 보여줄 것이기 때문에 8개 데이터만 저장
      });
    } catch (e) {
      console.log(e);
    }
  }, []);

코드의 위에서부터 보시면 일단 서버에서 데이터를 불러오기 때문에 비동기적인 로직을 처리해야 합니다. 그래서 getPostData 함수를 async 문법을 이용해 감싸줬습니다.
이제 try, catch 문을 만들고 try 문에 안에 서버에게 전체 데이터를 불러 올 로직을 작성 합니다.

그럼 이제 currentData 의 스테이트를 화면에 뿌려 줍니다.

그리고 postData 를 가지고 페이지 네이션 네비게이션 UI 를 만들어 줍니다.
아래는 페이지 네이션 네비게이션 UI 를 만드는 설명 입니다. 해당 코드에서 컴포넌트의 onClick 함수의 콜백함수는 다음 시간에 따로 정리해서 설명 하겠습니다. 해당 콜백함수들이 UI 를 클릭 했을때 페이지에 해당하는 데이터를 보여주는 로직 입니다.

왼쪽 페이지로 이동하는 화살표 UI 구현

 <HomeStyle.PrevBtn
            onClick={() => onclickPageHandler(2)}
            clickDisable={
              postData &&
              currentData &&
              postData[0].createTime !== currentData[0].createTime
                ? true
                : false
            }
          >
       // 화살표를 불러오는 icon 코드
          </HomeStyle.PrevBtn>

저는 styled components 를 사용해 일단 왼쪽 화살표 스타일 컴포넌트를 생성 하였습니다. 그리고 clickDisable 이라는 prop 를 PrevBtn 컴포넌트에 true 또는 false를 전달 하였는데 이 이유는 첫 페이지라면 더이상 옆으로 이동할 페이지가 없기 때문에 첫 페이지라면 PrevBtn 의 UI 의 클릭을 막기 위해서 입니다. 그 로직은 처음 전체 데이터를 가지고 있는 스테이스인 postData 의 가장 첫번째 데이터의 생성 시간과 현재 화면에 보여지고 있는 스테이스인 currentData 의 첫번째 데이터의 생성시간과 같지 않다면 UI 클릭을 활성화 하고 아니라면 비활성화 한다는 로직 입니다. 앞의 postData, currentData 가 && 연산자를 통해 모두 있다면 해당 연산을 하도록 한것은 처음 마운트 될때는 서버에 불러 온 데이터가 없기 때문에 데이터의 생성시간 프로퍼티에 접근 할 수 없기 때문 입니다.

사용자에게 몇 페이지까지 있는지 보여주는 UI

이제 여기서 postData 의 전체 데이터에서 8 을 나누고 몫의 올림 값에 맞게 숫자 UI 를 생성해주면 됩니다. 이때 8을 나눈 값의 올림값만큼 숫자 UI 를 만드는 이유는 8개의 게시글을 보여줄때 전체 게시글 데이터 중에 남은 데이터도 화면에 보여줘야 하기 때문에 하나의 페이지를 더 생성해 남은 데이터를 보여주는 것 입니다.
그럼 로직을 보시죠.

      {postData &&
            new Array(Math.ceil(postData.length / 8)).fill().map((i, l) => (
              <HomeStyle.PageNumberBtn
                onClick={() => onclickPageNumber(l)}
                currentPage={findCurrentPage() === l}
              >
                {l + 1}
              </HomeStyle.PageNumberBtn>
            ))}

위의 코드를 보시면 일단 postData 가 있다면 화면에 숫자 UI 를 보여주도록 코드를 작성한 것이고 postData 가 있다면 postData 길이에 8 을 나누고 그 몫으로 올림한 값의 길이만큼의 크기를 가지는 배열을 생성하고 그 배열을 map 함수로 index 를 이용해 숫자 UI를 만들어 화면에 보여줍니다.
여기서 PageNumberBtn 컴포넌트에 currentPage 속성은 스타일 컴포넌트에 속성 값을 보내서 속성 값에 따라서 해당 숫자 UI 에 파란색을 넣어주는 것 입니다. 그 이유는 사용자가 현재 내가 어디 페이지를 보고 있는지 알아야 하기 때문 입니다.

currentPage 에 전달하는 값은 findCurrentPage 함수의 반환 값과 현재 index 가 같으면 true, 아니면 false 를 보내는 것 입니다.
findCurrentPage 함수의 로직은

const findCurrentPage = () => {
    for (let i = 0; i < Math.ceil(postData.length / 8); ++i) {
      if (postData[i * 8].createTime === currentData[0].createTime) {
        return i;
      }
    }
  };

현재 페이지가 어딘지 찾아야 합니다. 제가 생각한 현재 페이지를 찾을 수 있는 방법은 postData 의 길이를 8로 나누고 올림한 값을 0 부터 그 값까지 for 문을 돌려 postData 의 생성시간과 현재 화면에 보여지고 있는 currentData의 0번 인덱스의 값이 같은 i 를 찾고 반환하는 로직 입니다. 이때 postData[i*8] 은 게시글을 8개씩 보여주고 있기 때문에 8개씩 나눴을때 첫번째 데이터를 찾기 위한 것 입니다.

오른쪽 페이지로 이동하는 화살표 UI 구현

오른쪽 페이지로 이동하는 화살표를 구현하는 로직 입니다.
오른쪽으로 이동하는 화살표는 보여 줄 데이터가 이제 없다면 더이상 클릭을 못하게 해야 합니다.
그래서 현재 보여주는 데이터를 가지고 있는 currentData 스테이트의 길이가 8보다 작다면 clickDisable 의 false 를 아니라면 true 를 반환해 클릭을 활성화 하거나 비활성화 하는 것 입니다.

        <HomeStyle.NextBtn
            onClick={() => onclickPageHandler(1, selectSortMethod)}
            clickDisable={currentData.length < 8 ? false : true}
          >
 /// 오른쪽 화살표 icon 코드
          </HomeStyle.NextBtn>

위의 코드를 한번에 보면

  <HomeStyle.PrevBtn
            onClick={() => onclickPageHandler(2)}
            clickDisable={
              postData &&
              currentData &&
              postData[0].createTime !== currentData[0].createTime
                ? true
                : false
            }
          >
            // 왼쪽 화살표 icon 코드
          </HomeStyle.PrevBtn>
          {postData &&
            new Array(Math.ceil(postData.length / 8)).fill().map((i, l) => (
              <HomeStyle.PageNumberBtn
                onClick={() => onclickPageNumber(l)}
                currentPage={findCurrentPage() === l}
              >
                {l + 1}
              </HomeStyle.PageNumberBtn>
            ))}
          <HomeStyle.NextBtn
            onClick={() => onclickPageHandler(1, selectSortMethod)}
            clickDisable={currentData.length < 8 ? false : true}
          >
              // 오른쪽 화살표 icon 코드
          </HomeStyle.NextBtn>

그럼 이제 화면에는

위와 같은 화면이 보일 것 입니다.

다음 시간에는 페이지 네이션 네비게이션 UI 를 클릭 했을때 UI 에 맞는 데이터를 화면에 보여주는 로직을 알아보겠습니다.

0개의 댓글

관련 채용 정보