게시판/내 게시물 페이지 구현 - 페이지네이션React+REST API 게시판 구현/FE - React2021. 10. 13. 15:58
Table of Contents
What to do?
전체 게시판 페이지 구현
- 전체 게시물을 볼 수 있는 페이지
- 화면에 제목, 사진, 작성자, 작성일을 나타내자.
- 게시판을 map함수를 사용해서 렌더링할 때, board의 id를 key로 사용하고 게시판에 있는 목록을 클릭했을 때, 상세보기 페이지인 동적 url인 /board/:board_id 로 이동할 수 있게 하자.
- 로그인이 필요한 페이지가 아니므로 jwt-token을 검증할 필요가 없으므로 일반 라우팅 처리를 한다.
- 보안과 관련해서 request 헤더에 jwt-token에 대한 정보를 보낼 필요가 없으므로 interceptor를 사용하지 않고 axios를 사용하자.
- 게시물은 한 페이지당 4개로 페이지네이션을 구현해서 사용하자
- 서버에서 페이징 기능을 구현했으므로 게시물 전체 갯수를 알려주는 API를 이용해서 페이지 번호를 구하고 API를 호출 할 때 아래처럼 쿼리 파라미터로 페이지의 번호와 페이지의 사이즈를 넘겨주자
- http://localhost:8080/api/board/list?&page_number=1&page_size=4
- 위와 같이 서버에 요청을 보내고 클라이언트가 사용할 url은 /board-list?page=1 와 같은 형식이다
내 게시물 페이지 구현
- 유저가 작성한 게시물만 볼 수 있는 페이지
- 게시판 페이지와 마찬가지로 화면에 제목, 사진, 작성자, 작성일을 나타내자.
- 게시판을 map함수를 사용해서 렌더링할 때, board의 id를 key로 사용하고 게시판에 있는 목록을 클릭했을 때, 상세보기 페이지인 동적 url /board/:board_id 로 이동할 수 있게 하자.
- 로그인이 필요한 페이지이므로 PrivateRoute 처리를 하자.
- request 헤더에 jwt-token에 대한 정보를 보내야 하므로 interceptor를 사용해서 내 게시물을 불러오자.
- 내 게시물은 user의 id를 포함해야 하므로 jwt-token에 포함된 user의 id를 쿼리 파라미터로 이용해서 API 요청을 하자.
→ http://localhost:8080/api/board/user/list?user_id=1&page_number=1&page_size=4 이런식으로 요청을 보내자- 이외의 페이지네이션은 게시판 구현과 같다
- 클라이언트가 사용할 url은 /myboard-list?page=1 와 같은 형식이다
페이지네이션
- 페이지 카운트는 (전체 게시물 갯수 / 페이지 사이즈)의 결과에 올림을 하면 된다.
- 서버에 페이지와 페이지의 사이즈를 보내주면 그에 해당하는 데이터를 받아와야 한다. 이건 백엔드랑 약속을 해야됨 ㅠㅠ
- 페이지 카운트를 구하고 아래 material ui의 페이지네이션 컴포넌트를 이용해보자
https://mui.com/material-ui/react-pagination/
게시물 컴포넌트(Card) 구현
- 게시판과 내 게시물에서 보여줄 제목, 사진, 작성자, 작성일을 담을 컴포넌트를 작성하자.
- components 디렉토리 아래 Card.js 파일과 card.scss 파일을 만들자.
- Card 컴포넌트는 아래처럼 제목, 내용 작성자, 작성일, 이미지에 대한 정보를 보여주고 props로 board의 id도 받아서 누르면 /board/:board_id로 이동하게 하자
- 그냥 카드를 만들면 너무 밋밋할 수 있으므로 크로스 브라우징 처리를 했고 hover시 약간 커지며 그림자가 생기게 만들었다.
components/Card.js
- props로 제목, 내용, 사진의 url, 작성자 이름, 날짜를 받아서 화면에 띄운다
- 또한 props로 DB에 저장된 board의 id를 받아서 클릭하면 /board/:board_id(게시물 상세보기 페이지)로 이동하게 한다.
import "./card.scss";
import {useNavigate} from "react-router-dom";
export const Card = ({board_id, title, content, img_url, username, date}) => {
const navigate = useNavigate();
return (
<div className="card-wrapper" onClick={() => {
navigate(`/board/${board_id}`)
}}>
<div className="card-body-img">
<img src={img_url}/>
</div>
<div className="card-body-text">
<div className="card-body-text-title">{title}</div>
<div className="card-body-text-content">{content}</div>
</div>
<div className="card-footer">
<div className="username">{username}</div>
<div className="date">{date}</div>
</div>
</div>
);
};
components/card.scss
- 미디어 쿼리를 사용해서 반응형으로 Card 컴포넌트를 만들었고 hover시에 약간 크기가 커지고 그림자가 생기게 했다.
- Card 컴포넌트의 요소들은 flex를 column으로 적용했고 justify-content: space-between을 준 후 제목과 내용이 보이는 부분에 flex-grow:1을 줘서 남은 부분을 채우게 배치했다.
- 이미지 같은 경우는 비율을 유지하면서 주어진 넓이와 높이를 꽉 채우게 object-fit: cover 속성을 넣었다.
- 마지막으로 제목과 내용이 부분에 word-break: break-all과 overflow: auto 속성을 넣어서 글씨가 요소 밖으로 튀어나지 않게 하고 스크롤을 내릴 시 보이지 않는 부분을 보이게 구성했다.
@media all and (min-width: 1024px) {
.card-wrapper {
width: 300px;
height: 600px;
&:hover {
width: 310px;
height: 610px;
box-shadow: rgba(0, 0, 0, 0.9) 0px 22px 70px 4px;
}
}
}
@media all and (max-width: 1024px) {
.card-wrapper {
width: 270px;
height: 540px;
&:hover {
width: 280px;
height: 550px;
box-shadow: rgba(0, 0, 0, 0.9) 0px 22px 70px 4px;
}
}
}
@media all and (max-width: 768px) {
.card-wrapper {
width: 240px;
height: 480px;
&:hover {
width: 250px;
height: 490px;
box-shadow: rgba(0, 0, 0, 0.9) 0px 22px 70px 4px;
}
}
}
.card-wrapper {
flex-shrink: 0;
margin: 15px;
font-family: 'Noto Sans KR', sans-serif;
border-radius: 10px;
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: width 1s, height 1s, box-shadow 1s;
cursor: pointer;
.card-body-img {
width: 100%;
height: 60%;
img {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.card-body-text {
flex-grow: 1;
word-break: break-all;
overflow: auto;
padding: 0.6rem;
&::-webkit-scrollbar {
display: none;
}
.card-body-text-title {
font-size: 1.3rem;
color: darkslategray;
font-weight: bold;
}
}
.card-footer {
border-top: 0.5px solid black;
padding: 0.6rem;
font-weight: 200;
display: flex;
color: #282c34;
font-size: 1.1rem;
justify-content: space-between;
}
}
게시판/내 게시물 버튼 수정
Header 컴포넌트 수정하기
components/Header.js
- 페이지네이션을 이용해서 게시판과 내 게시물 화면을 보여줄 것이므로 Header 컴포넌트에서 게시판 버튼과 내 게시물 버튼을 클릭하면 각각 해당하는 URL의 첫번째 페이지를 보여주도록 변경하자.
- 쿼리 파라미터로 page를 사용할 것이므로 ?page=1을 붙여주자
import "./header.scss";
import {Link, useNavigate} from "react-router-dom";
import {useDispatch, useSelector} from "react-redux";
import {jwtUtils} from "../utils/jwtUtils";
import {useEffect, useState} from "react";
import {setToken} from "../redux/reducers/AuthReducer";
const Header = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const token = useSelector(state => state.Auth.token);
const [isAuth, setIsAuth] = useState(false);
useEffect(() => {
if (jwtUtils.isAuth(token)) {
setIsAuth(true);
} else {
setIsAuth(false);
}
}, [token]);
const logout = async () => {
await dispatch(setToken(""));
alert("로그아웃 되었습니다😎");
navigate("/");
};
return (
<div className="header-wrapper">
<div className="header-title">
<Link to="/">
<span>Duckgugong</span>
</Link>
</div>
<div className="header-menu">
<Link to="/board-list?page=1">게시판</Link>
<Link to="/add-board">글쓰기</Link>
{isAuth ? (
<>
<Link to="/myboard-list?page=1">내 게시물</Link>
<Link to="#" onClick={logout}>로그아웃</Link>
</>
) : (
<>
<Link to="/login">로그인</Link>
<Link to="/sign-up">회원가입</Link>
</>
)}
</div>
</div>
);
};
export default Header;
게시판 페이지 구현
- pages 디렉토리 아래 board-list 디렉토리를 만들고 BoardList.js 파일과 boardList.scss 파일을 만들자.
pages/board-list/BoarList.js
- 컴포넌트가 마운트 될 때 딱 한번만 페이지의 전체 갯수를 구하고 페이지의 게시물을 담는 상태를 하나 만들어서 페이지에 해당하는 게시물을 가져오자.
- 만약 useNavigate로 같은 경로로 이동하고 쿼리 파라미터만 다르면 컴포넌트가 언마운트 되었다가 다시 마운트 되지 않기 때문에 페이지 카운트에 해당하는 버튼을 누를 때 마다 window.location.href로 페이지를 새로고침해서 컴포넌트를 다시 마운트 시키자.
- 만약 useEffect에 빈 배열을 넘기고 useNavigate로 경로를 바꾸고 페이지에 해당하는 데이터를 불러오면 마운트 되고 딱 한번만 게시물을 가져오기 때문에 페이지 쿼리 파라미터가 변경되어도 useEffect가 실행되지 않아서 처음 가져왔던 게시물을 그대로 가지고 있다.
→ 그래서 window.location.href로 새로고침 하면서 컴포넌트를 다시 마운트 시킨다
→ 다시 마운트 시키지 않으면 애니메이션 효과도 적용되지 않는다! - material-ui의 페이지네이션 컴포넌트를 사용해서 (전체 게시물 개수 / 페이지 사이즈)를 반올림해서 페이지 카운트를 정해주고 페이지 카운트를 누를 때 쿼리 파라미터를 이용해서 해당 페이지로 새로고침과 동시에 이동해서 데이터를 받아오게 하자.
- 시간은 mommet 라이브러리를 사용하고 9시간을 더한다. 서버에서 UTC로 저장하기 때문!
import {Pagination} from "@mui/material";
import {Card} from "../../components/Card";
import {useEffect, useState} from "react";
import axios from "axios";
import {useSearchParams} from "react-router-dom";
import "./boardList.scss";
import moment from "moment";
const BoardList = () => {
const [pageCount, setPageCount] = useState(0);
const [boardList, setBoardList] = useState([]);
const [searchParams, setSearchParams] = useSearchParams();
// 렌더링 되고 한번만 전체 게시물 갯수 가져와서 페이지 카운트 구하기
// 렌더링 되고 한번만 페이지에 해당하는 게시물 가져오기
useEffect(() => {
// 페이지에 해당하는 게시물 가져오기
const getBoardList = async () => {
const page_number = searchParams.get("page");
const {data} = await axios.get(`/api/board/list?page_number=${page_number}&page_size=4`);
return data;
}
// 현재 페이지에 해당하는 게시물로 상태 변경하기
getBoardList().then(result => setBoardList(result));
// 게시물 전체 갯수 구하기
const getTotalBoard = async () => {
const {data} = await axios.get("/api/board/count");
return data.total;
}
// 페이지 카운트 구하기: (전체 board 갯수) / (한 페이지 갯수) 결과 올림
getTotalBoard().then(result => setPageCount(Math.ceil(result / 4)));
}, [])
return (
<div className="boardList-wrapper">
<div className="boardList-header">
전체 게시물 📝
</div>
<div className="boardList-body">
{boardList.map((item, index) => (
<Card key={item.id} username={item.user.username} date={moment(item.created).add(9, "hour").format('YYYY-MM-DD')}
title={item.title} content={item.content}
board_id={item.id} img_url={`/api/image/view/${item.id}`}
/>
))}
</div>
<div className="boardList-footer">
{/*페이지네이션: count에 페이지 카운트, page에 페이지 번호 넣기*/}
<Pagination
variant="outlined" color="primary" page={Number(searchParams.get("page"))}
count={pageCount} size="large"
onChange={(e, value) => {
window.location.href = `/board-list?page=${value}`;
}}
showFirstButton showLastButton
/>
</div>
</div>
)
}
export default BoardList;
pages/board-list/boardList.scss
- keyframes을 이용해서 fade-in 효과를 넣어주고 flex로 배치하자.
@keyframes smoothAppear {
from {
opacity: 0;
transform: translate3d(0, -100%, 0);
}
to {
opacity: 1;
transform: translateZ(0);
}
}
.boardList-wrapper{
opacity: 0;
animation: smoothAppear 1.5s forwards;
animation-delay: 0.5s;
display: flex;
flex-direction: column;
align-items: center;
.boardList-header{
color: midnightblue;
font-weight: bold;
font-size: 2rem;
}
.boardList-body{
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.boardList-footer{
margin: 1.5rem;
}
}
내 게시물 페이지 구현
- pages 디렉토리 아래 myboard-list 디렉토리를 만들고 MyBoardList.js 파일을 만들자.
- 게시판 페이지 컴포넌트인 BoardList의 스타일을 그대로 사용할 것이기 때문에 별도의 스타일 파일은 만들지 않아도 된다.
pages/myboard-list/MyBoardList.js
- 게시판 페이지와 거의 똑같지만 다른 점은 로그인한 유저만 해당 페이지에 접근할 수 있으므로 서버에 request를 보낼 때 구현해 둔 axios interceptor를 사용하는 것과 자원을 가져오는 API의 주소만 다르다.
import {Pagination} from "@mui/material";
import {Card} from "../../components/Card";
import {useEffect, useState} from "react";
import api from "../../utils/api";
import {useSearchParams} from "react-router-dom";
import "../board-list/boardList.scss";
import {useSelector} from "react-redux";
import {jwtUtils} from "../../utils/jwtUtils";
import moment from "moment";
const MyBoardList = () => {
const [pageCount, setPageCount] = useState(0);
const [boardList, setBoardList] = useState([]);
const [searchParams, setSearchParams] = useSearchParams();
// user의 id를 알아내기 위해 token 가져오기
const token = useSelector(state => state.Auth.token);
// 렌더링 되고 한번만 전체 게시물 갯수 가져와서 페이지 카운트 구하기
// 렌더링 되고 한번만 페이지에 해당하는 게시물 가져오기
useEffect(() => {
// 페이지에 해당하는 게시물 가져오기
const getBoardList = async () => {
const page_number = searchParams.get("page");
const user_id = jwtUtils.getId(token);
const {data} = await api.get(`/api/board/user/list?page_number=${page_number}&page_size=4&user_id=${user_id}`);
return data;
}
// 현재 페이지에 해당하는 게시물로 상태 변경하기
getBoardList().then(result => setBoardList(result));
// 게시물 전체 갯수 구하기
const getTotalBoard = async () => {
const user_id = jwtUtils.getId(token);
const {data} = await api.get(`/api/board/user/count/${user_id}`);
return data.total;
}
// 페이지 카운트 구하기: (전체 board 갯수) / (한 페이지 갯수) 결과 올림
getTotalBoard().then(result => setPageCount(Math.ceil(result / 4)));
}, [])
return (
<div className="boardList-wrapper">
<div className="boardList-header">
나의 게시물 📝
</div>
<div className="boardList-body">
{boardList.map((item, index) => (
<Card key={item.id} username={item.user.username} date={moment(item.created).add(9, "hour").format('YYYY-MM-DD')}
title={item.title} content={item.content}
board_id={item.id} img_url={`/api/image/view/${item.id}`}
/>
))}
</div>
<div className="boardList-footer">
{/*페이지네이션: count에 페이지 카운트, page에 페이지 번호 넣기*/}
<Pagination
variant="outlined" color="primary" page={Number(searchParams.get("page"))}
count={pageCount} size="large"
onChange={(e, value) => {
window.location.href = `/myboard-list?page=${value}`;
}}
showFirstButton showLastButton
/>
</div>
</div>
)
}
export default MyBoardList;
라우팅 적용하기
- /board-list와 /myboard-list에 대한 라우팅을 적용하는데 /myboard-list는 로그인한 사용자만 이용할 수 있으므로 PrivateRoute로 처리를 하고 path에 쿼리 파라미터가 있으므로 현재 주소 전체를 넘겨준다
import React from "react";
import {Routes, Route, useLocation} from "react-router-dom";
import Header from "./components/Header";
import Home from "./pages/home/Home";
import SignUp from "./pages/sign-up/SignUp";
import Login from "./pages/login/Login";
import PrivateRoute from "./routes/PrivateRoute";
import AddBoard from "./pages/add-board/AddBoard";
import BoardList from "./pages/board-list/BoardList";
import MyBoardList from "./pages/myboard-list/MyBoardList";
const App = () => {
const location = useLocation();
return (
<React.Fragment>
<Header/>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/sign-up" element={<SignUp/>}/>
<Route path="/login" element={<Login/>}/>
<Route
path="/add-board"
element={
<PrivateRoute path="/add-board" component={AddBoard}/>
}
/>
<Route path="/board-list" element={<BoardList/>}/>
<Route
path="/myboard-list"
element={
// 쿼리 파라미터가 존재하므로 전체 url을 PrivateRoute에 넘겨준다
<PrivateRoute path={`${location.pathname}`} component={MyBoardList}/>
}
/>
</Routes>
</React.Fragment>
)
}
export default App;
'React+REST API 게시판 구현 > FE - React' 카테고리의 다른 글
게시물 수정, 삭제 구현 (0) | 2021.10.19 |
---|---|
게시물 상세보기 페이지 구현 (0) | 2021.10.18 |
게시물 등록하기 페이지 구현 (0) | 2021.10.11 |
axios interceptor 구현하기 (0) | 2021.10.04 |
인증 처리하기+PrivateRoute+로그아웃 (0) | 2021.10.04 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!