14일차 - Pagination, Lifting-State-Up

류연찬·2022년 11월 15일
0

Codecamp FE07

목록 보기
14/39

Pagination

페이지 번호를 클릭해서 이동하는 방식의 페이지 처리 방법입니다.
게시판 형태의 페이지에서 가장 일반적으로 사용되는 방식입니다.



1. page 인자를 사용해서 게시글 목록 불러오기

가장 먼저 fetchBoards API를 활용해서 게시글 목록을 불러옵니다.
이 때, playground Docs를 참고하여 page 인자도 함께 불러와줍니다.

// gql query
const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
    }
  }
`;

// fetchBoards
const { data } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });

return (
	<div>
    <h1>페이지네이션 연습 !!!</h1>
    {data?.fetchBoards?.map((el) => (
      <div key={el._id}>
        {el.title} {el.writer}
      </div>
    ))}
		<span> 1 </span>
		<span> 2 </span>
		<span> 3 </span>
  </div>
)



2. 페이지 클릭 시 게시글 목록 데이터 다시 불러오기(refetch)

이제 페이지네이션의 페이지 숫자를 클릭할 때마다
목록에 뿌려진 데이터가 해당 페이지에 해당하는 데이터로 변경되도록 합니다.

GraphQL의 useQuery에서 제공하는 refetch 함수를 활용하면 페이지 클릭 시 해당 페이지에 해당하는 데이터를 다시 불러올 수 있습니다.
refetch를 사용하기 위해서는 useQuery에서 data와 함께 refetch라는 함수를 불러와야 합니다.

필요한 부분에 불러운 refetch 함수를 넣어주고, refetch의 인자변경될 variables(이 경우에는 page)를 입력합니다.

// gql query
const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
    }
  }
`;

// fetchBoards
// data와 함께 refetch 가져오기
const { data, refetch } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });

const onClickPage = (event) => {
	// 위에서 가져온  refetch 사용하기
  refetch({ page: Number(event.target.id) });
};

return (
	<div>
    <h1>페이지네이션 연습 !!!</h1>
    {data?.fetchBoards?.map((el) => (
      <div key={el._id}>
        {el.title} {el.writer}
      </div>
    ))}
		<span onClick={onClickPage} id="1"> 1 </span>
		<span onClick={onClickPage} id="2"> 2 </span>
		<span onClick={onClickPage} id="3"> 3 </span>
  </div>
)

1페이지를 클릭하면 1페이지의 게시글 목록이 뿌려지고,
2페이지를 클릭하면 2페이지의 게시글 목록이 뿌려지고,
3페이지를 클릭하면 3페이지의 게시글 목록이 뿌려지는 것을 확인할 수 있습니다.

3. map을 이용해 페이지네이션 뿌리기

방금 한 것과 같이 모든 페이지네이션의 숫자를 직접 입력하는 작업은 비효율적입니다.
배열과 map을 사용해 페이지네이션을 만들 수 있습니다.

// gql query
const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
    }
  }
`;

// fetchBoards
// data와 함께 refetch 가져오기
const { data, refetch } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });

const onClickPage = (event) => {
	// 위에서 가져온  refetch 사용하기
  refetch({ page: Number(event.target.id) });
};

return (
	<div>
    <h1>페이지네이션 연습 !!!</h1>
    {data?.fetchBoards?.map((el) => (
      <div key={el._id}>
        {el.title} {el.writer}
      </div>
    ))}
		{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((el) => (
        <span onClick={onClickPage} id={String(el)} key={el}>
          {` ${el} `}
        </span>
      ))}
  </div>
)

하지만 이렇게 하드코딩을 하면 1~10페이지의 페이지네이션밖에 만들지 못합니다.

4. 페이지네이션 next/prev 구현

이전에 map을 사용할 때 index 도 인자로 받아올 수 있다는 사실을 학습했습니다.
그것을 활용하면 다음과 같은 방식으로 페이지네이션을 만들어 줄 수 있습니다.

export default function PaginationNextPage() {
	// 시작 페이지
  const [startPage, setStartPage] = useState(1);
  const { data, refetch } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });

  const onClickPage = (event) => {
    refetch({ page: Number(event.target.id) });
    console.log(event.target.id);
  };

	// 이전 페이지 클릭 시 실행할 함수
  const onClickPrevPage = () => {
    setStartPage((prev) => prev - 10);
  };
	// 다음 페이지 클릭 시 실행할 함수
  const onClickNextPage = () => {
    setStartPage((prev) => prev + 10);
  };

  return (
    <div>
      <h1>페이지네이션 연습 !!!</h1>
      {data?.fetchBoards?.map((el) => (
        <div key={el._id}>
          {el.title} {el.writer}
        </div>
      ))}

      <span onClick={onClickPrevPage}>이전페이지 |</span>

      {new Array(10).fill(1).map((_, index) => (
        <span
          onClick={onClickPage}
          id={String(index + startPage)}
          key={index + startPage}
        >
          {` ${index + startPage} `}
        </span>
      ))}

      <span onClick={onClickNextPage}> 다음페이지</span>

    </div>
  );
}

5. lastPage 설정

하지만 문제는 마지막 페이지를 훌쩍 넘어서 계속해서 다음 페이지를 불러옵니다.

이를 해결하기 위해서 가장 먼저 DB에 등록된 게시글의 총 개수를 불러와서 마지막 페이지의 값을 구합니다.

// fetchBoardsCount API 요청하기
const FETCH_BOARDS_COUNT = gql`
  query fetchBoardsCount {
    fetchBoardsCount
  }
