관리 메뉴

거니의 velog

(18) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 5 본문

React_프론트엔드 프로젝트

(18) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 5

Unlimited00 2024. 2. 23. 19:57

(4) 페이지네이션 구현하기

* 이번에는 페이지네이션 기능을 구현해 보자. list API를 만들 때 마지막 페이지 번호를 HTTP 헤더를 통해 클라이언트에 전달하도록 설정했다. 그러나 요청을 관리하는 사가를 쉽게 만들기 위해 작성한 createRequestSaga 에서는 SUCCESS 액션을 발생시킬 때 payload에 reponse.data 값만 넣어 주기 때문에 현재 구조로는 헤더를 확인할 수 없다.

* 그렇기 때문에 createRequestSaga를 조금 수정해 줄 것이다.

[lib/createRequestSaga.js]

(...)

export default function createRequestSaga(type, request) {
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;

  return function* (action) {
    yield put(startLoading(type)); // 로딩 시작
    try {
      const response = yield call(request, action.payload);
      yield put({
        type: SUCCESS,
        payload: response.data,
        meta: response,
      });
    } catch (e) {
      yield put({
        type: FAILURE,
        payload: e,
        error: true,
      });
    }
    yield put(finishLoading(type)); // 로딩 끝
  };
}

* 이렇게 액션 안에 meta 값을 response로 넣어 주면 나중에 HTTP 헤더 및 상태 코드를 쉽게 조회할 수 있다.

* posts 리덕스 모듈을 열어서 다음과 같이 수정해 주자.

[modules/posts.js]

(...)

const initialState = {
  posts: null,
  error: null,
  lastPage: 1,
};

const posts = handleActions(
  {
    [LIST_POSTS_SUCCESS]: (state, { payload: posts, meta: response }) => ({
      ...state,
      posts,
      lastPage: parseInt(response.headers['last-page'], 10), // 문자열을 숫자로 변환
    }),
    [LIST_POSTS_FAILURE]: (state, { payload: error }) => ({
      ...state,
      error,
    }),
  },
  initialState,
);

export default posts;

* 이제 리덕스 스토어 안에 마지막 페이지 번호를 lastPage라는 값으로 담아 둘 수 있다. 페이지네이션을 위한 컴포넌트 Pagination.js를 components/posts 디렉터리에 작성해 보자.

[components/posts/Pagination.js]

import React from 'react';
import styled from 'styled-components';
import qs from 'qs';
import Button from '../common/Button';

const PaginationBlock = styled.div`
  width: 320px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
  margin-bottom: 3rem;
`;
const PageNumber = styled.div``;

const buildLink = ({ username, tag, page }) => {
  const query = qs.stringify({ tag, page });
  return username ? `/@${username}?${query}` : `/?${query}`;
};

const Pagination = ({ page, lastPage, username, tag }) => {
  return (
    <PaginationBlock>
      <Button
        disabled={page === 1}
        to={
          page === 1 ? undefined : buildLink({ username, tag, page: page - 1 })
        }
      >
        이전
      </Button>
      <PageNumber>{page}</PageNumber>
      <Button
        disabled={page === lastPage}
        to={
          page === lastPage
            ? undefined
            : buildLink({ username, tag, page: page + 1 })
        }
      >
        다음
      </Button>
    </PaginationBlock>
  );
};

export default Pagination;

* 이 컴포넌트에서는 props로 현재 선택된 계정명, 태그, 현재 페이지 숫자, 마지막 페이지 숫자를 가져온다. 사용자가 이 컴포넌트에 있는 버튼을 클릭하면, props로 받아 온 값을 사용하여 이동해야 할 다음 경로를 설정해 준다. 그리고 첫 번째 페이지일 때는 이전 버튼이 비활성화되고, 마지막 페이지일 때는 다음 버튼이 비활성화된다.

* 컴포넌트를 다 만든 뒤에는 Button 컴포넌트에 비활성화된 스타일을 설정해 주자. 비활성화 스타일은 :disabled CSS 셀렉터를 사용하여 적용할 수 있다.

[components/common/Button.js]

import React from 'react';
import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom';
import palette from '../../lib/styles/palette';

const buttonStyle = css`
(...)

  &:disabled {
    background: ${palette.gray[3]};
    color: ${palette.gray[5]};
    cursor: not-allowed;
  }
`;

(...)

* 버튼 스타일을 수정한 후에는 Pagination 컴포넌트를 위한 컨테이너인 PaginationContainer 컴포넌트를 만들자.

[containers/posts/PaginationContainer.js]

import React from 'react';
import Pagination from '../../components/posts/Pagination';
import { useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import qs from 'qs';

const PaginationContainer = ({ location, match }) => {
  const { lastPage, posts, loading } = useSelector(({ posts, loading }) => ({
    lastPage: posts.lastPage,
    posts: posts.posts,
    loading: loading['posts/LIST_POSTS'],
  }));

  // 포스트 데이터가 없거나 로딩 중이면 아무것도 보여주지 않음
  if (!posts || loading) return null;
  const { username } = match.params;

  // page가 없으면 1을 기본값으로 사용
  const { tag, page = 1 } = qs.parse(location.search, {
    ignoreQueryPrefix: true,
  });

  return (
    <Pagination
      tag={tag}
      username={username}
      page={parseInt(page, 10)}
      lastPage={lastPage}
    />
  );
};

export default withRouter(PaginationContainer);

* 다음으로 이 컨테이너 컴포넌트를 PostListPage에서 렌더링해 주면 페이지네이션 기능의 구현이 모두 끝난다.

[pages/PostListPage.js]

import React from 'react';
import HeaderContainer from '../containers/common/HeaderContainer';
import PostListContainer from '../containers/posts/PostListContainer';
import PaginationContainer from '../containers/posts/PaginationContainer';

const PostListPage = () => {
  return (
    <div>
      <HeaderContainer />
      <PostListContainer />
      <PaginationContainer />
    </div>
  );
};

export default PostListPage;

* 다음과 같이 페이지 하단에 페이지네이션 UI가 나타났는가? 버튼을 눌러 잘 작동되는지 확인해 보자.

페이지네이션 기능

* 첫 번째 페이지일 때는 이전 버튼이 비활성화되고, 마지막 페이지일 때는 다음 버튼이 활성화된다. 계정명이나 태그를 클릭하여 포스트 쿼리 시스템도 잘 작동하는지 확인해 보자.

포스트 쿼리 시스템 1
포스트 쿼리 시스템 2


3. 정리

* 포스트 조회 기능에 대한 구현도 모두 마쳤다! 다음 장에서는 포스트 수정 및 삭제 기능을 구현하고 프로젝트를 마무리 해 볼 것이다.