틸틸이 프로젝트를 하면서 틸 조회와 관련된 부분들을 담당했었다. 블로그 사이트여서 한번에 많은 틸을 보여주기보다 페이지네이션으로 구현하여, 서버에서도 페이지별로 원하는 데이터만 받아오는 것으로 정하였다. 페이지네이션 라이브러리도 있었지만 직접 구현해보기로 했고 재사용이 가능하게 컴포넌트로 제작하였다. API요청을 서버에 어떻게 할 것인지, 화면에서 페이지 숫자는 어느정도 보여줄지, 검색여부에 따른 API 요청 등에 대한 고민을 많이 하게 되었다.
React 컴포넌트 간에 상태를 공유하고 관리하는 라이브러리로 Zustand를 사용하여, 전역 상태를 관리하고, useTilListStore라는 커스텀 스토어를 생성했다. 페이지와 URL을 인자로 받아와서 API를 호출하여 재사용성을 높이고, 여러 페이지에서 사용할 수 있도록 했다.
백엔드팀에서 페이지를 5페이지씩 나눠올 수 있었고, 페이지당 16개를 받아올 수 있도록 상의하였다. 그리고 시작페이지번호와 전체페이지번호, 총페이지번호 등에 대한 정보도 백엔드 팀에서 받아올 수 있어서 페이지네이션 구현하는데 편하게 할 수 있었다. 현재 페이지와 url을 받아와서 API 요청을 하는 방식으로 구현했으며 페이지가 바뀔때마다 현재 페이지를 업데이트 하도록 하였다.
default > tilComponents > useTilStore
import { create } from 'zustand';
import API from '../../API';
export const useTilListStore = create((set, get) => ({
data: [],
isLoading: false,
currentPage: 1,
pageSize: 16,
totalElements: 0,
totalPages: 0,
startPage: 0,
endPage: 0,
fetchData: async (page, url) => {
try {
set({ isLoading: true, currentPage: page });
const { pageSize } = get();
const response = await API.get(`${url}page=${page}&size=${pageSize}`);
//cards(틸 리스트), 전체 틸 숫자, 전체페이지번호, 시작페이지번호, 끝나는페이지번호(페이지가 5페이지씩 나눠옴)
const { cards, totalElements, totalPages, startPage, endPage } =
response.data;
set({
data: cards,
totalElements,
totalPages,
endPage,
startPage,
isLoading: false,
});
} catch (error) {
console.error(
`${page}페이지 데이터를 가져오는 중에 오류가 발생했습니다:`,
error
);
set({ isLoading: false });
}
},
//현재 페이지 정보를 업데이트
setCurrentPage: (page) => {
set({ currentPage: page });
},
}));
useTilListStore로부터 데이터 및 페이지 정보를 가져오고, TilList는 페이지네이션에 필요한 정보와 페이지 정보를 넘겨주고 페이지네이션의 숫자를 받아올 수 있도록 하였다. 그리고 키워드를 입력시에는 항상 1페이지를 다시 받아오도록 하였다.
tillist > searchTil
function SearchTil() {
const [keyword, setKeyword] = useState('');
const {
data,
isLoading,
currentPage,
totalPages,
startPage,
endPage,
fetchData,
setCurrentPage,
} = useTilListStore();
useEffect(() => {
let url = `${process.env.REACT_APP_API_URL}/til/list?`;
if (keyword) {
url += `searchKeyword=${keyword}&`;
}
fetchData(currentPage, url);
}, [currentPage, keyword]);
const handleSearchSubmit = (keyword) => {
setKeyword(keyword);
setCurrentPage(1);
};
if (isLoading) return <LoadingImage />;
return (
<TilWrapper>
<TilFlexContainer>
<Search onSubmit={handleSearchSubmit} />
{keyword && (
<SearchResult>
{data.length === 0 ? (
<h2>{keyword}에 대한 결과가 없습니다.</h2>
) : (
<h2>{keyword}에 대한 결과입니다.</h2>
)}
</SearchResult>
)}
</TilFlexContainer>
{!data && <LoadingImage />}
//TilList는 페이지네이션 숫자와 버튼 부분으로 페이지 정보를 넘겨주기
<TilList
currentPage={currentPage}
totalPages={totalPages}
startPage={startPage}
endPage={endPage}
setCurrentPage={setCurrentPage}
>
//tilCard는 TilLiST에 children(props)로 내려줌
<TilCardWrapper>
{data &&
data.map((data) => (
<li key={data.tilId}>
<TilCard data={data} />
</li>
))}
</TilCardWrapper>
</TilList>
<MoveButton />
</TilWrapper>
);
}
export default SearchTil;
TilList에서 props로 받아온 페이지 정보들을 확인하고, 현재 클릭된 페이지가 아닐 경우 페이지를 변경하기 위해 페이지 넘버를 api로 전달하여 다시 받아올 수 있도록 하였다. CSS는 styled-components를 사용하여서 선택된 페이지일때와 hover시 유저들이 잘 알아볼 수 있도록 설정하였다.
default > tilComponents > TilList
import styled, { css } from 'styled-components';
import { TilListWrapper } from '../styled';
import LeftArrow from '../image/leftArrow.svg';
import RightArrow from '../image/rightArrow.svg';
import LeftArrowHover from '../image/leftArrowHover.svg';
import RightArrowHover from '../image/rightArrowHover.svg';
const PageButtonWrapper = styled.div`
display: flex;
align-items: center;
margin: 50px 0px;
`;
const PageButton = styled.button`
margin: 5px;
width: 26px;
height: 26px;
background-size: cover;
font-weight: bold;
background-color: ${(props) =>
props.selected ? 'var(--brand-color)' : null};
border-radius: ${(props) => (props.selected ? '50%' : null)};
color: ${(props) =>
props.selected ? 'var(--color-white)' : 'var(--color-black)'};
&:hover {
background-color: ${(props) =>
props.selected ? 'var(--color-darkgreen)' : null};
color: ${(props) =>
props.selected ? 'var(--color-white)' : 'var(--brand-color)'};
}
`;
const PageArrowButton = styled.button`
background-size: cover;
margin: 12px;
width: 8px;
height: 12px;
${(props) =>
props.left &&
css`
background-image: url(${LeftArrow});
&:hover {
background-image: url(${LeftArrowHover});
}
`}
${(props) =>
props.right &&
css`
background-image: url(${RightArrow});
&:hover {
background-image: url(${RightArrowHover});
}
`}
`;
//페이지네이션을 원하는 페이지로부터 페이지 정보를 받아옴
function TilList({
currentPage,
totalPages,
setCurrentPage,
endPage,
startPage,
children,
}) {
//현재 클릭된 페이지가 아닐 경우 페이지를 변경하기 위해 페이지 넘버를 api로 전달
const handlePageClick = (pageNum) => {
if (pageNum !== currentPage) {
setCurrentPage(pageNum);
}
};
//왼쪽 화살표 클릭시 이전페이지로 이동 버튼 -> 페이지 이동시 현재 페이지 정보를 넘겨줌
const handlePrevClick = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
//오른쪽 화살표 클릭시 다음페이지로 이동 버튼 -> 페이지 이동시 현재 페이지 정보를 넘겨줌
const handleNextClick = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
//5개씩 페이지를 분리해서 데이터를 받아옴(페이지네이션 버튼 숫자는 5개씩만 출력됨) -> 5개당 페이지의 시작페이지와 끝나는 페이지를 서버로부터 받아와서 5개 페이지만 나오도록 설정
const pageNumbers = [];
for (let i = startPage; i <= endPage; i++) {
if (i !== 0) {
pageNumbers.push(i);
}
}
return (
<TilListWrapper>
{/*TilCard를 prop으로 전달 */}
{children}
<PageButtonWrapper>
{startPage > 0 && (
<PageArrowButton left onClick={handlePrevClick}></PageArrowButton>
)}
{pageNumbers &&
pageNumbers.map((pageNum) => (
<PageButton
key={pageNum}
onClick={() => handlePageClick(pageNum)}
selected={pageNum === currentPage}
>
{pageNum}
</PageButton>
))}
{startPage > 0 && (
<PageArrowButton right onClick={handleNextClick}></PageArrowButton>
)}
</PageButtonWrapper>
</TilListWrapper>
);
}
export default TilList;