`;

// lastPage 구하기
const lastPage = Math.ceil(dataBoardsCount?.fetchBoardsCount / 10);

그리고 이전 페이지, 다음 페이지를 클릭했을 때 실행되는 함수에 조건을 설정해서 1페이지 미만, 그리고 lastPage가 화면에 출력된 이후로는 이전 페이지, 다음 페이지 버튼이 동작하지 않도록 만들어 줍니다.

const onClickPrevPage = () => {
	// startPage가 1이면 하단 스크립트를 실행하지 않고 종료한다.
  if (startPage === 1) return; 
  setStartPage((prev) => prev - 10);
};

const onClickNextPage = () => {
	// startPage + 10가 lastPage보다 클 경우 하단 스크립트를 실행하지 않고 종료한다.
  if (startPage + 10 > lastPage) return;
  setStartPage((prev) => prev + 10);
};

그리고 페이지네이션 map에 조건부 렌더링을 걸어서 lastPage보다 큰 숫자는 출력되지 않도록 만들어줍니다.

{new Array(10).fill(1).map(
  (_, index) =>
    index + startPage <= lastPage && (
      <span
        onClick={onClickPage}
        id={String(index + startPage)}
        key={index + startPage}
      >
        {` ${index + startPage} `}
      </span>
    )
)}

6. 이전 페이지/다음 페이지 이동 시 refetch

이전 페이지, 다음 페이지 버튼을 눌러서 이동할 경우,
11~20 페이지라면 11페이지로,
21~30 페이지라면 21페이지로 게시글 목록이 함께 이동이 되어야 합니다.

첫번째 방법은 refetch를 이용하는 방법입니다.

const onClickPrevPage = () => {
	// startPage가 1이면 하단 스크립트를 실행하지 않고 종료한다.
  if (startPage === 1) return;
  setStartPage((prev) => prev - 10);
  refetch({ page: startPage - 10 });
};

const onClickNextPage = () => {
	// startPage + 10가 lastPage보다 클 경우 하단 스크립트를 실행하지 않고 종료한다.
  if (startPage + 10 > lastPage) return;
  setStartPage((prev) => prev + 10);
  refetch({ page: startPage + 10 });
};

두번째 방법은 게시글 목록을 불러오기 위하여 useQuery에 넣는 variables에 startPage라는 state를 넣어주는 방법입니다.
이렇게하면 이전 페이지, 다음 페이지를 눌러서 startPage가 변경될 때마다 바뀐 startPage의 데이터가 새로 뿌려지게 됩니다.

// 기존 useQuery
const { data, refetch } = useQuery(FETCH_BOARDS, {
  variables: { page: 1 },
});

// 변경 후 useQuery
const { data, refetch } = useQuery(FETCH_BOARDS, {
  variables: { page: startPage },
});



Lifting-State-Up

React의 데이터 흐름은 상위 컴포넌트에서 하위 컴포넌트로 전달하는 하향식, 단방향 데이터 흐름을 따르고 있습니다.

그렇다면 단방향 데이터 흐름의 장점은 무엇일까요?
우선, 기능 변경 사항에 대한 코드 수정이 적어집니다.
또한 복잡하기 않아 코드의 흐름을 알기 쉽다는 점이 있습니다.

하지만 단방향 데이터 흐름의 단점도 존재합니다.
다음과 같은 경우를 가정해봅시다.

단향방 데이터 흐름의 경우,
위와 같은 구조에서 자식 컴포넌트1의 state를 자식 컴포넌트2에서 보여주는 것불가능합니다.
또한, 자식 컴포넌트2의 state를 부모 컴포넌트에서 보여주는 것불가능합니다.

그래서 아래와 같이 자식 컴포넌트의 statesetState를 부모 컴포넌트로 끌어올려 선언해 줍니다.
그리고 props로 내려서 자식 컴포넌트1, 자식 컴포넌트2에서 모두 동일한 state를 사용할 수 있습니다.



State-Lifting 예제

하나의 부모 컴포넌트에 2개의 자식 컴포넌트를 import 합니다.
그리고 부모 컴포넌트에서 생성한 statesetStateprops 를 이용해 자식 컴포넌트로 넘겨줍니다.

index.tsx(부모 컴포넌트)

import Child1 from "../../src/components/units/14-lifting-state-up/Child1";
import Child2 from "../../src/components/units/14-lifting-state-up/Child2";
import { useState } from "react";

export default function LiftingStateUpPage() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Child1 count={count} setCount={setCount} />
      <div>=========================================</div>
      <Child2 count={count} />
    </div>
  );
}

Child1.tsx(자식 컴포넌트)

export default function Child1(props) {

  const onClickCountUp = () => {
    props.setCount((prev) => prev + 1);
  };

  return (
    <div>
      <div>자식 1 카운트: {props.count}</div>
      <button onClick={onClickCountUp}>카운트 올리기</button>
    </div>
  );
}

Child2.tsx(자식 컴포넌트)

// 자식 컴포넌트 - Child2.tsx
export default function Child2(props) {
  return (
    <div>
      <div>자식 2 카운트: {props.count}</div>
      <button>카운트 올리기</button>
    </div>
  );
}

그리고 자식 컴포넌트1에 있는 카운트 올리기 버튼을 클릭해줍니다.
자식 컴포넌트2로 보낸 count라는 state도 함께 증가하는 것을 확인할 수 있습니다.

0개의 댓글