이번 서울시 유치원 정보 사이트 프로젝트 대망의 마지막 기능 구현은 페이지네이션이었다.
페이지네이션은 목록의 양이 많아졌을 때 페이지로 정리해서 나눠담는 기능인데 이 기능의 경우 개발 초보인 나에겐 너무 어려운 기능이어서 이번 구현은 사실상 거의 gpt에게 맡겼었다.
하지만 이번 프로젝트는 단순히 기능 구현 보다는 개발 공부에 더 큰 의미가 있기 때문에 비록 gpt를 이용해서 만든 코드이지만 내가 이해할 수 있는 선까지는 최대한 분석해보기로 했다.
list.js 페이지에서 몇가지 전역변수를 지정하고 시작했다.
let currentPage = 1 ; // 현재 페이지 번호
const itemsPerPage = 10 ; // 페이지당 표시할 항목 수
let currentList = []; // 현재 목록을 저장할 배열
const maxPageButtons = 10 ; // 페이지네이션에서 보일 최대 페이지 번호 버튼 수
let startPage = 1 ; // 페이지 번호 버튼의 시작 페이지
let endPage = maxPageButtons ; // 페이지 번호 버튼의 끝 페이지
currentPage
현재 페이지 번호. 초기는 1로 세팅
itemsPerPage
페이지당 몇개의 목록을 표시할지를 정하는 변수
이번엔 10개씩 목록을 보여주기로 함
currentList
현재 목록을 저장하는 배열
여기에 10개씩의 목록이 들어올 것
이번 페이지네이션에서 가장 중요한 변수라고 할 수 있음
maxPageButtons
페이지네이션에서 보일 최대 페이지 번호 버튼의 수
10개를 맥시멈으로 정했음
startPage
페이지 번호 버튼의 시작 페이지
endPage
페이지 번호 버튼의 끝 페이지
기존 searchList 함수. 즉 '서울시 구 이름'을 통해 가져온 유치원 목록 생성을 하는 기능을 실행하면 renderPage와 setupPagination 함수를 수행하도록 세팅했다.
function searchList ( clickedGuName ) {
fetch ( "./json/kinderGeneral.json" )
. then (( response ) => response . json ())
. then (( data ) => {
currentList = data . DATA . filter (( item ) => item . addr . includes ( clickedGuName ));
renderPage ( currentList , currentPage );
setupPagination ( currentList );
});
}
renderPage, setupPagination 이 두 함수는 세팅은 곧 이어서 설명
그리고 기존의 직접 검색으로 가져온 유치원 목록을 생성하는 기능인 directSearchList 함수를 실행해도 검색 값이 있으면 똑같이 renderPage와 setupPagination 함수를 수행하고 검색 값이 없으면 renderNoResults라는 함수를 수행하도록 세팅했다.
function directSearchList ( searchedKeyword ) {
fetch ( "./json/kinderGeneral.json" )
. then (( response ) => response . json ())
. then (( data ) => {
currentList = data . DATA . filter (( item ) => item . kindername . includes ( searchedKeyword ));
if ( currentList . length > 0 ) {
renderPage ( currentList , currentPage );
setupPagination ( currentList );
} else {
renderNoResults ();
}
});
}
특히 이번에 페이지네이션을 짜면서 코드의 구조 개편이 이루어졌는데 기존에 searchList와 directSearchList가 데이터를 가져오는 방식과 변수를 다르게 세팅해서 서로 다른 필터링과 두번의 html 세팅이 필요했었는데 currentList라는 배열변수를 사용하게 되면서 이 두가지 함수의 사용방식을 거의 유사하게 통일시키고 html도 하나로 관리할 수 있게 되었다.
기존에 directSearchList는 let searchedArray = []; 라는 배열변수에 푸쉬시키던 방식으로 코드를 짰었는데 (기존 포스팅 참고)
https://designerdk.tistory.com/manage/newpost/15?type=post&returnURL=ENTRY
이번 페이지네이션 구현과 함께 searchList와 directSearchList 두 함수 다 currentList라는 배열변수 안에 각각 '구이름과 주소를 대조해서 일치하는 리스트', 그리고 '검색값과 유치원명이 일치하는 리스트'. 이 두 방식으로 필터링한 목록이 저장될 수 있도록 세팅했다.
이로써 좀 더 정돈된 코드가 되었다.
currentList = data . DATA . filter (( item ) => item . addr . includes ( clickedGuName ));
currentList = data . DATA . filter (( item ) => item . kindername . includes ( searchedKeyword ));
지금부터는 페이지네이션과 관련된 코드이다.
먼저 renderPage라는 함수이며 list와 page라는 인자를 받는다. 이 인자들은 이후에 실행될 때 각각 currentList와 currentPage
가 들어온다.
function renderPage ( list , page )
생성된 리스트를 넣어줄 .list-container를 잡고 listContainer라는 변수로 지정 후 innerHTML로 한번 비워주었다.
const listContainer = document . querySelector ( ".list-container" );
listContainer . innerHTML = "" ;
이후 start, end, pagination 이라는 3가지가 추가적으로 선언됨.
const start = ( page - 1 ) * itemsPerPage ;
const end = start + itemsPerPage ;
const paginatedItems = list . slice ( start , end );
start는 직접 수식을 대입해보면
1페이지 일 때는 0,
2페이지 일 때는 10,
3페이지 일 때는 20
이런식으로 값이 들어온다.
end는 start에 전역변수로 지정한 itemsPerPage를 더해준 값이며 아까 itemsPerPage가 10이었으므로
1페이지 일 때는 10,
2페이지 일 때는 20,
3페이지 일 때는 30
이런식으로 값이 들어온다.
paginatedItems은 list라는 인자. 즉 이후에 들어오게될 currentList라는 유치원 목록들 배열에 방금 설명한 start와 end라는 값을 이용해서 잘라준(slice) 아이템들을 저장한 배열이다.
즉 이후에 1번 페이지 버튼을 누르면 필터링 된 유치원 목록 중 0~9번 배열을 paginatedItems에 저장시키고 2번 페이지 버튼을 누르면 10~19번 배열을 저장시키는 방식이다.
이렇게 만든 paginatedItems라는 배열을 forEach를 돌려서 유치원목록 카드 디자인인 listCard라는 html을 채워준다.
paginatedItems . forEach (( item , i ) => {
let listCard = `
<div class="list-card" id="list ${ i + start } ">
<div class="list-contentsGrp">
<div class="list-contents">
<div class="kinder-title">
<div class="kinder-tag" id="list-establish ${ i + start } ">
${ item . establish }
</div>
<div class="kinder-name" id="list-name ${ i + start } ">
${ item . kindername }
</div>
</div>
<div class="list-infoGrp">
<div class="list-info" id="list-num ${ i + start } ">
${ item . telno }
</div>
<div class="list-info" id="list-addr ${ i + start } ">
${ item . addr }
</div>
</div>
</div>
<div class="icon">아이콘</div>
</div>
<div class="divider"></div>
</div>
` ;
이렇게 세팅하면 이후 1페이지 버튼을 눌렀을 때 start에 1이 대입되면서 ${i + start}는 0~9까지의 배열값을 돌리게 된다.
2페이지를 누르면 10~19까지의 배열을 돌리게되는 원리.
이렇게 반복문으로 html이 채워지면 화면에 10개씩 유치원 목록이 나오게 된다.
이후 리스트 카드를 넣어주고 이벤트를 발생시키는 방식은 이전과 동일
listContainer . insertAdjacentHTML ( "beforeend" , listCard );
document . getElementById ( `list ${ i + start } ` ). addEventListener ( "click" , function () {
clickedKinderName = document . getElementById ( `list-name ${ i + start } ` ). textContent ;
localStorage . setItem ( "clickedKinderName" , clickedKinderName );
clickedkinderCode = item . kindercode ;
localStorage . setItem ( "clickedkinderCode" , clickedkinderCode );
location . href = "detail.html" ;
검색결과가 없을 때에 대한 메시지를 표시해주는 디자인도renderNoResults라는 기능으로 재구성했다.
function renderNoResults () {
const listContainer = document . querySelector ( ".list-container" );
listContainer . innerHTML = `
<div class="empty-contents">
<div class="empty-msg">
<div class="empty-img">이미지</div>
<div class="empty-text">일치하는 유치원명이 없습니다.</div>
</div>
<a class="empty-back-btn" href="index.html">돌아가기</a>
</div>
` ;
}
이제 setupPagination 기능의 차례
function setupPagination ( list )
대부분 페이지네이션 버튼을 생성하고 렌더해주는 코드로 구성되어있다.
먼저 .pagination이라는 클래스를 잡고 paginationContainer라는 변수로 선언 후 한번 비워주고
const paginationContainer = document . querySelector ( ".pagination" );
paginationContainer . innerHTML = "" ;
그리고 총 리스트 수를 한페이지에 표시할 아이템 수(itemsPerPage)로 나눠준걸 반올림 해 준 값을 pageCount라는 변수로 저장
const pageCount = Math . ceil ( list . length / itemsPerPage );
Math.ceil()을 해주면 소수점 값을 무조건 올림시켜준다. (Ex. 1.2라는 값 넣으면 2로 변환)
이렇게 세팅하면 예를들어 총 8개 리스트를 한 페이지에 5개만 보여주겠다고 했을 때 8/5 즉 1.6이라는 수가 나오면 반올림을해서 2라는 값으로 변환시켜서 pageCount라는 변수에 저장하게 된다. - 총 2페이지 생성
이제 페이지네이션 버튼 생성을 하는 코드이다.
먼저 페이지 번호 버튼들부터 for문을 돌려서 만들건데 전역변수로 세팅해준 startPage(초기 1로 세팅) 값을 i로 넣어주고
Math.min(endPage, pageCount)보다 작거나 같은 값들까지만 반복시켜서 버튼을 생성하는 방식
for ( let i = startPage ; i <= Math . min ( endPage , pageCount ); i ++ )
참고로 Math.min()은 최소 값을 찾아주는 기능이다.
예를들어 Math.min(3, 7)은 3을 반환
이번엔 Math.min(endPage, pageCount)라고 적어주었으므로 endPage와 pageCount 중 더 작은 값을 반환하는 것이다.
endPage는 전역변수에서 maxPageButtons로 선언했고 maxPageButtons. 즉 페이지네이션에서 최대로 보이는 페이지 버튼 수는 10으로 설정했었다. 따라서 endPage 값은 10으로 고정이다.
반면 pageCount는 가져와지는 목록 양에 따라 달라지는데 예를들어 강남구 버튼을 통해 불러온 목록수를 통해 pageCount를 구하면 총 5페이지가 나온다.
console.log로 pageCount을 확인하면 5가 나옴
그런데 ‘서울’이라는 키워드로 검색해서 유치원 목록이 불러와지면 총 32페이지가 pageCount로 들어오게 된다.
console.log로 pageCount을 확인하면 32가 나옴
목록이 무한정 많아진다고 페이지네이션 버튼 양도 무한정 많아질 수 없기 때문에 최대 10개까지로 제한하고자 Math.min(endPage, pageCount)이 사용되었다. 이렇게 세팅되면 강남구 버튼으로 생성되는 경우 5개 페이지 버튼이 보이며, ‘서울’로 검색해서 32 페이지가 생성되더라도 페이지버튼은 10개까지만 보여서 안정적인 디자인을 유지할 수 있다. (넘치는 페이지는 '다음' 버튼을 통해 페이지를 넘김)
이후는 페이지 버튼을 생성하는 코드이다.
for ( let i = startPage ; i <= Math . min ( endPage , pageCount ); i ++ ) {
let pageButton = document . createElement ( "button" );
pageButton . textContent = i ;
if ( i === currentPage ) pageButton . classList . add ( "active" );
pageButton . addEventListener ( "click" , () => changePage ( i ));
paginationContainer . appendChild ( pageButton );
console . log ( pageCount )
}
페이지 버튼 넘버(i)가 현재페이지 currentPage와 같으면 pageButton 엘리먼트에 active라는 클래스를 추가해주며, 버튼을 눌렀을 때에는 changePage라는 함수에 인자로 i를 넣으면서 실행된다.
최종 실행 함수인 changePage은 i값이 인자로 들어오면서 실행되는데 이 값이 currentPage을 대체시키고 renderPage(currentList, currentPage)와 setupPagination(currentList)을 실행시킨다.
function changePage ( page ) {
currentPage = page ;
renderPage ( currentList , currentPage );
setupPagination ( currentList );
}
이 외에는 이전페이지 다음페이지로 가는 버튼, 첫페이지 끝페이지로 가는 버튼을 만드는 코드들이 있는데 약간씩은 다르지만 페이지 버튼 생성과 유사해서 포스팅으로 정리는 생략하려고한다.
function setupPagination ( list ) {
const paginationContainer = document . querySelector ( ".pagination" );
paginationContainer . innerHTML = "" ;
const pageCount = Math . ceil ( list . length / itemsPerPage );
// 처음 버튼
let firstPageButton = document . createElement ( "button" );
firstPageButton . textContent = "처음" ;
firstPageButton . disabled = currentPage === 1 ;
firstPageButton . addEventListener ( "click" , () => {
currentPage = 1 ;
startPage = 1 ;
endPage = maxPageButtons ;
renderPage ( currentList , currentPage );
setupPagination ( currentList );
});
paginationContainer . appendChild ( firstPageButton );
// 이전 페이지 세트 버튼
let prevSetButton = document . createElement ( "button" );
prevSetButton . textContent = "이전" ;
prevSetButton . disabled = startPage === 1 ;
prevSetButton . addEventListener ( "click" , () => {
startPage = Math . max ( 1 , startPage - maxPageButtons );
endPage = startPage + maxPageButtons - 1 ;
renderPage ( currentList , currentPage );
setupPagination ( currentList );
});
paginationContainer . appendChild ( prevSetButton );
// 페이지 번호 버튼 생성
for ( let i = startPage ; i <= Math . min ( endPage , pageCount ); i ++ ) {
let pageButton = document . createElement ( "button" );
pageButton . textContent = i ;
if ( i === currentPage ) pageButton . classList . add ( "active" );
pageButton . addEventListener ( "click" , () => changePage ( i ));
paginationContainer . appendChild ( pageButton );
console . log ( pageCount )
}
// 다음 페이지 세트 버튼
let nextSetButton = document . createElement ( "button" );
nextSetButton . textContent = "다음" ;
nextSetButton . disabled = endPage >= pageCount ;
nextSetButton . addEventListener ( "click" , () => {
startPage = Math . min ( pageCount - maxPageButtons + 1 , startPage + maxPageButtons );
endPage = startPage + maxPageButtons - 1 ;
renderPage ( currentList , currentPage );
setupPagination ( currentList );
});
paginationContainer . appendChild ( nextSetButton );
// 끝 버튼
let lastPageButton = document . createElement ( "button" );
lastPageButton . textContent = "끝" ;
lastPageButton . disabled = currentPage === pageCount ;
lastPageButton . addEventListener ( "click" , () => {
currentPage = pageCount ;
startPage = Math . max ( 1 , pageCount - maxPageButtons + 1 );
endPage = pageCount ;
renderPage ( currentList , currentPage );
setupPagination ( currentList );
});
paginationContainer . appendChild ( lastPageButton );
}
function changePage ( page ) {
currentPage = page ;
renderPage ( currentList , currentPage );
setupPagination ( currentList );
}
너무 난이도가 높아서 gpt가 거의 다 짜주었지만 짜준걸 복기해보는 과정만으로도 배울 부분이 많았음. 특히 이건 수학적인 부분이 많았어서 직접 짰다면 불가능했을 것 같다. 어찌보면 코딩하기 너무나 좋은 세상이다.
이제 프로젝트의 기능 구현은 완료. 디자인 작업을 들어가보겠다.